// RIVER Design-System: System - Functions / System //

// Our primary design system functions.
// These functions help us interact with the design system, and reach for design system values.


// KEYS
// Working with design system keys

// Whether this key is a valid design system reference
// A design system reference is:
// - a '.' seperated key [string]; eg, 'colors.brand-primary.normal'
// - a ('rangeKey' [string], finalKey) value pair; eg, ('colors.brand-primary', 900)
// A '@', '#', '~' is treated as a null (skip this value)
// Any other value is treated as a literal (often used as a default / fallback value)
@function _design-key-is-reference($key: null) {
    // Check for a simple string design system reference; 'colors.primary.normal'
    @if(type-of($key) == 'string' and str-contains($key, '.')) {
        @return true;
    }

    // Check for a (rangeKey, finalKey) pair design system reference
    @if(length($key) == 2 and type-of(nth($key, 1)) == 'string') {
        // If the rangeKey contains a ".", assume this is a design system reference; eg, ('colors.primary', 500)
        @if(str-contains(nth($key, 1), '.')) {
            @return true;
        }
        // If not, we return true if the rangeKey is a valid design map group; eg, ('colors', 'base')
        @if(type-of(design-map(nth($key, 1))) == 'map') {
            @return true;
        }
    }

    // Ignore design system transforms
    @if(type-of($key) == 'string' and ($key == '@' or $key == '#')) {
        @return null;
    }

    // Not a reference; most likely a literal value used for a default
    @return false;
}

// Return the group portion of a design key
// Assumes this is a valid key (ie, has been run through _design-key-is-reference())
// TODO: review whether this feature is commonly used, remove if not the case due to added complexity...
@function _design-key-group($key) {
    // Note the range (string) part of the $key
    // eg, 'colors.brand-primary.normal' => 'colors.brand-primary.normal', ('colors.brand-primary', 500) => 'colors.brand-primary'
    $rangeKey: if(length($key) > 1, nth($key, 1), $key);

    // Extract group and validate
    // Alo matches groups that are passed in without a range (eg, 'colors' => 'colors')
    $groupKey: if(type-of($rangeKey) == 'string', str-before($rangeKey, '.', true), null);
    $groupKey: if(map-has-key(design-map(), $groupKey), $groupKey, null);

    // Try and infer the group key from the range if we still don't have one
    // eg, 'brand-primary.normal' => 'colors'
    @if(not($groupKey)) {
        $rangeKey: str-before($rangeKey, '.');
        @each $group, $ranges in design-map() {
            @if(map-has-key($ranges, $rangeKey) or map-has-key($ranges, $key)) {
                @return $group;
            }
        }
    }

    // Return the $groupKey; null if not matched
    @return $groupKey;
}

// Match and resolve the given key within the design system
// This finds the $key within the design map, and then resolves it (as a value or variable)
// Assumes this is a valid key (ie, has been run through _design-key-is-reference())
@function _design-key-match-resolve($key, $type: 'value') {
    // Note the range (string) part of the $key
    $rangeKey: if(length($key) > 1, nth($key, 1), $key);

    // Note and validate the design group (infer one if needed)
    // eg, 'colors'; null if not matched
    $groupKey: _design-key-group($key);
    @if(not($groupKey)) { @return null; }

    // Prepare the searchKey format: as a rangeKey string, or (rangeKey, finalKey) pair
    $rangeKey: str-replace($rangeKey, $groupKey + '.', '');
    $rangeKey: if($groupKey == $rangeKey, null, $rangeKey);
    $searchKey: if(length($key) > 1, ($rangeKey, nth($key, 2)), $rangeKey);

    // Perform a deep search into the given group map
    // Value may be a literal (eg, red), range reference (eg, 500), or design system reference (eg, '@colors.brand-primary.normal')
    $rangeMapKeyValue: map-deep-get(design-group($groupKey), $searchKey, true); // (map, key, value) if matched

    @if(not($rangeMapKeyValue)) { @return null; }
    $finalKey: map-get($rangeMapKeyValue, 'key');
    $range: map-get($rangeMapKeyValue, 'map');
    $value: map-get($rangeMapKeyValue, 'value');

    // Resolve any aliases/references within the range map
    // Ignore cases where key and value match (eg, sizes 0: 0)
    @while(map-has-key($range, $value) and not($value == map-get($range, $value))) {
        $value: map-get($range, $value);
    }

    // Resolve the final value
    // Note, we do not check whether this variable group is enabled, as this is managed within design-variable(...)
    $variableEnabled: if(design-setting('variables'), true, false);
    @if($type == 'value' or not($variableEnabled)) {
        $value: design-value-resolve($value);
    }
    // Resolve the variable; provide all accompanying information
    @if($type == 'variable' and $variableEnabled) {
        $valueResolved: design-variable-resolve($value);
        $value: (
            'groupKey': $groupKey,
            'rangeKey': $rangeKey,
            'finalKey': $finalKey,
            'range': $range,
            'value': map-get($rangeMapKeyValue, 'value'), // eg, 'normal'
            'finalValue': $value, // eg, blue, '@colors.brand-secondary.normal', etc
            'resolvedValue': $valueResolved // eg, blue, red, etc
        );
    }

    // Return the matched value/variable; null if not matched
    @return $value;
}

// Whether a design key/value pair is part of a range
// A design key/value pair is part of a range if it's not included within the range, and it's a number
// This is useful when determining what values to output as utilities for example
// eg, design-key-is-range(design-range('colors.brand-primary'), 500) => true
// eg, design-key-is-range(design-range('colors.brand-primary'), 'normal') => false
@function design-key-is-range($values, $key) {
    // Check for a top-level value, note as a range to avoid default output
    // eg, colors: (white: white)
    @if(not(type-of($values) == 'map')) {
        @return true;
    }

    // Grab the value
    $value: map-get($values, $key);

    // A range must be a number
    @if(not(type-of($key) == 'number')) {
        @return false;
    }

    // A range must be a final value (not a reference to another value)
    // Ignore matching key/value pairs (eg, a size map may include (0: 0) as a valid key/value pair)
    @if(map-has-key($values, $value) and not($value == $key)) {
        @return false;
    }

    // This key/value pair is part of a range
    @return true;
}

// Whether a design key/value is a reference (not part of a range)
@function design-key-is-reference($values, $key) {
    // A key/value pair is a reference (or alias) if it's not a range
    // That is, they key is not numeric, and/or it points to another value in the map of values
    @return not(design-key-is-range($values, $key));
}


// VALUES
// Working with values in the design system

// Reach for a design value, providing as many keys / defaults as desired
// Each key can be:
// - a '.' seperated key [string]; eg, 'colors.brand-primary.normal'
// - a ('rangeKey' [string], finalKey [number]) value pair; eg, ('colors.brand-primary', 900)
// - any other default value; eg, white, 1rem 1rem, ...
@function design-value($keys...) {
    // Loop through and manage each key provided
    // Return the first matching design system reference, or first default value we get to
    @each $key in $keys {
        // Note whether the key is a reference
        // true = reference, false = a default value (not a reference), null = not applicable (skip this value)
        $keyIsReference: _design-key-is-reference($key);

        // Manage a design system reference
        // Return the value if it's not null
        @if($keyIsReference == true) {
            $value: _design-key-match-resolve($key, 'value');
            @if(not($value == null)) {
                @return $value;
            }
        }

        // Manage a default value (not a design system reference, '@', or '#')
        // Return the value right away
        @if($keyIsReference == false) {
            @return $key;
        }
    }

    // No key was matched
    $error: "design-value() unable to match a value from the ";
    $error: $error + if(length($keys) == 1, "key '#{$keys}'", "keys '#{$keys}'");
    @error($error);
}

// Resolve a design value, accounting for any external design system references
// A value is an external design system reference if it is:
// - a '.' seperated key starting with '@' or '#' [string]; eg, '@colors.brand-primary.normal'
// - a list with the first item '@' or '#', in which case they entire list is treated as multiple keys
// An external design system reference is then evaluated by passing it back to design-value($keys...)
// Any other value is resolved and returned as-is
@function design-value-resolve($value) {
    // Note a potential design system match
    $rangeMapKeyValue: null;

    // Check for a simple '@' or '#' string key, and reach for the corrosponding value
    // Pass through the $key aspect of the $value; eg, '@colors.brand-primary.normal' => 'colors.brand-primary.normal'
    @if(type-of($value) == 'string' and str-contains($value, '.')) {
        @if(str-char-first($value) == '@' or str-char-first($value) == '#') {
            $key: str-slice($value, 2);
            @return design-value($key);
        }
    }

    // Check for a list starting with '@' or '#" for the first item
    // Pass through the full set of keys to design-value(), excluding the first '@' or '#'
    @if(length($value) > 1) {
        @if(nth($value, 1) == '@' or nth($value, 1) == '#') {
            $keys: list-remove-nth($value, 1);
            @return design-value($keys...);
        }
    }

    // If we have a list, resolve each value (as any value may be a design system reference)
    @if(type-of($value) == 'list' and length($value) > 1) {
        $list: $value; // eg, (1rem '@sizes.size.normal')
        $value: ();
        @each $v in $list {
            $value: append($value, design-value-resolve($v));
        }
    }

    // Return the value (or list of values) as it is
    // This will be the value passed in from design-value(), and will be the end-result of our value search...
    // eg, blue (from $key: 'colors.brand-primary.normal', or red (from a default value of $key: red, etc)
    @return $value;
}


// VARIABLES
// Working with CSS variables within the design system

// Reach for a design variable, providing as many keys / defaults as desired
// Each key can be:
// - a '.' seperated key [string]; eg, 'colors.brand-primary.normal'
// - a ('rangeKey' [string], finalKey [number]) value pair; eg, ('colors.brand-primary', 900)
// - any other default value; eg, white, 1rem 1rem, ...
@function design-variable($keys...) {
    // Note whether we should resolve variables as values
    @if(not(design-setting('variables'))) { @return design-value($keys...); }

    // Loop through and manage each key provided
    // Return the first matching design system reference, or first default value we get to
    @each $key in $keys {
        // Note whether the key is a reference
        // true = reference, false = a default value (not a reference), null = not applicable (skip this value)
        $keyIsReference: _design-key-is-reference($key);

        // Manage a design system reference
        // Return the variable if it's not null
        @if($keyIsReference == true) {
            $variable: _design-key-match-resolve($key, 'variable');

            // Manage a retuened variable map
            // In this case we can build are return the var(...) output
            @if(type-of($variable) == 'map') {
                // Build up the variable name
                $groupKey: map-get($variable, 'groupKey'); // eg, 'colors'
                $rangeKey: map-get($variable, 'rangeKey'); // eg, 'brand-primary.normal', 'brand-primary'
                $finalKey: map-get($variable, 'finalKey'); // eg, 'normal', 500
                $values: map-get($variable, 'range'); // eg, ( '500: blue', 600: darkblue, ... )
                $value: map-get($variable, 'value'); // eg, '@colors.accent.normal', blue, etc

                // Check that this variable is enabled; resolve it if not
                @if(not(design-map-setting($groupKey, 'variables'))) {
                    // Resolve any aliases/references within the range map
                    // Ignore cases where key and value match (eg, sizes 0: 0)
                    @while(map-has-key($values, $value) and not($value == map-get($values, $value))) {
                        $value: map-get($values, $value);
                    }

                    // Resolve the final value
                    @return design-variable-resolve($value);
                }

                // Finalise and return the variable
                $rangeKeyWithoutFinal: if($rangeKey and not ($rangeKey == $finalKey), str-replace($rangeKey, '.' + $finalKey, ''), null);
                $rangeKeyWithoutFinal: if($rangeKeyWithoutFinal, design-setting('lang.variables.range-seperator') + $rangeKeyWithoutFinal, ''); // Include seperator
                $variableName: $groupKey + $rangeKeyWithoutFinal + design-setting('lang.variables.value-seperator') + $finalKey;
                $variableName: design-transform-variable-name($variableName);
                $variable: var(--#{$variableName}); // eg, 'var(--colors-brand-primary--normal');
                @return $variable;
            }

            // Manage a returned value
            @if($variable) {
                // If the variable has been resolved as a value (not a map), return it directly
                // This occurs if this variable points to another variable that is disabled
                @return $variable;
            }
        }

        // Manage a default value (not a design system reference, '@', or '#')
        // Return the value right away
        @if($keyIsReference == false) {
            @return $key;
        }
    }

    // No key was matched
    $error: "design-variable() unable to match a variable from the ";
    $error: $error + if(length($keys) == 1, "key '#{$keys}'", "keys '#{$keys}'");
    @error($error);
}


// Resolve a design variable, accounting for any external design system references
// A variable is an external design system reference if it is:
// - a '.' seperated key starting with '@' or '#' [string]; eg, '@colors.brand-primary.normal'
// - a list with the first item '@' or '#', in which case they entire list is treated as multiple keys
// An external design system reference is then evaluated by passing it back to design-value($keys...)
// Any other value is resolved and returned as-is
@function design-variable-resolve($value) {
    // Note whether we should resolve variables as values
    @if(not(design-setting('variables'))) { @return design-value-resolve($value); }

    // Note a potential design system match
    $rangeMapKeyValue: null;

    // Check for a simple '@' or '#' string key, and reach for the corrosponding value
    // Pass through the $key aspect of the $value; eg, '@colors.brand-primary.normal' => 'colors.brand-primary.normal'
    @if(type-of($value) == 'string' and str-contains($value, '.')) {
        $transform: str-char-first($value);
        @if($transform == '@' or $transform == '#') {
            $key: str-slice($value, 2);
            @if($transform == '@') { @return design-variable($key); }
            @if($transform == '#') { @return design-value($key); }
        }
    }

    // Check for a list starting with '@' or '#" for the first item
    // Pass through the full set of keys to design-value(), excluding the first '@' or '#'
    @if(length($value) > 1) {
        $transform: nth($value, 1);
        @if($transform == '@' or $transform == '#') {
            $keys: list-remove-nth($value, 1);
            @if($transform == '@') { @return design-variable($keys...); }
            @if($transform == '#') { @return design-value($keys...); }
        }
    }

    // If we have a list, resolve each value (as any value may be a design system reference)
    @if(type-of($value) == 'list' and length($value) > 1) {
        $list: $value; // eg, (1rem '@sizes.size.normal')
        $value: ();
        @each $v in $list {
            $value: append($value, design-variable-resolve($v));
        }
    }

    // Return the value (or list of values) as it is
    // This will be the value passed in from design-variable(), and will be the end-result of our value search...
    // eg, blue (from $key: 'colors.brand-primary.normal', or red (from a default value of $key: red, etc)
    @return $value;
}


// CSS OUTPUT
// Managing CSS output (for CSS variables and classes)

// Get a valid variable name
@function design-transform-variable-name($name) {
    $name: '' + $name;
    $name: str-replace($name, '.', '-');
    @return $name;
}

// Get a valid class name
@function design-transform-class-name($name) {
    $name: '' + $name;
    $name: str-replace($name, '.', '\\.');
    $name: str-replace($name, ':', '\\:');
    $name: str-replace($name, '!', '\\!');
    @return $name;
}
