@use 'sass:meta';
@use 'sass:map';
@use 'sass:list';

/// Designed to improve on the built-in `map.merge()` (formerly `map-merge()`
/// functionality so that it is recursive and able to merge more than 2 maps at
/// once.
///
/// About `map.merge()`:
/// * only takes 2 arguments
/// * is not recursive
///
/// @param {Map} $object - First map
/// @param {ArgList} $objects - Other maps
/// @param {Bool} $deep - Optional recursive mode
///
/// @return {Map} The merged map
///
/// @access public
/// @group Utilities
///
/// @throw Invalid $object type, not of type 'map'.
/// @throw Invalid type in $objects, not of type 'map'
/// @throw Invalid $deep type, not of type 'bool'
@function extend-map($object, $objects... /*, $deep */) {
  @if meta.type-of($object) != 'map' {
    @error 'Invalid $object type passed to the [ extend-map() ] function. ' +
        'The object passed via $object passed must be a map.';
  }

  @each $cur-obj in $objects {
    @if meta.type-of($cur-obj) != 'map' {
      @error 'Invalid type in $objects passed to the [ extend-map() ] function. ' +
          'Objects passed to $objects must be maps.';
    }
  }

  $last: list.nth($objects, -1);
  $deep: $last == true;
  $max: if($deep, list.length($objects) - 1, list.length($objects));

  // Loop through all maps in $objects...
  @for $i from 1 through $max {
    // Store current map
    $current: list.nth($objects, $i);

    // If not in deep mode, simply merge current map with object
    @if not $deep {
      $object: map.merge($object, $current);
    } @else {
      @if meta.type-of($deep) != 'bool' {
        @error 'Invalid $deep value of `#{meta.inspect($deep)}` passed to the ' +
            '[ extend-map() ] function. If you pass a value for the $deep ' +
            'argument, it must be a Bool value of either `true` or `false`.';
      }
      @each $key, $value in $current {
        // If value is a nested map and the same key from $object
        // is a nested map as well
        @if type-of($value) == 'map' and
            type-of(map.get($object, $key)) == 'map'
        {
          // Recursive extend
          $value: extend(map.get($object, $key), $value, true);
        }

        // Merge current tuple with object
        $object: map.merge(
          $object,
          (
            $key: $value
          )
        );
      }
    }
  }

  @return $object;
}

/// Designed to improve on the built-in `map.merge()` (formerly `map-merge()`
/// functionality so that it is recursive and able to merge more than 2 maps at
/// once.
///
/// About `map.merge()`:
/// * only takes 2 arguments
/// * is not recursive
///
/// @param {Map} $object - First map
/// @param {ArgList} $objects - Other maps
/// @param {Bool} $deep - Optional recursive mode
///
/// @return {Map} The merged map
///
/// @access public
/// @group Utilities
/// @require {function} extend-map
///
/// @throw Invalid $object type, not of type 'map'.
/// @throw Invalid type in $objects, not of type 'map'
/// @throw Invalid $deep type, not of type 'bool'
/// @alias extend-map
@function extend($object, $objects... /*, $deep */) {
  @if meta.type-of($object) != 'map' {
    @error 'Invalid $object type passed to the [ extend() ] function. ' +
        'Objects passed must be maps.';
  }

  $last: list.nth($objects, -1);
  $deep: $last == true;
  $max: if($deep, list.length($objects) - 1, list.length($objects));

  // Loop through all maps in $objects...
  @for $i from 1 through $max {
    // Store current map
    $current: list.nth($objects, $i);

    @if meta.type-of($current) != map {
      @error 'Invalid $objects type of #{meta.type-of($cur-obj)} passed to the [ extend-map() ] function. ' +
          'Objects passed must be maps.';
    }

    // If not in deep mode, simply merge current map with object
    @if not $deep {
      $object: map.merge($object, $current);
    } @else {
      @each $key, $value in $current {
        @if meta.type-of($deep) != 'bool' {
          @error 'Invalid $deep value of `#{meta.inspect($deep)}` passed to the ' +
              '[ extend() ] function. If you pass a value for the $deep ' +
              'argument, it must be a Boolean.';
        }

        // If value is a nested map and the same key from $object
        // is a nested map as well
        @if type-of($value) == 'map' and
            type-of(map.get($object, $key)) == 'map'
        {
          // Recursive extend
          $value: extend(map.get($object, $key), $value, true);
        }

        // Merge current tuple with object
        $object: map.merge(
          $object,
          (
            $key: $value
          )
        );
      }
    }
  }

  @return $object;
}
