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

// Useful SASS helper functions...


// STRING helpers

@function str-char-at($string, $position: 1) {
    $char: str-slice($string, $position, $position);
    @return $char;
}

@function str-char-first($string) {
    $char: str-slice($string, 1, 1);
    @return $char;
}

@function str-char-last($string) {
    $length: str-length($string);
    $char: str-slice($string, $length, $length);
    @return $char;
}

// Replace `$search` with `$replace` in `$string`
// Inspired by: https://css-tricks.com/snippets/sass/str-replace-function/
@function str-replace($string, $search, $replace: '') {
    $index: str-index($string, $search);
    
    @if $index {
        @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
    }
    
    @return $string;
}

// Check if a string contains a substring
@function str-contains($string, $substr) {
    $index: str-index($string, $substr);
    @return if($index, true, false);
}

// Return everything from a string, before a 
@function str-before($string, $substr, $returnStringIfCharMissing: false) {
    $index: str-index($string, $substr);
    @if($index) {
        @return str-slice($string, 0, $index - 1);
    }
    @return if($returnStringIfCharMissing, $string, null);
}

@function str-after($string, $substr, $returnStringIfCharMissing: false) {
    $index: str-index($string, $substr);
    @if($index) {
        @return str-slice($string, $index + 1);
    }
    @return if($returnStringIfCharMissing, $string, null);
}

// Parse a string into a number
// Accounts for negative values, and decimals
// Returns null if the string cannot be parsed
// Inspired by: https://kittygiraudel.com/2014/01/15/casting-a-string-to-a-number-in-sass/
@function str-to-number($string) {
    // Matrices
    $strings: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';
    $numbers: 0 1 2 3 4 5 6 7 8 9;

    // Result
    $result: 0;
    $minus: false;
    $divider: null;

    // Check for a minus
    @if(str-slice($string, 1, 1) == '-') {
        $minus: true;
        $string: str-slice($string, 2);
    }

    // Loop through all characters
    @for $i from 1 through str-length($string) {
        $character: str-slice($string, $i, $i);
        // Manage the type of character (minus, decimal, number, other)
        @if ($character == '.') {
            // Ignore multiple decimal points
            // You may want to return numm if $divider is already set to a value...
            $divider: if($divider, $divider, 1);
        } @else {
            $index: index($strings, $character);

            // Return null if we can't match the current character to a number
            @if (not $index) {
                @return null;
            }

            // Note the number value of the current number string
            $number: nth($numbers, $index);

            // Manage for decimals
            @if (not($divider)) {
                // Decimal hasn't been found yet
                $result: $result * 10;
            } @else {
                // Decimal dot present; move the decimal to the left
                $divider: $divider * 10;
                $number: $number / $divider;
            }
            $result: $result + $number;
        }
    }

    // Return the final number
    @return if($minus, $result * -1, $result);
}


// MATH helpers

// Return a number multiplied to the given exponent
// Inspired by: https://css-tricks.com/snippets/sass/power-function/
@function power-of($number, $exponent) {
    $value: 1;

    @if $exponent > 0 {
        @for $i from 1 through $exponent {
            $value: $value * $number;
        }
    }

    @return $value;
}


// LIST helpers
// For list helpers: https://kittygiraudel.com/2013/08/08/advanced-sass-list-functions/

@function list-contains($list, $item) {
    @return (not index($list, $item) == false);
}

@function list-remove-nth($list, $n) {
    $result: ();
    @for $i from 1 through length($list) {
        @if(not($i == $n)) {
            $result: append($result, nth($list, $i));
        }
    }
    @return $result;
}


// MAP helpers

// Perform a non-destructive deep merge of two maps
// Inspired by: https://pentzzsolt.medium.com/a-non-destructive-map-merge-function-for-sass-f91637f87b2e
@function map-deep-merge($parent-map, $child-map) {
    $result: $parent-map;

    @each $key, $value in $child-map {
        @if (not map-has-key($result, $key)) or (type-of(map-get($result, $key)) != type-of($value)) or (not (type-of(map-get($result, $key)) == map and type-of($value) == map)) {
            $result: map-merge($result, ($key: $value));
        }
        @else {
            $result: map-merge($result, ($key: map-deep-merge(map-get($result, $key), $value)));
        }
    }

    @return $result;
}

// Recursively check through maps for the given key, seperating child maps by $seperator
// Returns null if no match can be found, and the key cannot be split any further
// NOTE, $key can be passed in as a (rangeKey, finalKey) pair, useful if the finalKey is numeric
// NOTE, you can opt to return the final map, key, and value if desired
// NOTE, when seperating key by $seperator, each variation from left to right is checked
// NOTE, by default, as a last resort we also try and convert our final key to a number and check for a match
// eg, map-deep-get( ( 'brand.primary.normal': blue ), 'brand.primary.normal' ) => blue
// eg, map-deep-get( ( 'brand': ( 'primary': ( 'normal': blue ) ) ), 'brand.primary.normal' ) => blue
// eg, map-deep-get( ( 'brand.primary': ( 'normal': blue ) ), 'brand.primary.normal' ) => blue
// eg, map-deep-get( ( 'brand': ( 'primary': ( 500: blue ) ) ), ('brand.primary', 500) ) => blue
@function map-deep-get($map, $key, $returnMapKeyValue: false, $seperator: '.', $checkKeyAsNumber: true) {
    // Note whether we have matched
    $matched: false;
    $value: null;

    // Manage key format
    $finalKey: if(length($key) == 2, nth($key, 2), null);
    $rangeKey: if(not($finalKey == null), nth($key, 1), $key);

    // Check for a rangeKey match
    @if(map-has-key($map, $rangeKey)) {
        $value: map-get($map, $rangeKey);
        $matched: true;
    }

    // Check for a finalKey match, if our $rangeKey is null (a direct match)
    @if($rangeKey == null and not($finalKey == null)) {
        @if(type-of($map) == 'map' and map-has-key($map, $finalKey)) {
            $value: map-get($map, $finalKey);
            $matched: true;
        }
    }

    // Check for a finalKey match, if our range has matched
    @if($matched and not($finalKey == null)) {
        @if(type-of($value) == 'map' and map-has-key($value, $finalKey)) {
            $map: $value;
            $value: map-get($map, $finalKey);
            $matched: true; // ~ still true
        }
    }

    // If we haven't matched, see if we can match a recursive key from the range
    @if(not($matched) and str-contains($rangeKey, $seperator)) {
        // Check each variation of our $seperator within $rangeKey, from left to right
        $rangeKeyCheck: null;
        $rangeKeyRemaining: $rangeKey;
        @while($rangeKeyRemaining) {
            $rangeKeyCheck: if($rangeKeyCheck, $rangeKeyCheck + '.', '') + str-before($rangeKeyRemaining, $seperator);
            $rangeKeyRemaining: str-after($rangeKeyRemaining, $seperator);
            @if(map-has-key($map, $rangeKeyCheck)) {
                $keyNext: if(not($finalKey == null), ($rangeKeyRemaining, $finalKey), $rangeKeyRemaining);
                $mapNext: map-get($map, $rangeKeyCheck);
                @return map-deep-get($mapNext, $keyNext, $returnMapKeyValue, $seperator);
            }
        }
    }

    // If we still haven't matched, see if we can match our current key as a number
    @if(not($matched) and $finalKey == null and $checkKeyAsNumber) {
        $rangeKey: str-to-number($rangeKey);
        @if(not($rangeKey == null) and map-has-key($map, $rangeKey)) {
            $value: map-get($map, $rangeKey);
            $matched: true;
        }
    }

    // Return the matched value on success
    @if($matched) {
        $matchedKey: if(not($finalKey == null), $finalKey, $rangeKey);
        @return if($returnMapKeyValue,
            ('map': $map, 'key': $matchedKey, 'value': $value),
            $value);
    }

    // No match...
    @return null;
}

// Merge multiple maps into one
// Inspired by: https://gist.github.com/bigglesrocks/d75091700f8f2be5abfe
@function maps-merge($maps...) {
  $collection: ();
  @each $map in $maps {
    $collection: map-merge($collection, $map);
  }
  @return $collection;
}

// Insert the given key/value pair into a map
@function map-set($map, $key, $value, $insertAtEnd: true) {
    $map-merge: ($key: $value);
    @if($insertAtEnd) {
        $map: map-merge($map, $map-merge);
    } @else {
        $map: map-merge($map-merge, $map);
    }
    @return $map;
}

// Recursively insert the given $value into a map
// Split up the $key (by $seperator), and recursively look for / create child maps as required
// Use an existing child map if it exists, otherwise create new ones
// eg, map-deep-set( ('colors': ('primary': (...)) ), 'colors.secondary.normal', blue)
// => ('colors': ('primary': (...), 'secondary': ('normal': blue) ) )
@function map-deep-set($map, $key, $value, $seperator: '.', $keyOriginal: null) {
    // Note the original key
    $keyOriginal: if($keyOriginal, $keyOriginal, $key);

    // Try and match the current key
    // If it exists, set the new value
    @if(map-has-key($map, $key)) {
        $valueOriginal: map-get($map, $key);
        @if(type-of($valueOriginal) == 'map' and not(type-of($value) == 'map')) {
            @error("map-deep-set() the recursive key '#{$key}' ('#{$keyOriginal}') already exists as a map (not a value)");
        }
        $map: map-set($map, $key, $value);
        @return $map;
    }

    // If we haven't found a match, try and recursively split the key
    @if(str-contains($key, $seperator)) {
        // Split the key into the current and remaining values
        // eg, colors.primary.normal.values => colors, primary.normal.values
        $keyCurrent: str-before($key, $seperator);
        $keyRemaining: str-after($key, $seperator);

        // Create the child map to set onto the current map
        // Look for this in the current map first, otherwise spin up an empty map
        // From there, recursively insert our $value into the remaining key
        $childMap: if(map-has-key($map, $keyCurrent), map-get($map, $keyCurrent), ());
        @if(not(type-of($childMap) == 'map' or type-of($childMap) == 'list')) {
            @error("map-deep-set() the recursive key '#{$keyCurrent}' ('#{$keyOriginal}') already exists as a value (not a child map)");
        }
        $childMap: map-deep-set($childMap, $keyRemaining, $value, $seperator, $keyOriginal);

        // Update our current map with the recursive child map
        $map: map-set($map, $keyCurrent, $childMap);
    }

    // Insert the $value into the current map level/key pair (child most map)
    @if(not(str-contains($key, $seperator))) {
        $map: map-set($map, $key, $value);
    }

    // Return
    @return $map;
}
