/// A function that returns an empty map of type `map`. In Sass, Empty maps and lists can be declared using `()` and Sass will assign its type as `list`. This function ensures its type will be `map`.

@function map-new() {
	@return map-remove((), 'key');
}

// * SPDX-SnippetBegin
// * SPDX-License-Identifier: MIT
// * SPDX-SnippetCopyrightText: © 2014 Kitty Giraudel <https://kittygiraudel.com/>
// *

/// Add `$unit` to `$value`
/// @author Hugo Giraudel
/// @link https://kittygiraudel.com/2014/01/15/casting-a-string-to-a-number-in-sass/
/// @param {Number} $value - Value to add unit to
/// @param {String} $unit - String representation of the unit
///
/// @return {Number} - `$value` expressed in `$unit`

@function to-length($value, $unit) {
	$units: (
		'px': 1px,
		'cm': 1cm,
		'mm': 1mm,
		'%': 1%,
		'ch': 1ch,
		'pc': 1pc,
		'in': 1in,
		'em': 1em,
		'rem': 1rem,
		'pt': 1pt,
		'ex': 1ex,
		'vw': 1vw,
		'vh': 1vh,
		'vmin': 1vmin,
		'vmax': 1vmax,
	);

	@if not index(map-keys($units), $unit) {
		@error 'Invalid unit `#{$unit}`.';
	}

	@return $value * map-get($units, $unit);
}

/// Casts a string into a number
/// @author Kitty Giraudel
/// @link https://kittygiraudel.com/2014/01/15/casting-a-string-to-a-number-in-sass/
/// @param {String | Number} $value - Value to be parsed
/// @return {Number}

@function to-number($value) {
	@if type-of($value) == 'number' {
		@return $value;
	} @else if type-of($value) != 'string' {
		@error 'Value for `to-number` should be a number or a string.';
	}

	$digits: 0;
	$result: 0;
	$minus: str-slice($value, 1, 1) == '-';
	$numbers: (
		'0': 0,
		'1': 1,
		'2': 2,
		'3': 3,
		'4': 4,
		'5': 5,
		'6': 6,
		'7': 7,
		'8': 8,
		'9': 9,
	);

	@for $i from if($minus, 2, 1) through str-length($value) {
		$character: str-slice($value, $i, $i);

		@if not(index(map-keys($numbers), $character) or $character == '.') {
			@return to-length(
				if($minus, -$result, $result),
				str-slice($value, $i)
			);
		}

		@if $character == '.' {
			$digits: 1;
		} @else if $digits == 0 {
			$result: $result * 10 + map-get($numbers, $character);
		} @else {
			$digits: $digits * 10;
			$result: $result + clay-div(map-get($numbers, $character), $digits);
		}
	}

	@return if($minus, -$result, $result);
}

// * SPDX-SnippetEnd

// * SPDX-SnippetBegin
// * SPDX-License-Identifier: MIT
// * SPDX-SnippetCopyrightText: © 2014 Kitty Giraudel <https://github.com/KittyGiraudel/SassyJSON>
// *

/// Logs an error at `$pointer` with `$string` message
/// @author Kitty Giraudel
/// @param {String} $string - error message
/// @param {Number} $pointer - pointer position

@function throw($string, $pointer) {
	@error 'ERROR::#{$pointer}::#{$string}';
}

/// Parses a JSON encoded number to find the integer part
/// @author Kitty Giraudel
/// @param {String} $source - JSON complete source
/// @param {Number} $pointer - current pointer
/// @throw Unexpected token $token.
/// @return {List} (new pointer, parsed number)
/// @require {function} throw

@function find-integer($source, $pointer) {
	$source: to-lower-case($source);
	$length: str-length($source);
	$numbers: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';
	$result: 0;

	@while $pointer <= $length {
		$token: str-slice($source, $pointer, $pointer);
		$index: index($numbers, $token);

		@if $token == '-' {
			// do nothing
		} @else if $index {
			$result: $result * 10 + ($index - 1);
		} @else {
			@if index('e' '.' ' ' ',' ']' '}', $token) {
				@return $pointer, $result;
			}

			@return throw('Unexpected token `' + $token + '`.', $pointer);
		}

		$pointer: $pointer + 1;
	}

	@return $pointer, $result;
}

/// Parses a JSON encoded number to find the digits
/// @author Kitty Giraudel
/// @param {String} $source - JSON complete source
/// @param {Number} $pointer - current pointer
/// @throw Unexpected token $token.
/// @return {List} (new pointer, parsed number)
/// @require {function} throw

@function find-digits($source, $pointer) {
	$source: to-lower-case($source);
	$length: str-length($source);
	$numbers: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';
	$result: null;
	$runs: 1;

	@while $pointer <= $length {
		$token: str-slice($source, $pointer, $pointer);
		$index: index($numbers, $token);

		@if $token == '.' {
			// @continue;
		} @else if $index and $index > 0 {
			$runs: $runs * 10;
			$result: if(
				$result == null,
				($index - 1),
				$result * 10 + ($index - 1)
			);
		} @else {
			@if index('e' '.' ' ' ',' ']' '}', $token) {
				@return $pointer,
					if($result != null, clay-div($result, $runs), $result);
			}

			@return throw('Unexpected token `#{$token}`.', $pointer);
		}

		$pointer: $pointer + 1;
	}

	@return $pointer, if($result != null, clay-div($result, $runs), $result);
}

/// Parses a JSON encoded number to find the exponent part
/// @author Kitty Giraudel
/// @param {String} $source - JSON complete source
/// @param {Number} $pointer - current pointer
/// @throw Unexpected token $token.
/// @return {List} - (new pointer, parsed number)
/// @require {function} throw

@function find-exponent($source, $pointer) {
	$source: to-lower-case($source);
	$length: str-length($source);
	$numbers: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';
	$result: null;
	$minus: null;

	@while $pointer <= $length {
		$token: str-slice($source, $pointer, $pointer);
		$index: index($numbers, $token);

		@if $token == 'e' {
			// @continue;
		} @else if $token == '-' {
			@if $minus != null {
				@return throw('Unexpected token `-`.', $pointer);
			}
			$minus: true;
		} @else if $token == '+' {
			@if $minus != null {
				@return throw('Unexpected token `+`.', $pointer);
			}
			$minus: false;
		} @else if $index and $index > 0 {
			$result: if(
				$result == null,
				($index - 1),
				$result * 10 + ($index - 1)
			);
		} @else {
			@if index(' ' ',' ']' '}', $token) == null {
				@return throw('Unexpected token `' + $token + '`.', $pointer);
			}

			@return $pointer,
				if($minus and $result != null, $result * -1, $result);
		}

		$pointer: $pointer + 1;
	}

	@return $pointer, if($minus and $result != null, $result * -1, $result);
}

/// Power function
/// @author Kitty Giraudel
/// @param {Number} $x - number
/// @param {Number} $n - power
/// @return {Number}

@function pow($x, $n) {
	@if $n == 0 {
		@return 1;
	}
	$ret: 1;
	@if $n >= 0 {
		@for $i from 1 through $n {
			$ret: $ret * $x;
		}
	} @else {
		@for $i from $n to 0 {
			$ret: clay-div($ret, $x);
		}
	}

	@return $ret;
}

/// Parses a JSON encoded number
/// @author Kitty Giraudel
/// @param {String} $source - JSON complete source
/// @param {Number} $pointer - current pointer
/// @throw "Unexpected token $token."
/// @throw "Unexpected end of stream."
/// @return {List} (new pointer, parsed number)
/// @require {function} throw
/// @require {function} find-integer
/// @require {function} find-digits
/// @require {function} find-exponent
/// @require {function} pow
@function json-decode--number($source, $pointer) {
	$pointer: $pointer - 1; // Move back pointer to begininng of number
	$allowed: '-' '0' '1' '2' '3' '4' '5' '6' '7' '8' '9'; // Allowed characted to start with
	$first: str-slice(
		$source,
		$pointer,
		$pointer
	); // First character of the number
	$minus: $first == '-'; // Is it negative?

	// Early check for errors
	@if not index($allowed, $first) {
		@return throw('Unexpected token `' + $first + '`.', $pointer);
	}

	// Find the integer part
	$find-integer: find-integer($source, $pointer);
	$pointer: nth($find-integer, 1);
	$result: nth($find-integer, 2);
	@if not $result {
		// Error occured
		@return $find-integer;
	}

	// Find digits
	@if str-slice($source, $pointer, $pointer) == '.' {
		$find-digits: find-digits($source, $pointer);
		$pointer: nth($find-digits, 1);
		$digits: nth($find-digits, 2);

		@if $digits == null {
			// Empty digits, throw error
			@return throw('Unexpected end of stream.', $pointer);
		} @else if $digits == false {
			// Error occured, return it
			@return $find-digits;
		}

		$result: $result + $digits;
	}

	// Find exponent
	@if to-lower-case(str-slice($source, $pointer, $pointer)) == 'e' {
		$find-exponent: find-exponent($source, $pointer);
		$pointer: nth($find-exponent, 1);
		$exponent: nth($find-exponent, 2);

		@if $exponent == null {
			// Empty exponent, throw error
			@return throw('Unexpected end of stream.', $pointer);
		} @else if $exponent == false {
			// Error occured, return it
			@return $find-exponent;
		}

		$result: $result * pow(10, $exponent);
	}

	@return ($pointer, if($minus, $result * -1, $result));
}

/// Convert an hexadecimal number to a decimal number
/// @author Kitty Giraudel
/// @param {String} $string - hexadecimal value
/// @return {Number} - decimal number

@function hex-to-dec($string) {
	$hex: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9' 'a' 'b' 'c' 'd' 'e' 'f';
	$string: to-lower-case($string);
	$length: str-length($string);

	$dec: 0;
	@for $i from 1 through $length {
		$factor: 1 + (15 * ($length - $i));
		$index: index($hex, str-slice($string, $i, $i));
		$dec: $dec + $factor * ($index - 1);
	}

	@return $dec;
}

/// Cast a JSON encoded string into a hexadecimal color
/// @author Kitty Giraudel
/// @param {String} $string - JSON string
/// @return {Color | String} - string or hex color depending on the match
/// @require {function} hex-to-dec

@function from-hex($string) {
	$string-lower: to-lower-case($string);
	$r: '';
	$g: '';
	$b: '';
	$hex: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9' 'a' 'b' 'c' 'd' 'e' 'f';
	$length: str-length($string);
	$max: if($length == 4, 1, 2);

	// Check for length accuracy
	@if $length != 4 and $length != 7 {
		@return $string;
	}

	// Loop from the second character (omitting #)
	@for $i from 2 through $length {
		$c: str-slice($string-lower, $i, $i);

		// If wrong character, return
		@if index($hex, $c) == null {
			@return $string;
		}

		@if str-length($r) < $max {
			$r: $r + $c;
		} @else if str-length($g) < $max {
			$g: $g + $c;
		} @else if str-length($b) < $max {
			$b: $b + $c;
		}
	}

	@if $length == 4 {
		$r: $r + $r;
		$g: $g + $g;
		$b: $b + $b;
	}

	@return rgb(hex-to-dec($r), hex-to-dec($g), hex-to-dec($b));
}

/// Cast a stringified number / stringified percentage into number type
/// @author Kitty Giraudel
/// @param {String} $string - A string
/// @return {Number} - unitless number or percentage
/// @require {function} json-decode--number

@function get-color-value($string) {
	$first: str-slice($string, 1, 1);

	// Pad <1 values with a leading 0
	@if $first == '.' {
		$string: '0' + $string;
	}

	$last: str-slice($string, -1, -1);

	@return if(
		$last == '%',
		nth(json-decode--number(str-slice($string, 1, -2), 2), 2) * 1%,
		nth(json-decode--number($string, 2), 2)
	);
}

/// Cast a JSON encoded string into a hsl(a) color
/// @author Kitty Giraudel
/// @param {String} $string - JSON string
/// @return {Color | String} - string or hsl(a) color, depending on the match
/// @require {function} get-color-value

@function from-hsl($string) {
	$frags: ();
	$string-lower: to-lower-case($string);
	$is-alpha: str-slice($string-lower, 4, 4) == 'a';
	$length: str-length($string);
	$start: str-index($string, '(');

	@for $i from $start through $length {
		$token: str-slice($string-lower, $i, $i);
		@if $token == ' ' or $token == '	' {
			// @continue;
		} @else if $token == '(' or $token == ',' {
			$frags: append($frags, '');
		} @else if $token == ')' {
			@if length($frags) != if($is-alpha, 4, 3) {
				@return $string;
			} // Parsing error
			$hue: get-color-value(nth($frags, 1));
			$saturation: get-color-value(nth($frags, 2));
			$lightness: get-color-value(nth($frags, 3));

			@if not $hue or not $saturation or not $lightness {
				@return $string;
			}

			@if $is-alpha {
				@if length($frags) != 4 {
					@return $string;
				} // No alpha channel found
				$alpha: get-color-value(nth($frags, 4));
				@if not $alpha {
					@return $string;
				} // Error parsing alpha channel
				@return hsla($hue, $saturation, $lightness, $alpha);
			}

			@return hsl($hue, $saturation, $lightness);
		} @else {
			$frags: set-nth(
				$frags,
				length($frags),
				nth($frags, length($frags)) + $token
			);
		}
	}

	@return $string;
}

/// Cast a JSON encoded string into a rgb(a) color
/// @author Kitty Giraudel
/// @param {String} $string - JSON string
/// @return {Color | String} - string or rgb(a) color depending on the match
/// @require {function} get-color-value

@function from-rgb($string) {
	$string-lower: to-lower-case($string);
	$frags: ();
	$is-alpha: str-slice($string-lower, 4, 4) == 'a';
	$start: str-index($string, '(');
	$length: str-length($string);

	@for $i from $start through $length {
		$token: str-slice($string-lower, $i, $i);
		@if $token == ' ' or $token == '	' {
			// @continue;
		} @else if $token == '(' or $token == ',' {
			$frags: append($frags, '');
		} @else if $token == ')' {
			@if length($frags) != if($is-alpha, 4, 3) {
				@return $string;
			} // Parsing error
			$red: get-color-value(nth($frags, 1));
			$green: get-color-value(nth($frags, 2));
			$blue: get-color-value(nth($frags, 3));

			@if not $red or not $green or not $blue {
				@return $string;
			}

			@if $is-alpha {
				@if length($frags) != 4 {
					@return $string;
				} // No alpha channel found
				$alpha: get-color-value(nth($frags, 4));
				@if not $alpha {
					@return $string;
				} // Error parsing alpha channel
				@return rgba($red, $green, $blue, $alpha);
			}

			@return rgb($red, $green, $blue);
		} @else {
			$frags: set-nth(
				$frags,
				length($frags),
				nth($frags, length($frags)) + $token
			);
		}
	}

	@return $string;
}

/// A function that parses a string to see if it's a CSS color
/// @author Kitty Giraudel
/// @param {String} $string - JSON string
/// @return {Color | String} - string or number, depending on the match
/// @require {function} from-hex
/// @require {function} from-rgb
/// @require {function} from-hsl

@function to-color($string) {
	@if type-of($string) == 'color' {
		@return $string;
	}

	$string-lower: to-lower-case($string);
	$colors: transparent black silver gray white maroon red purple fuchsia green
		lime olive yellow navy blue teal aqua aliceblue antiquewhite aqua
		aquamarine azure beige bisque black blanchedalmond blue blueviolet brown
		burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk
		crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkgrey
		darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred
		darksalmon darkseagreen darkslateblue darkslategray darkslategrey
		darkturquoise darkviolet deeppink deepskyblue dimgray dimgrey dodgerblue
		firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold
		goldenrod gray green greenyellow grey honeydew hotpink indianred indigo
		ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue
		lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightgrey
		lightpink lightsalmon lightseagreen lightskyblue lightslategray
		lightslategrey lightsteelblue lightyellow lime limegreen linen magenta
		maroon mediumaquamarine mediumblue mediumorchid mediumpurple
		mediumseagreen mediumslateblue mediumspringgreen mediumturquoise
		mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite
		navy oldlace olive olivedrab orange orangered orchid palegoldenrod
		palegreen paleturquoise palevioletred papayawhip peachpuff peru pink
		plum powderblue purple red rosybrown royalblue saddlebrown salmon
		sandybrown seagreen seashell sienna silver skyblue slateblue slategray
		slategrey snow springgreen steelblue tan teal thistle tomato turquoise
		violet wheat white whitesmoke yellow yellowgreen;
	$keywords: ();

	// Filling $keywords with stringified color keywords
	@each $color in $colors {
		$keywords: append($keywords, $color + '');
	}

	// Deal with inherit keyword
	@if $string-lower == 'inherit' {
		@return unquote($string);
	}

	@if index($keywords, $string-lower) {
		// Deal with color keywords
		@return nth($colors, index($keywords, $string-lower));
	} @else if str-slice($string-lower, 1, 1) == '#' {
		// Deal with hexadecimal triplets
		@return from-hex($string);
	} @else if str-slice($string-lower, 1, 3) == 'rgb' {
		// Deal with rgb(a) colors
		@return from-rgb($string);
	} @else if str-slice($string-lower, 1, 3) == 'hsl' {
		// Deal with hsl(a) colors
		@return from-hsl($string);
	} @else {
		// Return string
		@return $string;
	}
}

// * SPDX-SnippetEnd
