@function get-variant-type($section) {
  $type: null;

  // Responsive
  @if (map-has-key($grid-breakpoints, $section)) {
    $type: responsive;
  }

  // Dark
  @if ($section == dark) {
    $type: dark;
  }

  // Basic
  @if (map-has-key($variants, $section)) {
    $type: basic;
  }

  @return $type;
}

@function get-variants($variants) {
  $basic-variant: null;
  $dark-variant: null;
  $responsive-variant: null;

  @for $i from 1 through length($variants) {
    $section: get-variant-type(list.nth($variants, $i));
    @if ($section == responsive) {
      $responsive-variant: list.nth($variants, $i);
    } @else if ($section == dark) {
      $dark-variant: true;
    } @else if ($section == basic) {
      $basic-variant: list.nth($variants, $i);
    }
  }

  @return (
    basic-variant: $basic-variant,
    dark-variant: $dark-variant,
    responsive-variant: $responsive-variant
  );
}

@function get-classname-without-prefix($computed-selector, $original-selector) {
  $sliced-prefix-selector: explode(
    string.slice(
      $computed-selector,
      0,
      str-index($computed-selector, ":#{$original-selector}")
    ),
    " "
  );

  $computed-prefix-selector: "";
  @if (length($sliced-prefix-selector) > 1) {
    $computed-prefix-selector: nth-implode(
      nth-remove($sliced-prefix-selector, -1),
      " "
    );
  }

  @return $computed-prefix-selector + " " +
    string.slice(
      $computed-selector,
      str-index($computed-selector, ":" + $original-selector) + 1
    ) + ", ";
}

@function get-computed-classname($parent-selectors, $variants) {
  $computed-selectors: ();
  $basic-variant: map-get($variants, basic-variant);
  $dark-variant: map-get($variants, dark-variant);

  @each $parent-selector in $parent-selectors {
    $computed-selector: #{$parent-selector};

    // Apply basic variant
    @if ($basic-variant != null) {
      $variant-string: call(get-function($basic-variant));
      $variant-string: str-replace(
        $variant-string,
        "{classname}",
        ":#{$parent-selector}"
      );
      $computed-selector: get-classname-without-prefix(
        $variant-string,
        $parent-selector
      );
    }

    // Apply dark variant
    @if ($dark-variant != null) {
      $computed-selector: ".dark #{$computed-selector}";
    }

    @if (string.length($computed-selector) > 0) {
      $computed-selectors: append($computed-selectors, $computed-selector);
    }
  }

  @return nth-implode($computed-selectors, ",");
}

@function escape-applied-utility($applied-utility) {
  $applied-utility: str-replace($applied-utility, "\\.", ".");
  $applied-utility: str-replace($applied-utility, "\\", "");
  $applied-utility: str-replace($applied-utility, " ", "");
  @return $applied-utility;
}

@function group-by-variant($applied-utilities) {
  $utilities: ();
  $index: 1;
  @each $applied-utility in $applied-utilities {
    $applied-utility: escape-applied-utility($applied-utility);
    $utility-string-list: explode($applied-utility, ":");

    @if (length($utility-string-list) > 1) {
      $classname: list.nth($utility-string-list, length($utility-string-list));
      $variant: str-slice(
        $applied-utility,
        1,
        str-index($applied-utility, $classname) - 3
      );
      $utility: get-utility($classname);

      // If utility not found
      @if ($utility == null) {
        $utilities: map-merge(
          $utilities,
          (
            $classname: null,
          )
        );
      } @else {
        $variants: get-variants($utility-string-list);
        $values: map-get($utility, values);
        $pseudo-selector: map-get($utility, pseudo-selector);
        $property: map-get($utility, property);

        @if (map-has-key($utilities, $variant) == false) {
          $utilities: map.set(
            $utilities,
            $variant,
            (
              variants: $variants,
              values: (
                $index: (
                  properties: $values,
                  property: $property,
                  pseudo-selector: $pseudo-selector,
                ),
              ),
            )
          );
        } @else {
          $utilities: map-merge(
            $utilities,
            (
              $variant: (
                variants: map-get($utilities, $variant, variants),
                pseudo-selector: map-get($utilities, $variant, pseudo-selector),
                values:
                  map.set(
                    map-get($utilities, $variant, values),
                    $index,
                    (
                      properties: $values,
                      property: $property,
                      pseudo-selector: $pseudo-selector,
                    )
                  ),
              ),
            )
          );
        }
      }
    } @else {
      $utility: get-utility($applied-utility);

      // If utility not found
      @if ($utility == null) {
        $utilities: map-merge(
          $utilities,
          (
            $applied-utility: null,
          )
        );
      } @else {
        $values: map-get($utility, values);
        $pseudo-selector: map-get($utility, pseudo-selector);
        $property: map-get($utility, property);

        @if (map-has-key($utilities, no-variant) == false) {
          $utilities: map.set(
            $utilities,
            no-variant,
            (
              variants: (
                basic-variant: null,
                dark-variant: null,
                responsive-variant: null,
              ),
              values: (
                $index: (
                  properties: $values,
                  property: $property,
                  pseudo-selector: $pseudo-selector,
                ),
              ),
            )
          );
        } @else {
          $utilities: map-merge(
            $utilities,
            (
              no-variant: (
                variants: map-get($utilities, no-variant, variants),
                pseudo-selector:
                  map-get($utilities, no-variant, pseudo-selector),
                values:
                  map.set(
                    map-get($utilities, no-variant, values),
                    $index,
                    (
                      properties: $values,
                      property: $property,
                      pseudo-selector: $pseudo-selector,
                    )
                  ),
              ),
            )
          );
        }
      }
    }

    $index: $index + 1;
  }

  @return $utilities;
}

@mixin generate-properties($utility) {
  @each $value-key, $value in map-get($utility, values) {
    @if (map-get($value, pseudo-selector) == null) {
      @if (type-of(map-get($value, properties)) == map) {
        @each $property-key, $property in map-get($value, properties) {
          #{$property-key}: #{$property};
        }
      } @else {
        @if (type-of(map-get($value, property)) == list) {
          @each $property in map-get($value, property) {
            #{$property}: #{map-get($value, properties)};
          }
        } @else {
          #{map-get($value, property)}: #{map-get($value, properties)};
        }
      }
    } @else {
      @if (type-of(map-get($value, properties)) == map) {
        &#{map-get($value, pseudo-selector)} {
          @each $property-key, $property in map-get($value, properties) {
            #{$property-key}: #{$property};
          }
        }
      } @else {
        @if (type-of(map-get($value, property)) == list) {
          &#{map-get($value, pseudo-selector)} {
            @each $property in map-get($value, property) {
              #{$property}: #{map-get($value, properties)};
            }
          }
        } @else {
          &#{map-get($value, pseudo-selector)} {
            #{map-get($value, property)}: #{map-get($value, properties)};
          }
        }
      }
    }
  }
}

@function get-matched-value($utility, $value) {
  $pseudo-selectors: "::placeholder" ">:not([hidden])~:not([hidden])";
  $matched-value: null;

  @if (
    map-has-key($utility, values, $value) or
      list.index(map-get($utility, values), $value)
  ) {
    $matched-value: (
      value: $value,
      pseudo-selector: null,
    );
  }

  @each $pseudo-selector in $pseudo-selectors {
    @if (
      $matched-value ==
        null and
        (
          map-has-key($utility, values, "#{$value}#{$pseudo-selector}") or
            list.index(
              map-get($utility, values),
              "#{$value}#{$pseudo-selector}"
            )
        )
    ) {
      $matched-value: (
        value: "#{$value}#{$pseudo-selector}",
        pseudo-selector: $pseudo-selector,
      );
    }
  }

  @return $matched-value;
}

$temp-utilities: ();

@function get-utility($applied-utility) {
  $utility-string-list: explode($applied-utility, ":");
  $classname: list.nth($utility-string-list, length($utility-string-list));

  @if (map-has-key($temp-utilities, $classname) == false) {
    $list: ();
    $keywords: ();
    $matched: null;

    @if (string.slice($classname, 1, 1) != "-") {
      $list: explode($classname, "-");
    } @else {
      $list: explode(string.slice($classname, 2), "-");
      $temp: ();
      @each $list-item in $list {
        @if (length($temp) == 0) {
          $temp: append($temp, "-#{$list-item}");
        } @else {
          $temp: append($temp, $list-item);
        }
      }
      $list: $temp;
    }

    @if (length($list) > 1) {
      @for $i from length($list) through 1 {
        $string: "";
        @for $z from 1 through $i {
          @if ($z == 1) {
            $string: list.nth($list, $z);
          } @else {
            $string: "#{$string}-#{list.nth($list, $z)}";
          }
        }

        $keywords: append($keywords, $string);
      }
    } @else {
      $keywords: append($keywords, $classname);
    }

    // By classname
    @each $keyword in $keywords {
      @each $utility-key, $utility in $utilities {
        @if (map-get($utility, class) == $keyword) {
          $value: string.slice($classname, string.length($keyword) + 2);
          $value: if(string.length($value) == 0, null, $value);
          $matched-value: get-matched-value($utility, $value);

          @if ($matched-value != null) {
            $matched: (
              values:
                if(
                  type-of(map-get($utility, values)) == list,
                  map-get($matched-value, value),
                  map.get($utility, values, map-get($matched-value, value))
                ),
              property: map-get($utility, property),
              pseudo-selector: map-get($matched-value, pseudo-selector),
            );
          }
        }
      }
    }

    // By value if search by classname has no result
    @if ($matched == null) {
      @each $keyword in $keywords {
        @each $utility-key, $utility in $utilities {
          @if (map-get($utility, class) == null) {
            $matched-value: get-matched-value($utility, $keyword);

            @if ($matched == null and $matched-value != null) {
              $matched: (
                values:
                  if(
                    type-of(map-get($utility, values)) == list,
                    map-get($matched-value, value),
                    map.get($utility, values, map-get($matched-value, value))
                  ),
                property: map-get($utility, property),
                pseudo-selector: map-get($matched-value, pseudo-selector),
              );
            }
          }
        }
      }
    }

    // Cache result
    $temp-utilities: map.set($temp-utilities, $classname, $matched) !global;

    @return $matched;
  } @else {
    @return map-get($temp-utilities, $classname);
  }
}

@mixin apply($applied-utilities) {
  @each $applied-utility-key,
    $applied-utility in group-by-variant($applied-utilities)
  {
    @if ($applied-utility != null) {
      $variants: map-get($applied-utility, variants);
      $responsive-variant: map-get($variants, responsive-variant);

      @if (& != null) {
        @if ($responsive-variant == null) {
          @at-root #{get-computed-classname(&, $variants)} {
            @include generate-properties($applied-utility);
          }
        } @else {
          @media (min-width: map-get($grid-breakpoints, $responsive-variant)) {
            @at-root #{get-computed-classname(&, $variants)} {
              @include generate-properties($applied-utility);
            }
          }
        }
      } @else {
        @include generate-properties($applied-utility);
      }
    } @else {
      @extend .#{$applied-utility-key} !optional;
    }
  }
}
