@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 brushes for chart themes.
/// @example scss
///   .enhanced-accessibility {
///      @include configure-colors(true);
///    }
/// @see $brushes-color-blind
@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
///   // Pass the required colors
///   $my-palette: palette(
///     $primary: rebeccapurple,
///     $secondary: orange,
///     $surface: white,
///   );
///
///   // Include the generated palette colors as CSS variables.
///   @include palette($my-palette);
@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($error, 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 color shades for a given color.
/// @access public
/// @group Palettes
/// @param {String} $name - The name of the color.
/// @param {Color} $color - The base color used to generate the shades.
/// @param {List} $shades - The list of shade variants.
/// @param {Color} $surface [null] - The surface color. Useful if generating shades of gray (optional).
/// @requires {function} adaptive-contrast
/// @returns {Map} A map consisting of color shades and their respective contrast colors.
///
/// @example scss
///   $color-name: 'accent';
///
///   // Include the generated palette colors as CSS variables.
///   @include palette(
///     (
///       #{$color-name}: shades($color-name, #a57865, $IColorShades),
///     )
///   );
/// @see $IColorShades
@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));

        @if #{$shade} == 'seed' {
            @return (raw: $color, hsl: $color);
        }

        $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.channel($color, 'hue', $space: hsl)),
            to-fixed(color.channel($color, 'saturation', $space: hsl)),
            $l
        );
        $hsl: #{hsl(from var(--ig-#{$name}-500) h s $l)};

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

        @return (raw: $raw, hsl: $hsl);
    } @else {
        $hsl: null;
        $raw: null;

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

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

/// Calculates a dynamic shade of a color by adjusting its lightness in LCH color space.
/// @access public
/// @param {Color} $color - The base color to shade.
/// @param {Number} $lightness [50] - The target lightness value to move toward.
/// @param {Number} $percentage [0.1] - The percentage to move toward the target lightness.
/// @param {Number} $offset [10] - The offset added or subtracted based on direction.
/// @returns {Color} The dynamically shaded color in LCH color space
@function dynamic-shade($color, $lightness: 50, $factor: 0.1, $offset: 5) {
    @return lch(from $color calc(l + ($lightness - l) * $factor + sign($lightness - l) * $offset) c h);
}

/// Normalizes a color value to ensure consistent format output.
/// This fixes issues with newer Sass versions that return RGB format with floating point precision.
/// @access private
/// @group Color
/// @param {Color} $color - The color to normalize.
/// @returns {Color} The normalized color in hex format.
@function _normalize-color($color) {
    @if meta.type-of($color) == 'color' {
        // Round RGB channels to integers to maintain backward compatibility with older Sass behavior
        $r: math.round(color.channel($color, 'red', $space: rgb));
        $g: math.round(color.channel($color, 'green', $space: rgb));
        $b: math.round(color.channel($color, 'blue', $space: rgb));

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

    @return $color;
}

/// 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 - Getting a color from the palette
///   // Without providing a palette
///   .my-component-1 {
///      background: color($color: primary, $variant: 200); // var(--ig-primary-200)
///    }
///
///    // With a specific palette
///   .my-component-2 {
///      background: color($my-palette, primary, 200); // #88c0e5
///    }
///
/// @example scss - Getting a color with opacity
///   .my-component-1 {
///      background: color($color: primary, $variant: 200, $opacity: 0.5); // hsl(from var(--ig-primary-200) h s l/0.5)
///    }
///
///    // With a specific palette
///   .my-component-2 {
///      background: color($my-palette, primary, 200, $opacity: 0.5); // rgba(136, 192, 229, 0.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, rgba($raw, $_alpha), _normalize-color($base)));
    }

    @return $_relative-color;
}

/// Retrieves a contrast color for a given color variant from a color palette. Works similarly to the color function.
/// @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.
///
/// @example scss
///   .my-component {
///      --bg: #09f;
///      background: var(--bg);
///      color: adaptive-contrast(var(--bg)); // hsl(from color(from var(--bg) var(--y-contrast)) h 0 l)
///    }
/// @returns {string} Returns a relative HSL color where the lightness is adjusted
@function adaptive-contrast($color) {
    $fn: meta.get-function('color', $css: true);

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

/// Returns a contrast color for a passed color.
/// @deprecated Use adaptive-contrast instead.
/// @access public
/// @group Color
/// @param {Color} $background - The background color used to return a contrast/forground color for.
/// @param {Color | List } $foreground [#fff] - A list of foreground colors that can be used with the provided background.
/// @param {String} $contrast [AAA] - The contrast level according to WCAG 2.0 standards.
/// @example scss
///   .my-component {
///      background: #09f;
///      color: text-contrast(#09f);
///    }
///
/// @require {function} contrast
/// @see {function} adaptive-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 WCAG 2.0 Contrast
@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.
/// @deprecated Switch to CSS color-mix.
/// @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.channel($color-1, 'red', $space: rgb);
        $green: color.channel($color-1, 'green', $space: rgb);
        $blue: color.channel($color-1, 'blue', $space: rgb);
        $a: color.channel($color-1, 'alpha');
        $r: math.floor($a * $red + (1 - $a) * color.channel($color-2, 'red', $space: rgb));
        $g: math.floor($a * $green + (1 - $a) * color.channel($color-2, 'green', $space: rgb));
        $b: math.floor($a * $blue + (1 - $a) * color.channel($color-2, 'blue', $space: rgb));

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

    @return $color-1;
}

/// Retruns a comma separated list of hue, saturation, and lightness values for a given color.
/// @deprecated Not needed anymore and will be removed in the future.
/// @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.channel($color, 'hue', $space: hsl)),
        math.round(color.channel($color, 'saturation', $space: hsl)),
        math.round(color.channel($color, 'lightness', $space: hsl))
    );
}

/// Calculates the contrast ratio between two colors.
/// @access public
/// @group Color
/// @param {Color} $background - The background color.
/// @param {Color} $foreground - The foreground color.
///
/// @example scss
///   $conrast: contrast(#09f, #000); // 7
///
/// @returns {Number} The contrast ratio between the background and foreground colors.
/// @require {function} luminance
/// @require {function} to-fixed
/// @link https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests WCAG 2.0 Contrast Ratio
@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.
/// @access public
/// @group Color
/// @param {Color} $color - The color to calculate luminance for.
///
/// @example scss
///   $white: luminance(#fff); // 1
///   $blue: luminance(#09f); // 0.3
///   $black: luminance(#000); // 0
///
/// @returns {Number | String} The calculated luminance of a given color
/// @link https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests. WCAG 2.0 Contrast Ratio
@function luminance($color) {
    @if meta.type-of($color) == 'color' {
        $r: math.div(color.channel($color, 'red', $space: rgb), 255);
        $g: math.div(color.channel($color, 'green', $space: rgb), 255);
        $b: math.div(color.channel($color, 'blue', $space: rgb), 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. The return value depends on the value of enhanced-accessibility.
/// @access public
/// @group Palettes
/// @returns { List } A list of colors to be used as chart brushes.
/// @see {mixin} configure-colors
/// @see $brushes-regular
/// @see $brushes-color-blind
@function chart-brushes() {
    @return if($_enhanced-accessibility, charts.$brushes-color-blind, charts.$brushes-regular);
}
