@use 'sass:list';
@use 'sass:map';
@use 'sass:meta';
@use '@material/elevation/elevation-theme' as mdc-elevation-theme;
@use '@material/theme/custom-properties' as mdc-custom-properties;
@use '@material/theme/theme' as mdc-theme;
@use '@material/theme/keys' as mdc-keys;
@use '@material/tokens/v0_161' as mdc-tokens;
@use '../style/sass-utils';
@use '../m2/palette' as m2-palette;
@use '../m2/theming' as m2-theming;
@use '../m2/typography' as m2-typography;

// Indicates whether we're building internally. Used for backwards compatibility.
$private-is-internal-build: false;

$_placeholder-color-palette: m2-theming.define-palette(m2-palette.$red-palette);

// Placeholder color config that can be passed to token getter functions when generating token
// slots.
$placeholder-color-config: (
  primary: $_placeholder-color-palette,
  accent: $_placeholder-color-palette,
  warn: $_placeholder-color-palette,
  is-dark: false,
  foreground: m2-palette.$light-theme-foreground-palette,
  background: m2-palette.$light-theme-background-palette,
);

$_placeholder-typography-level-config: m2-typography.typography-config-level-from-mdc(body1);

// Placeholder typography config that can be passed to token getter functions when generating token
// slots.
$placeholder-typography-config: (
  font-family: 'Roboto, sans-serif',
  headline-1: $_placeholder-typography-level-config,
  headline-2: $_placeholder-typography-level-config,
  headline-3: $_placeholder-typography-level-config,
  headline-4: $_placeholder-typography-level-config,
  headline-5: $_placeholder-typography-level-config,
  headline-6: $_placeholder-typography-level-config,
  subtitle-1: $_placeholder-typography-level-config,
  subtitle-2: $_placeholder-typography-level-config,
  body-1: $_placeholder-typography-level-config,
  body-2: $_placeholder-typography-level-config,
  caption: $_placeholder-typography-level-config,
  button: $_placeholder-typography-level-config,
  overline: $_placeholder-typography-level-config,
  subheading-1: $_placeholder-typography-level-config,
  title: $_placeholder-typography-level-config,
);

// Placeholder density config that can be passed to token getter functions when generating token
// slots.
$placeholder-density-config: 0;

$_tokens: null;
$_component-prefix: null;

@mixin _configure-token-prefix($first, $rest...) {
  $_component-prefix: '' !global;
  @each $item in $rest {
    $_component-prefix:
      if($_component-prefix == '', $item, '#{$_component-prefix}-#{$item}') !global;
  }
  @include mdc-custom-properties.configure($varname-prefix: $first) {
    @content;
  }
  $_component-prefix: null !global;
}

// Sets the token prefix and map to use when creating token slots.
@mixin use-tokens($prefix, $tokens) {
  $_tokens: $tokens !global;
  @include _configure-token-prefix($prefix...) {
    @content;
  }
  $_tokens: null !global;
}

// Emits a slot for the given token, provided that it has a non-null value in the token map passed
// to `use-tokens`.
@mixin create-token-slot($property, $token, $emit-fallback: false) {
  @if $_component-prefix == null or $_tokens == null {
    @error '`create-token-slot` must be used within `use-tokens`';
  }
  @if not map.has-key($_tokens, $token) {
    @error 'Token #{$token} does not exist. Configured tokens are: #{map.keys($_tokens)}';
  }
  @if map.get($_tokens, $token) != null {
    $fallback: null;

    @if ($emit-fallback == true) {
      $fallback: map.get($_tokens, $token);
    }
    @else if ($emit-fallback) {
      $fallback: $emit-fallback;
    }

    $value: mdc-custom-properties.create('#{$_component-prefix}-#{$token}', $fallback: $fallback);
    @include mdc-theme.property($property, $value);
  }
}

// Returns the name of a token including the current prefix. Intended to be used in calculations
// involving tokens. `create-token-slot` should be used when outputting tokens.
@function get-token-variable($token) {
  @if $_component-prefix == null or $_tokens == null {
    @error '`get-token-variable` must be used within `use-tokens`';
  }
  @if not map.has-key($_tokens, $token) {
    @error 'Token #{$token} does not exist. Configured tokens are: #{map.keys($_tokens)}';
  }

  @return mdc-custom-properties.create-varname('#{$_component-prefix}-#{$token}');
}

// TODO(crisbeto): should be able to replace the usages of `get-token-variable` with this.
// Returns a `var()` reference to a specific token. Intended for declarations
// where the token has to be referenced as a part of a larger expression.
@function get-token-variable-reference($token, $emit-fallback: false) {
  @if $_component-prefix == null or $_tokens == null {
    @error '`get-token-variable-reference` must be used within `use-tokens`';
  }
  @if not map.has-key($_tokens, $token) {
    @error 'Token #{$token} does not exist. Configured tokens are: #{map.keys($_tokens)}';
  }

  $var: get-token-variable($token);
  $fallback: if($emit-fallback, map.get($_tokens, $token), null);

  @if ($fallback != null) {
    @return var($var, $fallback);
  }
  @else {
    @return var($var);
  }
}

@mixin create-token-values($prefix, $tokens) {
  @include _configure-token-prefix($prefix...) {
    @include mdc-keys.declare-custom-properties($tokens, $_component-prefix);
  }
}

/// Gets all the MDC token values for a specific component. This function serves as single
/// point at which we directly reference a specific version of the MDC tokens.
/// @param {String} $component Name of the component for which to get the tokens
/// @param {Map} $systems The MDC system tokens
/// @param {Boolean} $exclude-hardcoded Whether to exclude hardcoded token values
/// @return {List} Map of token names to values
@function get-mdc-tokens($component, $systems, $exclude-hardcoded) {
  $fn: meta.get-function($name: 'md-comp-' + $component + '-values', $module: 'mdc-tokens');
  @return meta.call($fn, $systems, $exclude-hardcoded);
}

// MDC doesn't currently handle elevation tokens properly. As a temporary workaround we can combine
// the elevation and shadow-color tokens into a full box-shadow and use it as the value for the
// elevation token.
@function resolve-elevation($tokens, $elevation-token, $shadow-color-token) {
  $elevation: map.get($tokens, $elevation-token);
  $shadow-color: map.get($tokens, $shadow-color-token);
  @return map.merge($tokens, (
    $elevation-token: mdc-elevation-theme.elevation-box-shadow($elevation, $shadow-color),
    $shadow-color-token: null,
  ));
}

/// Checks whether a list starts wih a given prefix
/// @param {List} $list The list value to check the prefix of.
/// @param {List} $prefix The prefix to check.
/// @return {Boolean} Whether the list starts with the prefix.
@function _is-prefix($list, $prefix) {
  @for $i from 1 through list.length($prefix) {
    @if list.nth($list, $i) != list.nth($prefix, $i) {
      @return false;
    }
  }
  @return true;
}

/// Gets the supported color variants in the given token set for the given prefix.
/// @param {Map} $tokens The full token map.
/// @param {List} $prefix The component prefix to get color variants for.
/// @return {List} The supported color variants.
@function _supported-color-variants($tokens, $prefix) {
  $result: ();
  @each $namespace in map.keys($tokens) {
    @if list.length($prefix) == list.length($namespace) - 1 and _is-prefix($namespace, $prefix) {
      $result: list.append($result, list.nth($namespace, list.length($namespace)), comma);
    }
  }
  @return $result;
}

/// Gets the token values for the given components prefix with the given options.
/// @param {Map} $tokens The full token map.
/// @param {List} $prefix The component prefix to get the token values for.
/// @param {ArgList} Any additional options
///   Currently the additional supported options are:
//     - $color-variant - The color variant to use for the component
//     - $emit-overrides-only - Whether to emit *only* the overrides for the
//                              specific color variant, or all color styles. Defaults to false.
/// @throws If given options are invalid
/// @return {Map} The token values for the requested component.
@function get-tokens-for($tokens, $prefix, $options...) {
  $options: sass-utils.validate-keyword-args($options, (color-variant, emit-overrides-only));
  @if $tokens == () {
    @return ();
  }
  $values: map.get($tokens, $prefix);
  $color-variant: map.get($options, color-variant);
  $emit-overrides-only: map.get($options, emit-overrides-only);
  @if $color-variant == null {
    @return $values;
  }
  $overrides: map.get($tokens, list.append($prefix, $color-variant));
  @if $overrides == null {
    $variants: _supported-color-variants($tokens, $prefix);
    $secondary-message: if($variants == (),
      'Mixin does not support color variants',
      'Supported color variants are: #{$variants}'
    );

    @error 'Invalid color variant: #{$color-variant}. #{$secondary-message}.';
  }
  @return if($emit-overrides-only, $overrides, map.merge($values, $overrides));
}

/// Emits new token values for the given token overrides.
/// Verifies that the overrides passed in are valid tokens.
/// New token values are emitted under the current selector or root.
@mixin batch-create-token-values($overrides: (), $token-maps...) {
  @include _validate-token-overrides($overrides, $token-maps);

  @each $token-map in $token-maps {
    $prefix: map.get($token-map, prefix);
    $tokens: map.get($token-map, tokens);

    @each $name, $value in $tokens {
      $tokens: map.set($tokens, $name, map.get($overrides, $name));
    }

    @include sass-utils.current-selector-or-root() {
      @include create-token-values($prefix, $tokens);
    }
  }
}

/// Gets the MDC tokens for the given prefix, M3 token values, and supported token slots.
/// @param {List} $prefix The token prefix for the given tokens.
/// @param {Map|(Map, Map)} $values A map of M3 token values for the given prefix.
///  This param may also be a tuple of maps, the first one representing the default M3 token values,
//   and the second containing overrides for different color variants.
//   Single map example:
//     (token1: green, token2: 2px)
//   Tuple example:
//     (
//       (token1: green, token2: 2px),
//       (
//         secondary: (token1: blue),
//         error: (token1: red),
//       )
//     )
/// @param {Map} $slots A map of token slots, with null value indicating the token is not supported.
/// @param {String|null} $variant The name of the variant the token values are for.
/// @return {Map} A map of fully qualified token names to values, for only the supported tokens.
@function namespace-tokens($prefix, $values, $slots, $variant: null) {
  $result: ();
  @if $variant == null and meta.type-of($values) == 'list' and list.length($values == 2) {
    $variants: list.nth($values, 2);
    $values: list.nth($values, 1);
    @each $variant, $overrides in $variants {
      $result: map.merge($result, namespace-tokens($prefix, $overrides, $slots, $variant));
    }
  }
  $used-token-names: map.keys(_filter-nulls(map.get($slots, $prefix)));
  $used-m3-tokens: _pick(_filter-nulls($values), $used-token-names);
  $prefix: if($variant == null, $prefix, list.append($prefix, $variant));
  @return map.merge($result, ($prefix: $used-m3-tokens));
}

/// Hardcode the given value, or null if hardcoded values are excluded.
@function hardcode($value, $exclude-hardcoded) {
  @return if($exclude-hardcoded, null, $value);
}

/// Sets all of the standard typography tokens for the given token base name to the given typography
/// level.
/// @param {Map} $systems The MDC system tokens
/// @param {String} $base-name The token base name to get the typography tokens for
/// @param {String} $typography-level The typography level to base the token values on
/// @return {Map} A map containing the typography tokens for the given base token name
@function generate-typography-tokens($systems, $base-name, $typography-level) {
  $result: ();
  @each $prop in (font, line-height, size, tracking, weight) {
    $result: map.set($result, #{$base-name}-#{$prop},
      map.get($systems, md-sys-typescale, #{$typography-level}-#{$prop}));
  }
  @return $result;
}

/// Maps the values in a map to new values using the given mapping function
/// @param {Map} $map The maps whose values will be mapped to new values.
/// @param {Function} $fn The value mapping function.
/// @param {Map} A new map with its values updated using the mapping function.
@function map-values($map, $fn) {
  $result: ();
  @each $key, $value in $map {
    $result: map.set($result, $key, meta.call($fn, $value));
  }
  @return $result;
}

/// Renames the keys in a map
/// @param {Map} $map The map whose keys should be renamed
/// @param {Map} $rename-keys A map of original key to renamed key to apply to $map
/// @return {Map} The result of applying the given key renames to the given map.
@function rename-map-keys($map, $rename-keys) {
  $result: $map;
  @each $old-key-name, $new-key-name in $rename-keys {
    @if map.has-key($map, $old-key-name) {
      $result: map.set($result, $new-key-name, map.get($map, $old-key-name));
    }
  }
  @return $result;
}

/// At the time of writing, some color tokens (e.g. disabled state) are defined as a solid color
/// token and a separate opacity token. This function applies the opacity to the color and drops the
/// opacity key from the map. Can be removed once b/213331407 is resolved.
/// @param {Map} $tokens The map of tokens currently being generated
/// @param {Map} $all-tokens A map of all tokens, including hardcoded values
/// @param {List} $pairs Pairs of color token names and their opacities. Should be in the shape of
/// `((color: 'color-key', opacity: 'opacity-key'))`.
/// @return {Map} The initial tokens with the combined color values.
@function combine-color-tokens($tokens, $opacity-lookup, $pairs) {
  $result: $tokens;

  @each $pair in $pairs {
    $color-key: map.get($pair, color);
    $opacity-key: map.get($pair, opacity);
    $color: map.get($tokens, $color-key);
    $opacity: map.get($opacity-lookup, $opacity-key);

    @if(meta.type-of($color) == 'color') {
      $result: map.remove($result, $opacity-key);
      $result: map.set($result, $color-key, rgba($color, $opacity));
    }
    @else if($color != null) {
      $result: map.remove($result, $opacity-key);
      $combined-color: #{color-mix(in srgb, #{$color} #{($opacity * 100) + '%'}, transparent)};
      $result: map.set($result, $color-key, $combined-color);
    }
  }

  @return $result;
}

/// Verifies that the token overrides exist and are used in one of the given token maps.
@mixin _validate-token-overrides($overrides: (), $token-maps) {
  $valid-token-names: ();

  @each $token-map in $token-maps {
    @each $name, $value in map.get($token-map, tokens) {
      @if ($value != null and list.index($valid-token-names, $name) == null) {
        $valid-token-names: list.append($valid-token-names, $name);
      }
    }
  }

  @each $name in map.keys($overrides) {
    @if (list.index($valid-token-names, $name) == null) {
      @error (
        'Invalid token: "' + $name + '"'
        'Valid tokens include: ' $valid-token-names
      );
    }
  }
}

/// Picks a submap containing only the given keys out the given map.
/// @param {Map} $map The map to pick from.
/// @param {List} $keys The map keys to pick.
/// @return {Map} A submap containing only the given keys.
@function _pick($map, $keys) {
  $result: ();
  @each $key in $keys {
    @if map.has-key($map, $key) {
      $result: map.set($result, $key, map.get($map, $key));
    }
  }
  @return $result;
}


/// Filters keys with a null value out of the map.
/// @param {Map} $map The map to filter.
/// @return {Map} The given map with all of the null keys filtered out.
@function _filter-nulls($map) {
  $result: ();
  @each $key, $val in $map {
    @if $val != null {
      $result: map.set($result, $key, $val);
    }
  }
  @return $result;
}
