@function __pop($list) {
  $length: length($list);
  $index: 2;
  $popped-list: ();

  @while $index <= $length {
    $popped-list: append($popped-list, nth($list, $index));
    $index: $index + 1;
  }

  @return $popped-list;
}

@function __list-get($list, $index) {
  $index: __parse-float($index);

  @if type-of($index) != "number" or $index < 1 {
    @return $__undefined__;
  }

  @if $index > length($list) {
    @return $__undefined__;
  }

  @return nth($list, $index);
}

@function __string-get($string, $index, $default: null) {
  $index: __parse-float($index);

  @if type-of($index) != "number" {
    @error '[__string-get] $index #{inspect($index)} for $string #{inspect($string)} is not a number.';
    @return $default;
  }

  @if $index > str-length($string) {
    @return $default;
  }

  @return str-slice($string, $index, $index);
}

@function __to-keys($keys, $map) {
  @if type-of($keys) == "string" {
    @if not(type-of($map) == "map" and map-has-key($map, $keys)) {
      $keys: __string-split($keys, ".");
    }
  }

  @return $keys;
}

@function __get($map, $keys, $default: null) {
  $value-type: type-of($map);

  $original-key: $keys;
  $keys: __to-keys($keys, $map);

  @if $value-type == "string" {
    @return __string-get($map, nth($keys, 1), $default);
  }

  @if $value-type != "list" and $value-type != "map" {
    @return $default;
  }

  @if $value-type == "list" {
    $index: nth($keys, 1);

    $map: __list-get($map, $index);
  } @else if $value-type == "map" {
    // Check if map has original key
    @if map-has-key($map, $original-key) {
      @return map-get($map, $original-key);
    }

    $key: nth($keys, 1);

    @if map-has-key($map, $key) {
      $map: map-get($map, nth($keys, 1));
    } @else {
      @return $default;
    }
  }

  $keys: __pop($keys);

  @if __is-falsey($map) and length($keys) > 0 {
    @return $default;
  }

  @if length($keys) == 0 {
    @return if(__is-undefined($map), $default, $map);
  }

  @return __get($map, $keys, $default);
}

@function __set-list($list, $index, $value) {
  $length: length($list);

  @if type-of($index) != "number" {
    @error '[__set-list] $index #{$index} for list #{inspect($list)} is not a number.';
    @return null;
  }

  @if $index <= $length {
    @return set-nth($list, $index, $value);
  }

  @while $index > $length + 1 {
    $list: append($list, null);

    $index: $index - 1;
  }

  $list: append($list, $value);

  @return $list;
}

@function __set($map, $keys, $value) {
  $property: nth($keys, 1);
  $keys: __pop(__to-keys($keys, $map));
  $child-map: null;

  @if type-of($map) == "list" and length($map) > 0 {
    @if $property > length($map) {
      $child-map: ();
    } @else {
      $child-map: nth($map, $property);
    }
  } @else if map-has-key($map, $property) {
    $child-map: map-get($map, $property);
  } @else {
    $child-map: ();
  }

  @if length($keys) == 0 {
    @if type-of($map) == "list" and length($map) > 0 {
      @return __set-list($map, $property, $value);
    }

    @return map-merge(
      $map,
      (
        $property: $value
      )
    );
  }

  @if type-of($map) == "list" and length($map) > 0 {
    @return __set-list($map, $property, __set($child-map, $keys, $value));
  }

  @return map-merge(
    $map,
    (
      $property: __set($child-map, $keys, $value)
    )
  );
}

@function __push($map, $properties, $value) {
  $list: append(__get($map, $properties), $value);

  @return __set($map, $properties, $list);
}

@function __new($type, $params: (), $extends: false, $defaults: ()) {
  $instance-meta: (
    "_type": $type,
    "_extends": $extends
  );

  $instance: ();

  @if $defaults {
    $instance: map-merge($instance, $defaults);
  }

  @if $extends {
    $instance: map-merge($instance, __new($extends, $params));
  }

  $instance: map-merge($instance, call(get_function($type), $params...));
  $instance: map-merge($instance, $instance-meta);

  @return $instance;
}

@function __instance-of($value, $constructor) {
  @if not type-of($value) == "map" {
    @return type-of($value) == $constructor;
  }

  @return __get($value, "_type") == $constructor;
}

/// Gets the value of a source `$value` from the specified key, or list
/// of keys for deep map retrieval, or `null` if any of the keys are
/// undefined.
///
/// If `$value` is list-like, indexes are used as keys.
///
///
/// @access public
/// @group Utility
/// @param {Map|List|String|Arglist} $value - Source value.
/// @param {*|List} $keys - Single key or list of keys for value retrieval
/// @returns {*} Returns the retrieved value.
/// @example scss
/// $palette: (
///     'primary': (
///         'default': red,
///         'dark': darken(red, 10%)
///     )
/// );
///
/// $color-primary: _get($palette, 'primary' 'default');
/// // => red
///
/// $color-dark: _get($palette, 'primary.dark');
/// // => #CC0000

@function _get($args...) {
  @return call(get-function("__get"), $args...);
}

/// Sets the value of a source `$value` at the specified key, or list
/// of keys for deep map setting.
///
/// If `$value` is list-like, indexes are used as keys.
///
///
/// @access public
/// @group Utility
/// @param {Map|List|String|Arglist} $value - Source value.
/// @param {*|List} $keys - Single key or list of keys for value setting
/// @returns {*} Returns a new `$value` with the set value.
/// @example scss
/// $palette: (
///     'primary': (
///         'default': red,
///         'dark': darken(red, 10%)
///     )
/// );
///
/// $palette: _set($palette, 'primary' 'default', blue);
/// // => (
/// //     'primary': (
/// //         'default': blue,
/// //         'dark': darken(red, 10%)
/// //     )
/// // );

@function _set($args...) {
  @return call(get-function("__set"), $args...);
}

@function _new($args...) {
  @return call(get-function("__new"), $args...);
}

@function _instance-of($args...) {
  @return call(get-function("__instance-of"), $args...);
}
