@use "sass:list";
@use "sass:map";
@use "sass:meta";

// stylelint-disable scss/dollar-variable-pattern

// Utility generator

// - Utilities can use three different types of selectors:
//   - class: .class
//   - attr-starts: [class^="class"]
//   - attr-includes: [class*="class"]
// - Utilities can target children via `child-selector`, wrapped in :where() for zero specificity
// - Utilities can generate regular CSS properties and CSS custom properties
// - Utilities can be responsive or not
// - Utilities can have state variants (e.g., hover, focus, active)
// - Utilities can define local CSS variables
//
// CSS custom properties can be generated in two ways:
//
// 1. Property map with null values (CSS var receives the utility value):
// "bg-color": (
//   property: (
//     "--bg": null,
//     "background-color": var(--bg)
//   ),
//   class: bg,
//   values: (
//     primary: var(--blue-500),
//   )
// )
// Generates:
// .bg-primary {
//   --bs-bg: var(--bs-blue-500);
//   background-color: var(--bs-bg);
// }
//
// 2. Variables map (static CSS custom properties on every class):
// "link-underline": (
//   property: text-decoration-color,
//   class: link-underline,
//   variables: (
//     "link-underline-opacity": 1
//   ),
//   values: (...)
// )
// Generates:
// .link-underline {
//   --bs-link-underline-opacity: 1;
//   text-decoration-color: ...;
// }

// Helper mixin to generate CSS properties for both legacy and property map approaches
@mixin generate-properties($utility, $propertyMap, $properties, $value) {
  @if $propertyMap != null {
    // New Property-Value Mapping approach
    @each $property, $defaultValue in $propertyMap {
      // If value is a map, check if it has a key for this property
      // Otherwise, use defaultValue (or $value if defaultValue is null)
      $actualValue: $defaultValue;
      @if meta.type-of($value) == "map" and map.has-key($value, $property) {
        $actualValue: map.get($value, $property);
      } @else if $defaultValue == null {
        $actualValue: $value;
      }
      @if map.get($utility, important) {
        #{$property}: $actualValue !important; // stylelint-disable-line declaration-no-important
      } @else {
        #{$property}: $actualValue;
      }
    }
  } @else {
    // Legacy approach
    @each $property in $properties {
      @if map.get($utility, important) {
        #{$property}: $value !important; // stylelint-disable-line declaration-no-important
      } @else {
        #{$property}: $value;
      }
    }
  }
}

@mixin generate-utility($utility, $prefix: "") {
  // Validate required keys
  @if not map.has-key($utility, property) {
    @error "Utility is missing required `property` key: #{$utility}";
  }
  @if not map.has-key($utility, values) {
    @error "Utility is missing required `values` key: #{$utility}";
  }

  // Warn on unknown keys (likely typos)
  $valid-keys: property, values, class, selector, responsive, print, important, state, variables, child-selector;
  @each $key in map.keys($utility) {
    @if not list.index($valid-keys, $key) {
      @warn "Unknown utility key `#{$key}` found. Valid keys are: #{$valid-keys}";
    }
  }

  // Determine if we're generating a class, or an attribute selector
  $selectorType: "class";
  @if map.has-key($utility, selector) {
    $selectorType: map.get($utility, selector);
    // Validate selector type
    $valid-selectors: "class", "attr-starts", "attr-includes";
    @if not list.index($valid-selectors, $selectorType) {
      @error "Invalid `selector` value `#{$selectorType}`. Must be one of: #{$valid-selectors}";
    }
  }
  // Then get the class name to use in a class (e.g., .class) or in a attribute selector (e.g., [class^="class"])
  $selectorClass: map.get($utility, class);

  // Attribute selectors require a `class` key
  @if $selectorType != "class" and not map.has-key($utility, class) {
    @error "Utility with `selector: #{$selectorType}` requires a `class` key.";
  }

  // Get the list or map of values and ensure it's a map
  $values: map.get($utility, values);
  @if meta.type-of($values) != "map" {
    @if meta.type-of($values) == "list" {
      $list: ();
      @each $value in $values {
        $list: map.merge($list, ($value: $value));
      }
      $values: $list;
    } @else {
      $values: (null: $values);
    }
  }

  @each $key, $value in $values {
    $properties: map.get($utility, property);
    $propertyMap: null;
    $customClass: "";

    // Check if property is a map (new Property-Value Mapping approach)
    @if meta.type-of($properties) == "map" {
      $propertyMap: $properties;
      $customClass: "";
      @if map.has-key($utility, class) {
        $customClass: map.get($utility, class);
      }
    } @else {
      // Legacy approach: Multiple properties are possible, for example with vertical or horizontal margins or paddings
      @if meta.type-of($properties) == "string" {
        $properties: list.append((), $properties);
      }
      // Use custom class if present, otherwise use the first value from the list of properties
      @if map.has-key($utility, class) {
        $customClass: map.get($utility, class);
      } @else {
        $customClass: list.nth($properties, 1);
      }
      @if $customClass == null {
        $customClass: "";
      }
    }

    // State params to generate state variants
    $state: ();
    @if map.has-key($utility, state) {
      $state: map.get($utility, state);
    }

    // Don't add a dash before value key if value key is null (e.g. with shadow class)
    $customClassModifier: "";
    @if $key {
      @if $customClass == "" {
        $customClassModifier: $key;
      } @else {
        $customClassModifier: "-" + $key;
      }
    }

    // Build the class name fragment (without prefix or dot) for reuse in state variants
    $className: "";
    @if $selectorType == "class" {
      @if $customClass != "" {
        $className: $customClass + $customClassModifier;
      } @else if $selectorClass != null and $selectorClass != "" {
        $className: $selectorClass + $customClassModifier;
      } @else {
        $className: $customClassModifier;
      }
    }

    $selector: "";
    @if $selectorType == "class" {
      $selector: ".#{$prefix + $className}";
    } @else if $selectorType == "attr-starts" {
      $selector: "[class^=\"#{$selectorClass}\"]";
    } @else if $selectorType == "attr-includes" {
      $selector: "[class*=\"#{$selectorClass}\"]";
    }

    // @debug $utility;
    // @debug $selectorType;
    // @debug $selector;
    // @debug $properties;
    // @debug $values;

    // Apply child-selector wrapping if present (wraps in :where() for zero specificity)
    $child-sel: null;
    @if map.has-key($utility, child-selector) {
      $child-sel: map.get($utility, child-selector);
    }

    $final-selector: $selector;
    @if $child-sel {
      $final-selector: ":where(#{$selector} #{$child-sel})";
    }

    #{$final-selector} {
      // Generate CSS custom properties (variables) if provided
      // Variables receive the current utility value, then properties reference them
      @if map.has-key($utility, variables) {
        $variables: map.get($utility, variables);
        @if meta.type-of($variables) == "list" {
          // If variables is a list, each variable gets the utility value
          @each $var-name in $variables {
            --#{$var-name}: #{$value};
          }
        } @else if meta.type-of($variables) == "map" {
          // If variables is a map, use the provided values (for static variables)
          @each $var-key, $var-value in $variables {
            --#{$var-key}: #{$var-value};
          }
        }
      }
      @include generate-properties($utility, $propertyMap, $properties, $value);
    }

    // Generate state variants (e.g., hover:link-10 instead of link-10-hover)
    @if $state != () {
      @each $state-variant in $state {
        $state-selector: ".#{$prefix}#{$state-variant}\\:#{$className}:#{$state-variant}";
        @if $child-sel {
          $state-selector: ":where(#{$state-selector} #{$child-sel})";
        }

        #{$state-selector} {
          // Generate CSS custom properties (variables) if provided
          @if map.has-key($utility, variables) {
            $variables: map.get($utility, variables);
            @if meta.type-of($variables) == "list" {
              // If variables is a list, each variable gets the utility value
              @each $var-name in $variables {
                --#{$var-name}: #{$value};
              }
            } @else if meta.type-of($variables) == "map" {
              // If variables is a map, use the provided values (for static variables)
              @each $var-key, $var-value in $variables {
                --#{$var-key}: #{$var-value};
              }
            }
          }
          @include generate-properties($utility, $propertyMap, $properties, $value);
        }
      }
    }
  }
}
