@use 'sass:list';
@use 'sass:map';
@use '../style/validation';
@use './m2-inspection';

$internals: _mat-theming-internals-do-not-access;

$_m3-typescales: (
  display-large,
  display-medium,
  display-small,
  headline-large,
  headline-medium,
  headline-small,
  title-large,
  title-medium,
  title-small,
  label-large,
  label-medium,
  label-small,
  body-large,
  body-medium,
  body-small,
);

$_typography-properties: (font, font-family, line-height, font-size, letter-spacing, font-weight);

/// Validates that the given value is a versioned theme object.
/// @param {Any} $theme The theme object to validate.
/// @return {Boolean|Null} true if the theme has errors, else null.
@function _validate-theme-object($theme) {
  $err: validation.validate-type($theme, 'map') or
        map.get($theme, $internals, theme-version) == null;
  @return if($err, true, null);
}

/// Gets the version number of a theme object. A theme that is not a valid versioned theme object is
/// considered to be version 0.
/// @param {Map} $theme The theme to check the version of
/// @return {Number} The version number of the theme (0 if unknown).
@function get-theme-version($theme) {
  $err: _validate-theme-object($theme);
  @return if($err, 0, map.get($theme, $internals, theme-version) or 0);
}

/// Gets the type of theme represented by a theme object (light or dark).
/// @param {Map} $theme The theme
/// @return {String} The type of theme (either `light` or `dark`).
@function get-theme-type($theme) {
  $version: get-theme-version($theme);
  @if $version == 0 {
    @return m2-inspection.get-theme-type($theme);
  }
  @else if $version == 1 {
    @if not theme-has($theme, color) {
      @error 'Color information is not available on this theme.';
    }
    @return map.get($theme, $internals, theme-type) or light;
  }
  @else {
    @error #{'Unrecognized theme version:'} $version;
  }
}



/// Gets a color from a theme object. This function take a different amount of arguments depending
/// on if it's working with an M2 or M3 theme:
/// - With an M3 theme it accepts either 2 or 3 arguments. If 2 arguments are passed, the second
/// argument is treated as the name of a color role. If 3 arguments are passed, the second argument
/// is treated as the name of a color palette (primary, secondary, etc.) and the third is treated
/// as the palette hue (10, 50, etc.).
/// - With an M2 theme theme it accepts between 2 and 4 arguments, or the equivalent of calling
/// the `m2-get-theme-color` function. The first argument is the theme, the second one is the
/// palette from which to extract the color, the third one is the hue within the palette and the
/// fourth is the opacity of the returned color.
/// the second one is the
/// @param {Map} $theme The theme
/// @param {String} $color-role-or-palette-name The name of the color role to get, or the name of a
///   color palette.
/// @param {Number} $hue The palette hue to get (passing this argument means the second argument is
///   interpreted as a palette name).
/// @return {Color} The requested theme color.
@function get-theme-color($theme, $args...) {
  $version: get-theme-version($theme);
  $args-count: list.length($args) + 1;

  // M2 theme
  @if $version == 0 {
    @if $args-count < 2 or $args-count > 4 {
      @error 'Expected between 2 and 4 arguments when working with an M2 theme. ' +
             'Got: #{$args-count}';
    }
    @return m2-inspection.get-theme-color($theme, $args...);
  }

  // M3 theme
  @if $version == 1 {
    @if $args-count < 2 or $args-count > 3 {
      @error 'Expected either 2 or 3 arguments when working with an M3 theme. Got: #{$args-count}';
    }
    @return if($args-count == 2,
      _get-theme-role-color($theme, $args...),
      _get-theme-palette-color($theme, $args...)
    );
  }

  @error 'Unrecognized theme version: #{$version}';
}

/// Gets a role color from a theme object.
/// @param {Map} $theme The theme
/// @param {String} $color-role-name The name of the color role to get.
/// @return {Color} The requested role color.
@function _get-theme-role-color($theme, $color-role-name) {
  $err: _validate-theme-object($theme);
  @if $err {
    // TODO(mmalerba): implement for old style theme objects.
    @error #{'get-theme-color does not support legacy theme objects.'};
  }
  @if not theme-has($theme, color) {
    @error 'Color information is not available on this theme.';
  }
  $color-roles: map.get($theme, $internals, color-tokens, (mat, theme));
  $result: map.get($color-roles, $color-role-name);
  @if not $result {
    @error #{'Valid color roles are: #{map.keys($color-roles)}. Got:'} $color-role-name;
  }
  @return $result;
}

/// Gets a palette color from a theme object.
/// @param {Map} $theme The theme
/// @param {String} $palette-name The name of the palette to get the color from.
/// @param {Number} $hue The hue to read from the palette.
/// @return {Color} The requested palette color.
@function _get-theme-palette-color($theme, $palette-name, $hue) {
  $err: _validate-theme-object($theme);
  @if $err {
    // TODO(mmalerba): implement for old style theme objects.
    @error #{'get-theme-color does not support legacy theme objects.'};
  }
  @if not theme-has($theme, color) {
    @error 'Color information is not available on this theme.';
  }
  $palettes: map.get($theme, $internals, palettes);
  $palette: map.get($palettes, $palette-name);
  @if not $palette {
    $supported-palettes: map.keys($palettes);
    @error #{'Valid palettes are: #{$supported-palettes}. Got:'} $palette-name;
  }
  $result: map.get($palette, $hue);
  @if not $result {
    $supported-hues: map.keys($palette);
    @error #{'Valid hues for'} $palette-name #{'are: #{$supported-hues}. Got:'} $hue;
  }
  @return $result;
}

/// Gets a typography value from a theme object.
/// @param {Map} $theme The theme
/// @param {String} $typescale The typescale name.
/// @param {String} $property The CSS font property to get
///   (font, font-family, font-size, font-weight, line-height, or letter-spacing).
/// @return {*} The value of the requested font property.
@function get-theme-typography($theme, $typescale, $property: font) {
  $version: get-theme-version($theme);
  @if $version == 0 {
    @return m2-inspection.get-theme-typography($theme, $typescale, $property);
  }
  @else if $version == 1 {
    @if not theme-has($theme, typography) {
      @error 'Typography information is not available on this theme.';
    }
    @if not list.index($_m3-typescales, $typescale) {
      @error #{'Valid typescales are: #{$_m3-typescales}. Got:'} $typescale;
    }
    @if not list.index($_typography-properties, $property) {
      @error #{'Valid typography properties are: #{$_typography-properties}. Got:'} $property;
    }
    $property-key: map.get((
      font: '',
      font-family: '-font',
      line-height: '-line-height',
      font-size: '-size',
      letter-spacing: '-tracking',
      font-weight: '-weight'
    ), $property);
    $token-name: '#{$typescale}#{$property-key}';
    @return map.get($theme, $internals, typography-tokens, (mat, typography), $token-name);
  }
  @else {
    @error #{'Unrecognized theme version:'} $version;
  }
}

/// Gets the density scale from a theme object.
/// @param {Map} $theme The theme
/// @return {Number} The density scale.
@function get-theme-density($theme) {
  $version: get-theme-version($theme);
  @if $version == 0 {
    @return m2-inspection.get-theme-density($theme);
  }
  @else if $version == 1 {
    @if not theme-has($theme, density) {
      @error 'Density information is not available on this theme.';
    }
    @return map.get($theme, $internals, density-scale);
  }
  @else {
    @error #{'Unrecognized theme version:'} $version;
  }
}

/// Checks whether the theme has information about given theming system.
/// @param {Map} $theme The theme
/// @param {String} $system The system to check
/// @param {Boolean} Whether the theme has information about the system.
@function theme-has($theme, $system) {
  $version: get-theme-version($theme);
  @if $version == 0 {
    @return m2-inspection.theme-has($theme, $system);
  }
  @else if $version == 1 {
    @if $system == base {
      @return map.get($theme, $internals, base-tokens) != null;
    }
    @if $system == color {
      @return map.get($theme, $internals, color-tokens) != null and
        map.get($theme, $internals, theme-type) != null and
        map.get($theme, $internals, palettes) != null;
    }
    @if $system == typography {
      @return map.get($theme, $internals, typography-tokens) != null;
    }
    @if $system == density {
      @return map.get($theme, $internals, density-scale) != null;
    }
    @error 'Valid systems are: base, color, typography, density. Got:' $system;
  }
  @else {
    @error #{'Unrecognized theme version:'} $version;
  }
}

/// Removes the information about the given theming system(s) from the given theme.
/// @param {Map} $theme The theme to remove information from
/// @param {String...} $systems The systems to remove
/// @return {Map} A version of the theme without the removed theming systems.
@function theme-remove($theme, $systems...) {
  $err: validation.validate-allowed-values($systems, color, typography, density, base);
  @if $err {
    @error #{'Expected $systems to contain valid system names (color, typography, density, or'}
    #{'base). Got invalid system names:'} $err;
  }
  $version: get-theme-version($theme);
  @if $version == 0 {
    @return m2-inspection.theme-remove($theme, $systems...);
  }
  @else if $version == 1 {
    @each $system in $systems {
      @if $system == base {
        $theme: map.deep-remove($theme, $internals, base-tokens);
      }
      @else if $system == color {
        $theme: map.deep-remove($theme, $internals, color-tokens);
        $theme: map.deep-remove($theme, $internals, theme-type);
        $theme: map.deep-remove($theme, $internals, palettes);
      }
      @else if $system == typography {
        $theme: map.deep-remove($theme, $internals, typography-tokens);
      }
      @else if $system == density {
        $theme: map.deep-remove($theme, $internals, density-scale);
        $theme: map.deep-remove($theme, $internals, density-tokens);
      }
    }
    @return $theme;
  }
}
