@use 'sass:list';
@use 'sass:map';
@use 'sass:math' as sass-math;
@use 'sass:meta';
@use 'sass:string';
////
/// @group utils.units
////

@use 'math.scss' as math;
@use 'strings.scss' as strings;

/// Strip the unit from a number
///
/// @param {*} $value
///
/// @example strip-unit(100px) // => 100

@function strip-unit($number) {
    @if meta.type-of($number) == 'number' and not sass-math.is-unitless($number) {
        @return math.divide($number, $number * 0 + 1);
    }
    @return $number;
}

/// Re-attach a unit to a unitless number (inverse of strip-unit). Used by
/// to-number to rebuild a value like 2px after parsing its digits.
///
/// @param {number} $number
/// @param {string} $unit
@function to-length($number, $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,
        'fr': 1fr,
        'deg': 1deg,
        'rad': 1rad,
        's': 1s,
        'ms': 1ms,
    );
    @if not map.has-key($units, $unit) {
        @error 'to-number: unknown unit `#{$unit}`.';
    }
    @return $number * map.get($units, $unit);
}

/// Convert a number-like string value to a proper number (that can be used for
/// arithmetic, etc.)
///
/// @param {*} $value
///
/// @example to-number('234.8') // => 234.8
@function to-number($value) {
    @if meta.type-of($value) == 'number' {
        @return $value;
    } @else if meta.type-of($value) != 'string' {
        $_: log('Value for `to-number` should be a number or a string.');
    }

    $result: 0;
    $digits: 0;
    $minus: string.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(sass($minus): 2; else: 1) through string.length($value) {
        $character: string.slice($value, $i, $i);

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

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

    @return if(sass($minus): -$result; else: $result);
}

/// Return true if the provided value is a number
///
/// @param {*} $value
///
/// @example is-number(4) // => true

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

/// Return true if the provided value is in rem units
///
/// @param {*} $value
///
/// @example is-rems(3rem) // => true

@function is-rems($value) {
    @return (sass-math.unit($value) == 'rem');
}

/// Return true if the provided value is in px units
///
/// @param {*} $value
///
/// @example is-px(10px) // => true

@function is-px($val) {
    @return (sass-math.unit($val) == 'px');
}

/// Return true if the provided value is a css duration / time
///
/// @param {*} $value
///
/// @example is-time(5000ms) // => true

@function is-time($value) {
    @return is-number($value) and list.index('ms' 's', sass-math.unit($value)) != null;
}

/// Return true if the provided value is a css angle
///
/// @param {*} $value
///
/// @example is-angle(45deg) // => true

@function is-angle($value) {
    @return is-number($value) and list.index('deg' 'rad' 'grad' 'turn', sass-math.unit($value)) !=
        null;
}

/// Return true if the provided value is an integer
///
/// @param {*} $value
///
/// @example is-integer(456) // => true

@function is-integer($value) {
    @return is-number($value) and sass-math.round($value) == $value;
}

/// Return true if the provided value is a relative length value (if you don't
/// know about vmin/vmax then look them up because they are cool).
///
/// @param {*} $value
///
/// @example is-relative-length(20vmin) // => true

@function is-relative-length($value) {
    @return is-number($value) and
        list.index('em' 'ex' 'ch' 'rem' 'vw' 'vh' 'vmin' 'vmax', sass-math.unit($value)) != null;
}

/// Return true if the provided value is an absolute length value
///
/// @param {*} $value
/// @example is-absolute-length(10cm) // => true

@function is-absolute-length($value) {
    @return is-number($value) and list.index('cm' 'mm' 'in' 'px' 'pt' 'pc', sass-math.unit($value))
        != null;
}

/// Return true if the provided value is an absolute length value
///
/// @param {*} $value
///
/// @example is-percentage(100%) // => true

@function is-percentage($value) {
    @return is-number($value) and sass-math.unit($value) == '%';
}

/// Return true if the provided value is a length
///
/// @param {*} $value
///
/// @example is-length(100%) // => true

@function is-length($value) {
    @return is-relative-length($value) or is-absolute-length($value) or is-percentage($value);
}

/// Return true if the provided value is a string
///
/// @param {*} $value
///
/// @example is-string('Hello, world') // => true

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

/// Return true if the provided value is a map
///
/// @param {*} $value
///
/// @example is-map((red: #f00, blue: #00f)) // => true

@function is-map($value, $strict: false) {
    @return meta.type-of($value) == 'map' or
        (not $strict and is-list($value) and list.length($value) == 0);
}

/// Return true if the provided value is a string
///
/// @param {*} $value
///
/// @example is-list((1 2 3)) // => true

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

/// Return true if the provided value is a color
///
/// @param {*} $value
///
/// @example is-color(blue) // => true
/// @example is-color(#444) // => true
/// @example is-color(5) // => false

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

/// Provide an alternate value when $value is null
///
/// @param {*} $value
/// @param {*} $fallback
///
/// @example if-null(10, 20) // => 10
/// @example if-null(null, 20) // => 20

@function if-null($value, $fallback) {
    @if ($value != null) {
        @return $value;
    }
    @return $fallback;
}

/// Choose a value based on whether another value is a string or not
///
/// @param {*} $value
/// @param {*} $trueval
/// @param {*} $falseval
///
/// @example if-string(10, 'hello', 20) // => 20
/// @example if-string('tree', 'hello', 20) // => 'hello'

@function if-string($value, $trueval, $falseval) {
    @if (is-string($value)) {
        @return $trueval;
    }
    @return $falseval;
}

/// Choose a value based on whether another value is a string or not
///
/// @param {*} $value
/// @param {*} $trueval
/// @param {*} $falseval
///
/// @example if-number(10, 'hello', 20) // => "hello"

@function if-number($value, $trueval, $falseval) {
    @if (is-number($value)) {
        @return $trueval;
    }
    @return $falseval;
}

/// Choose a value based on whether another value is a string or not
///
/// @param {*} $value
/// @param {*} $trueval
/// @param {*} $falseval
///
/// @example if-list(10, 'hello', 20) // => 20
/// @example if-list((10, 20), 'hello', 20) // => 'hello'
///
/// @todo Robby?
@function if-list($value, $trueval, $falseval) {
    @if (is-list($value)) {
        @return $trueval;
    }
    @return $falseval;
}

/// Choose a value based on whether another value is a string or not
///
/// @param {*} $value
/// @param {*} $trueval
/// @param {*} $falseval
///
/// @example if-map((10, 20), 'hello', 20) // => 20
/// @example if-map((color: red), 'hello', 20) // => 'hello'

@function if-map($value, $trueval, $falseval) {
    @if (is-map($value)) {
        @return $trueval;
    }
    @return $falseval;
}

/// Choose a value based on whether another value is a color or not
///
/// @param {*} $value
/// @param {*} $trueval
/// @param {*} $falseval
///
/// @example if-color(pink, #fefe, 'not a color') // => #fefe

@function if-color($value, $trueval, $falseval) {
    @if (is-color($value)) {
        @return $trueval;
    }
    @return $falseval;
}

/// Convert any valid four-part value (like those used for margin or padding) into a map with
/// the correct values for top, right, bottom, left)
///
/// @param {*} $value
///
/// @example get-fourpart(2px) => (top: 2px, right: 2px, bottom: 2px, left: 2px)
/// @example get-fourpart(2px 4px) => (top: 2px, right: 4px, bottom: 2px, left: 4px)
/// @example get-fourpart(2px 4px 8px) => (top: 2px, right: 4px, bottom: 8px, left: 4px)
/// @example get-fourpart(2px 4px 8px 16px) => (top: 2px, right: 4px, bottom: 8px, left: 16px)
///

@function get-fourpart($value) {
    @if (meta.type-of($value) == 'list') {
        $length: list.length($value);
        // A single-element list has no nth(2) to fall back to for `left`;
        // treat it like a scalar (same value on all sides).
        @if ($length == 1) {
            $v: to-number(list.nth($value, 1));
            @return (top: $v, right: $v, bottom: $v, left: $v);
        }
        @return (
            top: to-number(list.nth($value, 1)),
            right: to-number(if(sass($length > 1): list.nth($value, 2) ; else: list.nth($value, 1))),
            bottom: to-number(
                    if(sass($length > 2): list.nth($value, 3) ; else: list.nth($value, 1))
                ),
            left: to-number(if(sass($length > 3): list.nth($value, 4) ; else: list.nth($value, 2)))
        );
    } @else if (meta.type-of($value) == 'string') {
        $list: strings.str-split($value, ' ');

        @if (meta.type-of($list) == 'list') {
            @return get-fourpart($list);
        }

        $value: to-number($value);
        @return (top: $value, right: $value, bottom: $value, left: $value);
    } @else {
        // single number/length — same value on all sides
        @return (top: $value, right: $value, bottom: $value, left: $value);
    }
}

/// Convenience function to just retrieve the left value from get-fourpart
///
/// @param {*} $value
///
/// @example get-fourpart-left(10px) // => 10px
@function get-fourpart-left($value) {
    @return map.get(get-fourpart($value), left);
}

/// Convenience function to just retrieve the top value from get-fourpart
///
/// @param {*} $value
/// @example get-fourpart-top(10px) // => 10px
@function get-fourpart-top($value) {
    @return map.get(get-fourpart($value), top);
}

/// Convenience function to just retrieve the bottom value from get-fourpart
///
/// @param {*} $value
/// @example get-fourpart-bottom(10px) // => 10px
@function get-fourpart-bottom($value) {
    @return map.get(get-fourpart($value), bottom);
}

/// Convenience function to just retrieve the right value from get-fourpart
///
/// @param {*} $value
/// @example get-fourpart-right(10px) // => 10px
@function get-fourpart-right($value) {
    @return map.get(get-fourpart($value), right);
}

/// Extract the width from a shorthand border css value
///
/// @param {*} $input
///
/// @example get-border-width(2px solid red) // => 2px

@function get-border-width($input) {
    @each $part in $input {
        @if meta.type-of($part) == number {
            @return $part;
        }
    }
    @return 0;
}

/// Extract the style from a shorthand border css value
///
/// @param {*} $input
///
/// @example get-border-style('2px solid red') // => solid

@function get-border-style($input) {
    @each $part in $input {
        @if meta.type-of($part) == string {
            @return $part;
        }
    }
    @return null;
}

/// Extract the style from a shorthand border css value
///
/// @param {*} $input
///
/// @example get-border-color('2px solid red') // => red

@function get-border-color($input) {
    @each $part in $input {
        @if meta.type-of($part) == color {
            @return $part;
        }
    }
    @return null;
}

/// Converts an aspect string (like '16:9') to a number (the equivalent of 16/9)
///
/// @param {string | number} $aspect The string representation of the aspect (should be two numbers
/// separated by a colon, e.g. '16:9'). Alternatively, if the input is a number then it will be
/// returned directly. This makes it possible to use the function as a safety where a number is
/// expected but a string could be used for convenience.
///
/// @example $aspect: aspect-to-number('16:9') // => 1.77777778;
/// @example $aspect: aspect-to-number(1.77778) // => 1.77778

@function aspect-to-number($aspect) {
    @if (is-number($aspect)) {
        @return $aspect;
    }
    $split: strings.str-split($aspect, ':');
    $w: to-number(list.nth($split, 1));
    $h: to-number(list.nth($split, 2));
    @return math.divide($w, $h);
}
