////
/// @group utils.collections
////

@use 'json.scss' as *;
@use 'strings.scss' as *;
@use 'units.scss' as *;

/// Get a value from a map if the key exists, otherwise return a fallback
///
/// @example map-get-default((a:1, b:2), a, 3) => 1
/// @example map-get-default((a:1, b:2), wontwork, 3) => 3

@function map-get-default($map, $key, $default) {
    @if (map-has-key($map, $key)) {
        @return map-get($map, $key);
    } @else {
        @return $default;
    }
}

/// Deep lookup for map values, each parameter represents the next level lookup
/// @param {map} $map - Map
/// @param {arglist} $keys - Key chain
/// @example map-deep-get((a:1, b:2), b) // => 2

@function map-deep-get($map, $keys...) {
    @each $key in $keys {
        $map: map-get($map, $key);
    }
    @return $map;
}

// /// Get just the keys from a map (similar to JavaScript's Object.keys)
// /// @param {*} $map

// @function map-keys($map) {
//     $keys: ();
//     @each $key, $val in $map {
//         $keys: append($keys, $key);
//     }
//     @return $keys;
// }

// /// Get just the keys from a map (similar to JavaScript's Object.values)
// /// @param {*} $map

// @function map-values($map) {
//     $vals: ();
//     @each $key, $val in $map {
//         $vals: append($vals, $val);
//     }
//     @return $vals;
// }

/// If $list has an $nth element, return that element. Otherwise return $fallback.
///@example nth-if-length((a, b, c), 1) // => a
@function nth-if-length($list, $n, $fallback: null) {
    @if ($n <= length($list)) {
        @return nth($list, $n);
    }
    @return $fallback;
}

/// If $list-or-val is a list, return the nth value; if the item is a value
/// return the value; if the value is null return the default
///
/// @example nth-or-val((a, b, c), 1, d) => a
/// @example nth-or-val(a, 1, d) => a
/// @example nth-or-val(null, 1, d) => d

@function nth-or-val($list-or-value, $n, $default: null) {
    @if (not $list-or-value) {
        @return $default;
    }
    @if (type-of($list-or-value) == 'list') {
        @return nth($list-or-value, $n);
    }
    @return $list-or-value;
}

/// Return a copy of a list with the nth value removed
/// @example list-remove((a, b, c), 1) // => b, c

@function list-remove($list, $n) {
    $result: ();
    $n: if($n < 0, length($list) + $n + 1, $n);
    $bracketed: is-bracketed($list);
    $separator: list-separator($list);
    @for $i from 1 through length($list) {
        @if $i != $n {
            $result: append($result, nth($list, $i));
        }
    }
    @return join((), $result, $separator, $bracketed);
}

/// Performas a find and replace of items within a list
///
/// @param {list} $list The list on which the replacement will be performed
/// @param {map} $replacements A map of the search/replacent values, e.g. (searchForThis: newKey)
/// @example list-replace((a, b, c), (c: d)) // => a, b, d

@function list-replace($list, $replacements) {
    $result: ();

    @each $item in $list {
        $new-item: map-get-default($replacements, $item, $item);
        $result: append($result, $new-item);
    }

    @return $result;
}

/// Returns true if the list contains a value, otherwise false
/// @example list-contains((a, b, c), c) // => true

@function list-contains($list, $val) {
    @return index($list, $val) != null;
}

/// Return a copy of a list sorted numerically
/// @example list-sort-num(2, 4, 1, 5, 3) // => 1 2 3 4 5

@function list-sort-num($list) {
    $sortedlist: ();
    @while length($list) > 0 {
        $value: nth($list, 1);
        @each $item in $list {
            @if $item < $value {
                $value: $item;
            }
        }
        $sortedlist: append($sortedlist, $value, 'space');
        $list: list-remove($list, index($list, $value));
    }
    @return $sortedlist;
}

/// Return a copy of a map sorted by the value of each entry
/// @example map-sort-by-values((a:1, b:2, d:4, c:3))

@function map-sort-by-values($map) {
    $keys: ();
    $values: ();
    $sortedMap: ();

    @each $key, $val in $map {
        $keys: append($keys, $key);
        $values: append($values, $val);
    }

    $list: zip($keys, $values);

    @while length($list) > 0 {
        // find smallest pair
        $smallestPair: nth($list, 1);
        @each $pair in $list {
            $value: nth($pair, 2);
            $smallestValue: nth($smallestPair, 2);
            @if $value < $smallestValue {
                $smallestPair: $pair;
            }
        }

        // add smallest pair to sorted map
        $key: nth($smallestPair, 1);
        $value: nth($smallestPair, 2);
        $sortedMap: map-merge(
            $sortedMap,
            (
                $key: $value,
            )
        );

        // remove from list smallest pair
        $smallestPairIndex: index($list, $smallestPair);
        $newList: ();
        @for $i from 1 through length($list) {
            @if $i != $smallestPairIndex {
                $newList: append($newList, nth($list, $i), 'space');
            }
        }
        $list: $newList;
    }

    @return $sortedMap;
}

/// Creates a new list with the results of calling a function once for every
/// item in this list. Equivalent to Array.map in JavaScript.
///
/// @param {List} $list the list to which the function is being applied
///
/// @param {Function} $fn reference to a function(item, index) that will be
/// applied to every item.
///
/// @example
///   @function square($x, $i) { @return $x * $x }
///   list-map(1 2 3 4, square); // => 1 4 9 16
///
@function list-map($list, $fn) {
    @if length($list) == 0 {
        @return ();
    }
    $new-list: $list;
    @for $i from 1 through length($list) {
        $new-list: set-nth($new-list, $i, call($fn, nth($list, $i)));
    }
    @return $new-list;
}

/// Creates a new list with all items that pass the test implemented by the
/// provided function. Similar to Array.filter in JavaScript.
///
/// @param {List} $list the list being filtered
/// @param {Function} $fn reference to a filtering function
///
/// @example
///   @function aboveTen($x) { @return x > 10 }
///   list-filter(2 20 30 3 4 100, aboveTen); // => (20 30 100)

@function list-filter($list, $fn) {
    $new-list: ();
    @each $item in $list {
        @if call($fn, $item) {
            $new-list: append($new-list, $item);
        }
    }
    @return $new-list;
}

/// Merge multiple maps into a single map
/// @param {*} $maps... One or more maps that should be merged into a single map.
/// @example map-collect()
@function map-collect($maps...) {
    $collection: ();

    @each $map in $maps {
        $collection: map-merge($collection, $map);
    }
    @return $collection;
}
