/**
 * @license
 * Copyright 2023 Tyler Technologies, Inc. 
 * License: Apache-2.0
 */
@use 'sass:map';
@use '@material/theme/css';
@use '@material/theme/theme' as mdc-theme;
@use '@material/theme/theme-color' as mdc-theme-color;
@use '@material/theme/replace';
@use '@material/theme/keys' as mdc-keys;
@use '@material/elevation/elevation-theme' as elevation-theme;
@use './custom-properties';
@use './keys';
@use './theme-values';
@use './theme-utils';
@use 'sass:list';

/// Overrides the MDC theme key store with our own values to ensure that the fallback
/// values for the custom properties are using our theme
@include mdc-keys.register-theme(theme-values.$mdc-property-values);

@mixin theme-styles($prefix: forge) {
  @include mdc-theme-properties;
  @include forge-theme-properties($prefix);
  @include forge-z-index-properties($prefix);
  @include theme-utils.core-styles;
}

/// MDC theme property overrides
@mixin mdc-theme-properties() {
  :root {
    @each $style in map.keys(theme-values.$mdc-property-values) {
      --mdc-theme-#{$style}: #{map.get(theme-values.$mdc-property-values, $style)};
    }
  }
}

/// Tyler-specific theme properties
@mixin forge-theme-properties($prefix: forge) {
  :root {
    @each $style in map.keys(theme-values.$property-values) {
      --#{$prefix}-theme-#{$style}: #{map.get(theme-values.$property-values, $style)};
    }
  }
}

/// Tyler-specific z-index properties
@mixin forge-z-index-properties($prefix: forge) {
  :root {
    @each $type in map.keys(theme-values.$z-index-values) {
      --#{$prefix}-z-index-#{$type}: #{map.get(theme-values.$z-index-values, $type)};
    }
  }
}

@function text-emphasis($emphasis) {
  @return map.get(theme-values.$text-emphasis, $emphasis);
}

@function is-theme-key($style) {
  @return map.has-key(theme-values.$property-values, $style);
}

/// Applies a dynamic value to the specified property. This mixin should be used
/// in theme style mixins when setting properties.
@mixin _property($property, $value, $gss: (), $replace: null, $important: false) {
  @if custom-properties.is-custom-prop($value) {
    // $value is a custom property Map
    @include custom-properties.declaration($property, $value, $gss: $gss, $important: $important);
  } @else if keys.is-key($value) {
    // $value is a key String
    $custom-prop: keys.create-custom-property($value);

    @if is-theme-key($value) {
      // Determine if we need to use a compile-time updated value to support
      // Angular.
      $key: $value;
      // (changed: Bool, value: *)
      $result: deprecated-get-global-theme-key-value-if-changed($key);

      @if map.get($result, changed) {
        // $property-values was changed at compile time. Use the
        // global value instead. Otherwise if it was not changed, continue
        // using the key store normally.
        $custom-prop: keys.create-custom-property($key);
        $custom-prop: custom-properties.set-fallback($custom-prop, map.get($result, value));
      }
    }

    @include custom-properties.declaration($property, $custom-prop, $gss: $gss, $important: $important);
  } @else {
    // $value is a standard CSS value
    $fallback: null;
    @if $replace {
      // If any replacements are null, treat the entire value as null (do not
      // emit anything).
      @each $name, $replacement in $replace {
        @if $replacement == null {
          $value: null;
        }
      }
    }

    @if $replace and $value {
      @if meta.type-of($replace) != 'map' {
        @error 'forge-theme: Invalid replacement #{$replace}. Must be a Map.';
      }

      $replace-map-fallback: ();
      $replace-map-value: ();
      $needs-fallback: false;

      @each $name, $replacement in $replace {
        @if custom-properties.is-custom-prop($replacement) {
          $replace-value: custom-properties.get-declaration-value($replacement);
          $replace-fallback: custom-properties.get-declaration-fallback($replacement);

          @if $replace-fallback {
            $needs-fallback: true;
          }

          $replace-map-value: map.set($replace-map-value, $name, $replace-value);
          $replace-map-fallback: map.set($replace-map-fallback, $name, $replace-fallback);
        } @else {
          $replace-map-value: map.set($replace-map-value, $name, $replacement);
          $replace-map-fallback: map.set($replace-map-fallback, $name, $replacement);
        }
      }

      @if meta.type-of($value) == 'string' {
        @if $needs-fallback {
          $fallback: replace.replace-string($value, $replace-map-fallback);
        }
        $value: replace.replace-string($value, $replace-map-value);
      } @else if meta.type-of($value) == 'list' {
        @if $needs-fallback {
          $fallback: replace.replace-list($value, $replace-map-fallback);
        }
        $value: replace.replace-list($value, $replace-map-value);
      } @else {
        @error 'forge-theme: Invalid replacement value #{$value}. $replace may only be used with string or list values.';
      }
    }

    @include css.declaration($property, $value, $fallback-value: $fallback, $gss: $gss, $important: $important);
  }
}

/// Checks if the global $forge-theme-property-values was dynamically changed at
/// compile time. Typically, $property-values is configured once, but a Sass
/// hack allows the variable to be changed multiple times and effectively
/// support dynamic values.
@function deprecated-get-global-theme-key-value-if-changed($key) {
  $current-global-value: map.get(theme-values.$property-values, $key);
  $original-property-values: theme-values.original-property-values();
  $configured-global-value: map.get($original-property-values, $key);
  @if $current-global-value != $configured-global-value {
    @return (value: $current-global-value, changed: true);
  }
  @return (changed: false);
}

/// Consolidates the MDC and Forge theming systems by conditionally checking for existing theme keys
/// and fetching the value from either system based on existence.
@mixin property($property, $value, $gss: (), $replace: null, $important: false) {
  @if map.has-key(theme-values.$property-values, $value) {
    @include _property($property, $value);
  } @else {
    @include mdc-theme.property($property, $value);
  }
}

/// Creates a z-index style from a prefined z-index map using a CSS custom property (with a fallback value);
@mixin z-index($type, $customPropName: null, $prefix: forge) {
  @if not map.has-key(theme-values.$z-index-values, $type) {
    @error "Invalid type: '#{$type}'. Choose one of: #{map.keys($z-index-values)}";
  }

  $value: map.get(theme-values.$z-index-values, $type);

  @if $customPropName == null {
    $customPropName: --#{$prefix}-z-index-#{$type};
  }

  @include css-custom-property(z-index, $customPropName, $value);
}

/// Allow for setting a custom css property with a fallback property value
@mixin css-custom-property($property, $varname, $fallback-value: null, $important: false) {
  @include custom-properties.declaration($property, (
    varname: $varname,
    fallback: $fallback-value
  ), $important: $important);
}

@mixin scrollbar() {
  &::-webkit-scrollbar {
    @include css-custom-property(width, --forge-scrollbar-width, theme-values.$scrollbar-width);
    @include css-custom-property(height, --forge-scrollbar-height, theme-values.$scrollbar-height);
  }
  
  &::-webkit-scrollbar-corner {
    @include property(background-color, scrollbar-track);
  }

  &::-webkit-scrollbar-track {
    @include property(background-color, scrollbar-track);
    
    &:hover {
      @include property(background-color, scrollbar-track-hover);
    }
  }

  &::-webkit-scrollbar-thumb {
    @include property(background-color, scrollbar-thumb);
    @include css-custom-property(height, --forge-scrollbar-min-height, theme-values.$scrollbar-thumb-min-height);
    @include css-custom-property(width, --forge-scrollbar-min-width, theme-values.$scrollbar-thumb-min-width);
    @include css-custom-property(border-radius, --forge-scrollbar-border-radius, 10px);
    @include css-custom-property(border-width, --forge-scrollbar-border-width, 3px);

    border-style: solid;
    border-color: transparent;
    background-clip: content-box;

    &:hover {
      @include property(background-color, scrollbar-thumb-hover);
    }
  }
}

@mixin scrollbar-theme-styles() {
  * {
    @include scrollbar;
  }
}

@function elevation($z-value, $color: elevation-theme.$baseline-color, $opacity-boost: 0) {
  @if type-of($z-value) != number or not unitless($z-value) {
    @error '$z-value must be a unitless number, but received "#{$z-value}"';
  }

  @if $z-value < 0 or $z-value > 24 {
    @error '$z-value must be between 0 and 24, but received "#{$z-value}"';
  }

  $color: mdc-theme-color.prop-value($color);

  $umbra-z-value: map.get(elevation-theme.$umbra-map, $z-value);
  $penumbra-z-value: map.get(elevation-theme.$penumbra-map, $z-value);
  $ambient-z-value: map.get(elevation-theme.$ambient-map, $z-value);

  $umbra-color: rgba($color, elevation-theme.$umbra-opacity + $opacity-boost);
  $penumbra-color: rgba($color, elevation-theme.$penumbra-opacity + $opacity-boost);
  $ambient-color: rgba($color, elevation-theme.$ambient-opacity + $opacity-boost);

  @return #{$umbra-z-value} #{$umbra-color}, #{$penumbra-z-value} #{$penumbra-color}, #{$ambient-z-value} #{$ambient-color};
}

@mixin shadow($z-value, $color: elevation-theme.$baseline-color, $opacity-boost: 0) {
  box-shadow: elevation($z-value, $color, $opacity-boost);
}


/// Validates keys in provided theme configuration by comparing with original
/// theme configuration.
/// Use this in component's `theme()` mixin implementation to validate if
/// provided theme configuration has supported theme configuration keys.
/// @param {Map} $origin-theme - Original theme configuration in Sass map format
///     that has all supported keys.
/// @param {Map} $custom-theme - Provided theme configuration in Sass map format
///     that should be validated against `$origin-theme`.
/// @examples
///     @mixin theme($theme) {
///       @include theme.validate-theme($light-theme, $theme);
///
///       // ...
///     }
@mixin validate-theme($origin-theme, $custom-theme, $test-only: false) {
  $origin-keys: map.keys($origin-theme);
  $unsupported-keys: ();
  $unsupported-custom-prop-keys: ();

  @each $key, $value in $custom-theme {
    @if (not list.index($origin-keys, $key)) {
      $unsupported-keys: list.append(
        $unsupported-keys,
        $key,
        $separator: comma
      );
    }

    $is-prop: custom-properties.is-custom-prop($value) or
      custom-properties.is-custom-prop-string($value);
    @if $is-prop {
      $unsupported-custom-prop-keys: list.append(
        $unsupported-custom-prop-keys,
        $key,
        $separator: comma
      );
    }
  }

  @if list.length($unsupported-keys) > 0 {
    $error-message: 'Unsupported keys found: #{$unsupported-keys}. Expected one of: #{$origin-keys}.';

    @if $test-only {
      content: $error-message;
    } @else {
      @error $error-message;
    }
  }

  @if list.length($unsupported-custom-prop-keys) > 0 {
    $error-message: 'Custom properties are not supported for theme map keys: #{$unsupported-custom-prop-keys}';

    @if $test-only {
      content: $error-message;
    } @else {
      @error $error-message;
    }
  }
}

/// Generates CSS custom property definitions based on theme value overrides provided.
/// @param {Map} $theme - Theme configuration in Sass map format that has all supported keys.
/// @examples
///     :root {
///       @include theme.provide-theme((
///         primary: #ff0000,
///         secondary: #00ff00
///       ));
///     }
@mixin provide-theme($theme, $auto: false, $prefix: forge) {
  @each $style, $value in $theme {
    @if map.has-key(theme-values.$mdc-property-values, $style) {
      --mdc-theme-#{$style}: #{$value};
    } @else if map.has-key(theme-values.$property-values, $style) {
      --#{$prefix}-theme-#{$style}: #{$value};
    } @else {
      @error 'Invalid theme property provided: #{$style}';
    }
  }
}

@mixin theme-properties($component, $theme, $default-theme, $prefix: forge) {
  @each $style, $value in $theme {
    @if map.has-key($default-theme, $style) {
      --#{$prefix}-#{$component}-theme-#{$style}: #{$value};
    } @else {
      @include provide-theme(($style: $value));
    }
  }
}
