@use 'sass:list';
@use 'sass:map';
@use 'sass:math';
@use 'sass:meta';
@use 'sass:color';

// Whether to enable compatibility with legacy methods for accessing theme information.
$theme-legacy-inspection-api-compatibility: true !default;

// Whether duplication warnings should be disabled. Warnings enabled by default.
$theme-ignore-duplication-warnings: false !default;

// Whether density should be generated by default.
$_generate-default-density: true !default;

// Warning that will be printed if duplicated styles are generated by a theme.
$_duplicate-warning: 'Read more about how style duplication can be avoided in a dedicated ' +
  'guide. https://github.com/angular/components/blob/main/guides/duplicate-theming-styles.md';

// Warning that will be printed if the legacy theming API is used.
$private-legacy-theme-warning: 'Angular Material themes should be created from a map containing ' +
  'the keys "color", "typography", and "density". The color value should be a map containing the ' +
  'palette values for "primary", "accent", and "warn". ' +
  'See https://material.angular.io/guide/theming for more information.';

// Flag whether to disable theme definitions copying color values to the top-level theme config.
// This copy is to preserve backwards compatibility.
$_disable-color-backwards-compatibility: false;

// These variable are not intended to be overridden externally. They use `!default` to
// avoid being reset every time this file is imported.
$_emitted-color: () !default;
$_emitted-typography: () !default;
$_emitted-density: () !default;
$_emitted-base: () !default;

//
// Private APIs
//

$private-internal-name: _mat-theming-internals-do-not-access;

// Checks if configurations that have been declared in the given theme have been generated
// before. If so, warnings will be reported. This should notify developers in case duplicate
// styles are accidentally generated due to wrong usage of the all-theme mixins.
//
// Additionally, this mixin controls the default value for the density configuration. By
// default, density styles are generated at scale zero. If the same density styles would be
// generated a second time though, the default value will change to avoid duplicate styles.
//
// The mixin keeps track of all configurations in a list that is scoped to the specified
// id. This is necessary because a given theme can be passed to multiple disjoint theme mixins
// (e.g. `all-component-themes` and `all-legacy-component-themes`) without causing any
// style duplication.
@mixin private-check-duplicate-theme-styles($theme-or-color-config, $id) {
  // TODO(mmalerba): use get-theme-version for this check when its moved out of experimental.
  @if map.get($theme-or-color-config, $private-internal-name, theme-version) == 1 {
    @include _check-duplicate-theme-styles-v1($theme-or-color-config, $id) {
      // Optionally, consumers of this mixin can wrap contents inside so that nested
      // duplicate style checks do not report another warning. e.g. if developers include
      // the `all-component-themes` mixin twice, only the top-level duplicate styles check
      // should report a warning. Not all individual components should report a warning too.
      $orig-mat-theme-ignore-duplication-warnings: $theme-ignore-duplication-warnings;
      $theme-ignore-duplication-warnings: true !global;
      @content;
      $theme-ignore-duplication-warnings: $orig-mat-theme-ignore-duplication-warnings !global;
    }
  }
  @else {
    @include _check-duplicate-theme-styles-v0($theme-or-color-config, $id) {
      // Optionally, consumers of this mixin can wrap contents inside so that nested
      // duplicate style checks do not report another warning. e.g. if developers include
      // the `all-component-themes` mixin twice, only the top-level duplicate styles check
      // should report a warning. Not all individual components should report a warning too.
      $orig-mat-theme-ignore-duplication-warnings: $theme-ignore-duplication-warnings;
      $theme-ignore-duplication-warnings: true !global;
      @content;
      $theme-ignore-duplication-warnings: $orig-mat-theme-ignore-duplication-warnings !global;
    }
  }
}

/// Strip out any settings map entries that have empty values (null or ()).
@function _strip-empty-settings($settings) {
  $result: ();
  @each $key, $value in $settings {
    @if $value != null and $value != () {
      $result: map.set($result, $key, $value);
    }
  }
  @return if($result == (), null, $result);
}

// Checks for duplicate styles in a `theme-version: 1` style theme.
@mixin _check-duplicate-theme-styles-v1($theme-or-color-config, $id) {
  $color-settings: _strip-empty-settings((
    theme-type: map.get($theme-or-color-config, $private-internal-name, theme-type),
    color-tokens: map.get($theme-or-color-config, $private-internal-name, color-tokens),
  ));
  $typography-settings: _strip-empty-settings((
    typography-tokens:
      map.get($theme-or-color-config, $private-internal-name, typography-tokens),
  ));
  $density-settings: _strip-empty-settings((
    density-scale:
      map.get($theme-or-color-config, $private-internal-name, density-scale),
    density-tokens:
      map.get($theme-or-color-config, $private-internal-name, density-tokens),
  ));
  $base-settings: _strip-empty-settings((
    base-tokens: map.get($theme-or-color-config, $private-internal-name, base-tokens),
  ));
  $previous-color-settings: map.get($_emitted-color, $id) or ();
  $previous-typography-settings: map.get($_emitted-typography, $id) or ();
  $previous-density-settings: map.get($_emitted-density, $id) or ();
  $previous-base-settings: map.get($_emitted-base, $id) or ();

  // Check if the color configuration has been generated before.
  @if $color-settings != null {
    @if list.index($previous-color-settings, $color-settings) != null and
        not $theme-ignore-duplication-warnings {
      @warn 'The same color styles are generated multiple times. ' + $_duplicate-warning;
    }
    $previous-color-settings: list.append($previous-color-settings, $color-settings);
  }

  // Check if the typography configuration has been generated before.
  @if $typography-settings != null {
    @if list.index($previous-typography-settings, $typography-settings) != null and
        not $theme-ignore-duplication-warnings {
      @warn 'The same typography styles are generated multiple times. ' + $_duplicate-warning;
    }
    $previous-typography-settings: list.append($previous-typography-settings, $typography-settings);
  }

  // Check if the density configuration has been generated before.
  @if $density-settings != null {
    @if list.index($previous-density-settings, $density-settings) != null and
        not $theme-ignore-duplication-warnings {
      @warn 'The same density styles are generated multiple times. ' + $_duplicate-warning;
    }
    $previous-density-settings: list.append($previous-density-settings, $density-settings);
  }

  // Check if the base configuration has been generated before.
  @if $base-settings != null {
    @if list.index($previous-base-settings, $base-settings) != null and
        not $theme-ignore-duplication-warnings {
      @warn 'The same base theme styles are generated multiple times. ' + $_duplicate-warning;
    }
    $previous-base-settings: list.append($previous-base-settings, $base-settings);
  }

  $_emitted-color: map.set($_emitted-color, $id, $previous-color-settings) !global;
  $_emitted-density: map.set($_emitted-density, $id, $previous-density-settings) !global;
  $_emitted-typography: map.set($_emitted-typography, $id, $previous-typography-settings) !global;
  $_emitted-base: map.set($_emitted-base, $id, $previous-base-settings) !global;

  @content;
}

// Checks for duplicate styles in a `theme-version: 0` style theme.
@mixin _check-duplicate-theme-styles-v0($theme-or-color-config, $id) {
  $theme: private-legacy-get-theme($theme-or-color-config);
  $color-config: map.get($theme, $private-internal-name, m2-config, color) or
    private-get-color-config($theme);
  $density-config: map.get($theme, $private-internal-name, m2-config, density) or
    private-get-density-config($theme);
  $typography-config: map.get($theme, $private-internal-name, m2-config, typography) or
    private-get-typography-config($theme);
  // Lists of previous `color`, `density` and `typography` configurations.
  $previous-color: map.get($_emitted-color, $id) or ();
  $previous-typography: map.get($_emitted-typography, $id) or ();
  $previous-density: map.get($_emitted-density, $id) or ();
  // Whether duplicate legacy density styles would be generated.
  $duplicate-legacy-density: false;

  // Check if the color configuration has been generated before.
  @if $color-config != null {
    @if list.index($previous-color, $color-config) != null and
        not $theme-ignore-duplication-warnings {
      @warn 'The same color styles are generated multiple times. ' + $_duplicate-warning;
    }
    $previous-color: list.append($previous-color, $color-config);
  }

  // Check if the typography configuration has been generated before.
  @if $typography-config != null {
    @if list.index($previous-typography, $typography-config) != null and
        not $theme-ignore-duplication-warnings {
      @warn 'The same typography styles are generated multiple times. ' + $_duplicate-warning;
    }
    $previous-typography: list.append($previous-typography, $typography-config);
  }

  // Check if the density configuration has been generated before.
  @if $density-config != null {
    @if list.index($previous-density, $density-config) != null {
      // Only report a warning if density styles would be duplicated for non-legacy theme
      // definitions. For legacy themes, we have compatibility logic that avoids duplication
      // of default density styles. We don't want to report a warning in those cases.
      @if private-is-legacy-constructed-theme($theme) {
        $duplicate-legacy-density: true;
      }
      @else if not $theme-ignore-duplication-warnings {
        @warn 'The same density styles are generated multiple times. ' + $_duplicate-warning;
      }
    }
    $previous-density: list.append($previous-density, $density-config);
  }

  $_emitted-color: map.merge($_emitted-color, ($id: $previous-color)) !global;
  $_emitted-density: map.merge($_emitted-density, ($id: $previous-density)) !global;
  $_emitted-typography: map.merge($_emitted-typography, ($id: $previous-typography)) !global;

  @content;
}

// Checks whether the given value resolves to a theme object. Theme objects are always
// of type `map` and can optionally only specify `color`, `density` or `typography`.
@function private-is-theme-object($value) {
  @return meta.type-of($value) == 'map' and (
    map.has-key($value, color) or
    map.has-key($value, density) or
    map.has-key($value, typography) or
    list.length($value) == 0
  );
}

// Checks whether a given value corresponds to a legacy constructed theme.
@function private-is-legacy-constructed-theme($value) {
  @return meta.type-of($value) == 'map' and map.get($value, '_is-legacy-theme');
}

// This is the implementation of the `m2-get-color-config` function.
// It's declared here to avoid a circular reference between this file and `m2/_theming.scss`.
@function private-get-color-config($theme, $default: null) {
  // If a configuration has been passed, return the config directly.
  @if not private-is-theme-object($theme) {
    @return $theme;
  }
  // If the theme has been constructed through the legacy theming API, we use the theme object
  // as color configuration instead of the dedicated `color` property. We do this because for
  // backwards compatibility, we copied the color configuration from `$theme.color` to `$theme`.
  // Hence developers could customize the colors at top-level and want to respect these changes
  // TODO: Remove when legacy theming API is removed.
  @if private-is-legacy-constructed-theme($theme) {
    @return $theme;
  }
  @if map.has-key($theme, color) {
    @return map.get($theme, color);
  }
  @return $default;
}

// This is the implementation of the `m2-get-density-config` function.
// It's declared here to avoid a circular reference between this file and `m2/_theming.scss`.
@function private-get-density-config($theme-or-config, $default: 0) {
  // If a configuration has been passed, return the config directly.
  @if not private-is-theme-object($theme-or-config) {
    @return $theme-or-config;
  }
  // In case a theme has been passed, extract the configuration if present,
  // or fall back to the default density config.
  @if map.has-key($theme-or-config, density) {
    @return map.get($theme-or-config, density);
  }
  @return $default;
}

// This is the implementation of the `m2-get-typography-config` function.
// It's declared here to avoid a circular reference between this file and `m2/_theming.scss`.
@function private-get-typography-config($theme-or-config, $default: null) {
  // If a configuration has been passed, return the config directly.
  @if not private-is-theme-object($theme-or-config) {
    @return $theme-or-config;
  }
  // In case a theme has been passed, extract the configuration if present,
  // or fall back to the default typography config.
  @if (map.has-key($theme-or-config, typography)) {
    @return map.get($theme-or-config, typography);
  }
  @return $default;
}

// Creates a backwards compatible theme. Previously in Angular Material, theme objects
// contained the color configuration directly. With the recent refactoring of the theming
// system to allow for density and typography configurations, this is no longer the case.
// To ensure that constructed themes which will be passed to custom theme mixins do not break,
// we copy the color configuration and put its properties at the top-level of the theme object.
// Here is an example of a pattern that should still work until it's officially marked as a
// breaking change:
//
//    @mixin my-custom-component-theme($theme) {
//      .my-comp {
//        background-color: mat.m2-get-color-from-palette(map.get($theme, primary));
//      }
//    }
//
// Note that the `$theme.primary` key does usually not exist since the color configuration
// is stored in `$theme.color` which contains a property for `primary`. This method copies
// the map from `$theme.color` to `$theme` for backwards compatibility.
@function private-create-backwards-compatibility-theme($theme) {
  @if ($_disable-color-backwards-compatibility or not map.get($theme, color)) {
    @return $theme;
  }
  $color: map.get($theme, color);
  @return map.merge($theme, $color);
}

// Gets the theme from the given value that is either already a theme, or a color configuration.
// This handles the legacy case where developers pass a color configuration directly to the
// theme mixin. Before we introduced the new pattern for constructing a theme, developers passed
// the color configuration directly to the theme mixins. This can be still the case if developers
// construct a theme manually and pass it to a theme. We support this for backwards compatibility.
// TODO(devversion): remove this in the future. Constructing themes manually is rare,
// and the code can be easily updated to the new API.
@function private-legacy-get-theme($theme-or-color-config) {
  @if private-is-theme-object($theme-or-color-config) or
      map.get($theme-or-color-config, $private-internal-name, theme-version) == 1 {
    @return $theme-or-color-config;
  }

  @warn $private-legacy-theme-warning;
  @return private-create-backwards-compatibility-theme((
    _is-legacy-theme: true,
    color: $theme-or-color-config
  ));
}

// Approximates an rgba color into a solid hex color, given a background color.
@function private-rgba-to-hex($color, $background-color) {
  // We convert the rgba color into a solid one by taking the opacity from the rgba
  // value and using it to determine the percentage of the background to put
  // into foreground when mixing the colors together.
  @return color.mix($background-color, rgba($color, 1), (1 - color.opacity($color)) * 100%);
}

// Clamps the density scale to a number between the given min and max.
// 'minimum' and 'maximum' are converted to the given min or max number respectively.
@function clamp-density($density-scale, $min, $max: 0) {
  @if $density-scale == minimum {
    @return $min;
  }
  @if $density-scale == maximum {
    @return $max;
  }
  @if meta.type-of($density-scale) != 'number' or not math.is-unitless($density-scale) {
    @return 0;
  }
  @if $density-scale < $min {
    @return $min;
  }
  @if $density-scale > $max {
    @return $max;
  }
  @return $density-scale;
}
