@use 'sass:list';
@use 'sass:map';
@use 'sass:meta';
@use 'sass:string';
////
/// @group utils.collections
////

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

/// A function that retrieves a "deep" value from a map using path syntax where key names are
/// separated by periods.
///
/// @param {map} $map An existing map that serves as the lookup source
///
/// @param {string} $path The path where the value is found, expressed as a list of keys separated
/// by commas (e.g. 'colors.accent')
///
/// @param {*} $fallback [null] An optional fallback value that will be returned in the value isn't
/// found
///
/// @example
/// $color: flat-get((colors: (somecolor: #F00)), 'colors.somecolor')
/// ==> #F00

@function flat-get($map, $path, $fallback: null) {
    // TEMPORARY: replace colons with periods for backwards compatibility
    @if (string.index($path, ':') != null) {
        @warn 'Using colons in flat-maps is deprecated. Use periods instead. (#{$path})';
        $path: str-replace($path, ':', '.');
    }

    $keys: str-split($path, '.');
    $found: false;

    @each $key in $keys {
        // Guard the type: once a path segment resolves to a non-map, map.has-key
        // would error. Treat "can't descend further" as not-found → fallback.
        @if (meta.type-of($map) == 'map' and map.has-key($map, $key)) {
            $map: map.get($map, $key);
            $found: true;
        } @else {
            @return $fallback;
        }
    }

    @if (not $found) {
        @return $fallback;
    }

    @return $map;
}

/// A function that allows you to set a "deep" value in a map using path syntax where key names are
/// separated by periods.
///
/// @param {map} $map An existing map that serves as the basis for the returned value
///
/// @param {string} $path The path where the value should be set, expressed as a list of keys
/// separated by perods (e.g. 'colors.accent')
///
/// @param {*} $val The value to be set at the path
///
/// @return {map} A new map with the value set (all intermediate objects will be created and values
/// overwritten as needed)
///
/// @example
/// $map: flat-set((), 'colors.somecolor', #F00) // ==> (colors: (somecolor: #F00))
///
/// @example
/// $map: flat-set((colors: (clr1: #F00)), 'colors.clr2', #00F) // ==> {colors: {clr1: #F00, clr2: #00F}}

@function flat-set($map, $path, $val, $merge-maps: false) {
    // TEMPORARY: replace colons with periods for backwards compatibility
    @if (string.index($path, ':') != null) {
        @warn 'Using colons in flat-maps is deprecated. Use periods instead. (#{$path})';
        $path: str-replace($path, ':', '.');
    }

    $map: if-null($map, ());
    $keys: str-split($path, '.');
    $key: list.nth($keys, 1);

    $next-path: list-join(list-remove($keys, 1), '.');
    $merge: ();

    @if (is-map($val)) {
        $val: map-keys-ensure-str($val);
    }

    @if (map.has-key($map, $key)) {
        $existing-value: map.get($map, $key);
        $merge: if(sass(is-map($existing-value)): $existing-value; else: $merge);
    }

    @if (list.length($keys) > 1) {
        $val: flat-set($merge, $next-path, $val);
    }

    @if ($merge-maps and is-map($merge) and is-map($val)) {
        $val: map.merge($merge, $val);
    }

    @return map.merge(
        $map,
        (
            $key: $val,
        )
    );
}

/// Same as flat-set, but will not overwrite a value if it already exists
///
/// @example flat-default((colors: (clr1: #F00)), 'colors:clr2', #00F) // => {colors: {clr1: #F00, clr2: #00F}}
///
/// @param {*} $map The flat map that will be modified
/// @param {*} $path The path of the new value
/// @param {*} $val The new value
/// @param {*} $merge-maps [false] Determines what to do if the key refers to a value that is already a map. See example of both behaviors below.
///
/// @example
///     // example with $merge-maps true (not that the result is merged)
///     $map: (config: (bluecolor: blue));
///     $result: flat-map($map, 'config:colors', (redcolor: red), true); => (config:(colors:(bluecolor:blue, redcolor:red)))
///
/// @example
///     // example with $merge-maps false (not that the result is replaced)
///     $map: (config: (bluecolor: blue));
///     $result: flat-map($map, 'config:colors', (redcolor: red), true); => (config:(colors:(redcolor:red)))
///
/// @todo need to document the $merge-maps parameter (also for other functions in flat-map)

@function flat-default($map, $path, $val, $merge-maps: false) {
    $current: flat-get($map, $path, $fallback: 'NOTFOUND');
    @if ($current == 'NOTFOUND') {
        @return flat-set($map, $path, $val, $merge-maps);
    }
    @return $map;
}

/// A function that flattens a map to it's "flat" equivalent where every key is replaced with a deep path
/// (see examples) into the original map.
///
/// @param {map} $map The map to be flattened
///
/// @example
///     flatten-map((colors: (clr1: #f00, clr2: #00f)))
///     ==> ("colors:clr1": #f00, "colors:clr2": #00f)

@function flatten-map($map) {
    $flat: ();
    @each $key, $value in $map {
        @if (meta.type-of($value) == 'map') {
            $fmap: flatten-map($value);
            @each $fkey, $fvalue in $fmap {
                $flat: map.merge($flat, (#{$key + '.' + $fkey}: $fvalue));
            }
        } @else {
            $flat: map.merge(
                $flat,
                (
                    $key: $value,
                )
            );
        }
    }
    @return $flat;
}

/// A function that restores a "flat" map (like one produced by the flatten-map function) to a
/// normal deep map.
///
/// @param {map} $map The map to be unflattened
///
/// @example
/// unflatten-map(("colors:clr1": #f00, "colors:clr2": #00f))
/// ==> (colors: (clr1: #f00, clr2: #00f))

@function unflatten-map($map) {
    $unflat: ();
    @each $key, $value in $map {
        $unflat: flat-set($unflat, $key, $value);
    }
    @return $unflat;
}

/// A function which will flatten and then merge two maps. This is not the same as a deep merge
/// because it allows for keys in one (or both) of the maps to be complete paths. If there are
/// conflicts, last in list wins.
///
/// @example
/// flatten-map-merge((colors: (clr1: #f00)), ('colors:clr2': #00f))
///     -> (colors: (clr1: #f00, clr2: #00f))
///
/// @example
/// flat-merge(('clr:main': blue),('clr:main': red));
///     -> (clr: (main: red))

@function flat-merge($maps...) {
    $flat-maps: ();
    @each $map in $maps {
        @if ($map) {
            $flat-maps: list.append($flat-maps, flatten-map($map));
        }
    }

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

    @return unflatten-map($flat-collection);
}

/// Returns true if the provided map, once flattened, contains the deep path $key;
///
/// @param {map} $map
/// @param {string} $key
///
/// @example flat-has-key((colors: (clr1: #f00, clr2: #00f)), 'colors:clr2') => true

@function flat-has-key($map, $key) {
    $val: flat-get($map, $key, 'INVALID_FALLBACK');
    @return $val != 'INVALID_FALLBACK';
}
