//
// This file defines the public Sass API to ag-Grid's styles.
// https://ag-grid.com/javascript-data-grid/global-style-customisation-sass/
//

@use 'sass:map';
@use 'sass:list';
@use 'sass:meta';
@use 'sass:string';
@use 'sass:color';
@use 'sass:math';
@use './css-content';
@use './icon-font-codes' as *;

@forward './shared';

// Emit styles for the grid. This mixin validates the parameters passed to it,
// converts the parameters to CSS variables, and combines all the necessary CSS
// files for the grid and selected theme.
@mixin grid-styles($global-params: ()) {
    $no-native-widgets: map.get($global-params, 'suppress-native-widget-styling');
    $global-params: map.remove($global-params, 'suppress-native-widget-styling');

    $themes: -get-themes($global-params);

    @if $no-native-widgets {
        @include -load-css-file('ag-grid-no-native-widgets.css');
    } @else {
        @include -load-css-file('ag-grid.css');
    }

    $global-extended-theme-name: -clean-theme-name(map.get($global-params, 'extend-theme'));

    @each $theme-name, $theme-params in $themes {
        @include -validate-params($theme-params, $theme-name);

        @include -load-theme-css-file($theme-name);
        $extended-theme-name: -get-extended-theme-name($theme-params, $default: $global-extended-theme-name);
        @if $extended-theme-name and not -is-provided-theme($extended-theme-name) {
            @error "Invalid extend-theme: \"#{$extended-theme-name}\": only provided themes can be extended, use one of #{meta.inspect(map.keys($-theme-css-files))}";
        }
        @if $extended-theme-name and -is-provided-theme($theme-name) {
            @error "Can't use a provided theme (#{$theme-name}) and also extend a provided theme (#{$extended-theme-name}). If you want to extend #{$extended-theme-name}, provide your own custom theme name.";
        }
        @if $extended-theme-name {
            @include -load-theme-css-file($extended-theme-name);
        }

        $font: map.get($theme-params, '--ag-icon-font-family');
        $font: map.get($-theme-fonts, $theme-name) !default;
        $font: map.get($-theme-fonts, $extended-theme-name) !default;
        @if $font {
            @include -load-css-file('#{$font}Font.css', $ignore-missing: true);
        }
    }

    @each $theme-name, $theme-params in $themes {
        $extended-theme-name: -get-extended-theme-name($theme-params, $default: $global-extended-theme-name);

        @if $extended-theme-name and $extended-theme-name != $theme-name {
            // alias all the styles in the extended theme to the new theme name
            .ag-theme-#{$theme-name} {
                @extend .ag-theme-#{$extended-theme-name};
            }
        }

        // Emit CSS variable declarations for a set of params, converting (foo: bar) to `--ag-foo: bar`
        .ag-theme-#{$theme-name} {
            $color-blend-theme-name: $extended-theme-name;
            $color-blend-theme-name: $theme-name !default;
            $blend-function-name: '-theme-#{$color-blend-theme-name}-color-blends';
            @if meta.function-exists($blend-function-name) {
                $theme-color-blends: meta.get-function($blend-function-name);
                $theme-params: meta.call($theme-color-blends, $theme-params);
            }
            @if (not -is-quartz($theme-name) and not -is-quartz($extended-theme-name)) {
                $theme-params: -base-color-blends($theme-params);
            }

            @each $name, $value in $theme-params {
                // use meta.inspect to preserve the quote style of strings
                @if map.has-key($-variable-param-types, $name) {
                    #{$name}: #{meta.inspect($value)};
                }
            }
        }
    }
}

//
// PRIVATE IMPLEMENTATION
//

$-loaded-css-files: ();

// Fulfils the same role as meta.load-css, which we can't use due to this issue:
// https://github.com/sass/dart-sass/issues/1627
//
// NOTE the above bug was fixed in Sass 1.52.4 in June 2022, this workaround should
// be kept as long as we want to support earlier Sass versions
@mixin -load-css-file($file, $ignore-missing: false) {
    @if not map.get($-loaded-css-files, $file) {
        $-loaded-css-files: map.set($-loaded-css-files, $file, true) !global;
        @include css-content.output-css-file($file, $ignore-missing: $ignore-missing);
    }
}

@mixin -load-theme-css-file($theme-name, $ignore-missing: false) {
    $css-file: map.get($-theme-css-files, $theme-name);
    @if $css-file {
        @include -load-css-file($css-file);
    }
}

@function -clean-theme-name($name) {
    @if $name == null {
        @return null;
    }
    $name: string.to-lower-case($name);
    $remove-prefix: 'ag-theme-';
    @if string.index($name, $remove-prefix) == 1 {
        $name: string.slice($name, string.length($remove-prefix) + 1);
    }
    @return $name;
}

@function -is-quartz($theme) {
    @return $theme and string.index(-clean-theme-name($theme), 'quartz') == 1;
}

@function -clean-variable-name($name) {
    @if $name == null {
        @return null;
    }
    $name: string.to-lower-case($name);

    $proposed-variable-name: '--ag-#{$name}';
    @if map.has-key($-variable-param-types, $proposed-variable-name) {
        $name: $proposed-variable-name;
    }

    $remove-prefix: 'ag-theme-';
    @if string.index($name, $remove-prefix) == 1 {
        $name: string.slice($name, string.length($remove-prefix) + 1);
    }
    @return $name;
}

@function -get-extended-theme-name($theme-params, $default) {
    $extended-theme-name: -clean-theme-name(map.get($theme-params, 'extend-theme'));
    @if $extended-theme-name {
        @return $extended-theme-name;
    }
    @return $default;
}

$-theme-variables: (
    'quartz': (
        active-color: 'color',
    ),
    'quartz-dark': (
        active-color: 'color',
    ),
    'alpine': (
        alpine-active-color: 'color',
    ),
    'alpine-dark': (
        alpine-active-color: 'color',
    ),
    'balham': (
        balham-active-color: 'color',
    ),
    'balham-dark': (
        balham-active-color: 'color',
    ),
    'material': (
        material-primary-color: 'color',
        material-accent-color: 'color',
    ),
);

$-theme-css-files: (
    'quartz': 'ag-theme-quartz-no-font.css',
    'quartz-dark': 'ag-theme-quartz-no-font.css',
    'alpine': 'ag-theme-alpine-no-font.css',
    'alpine-dark': 'ag-theme-alpine-no-font.css',
    'balham': 'ag-theme-balham-no-font.css',
    'balham-dark': 'ag-theme-balham-no-font.css',
    'material': 'ag-theme-material-no-font.css',
    'material-dark': 'ag-theme-material-no-font.css',
);

$-theme-fonts: (
    'quartz': 'agGridQuartz',
    'quartz-dark': 'agGridQuartz',
    'alpine': 'agGridAlpine',
    'alpine-dark': 'agGridAlpine',
    'balham': 'agGridBalham',
    'balham-dark': 'agGridBalham',
    'material': 'agGridMaterial',
    'material-dark': 'agGridMaterial',
);

@function -is-provided-theme($theme-name) {
    @return map.has-key($-theme-variables, $theme-name);
}

@function -get-themes($params) {
    $themes: map.get($params, 'themes');
    $themes: () !default;

    @if meta.type-of($themes) == string {
        $themes: ($themes); // comma makes this a single element list
    }

    // treat ("alpine", "balham") as (alpine: (), "balham": ())
    @if meta.type-of($themes) == list {
        $themes-list: $themes;
        $themes: ();
        @each $theme in $themes-list {
            $themes: map.set($themes, $theme, ());
        }
    }

    @if map.has-key($params, 'theme') {
        $theme: map.get($params, 'theme');
        @if meta.type-of($theme) == list {
            @error "Expected theme to be a string, got a list (if you intend to use multiple themes, use the plural `themes` parameter)";
        } @else if meta.type-of($theme) != string {
            @error "Expected theme to be a string, got #{meta.inspect($theme)})";
        }
        $themes: map.set($themes, $theme, ());
    }

    @if list.length($themes) == 0 {
        $themes: (
            quartz: (),
        );
    }

    $params: map.remove($params, 'theme', 'themes');

    $cleaned: ();
    @each $name, $extra-params in $themes {
        $theme-params: internal-preprocess-params(map.merge($params, $extra-params));
        $cleaned: map.set($cleaned, -clean-theme-name($name), $theme-params);
    }
    @return $cleaned;
}

// Return a version of $params with colour blending applied
@function -base-color-blends($params) {
    // Simple defaults. We need to define the defaults for any parameters
    // that are used in blending below
    $params: -param-default($params, --ag-foreground-color, #000);
    $params: -param-default($params, --ag-background-color, #fff);
    $params: -param-default($params, --ag-range-selection-border-color, --ag-foreground-color);

    // Blended defaults.
    $params: -param-default($params, --ag-disabled-foreground-color, --ag-foreground-color, $alpha: 0.5);
    $params: -param-default($params, --ag-modal-overlay-background-color, --ag-background-color, $alpha: 0.66);
    $params: -param-default(
        $params,
        --ag-range-selection-background-color,
        --ag-range-selection-border-color,
        $alpha: 0.2
    );
    $params: -param-default(
        $params,
        --ag-range-selection-background-color-2,
        --ag-range-selection-background-color,
        $self-overlay: 2
    );
    $params: -param-default(
        $params,
        --ag-range-selection-background-color-3,
        --ag-range-selection-background-color,
        $self-overlay: 3
    );
    $params: -param-default(
        $params,
        --ag-range-selection-background-color-4,
        --ag-range-selection-background-color,
        $self-overlay: 4
    );
    $params: -param-default($params, --ag-border-color, --ag-foreground-color, $alpha: 0.25);
    $params: -param-default($params, --ag-header-column-separator-color, --ag-border-color, $alpha: 0.5);
    $params: -param-default($params, --ag-header-column-resize-handle-color, --ag-border-color, $alpha: 0.5);
    $params: -param-default($params, --ag-input-disabled-border-color, --ag-input-border-color, $alpha: 0.3);
    @return $params;
}

@function -theme-alpine-color-blends($params) {
    // Simple defaults. We need to define the defaults for any parameters
    // that are used in blending either below or in the base color blends
    $params: -param-default($params, --ag-background-color, #fff);
    $params: -param-default($params, --ag-foreground-color, #181d1f);
    $params: -param-default($params, --ag-subheader-background-color, #fff);
    $params: -param-default($params, --ag-alpine-active-color, #2196f3);
    $params: -param-default($params, --ag-range-selection-border-color, --ag-alpine-active-color);

    // Blended defaults
    $params: -param-default(
        $params,
        --ag-subheader-toolbar-background-color,
        --ag-subheader-background-color,
        $alpha: 0.5
    );
    $params: -param-default($params, --ag-selected-row-background-color, --ag-alpine-active-color, $alpha: 0.1);
    $params: -param-default($params, --ag-row-hover-color, --ag-alpine-active-color, $alpha: 0.1);
    $params: -param-default($params, --ag-column-hover-color, --ag-alpine-active-color, $alpha: 0.1);
    $params: -param-default($params, --ag-chip-background-color, --ag-foreground-color, $alpha: 0.07);
    $params: -param-default($params, --ag-input-disabled-background-color, --ag-border-color, $alpha: 0.15);
    $params: -param-default($params, --ag-input-disabled-border-color, --ag-border-color, $alpha: 0.3);
    $params: -param-default($params, --ag-disabled-foreground-color, --ag-foreground-color, $alpha: 0.5);
    $params: -param-default($params, --ag-input-focus-border-color, --ag-alpine-active-color, $alpha: 0.4);
    @return $params;
}

@function -theme-alpine-dark-color-blends($params) {
    $params: -param-default($params, --ag-background-color, #181d1f);
    $params: -param-default($params, --ag-foreground-color, #fff);
    $params: -param-default($params, --ag-subheader-background-color, #000);

    @return -theme-alpine-color-blends($params);
}

@function -theme-balham-color-blends($params) {
    // Simple defaults. We need to define the defaults for any parameters
    // that are used in blending either below or in the base color blends
    $params: -param-default($params, --ag-background-color, #fff);
    $params: -param-default($params, --ag-foreground-color, #000);
    $params: -param-default($params, --ag-border-color, #bdc3c7);
    $params: -param-default($params, --ag-subheader-background-color, #e2e9eb);
    $params: -param-default($params, --ag-balham-active-color, #0091ea);
    $params: -param-default($params, --ag-range-selection-border-color, --ag-balham-active-color);

    // Blended defaults
    $params: -param-default($params, --ag-secondary-foreground-color, --ag-foreground-color, $alpha: 0.54);
    $params: -param-default($params, --ag-disabled-foreground-color, --ag-foreground-color, $alpha: 0.38);
    $params: -param-default(
        $params,
        --ag-subheader-toolbar-background-color,
        --ag-subheader-background-color,
        $alpha: 0.5
    );
    $params: -param-default($params, --ag-row-border-color, --ag-border-color, $alpha: 0.58);
    $params: -param-default($params, --ag-chip-background-color, --ag-foreground-color, $alpha: 0.1);
    $params: -param-default($params, --ag-selected-row-background-color, --ag-balham-active-color, $alpha: 0.28);
    $params: -param-default($params, --ag-header-column-separator-color, --ag-border-color, $alpha: 0.5);
    @return $params;
}

@function -theme-balham-dark-color-blends($params) {
    $params: -param-default($params, --ag-background-color, #181d1f);
    $params: -param-default($params, --ag-foreground-color, #fff);
    $params: -param-default($params, --ag-border-color, #424242);
    $params: -param-default($params, --ag-subheader-background-color, #000);
    $params: -param-default($params, --ag-balham-active-color, #00b0ff);

    $params: -param-default($params, --ag-disabled-foreground-color, --ag-foreground-color, $alpha: 0.38);
    $params: -param-default($params, --ag-header-foreground-color, --ag-foreground-color, $alpha: 0.64);

    @return -theme-balham-color-blends($params);
}

@function -theme-material-color-blends($params) {
    // Simple defaults. We need to define the defaults for any parameters
    // that are used in blending either below or in the base color blends
    $params: -param-default($params, --ag-background-color, #fff);
    $params: -param-default($params, --ag-foreground-color, rgba(0, 0, 0, 0.87));
    $params: -param-default($params, --ag-subheader-background-color, #eee);
    $params: -param-default($params, --ag-material-primary-color, #3f51b5);
    $params: -param-default($params, --ag-range-selection-border-color, --ag-material-primary-color);
    $params: -param-default($params, --ag-range-selection-background-color, rgba(122, 134, 203, 0.1));
    $params: -param-default($params, --ag-border-color, #e2e2e2);

    // Blended defaults
    $params: -param-default($params, --ag-secondary-foreground-color, --ag-foreground-color, $alpha: 0.54);
    $params: -param-default($params, --ag-disabled-foreground-color, --ag-foreground-color, $alpha: 0.38);
    $params: -param-default(
        $params,
        --ag-subheader-toolbar-background-color,
        --ag-subheader-background-color,
        $alpha: 0.5
    );
    @return $params;
}

// Apply a default value to a parameter
//  $params: -param-default($params, x, #f08) - default x to a specific color value
//  $params: -param-default($params, x, y) - default x to the value of y (if y is set)
//  $params: -param-default($params, x, y, $alpha: 0.5) - default x to the value of y made 50% transparent
@function -param-default($params, $target, $source, $alpha: null, $self-overlay: null) {
    @if string.index($target, '--ag-') != 1 {
        @error "Internal error: $target to -param-default should start --ag-";
    }
    $value: null;
    @if type-of($source) == 'color' {
        $value: $source;
    } @else {
        @if string.index($source, '--ag-') != 1 {
            @error "Internal error: $source to -param-default should start --ag-";
        }
        $value: map.get($params, $source);
    }
    @if map.has-key($params, $target) or $value == null {
        @return $params;
    }
    @if $alpha != null {
        $value: color.change($value, $alpha: color.alpha($value) * $alpha);
    }
    @if $self-overlay != null {
        // this formula produces the same opacity value as overlaying the color on top
        // of itself $self-overlay times
        $value: color.change($value, $alpha: 1-(math.pow(1 - color.alpha($value), $self-overlay)));
    }
    @return map.set($params, $target, $value);
}

$-param-type-descriptions: (
    'color': 'a CSS color (e.g. `red` or `#fff`)',
    'length': 'a CSS length (e.g. `0`, `4px` or `50%`)',
    'border-style': 'a CSS border style (e.g. `dotted` or `solid`)',
    'duration': 'a number with time duration units (e.g. `3s` or `250ms`)',
    'border-style-and-size':
        'either `none`, or a CSS border-style and size (e.g. `solid 1px`), or a boolean (true -> `solid 1px` and false -> `none`)',
    'border-style-and-color': 'a CSS border-style and color (e.g. `solid red`)',
    'display': 'A CSS display value (`block` to show, `none` to hide - `true` and `false` are also accepted)',
    'any': 'any value',
);

// params that are copied to --ag-param-name variables
$-variable-param-types: (
    --ag-foreground-color: 'color',
    --ag-data-color: 'color',
    --ag-secondary-foreground-color: 'color',
    --ag-header-foreground-color: 'color',
    --ag-disabled-foreground-color: 'color',
    --ag-background-color: 'color',
    --ag-header-background-color: 'color',
    --ag-tooltip-background-color: 'color',
    --ag-subheader-background-color: 'color',
    --ag-subheader-toolbar-background-color: 'color',
    --ag-control-panel-background-color: 'color',
    --ag-side-button-selected-background-color: 'color',
    --ag-selected-row-background-color: 'color',
    --ag-odd-row-background-color: 'color',
    --ag-modal-overlay-background-color: 'color',
    --ag-row-hover-color: 'color',
    --ag-column-hover-color: 'color',
    --ag-range-selection-border-color: 'color',
    --ag-range-selection-border-style: 'border-style',
    --ag-range-selection-background-color: 'color',
    --ag-range-selection-background-color-2: 'color',
    --ag-range-selection-background-color-3: 'color',
    --ag-range-selection-background-color-4: 'color',
    --ag-range-selection-highlight-color: 'color',
    --ag-selected-tab-underline-color: 'color',
    --ag-selected-tab-underline-width: 'length',
    --ag-selected-tab-underline-transition-speed: 'duration',
    --ag-range-selection-chart-category-background-color: 'color',
    --ag-range-selection-chart-background-color: 'color',
    --ag-header-cell-hover-background-color: 'color',
    --ag-header-cell-moving-background-color: 'color',
    --ag-value-change-value-highlight-background-color: 'color',
    --ag-value-change-delta-up-color: 'color',
    --ag-value-change-delta-down-color: 'color',
    --ag-chip-background-color: 'color',
    --ag-borders: 'border-style-and-size',
    --ag-border-color: 'color',
    --ag-borders-critical: 'border-style-and-size',
    --ag-borders-secondary: 'border-style-and-size',
    --ag-secondary-border-color: 'color',
    --ag-row-loading-skeleton-effect-color: 'color',

    --ag-row-border-style: 'border-style',
    --ag-row-border-width: 'length',
    --ag-row-border-color: 'color',

    --ag-cell-horizontal-border: 'border-style-and-color',
    --ag-borders-input: 'border-style-and-size',
    --ag-input-border-color: 'color',
    --ag-borders-input-invalid: 'border-style-and-size',
    --ag-input-border-color-invalid: 'color',
    --ag-borders-side-button: 'border-style-and-size',
    --ag-border-radius: 'length',
    --ag-header-column-separator-display: 'display',
    --ag-header-column-separator-height: 'length',
    --ag-header-column-separator-width: 'length',
    --ag-header-column-separator-color: 'color',
    --ag-header-column-resize-handle-display: 'display',
    --ag-header-column-resize-handle-height: 'length',
    --ag-header-column-resize-handle-width: 'length',
    --ag-header-column-resize-handle-color: 'color',
    --ag-invalid-color: 'color',
    --ag-input-disabled-border-color: 'color',
    --ag-input-disabled-background-color: 'color',
    --ag-checkbox-background-color: 'color',
    --ag-checkbox-border-radius: 'length',
    --ag-checkbox-checked-color: 'color',
    --ag-checkbox-unchecked-color: 'color',
    --ag-checkbox-indeterminate-color: 'color',
    --ag-toggle-button-off-border-color: 'color',
    --ag-toggle-button-off-background-color: 'color',
    --ag-toggle-button-on-border-color: 'color',
    --ag-toggle-button-on-background-color: 'color',
    --ag-toggle-button-switch-background-color: 'color',
    --ag-toggle-button-switch-border-color: 'color',
    --ag-toggle-button-border-width: 'length',
    --ag-toggle-button-height: 'length',
    --ag-toggle-button-width: 'length',
    --ag-input-focus-box-shadow: 'any',
    --ag-input-focus-border-color: 'color',
    --ag-minichart-selected-chart-color: 'color',
    --ag-minichart-selected-page-color: 'color',
    --ag-grid-size: 'length',
    --ag-icon-size: 'length',
    --ag-widget-container-horizontal-padding: 'length',
    --ag-widget-container-vertical-padding: 'length',
    --ag-widget-horizontal-spacing: 'length',
    --ag-widget-vertical-spacing: 'length',
    --ag-cell-horizontal-padding: 'length',
    --ag-cell-widget-spacing: 'length',
    --ag-row-height: 'length',
    --ag-header-height: 'length',
    --ag-list-item-height: 'length',
    --ag-column-select-indent-size: 'length',
    --ag-set-filter-indent-size: 'length',
    --ag-advanced-filter-builder-indent-size: 'length',
    --ag-row-group-indent-size: 'length',
    --ag-filter-tool-panel-group-indent: 'length',
    --ag-tab-min-width: 'length',
    --ag-menu-min-width: 'length',
    --ag-side-bar-panel-width: 'length',
    --ag-font-family: 'any',
    --ag-font-size: 'length',
    --ag-card-radius: 'length',
    --ag-card-shadow: 'any',
    --ag-popup-shadow: 'any',
    --ag-active-color: 'color',
    --ag-alpine-active-color: 'color',
    --ag-balham-active-color: 'color',
    --ag-material-primary-color: 'color',
    --ag-material-accent-color: 'color',
    --ag-icon-font-family: 'any',
);

@each $icon-name in map.keys($icon-font-codes) {
    $-variable-param-types: map.set($-variable-param-types, '--ag-icon-font-code-#{$icon-name}', 'any');
}

// params that are not copied to CSS variables
$-non-variable-param-types: (
    suppress-native-widget-styling: 'bool',
    extend-theme: 'string',
);

// Apply Sass API sugar over CSS variable API
@function internal-preprocess-params($params) {
    $processed: ();
    @each $name, $value in $params {
        $name: -clean-variable-name($name);
        $type: map.get($-variable-param-types, $name);
        // Allow `null` for colours
        @if $type == 'color' and $value == null {
            $value: transparent;
        }
        // Allow booleans for borders params
        @if string.index($name, 'borders') and $value == true {
            $value: solid 1px;
        }
        @if string.index($name, 'borders') and $value == false {
            $value: none;
        }
        // Allow booleans for display params
        @if $type == 'display' and not $value {
            $value: none;
        }
        @if $type == 'display' and $value == true {
            $value: block;
        }
        $processed: map.set($processed, $name, $value);
    }
    @return $processed;
}

@mixin -validate-params($params, $theme: null) {
    $error: internal-get-params-error($params, $theme);
    @if $error {
        @error $error;
    }
}

@function internal-get-params-error($params, $theme: null) {
    $theme-variables: ();
    @if $theme and map.has-key($-theme-variables, $theme) {
        $theme-variables: map.get($-theme-variables, $theme);
    }

    $param-types: map.merge($-variable-param-types, $theme-variables);
    $errors: ();
    @each $name, $value in $params {
        @if $name == 'suppress-native-widget-styling' {
            $errors: list.append(
                $errors,
                'suppress-native-widget-styling can not be specified at the theme level, only at the top level'
            );
        }

        @if -is-runtime-expression($value) {
            // You can pass variable expressions as values and we'll just trust
            // that it's an appropriate value because we can't check at compile time
        } @else {
            $expected-type: map.get($param-types, $name);
            @if $expected-type {
                $is-valid: -validate-value($value, $expected-type);

                @if not $is-valid {
                    $expected: map.get($-param-type-descriptions, $expected-type);
                    $expected: $expected-type !default;
                    $errors: list.append(
                        $errors,
                        'Invalid parameter `#{$name}: #{meta.inspect($value)}` (expected #{$expected})'
                    );
                }
            } @else if not map.has-key($-non-variable-param-types, $name) {
                $errors: list.append($errors, "Unrecognised parameter '#{$name}'");
            }
        }
    }
    @if list.length($errors) > 0 {
        $message: '';
        @each $error in $errors {
            @if $message == '' {
                $message: $error;
            } @else {
                $message: '#{$message}; #{$error}';
            }
        }
        @if list.length($errors) > 1 {
            $message: '#{list.length($errors)} errors in #{$theme} parameters: #{$message}';
        } @else {
            $message: 'Error in #{$theme} parameters: #{$message}';
        }
        @return $message;
    }
    @return null;
}

@function -validate-value($value, $expected-type) {
    $validator-name: '-validate-#{$expected-type}';
    $validator: meta.get-function($validator-name);
    @if not $validator {
        @error "Internal error: no validator function #{$validator-name}";
    }
    @return meta.call($validator, $value);
}

@function -is-runtime-expression($value) {
    $value: string.to-lower-case(meta.inspect($value));
    @return string.index($value, 'var(') != null or string.index($value, 'calc(') != null;
}

@function -validate-color($value) {
    @return meta.type-of($value) == 'color';
}

@function -validate-length($value) {
    @return meta.type-of($value) == 'number' and (unit($value) != '' or $value == 0);
}

$border-styles: (none, hidden, dotted, dashed, solid, double, groove, ridge, inset, outset, initial);
@function -validate-border-style($value) {
    $value-preserve-quotes: meta.inspect($value);
    @return list.index($border-styles, $value-preserve-quotes) != null;
}

@function -validate-duration($value) {
    @return meta.type-of($value) == 'number' and (unit($value) == 's' or unit($value) == 'ms' or $value == 0);
}

@function -validate-display($value) {
    @return $value == block or $value == none;
}

@function -validate-border-style-and-size($value) {
    @return -validate-two-in-any-order($value, 'border-style', 'length');
}

@function -validate-border-style-and-color($value) {
    @return -validate-two-in-any-order($value, 'border-style', 'color');
}

@function -validate-bool($value) {
    @return meta.type-of($value) == 'bool';
}

@function -validate-string($value) {
    @return meta.type-of($value) == 'string';
}

@function -validate-any($value) {
    @return true;
}

@function -validate-two-in-any-order($value, $type-a, $type-b) {
    @if meta.type-of($value) == 'string' and $value == 'none' {
        @return true;
    }

    @if meta.type-of($value) != 'list' or list.length($value) != 2 {
        @return false;
    }

    $value-1: list.nth($value, 1);
    $value-2: list.nth($value, 2);
    @return (
        (-validate-value($value-1, $type-a) and -validate-value($value-2, $type-b)) or
            (-validate-value($value-2, $type-a) and -validate-value($value-1, $type-b))
    );
}

// check that we defined all the validator functions
@each $variable-name, $type in $-variable-param-types {
    @if not map.has-key($-param-type-descriptions, $type) {
        @error "Internal error: missing type description for #{$type} (used on #{$variable-name})";
    }
    @if not meta.function-exists('-validate-#{$type}') {
        @error "Internal error: missing validator function for #{$type} (used on #{$variable-name})";
    }
}
