@use 'sass:map';
@use 'sass:meta';
@use 'sass:math';
@use 'sass:list';
@use 'sass:color';
@use 'sass:string';
@use 'charts';
@use 'multipliers';
@use 'types';
@use '../utils/math' as *;

////
/// @package theming
////

$_enhanced-accessibility: false;

/// Configures the color module.
/// @access public
/// @group color
/// @param {Boolean} $enhanced-accessibility [null] - Enables features like color blind palettes.
/// @example scss
///   .enhanced-accessibility {
///      @include configure-colors(true);
///    }
@mixin configure-colors($enhanced-accessibility: null) {
    @if $enhanced-accessibility {
        $_enhanced-accessibility: $enhanced-accessibility !global;
    }
}

/// Generates a color palette.
/// @access public
/// @group Palettes
/// @param {Color} $primary - The primary color used to generate the primary color palette (required).
/// @param {Color} $secondary - The secondary color used to generate the secondary color palette (required).
/// @param {Color} $surface - The color used as a background in components (required).
/// @param {Color} $gray [null] - The color used for generating the grayscale palette (optional).
/// @param {Color} $info [#1377d5] - The information color used throughout the application (optional).
/// @param {Color} $success [#4eb862] - The success color used throughout the application (optional).
/// @param {Color} $warn [#faa419] - The warning color used throughout the application (optional).
/// @param {Color} $error [#ff134a] - The error color used throughout the application (optional).
/// @param {String} $variant [null] - Used internally (optional).
/// @requires {function} shades
/// @returns {Map} - A map consisting of color shades for the passed base colors (primary, secondary, gray, etc).
/// @example scss
///   // $primary, $secondary, $surface are required parameters.
///   $my-palette: palette($primary: #09f,$secondary #222, $surface: #fff);
@function palette(
    $primary,
    $secondary,
    $surface,
    $gray: null,
    $info: #1377d5,
    $success: #4eb862,
    $warn: #faa419,
    $error: #ff134a,
    $variant: null
) {
    $color-shades: types.$IColorShades;
    $gray-shades: types.$IGrayShades;
    $primary-palette: shades('primary', $primary, $color-shades);
    $secondary-palette: shades('secondary', $secondary, $color-shades);
    $surface-palette: shades('surface', $surface, $color-shades);
    $grayscale-palette: shades('gray', $gray, $gray-shades, $surface);
    $info-palette: if($info, shades('info', $info, $color-shades), ());
    $success-palette: if($success, shades('success', $success, $color-shades), ());
    $warn-palette: if($warn, shades('warn', $warn, $color-shades), ());
    $error-palette: if($success, shades('error', $error, $color-shades), ());

    @return (
        'primary': $primary-palette,
        'secondary': $secondary-palette,
        'gray': $grayscale-palette,
        'surface': $surface-palette,
        'info': $info-palette,
        'success': $success-palette,
        'warn': $warn-palette,
        'error': $error-palette,
        '_meta': (
            'variant': $variant,
        )
    );
}

/// Generates a Material-like color palette from a base color.
/// @access public
/// @group Palettes
/// @param {String} $name - The name of the color.
/// @param {Color} $color - The base color used to generate the palette.
/// @param {List} $shades - The list of shades.
/// @param {Color} $surface [null] - The surface color. Useful when generating shades of gray (optional).
/// @requires {function} shade
/// @requires {function} text-contrast
/// @returns {Map} - A map consisting of hsla color shades and their respective contrast colors.
/// @example scss
///   $special-color-shades: shades('accent', #09f, (50,100,200,300,400,500,600,700,800,900))
@function shades($name, $color, $shades, $surface: null) {
    $result: ();

    @each $variant in $shades {
        $shade: shade($name, $color, $variant, $surface);
        $result: map.merge(
            $result,
            (
                $variant: map.get($shade, 'hsl'),
                '#{$variant}-contrast': adaptive-contrast(#{var(--ig-#{$name}-#{$variant})}),
                '#{$variant}-raw': map.get($shade, 'raw'),
            )
        );
    }

    @return $result;
}

/// Generates a color shade for a given base colors.
/// @access private
/// @group Palettes
/// @param {String} $name - The base color name (primary, secondary, etc.) to be used to generate a color variant.
/// @param {Color} $color - The color value to be used to generate a color shade.
/// @param {String | Number} $shade - The color shade variant.
/// @param {Color} $surface [null] - The surface color. Useful when generating a shade of gray (optional).
/// @require {function} luminance
/// @require {function} to-fixed
/// @returns {Map} - A map containing a list of HSL values and a raw color value.
@function shade($name, $color, $shade, $surface: null) {
    $h: var(--ig-#{$name}-h);
    $s: var(--ig-#{$name}-s);
    $l: var(--ig-#{$name}-l);

    @if #{$name} == 'gray' {
        $lum: luminance($surface);
        $color: if($color, $color, if($lum > 0.5, #000, #fff));
        $lmap: map.get(multipliers.$grayscale, 'l');
        $len: list.length($lmap);
        $i: list.index(map.keys($lmap), #{$shade});
        $l: list.nth(map.values($lmap), if($lum > 0.5, $len - $i + 1, $i));
        $raw: hsl(to-fixed(color.hue($color)), to-fixed(color.saturation($color)), $l);
        $hsl: #{hsl(from var(--ig-#{$name}-500) h s $l)};

        @if #{$shade} == '500' {
            $hsl: $raw;
        }

        @return (raw: $raw, hsl: $hsl);
    } @else {
        $sx: map.get(multipliers.$color, 's', #{$shade});
        $lx: map.get(multipliers.$color, 'l', #{$shade});
        $raw: hsl(
            to-fixed(color.hue($color)),
            to-fixed(color.saturation($color) * $sx),
            to-fixed(color.lightness($color) * $lx)
        );
        $hsl: #{hsl(from var(--ig-#{$name}-500) h calc(s * $sx) calc(l * $lx))};

        @if #{$shade} == '500' {
            $hsl: $color;
        }

        @return (raw: $raw, hsl: $hsl);
    }
}

// $special-color-shades: shades('accent', #09f, (50,100,200,300,400,500,600,700,800,900));
// @debug $special-color-shades;

/// Retrieves a color from a color palette.
/// @access public
/// @group Palettes
/// @param {Map} $palette [null] - The source palette map (optional).
/// @param {String} $color [primary] - The target color from the color palette.
/// @param {Number | String} $variant [500] - The target color shade from the color palette.
/// @param {Number} $opacity [null] - Optional opacity to apply to the color shade.
/// @returns {Color | String} - The raw color shade or CSS variable reference from the palette.
/// @example scss
///   // No palette
///   .my-component-1 {
///      background: color($color: 'primary', $variant: 200);
///    }
///    // Using certain palette and adding .5% opacity to the color variant
///   .my-component-2 {
///      background: color($my-palette, 'primary', 200, .5);
///    }
@function color($palette: null, $color: primary, $variant: 500, $opacity: null) {
    $s: #{var(--ig-#{$color}-#{$variant})};
    $contrast: if(meta.type-of($variant) == string, string.index($variant, 'contrast'), false);
    $_alpha: if($opacity, $opacity, 1);
    $_relative-color: if($opacity, hsl(from $s h s l / $opacity), $s);

    @if $palette {
        $s: map.get($palette, #{$color});
        $base: map.get($s, #{$variant});
        $raw: map.get($s, #{$variant}-raw);

        @return if($contrast, $_relative-color, if($raw and $variant != '500', rgba($raw, $_alpha), $base));
    }

    @return $_relative-color;
}

/// Retrieves a contrast text color for a given color variant from a color palette.
/// @access public
/// @group Palettes
/// @param {Map} $palette [null] - The source palette map (optional).
/// @param {String} $color [primary] - The target color from the color palette.
/// @param {Number | String} $variant [500] - The target color shade from the color palette.
/// @param {Number} $opacity [null] - Optional opacity to apply to the color shade.
/// @requires {function} color
/// @returns {Color | String} - The raw contrast color shade or CSS variable from the palette.
/// @example scss without passing a palette
///   .my-component {
///      background: color($color: 'primary', $variant: 200);
///      color: contrast-color($color: 'primary', $variant: 200);
///    }
@function contrast-color($palette: null, $color: primary, $variant: 500, $opacity: null) {
    @return color($palette, $color, #{$variant}-contrast, $opacity);
}

/// Returns a CSS runtime calculated relative color(black or white) for a given color.
/// @access public
/// @group Color
/// @param {Color} $color - The base color used in the calculation.
/// @returns {string} - Returns a relative syntax OKLCH color where the lightness is adjusted
/// based on the specified contrast level, resulting in either black or white.
/// @example scss
///   .my-component {
///      --bg: #09f;
///      background: var(--bg);
///      color: adaptive-contrast(var(--bg));
///    }
@function adaptive-contrast($color) {
    $fn: meta.get-function('color', $css: true);

    @return hsl(from meta.call($fn, from $color var(--y-contrast)) h 0 l);
}

/// Returns a contrast color for a passed color.
/// @access public
/// @group Color
/// @param {Color} $background - The background color used to return a contrast/forground color for.
/// @param {Color | List<Color>} $foreground [#fff] - A list of foreground colors
/// that can be used with the provided background.
/// @param {AAA | AA | A} $contrast [AAA] - The contrast level according to WCAG 2.0 standards.
/// @require {function} contrast
/// @returns {Color} - Returns either white, black, or the provided foreground
/// color if it meets the required contrast level.
/// @link https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html
/// @example scss
///   .my-component {
///      background: #09f
///      color: text-contrast(#09f)
///    }
@function text-contrast($background, $foreground: white, $contrast: 'AAA') {
    @if meta.type-of($foreground) != 'list' and meta.type-of($background) != 'color' {
        @return $background;
    }

    $level: map.get(
        (
            'a': 3,
            'aa': 4.5,
            'aaa': 7,
        ),
        string.to-lower-case($contrast)
    );

    @if not($level) {
        @error '$contrast must be \'A\', \'AA\', or \'AAA\'';
    }

    $candidates: ();

    @each $color in $foreground {
        @if meta.type-of($color) != 'color' {
            @return $background;
        }

        $candidates: list.append($candidates, contrast($background, $color));
    }

    $foreground: list.nth($foreground, list.index($candidates, math.max($candidates...)));

    @if contrast($background, $foreground) >= $level {
        @return $foreground;
    } @else {
        $lightContrast: contrast($background, white);
        $darkContrast: contrast($background, black);

        @if $lightContrast > $darkContrast {
            @return white;
        } @else {
            @return black;
        }
    }
}

/// Mixes two colors to produce an opaque color.
/// @access private
/// @group Color
/// @param {Color} $color-1 - The first color, usually transparent.
/// @param {Color} $color-2 [#fff] - The second color, usually opaque.
/// @return {Color} - The color representation of the rgba value.
@function to-opaque($color-1, $color-2: #fff) {
    @if meta.type-of($color-1) == color and meta.type-of($color-2) == color {
        $red: color.red($color-1);
        $green: color.green($color-1);
        $blue: color.blue($color-1);
        $a: color.alpha($color-1);
        $r: math.floor($a * $red + (1 - $a) * color.red($color-2));
        $g: math.floor($a * $green + (1 - $a) * color.green($color-2));
        $b: math.floor($a * $blue + (1 - $a) * color.blue($color-2));

        @return rgb($r, $g, $b);
    }

    @return $color-1;
}

/// Retruns a comma separated list of hue, saturation, and lightness values for a given color.
/// @access public
/// @group Color
/// @param {Color} $color - The color to be converted to an HSL list of values.
/// @example scss Turn #000 into an HSL list of values
///   $hsl-list: to-hsl(#000); // (0deg, 0%, 0%);
/// @return {List} - This list of HSL values.
@function to-hsl($color) {
    @return (math.round(color.hue($color)), math.round(color.saturation($color)), math.round(color.lightness($color)));
}

/// Calculates the contrast ratio between two colors.
/// See https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests
/// @access public
/// @group Color
/// @param {Color} $background - The background color.
/// @param {Color} $foreground - The foreground color.
/// @require {function} luminance
/// @require {function} to-fixed
/// @example scss
///   contrast(#09f, #000);
/// @returns {Number} - The contrast ratio between the background and foreground colors.
@function contrast($background, $foreground) {
    $backLum: luminance($background) + 0.05;
    $foreLum: luminance($foreground) + 0.05;

    @return to-fixed(math.div(math.max($backLum, $foreLum), math.min($backLum, $foreLum)));
}

/// Calculates the luminance for a given color.
/// See https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests.
/// @access public
/// @group Color
/// @param {Color} $color - The color to calculate luminance for.
/// @example scss
///   luminance(#09f);
/// @returns {Number | String} - The calculated luminance of a given color
@function luminance($color) {
    @if meta.type-of($color) == 'color' {
        $r: math.div(color.red($color), 255);
        $g: math.div(color.green($color), 255);
        $b: math.div(color.blue($color), 255);

        @return 0.2126 * _lcv($r) + 0.7152 * _lcv($g) + 0.0722 * _lcv($b);
    }

    @return $color;
}

/// Calculates the linear channel value for a given sRGB color.
/// @access private
/// @group Color
/// @param {Number} $value - The sRGB color
/// @returns {Number} - The calculated linear channel value
@function _lcv($value) {
    // stylelint-disable number-max-precision
    @return if($value < 0.03928, math.div($value, 12.92), math.pow(math.div($value + 0.055, 1.055), 2.4));
}

/// Returns a list of colors to be used as chart brushes.
/// @access public
/// @group Palettes
/// @returns { List } - A list of colors to be used as chart brushes.
@function chart-brushes() {
    @return if($_enhanced-accessibility, charts.$brushes-color-blind, charts.$brushes-regular);
}
