<?php

/**
 * SCSSPHP
 *
 * @copyright 2012-2020 Leaf Corcoran
 *
 * @license http://opensource.org/licenses/MIT MIT
 *
 * @link http://scssphp.github.io/scssphp
 */

namespace ScssPhpRBE\ScssPhp\Collection;

use ScssPhpRBE\ScssPhp\Value\Value;

/**
 * A map using Sass values as keys based on Value::equals.
 *
 * The map can be either modifiable or unmodifiable. For unmodifiable
 * maps, all mutators will throw a LogicException.
 *
 * Iteration preserves the order in which keys have been inserted.
 *
 * @template T
 * @template-implements \IteratorAggregate<Value, T>
 */
final class Map implements \Countable, \IteratorAggregate
{
    /**
     * @var bool
     */
    private $modifiable = true;

    // TODO implement a better internal storage to allow reading keys in O(1).

    /**
     * @var array<int, array{Value, T}>
     */
    private $pairs = [];

    /**
     * Returns a modifiable version of the Map.
     *
     * @template V
     * @param Map<V> $map
     *
     * @return Map<V>
     */
    public static function of(Map $map): Map
    {
        $modifiableMap = clone $map;
        $modifiableMap->modifiable = true;

        return $modifiableMap;
    }

    /**
     * Returns an unmodifiable version of the Map.
     *
     * All mutators will throw a LogicException when trying to use them.
     *
     * @template V
     * @param Map<V> $map
     *
     * @return Map<V>
     */
    public static function unmodifiable(Map $map): Map
    {
        if (!$map->modifiable) {
            return $map;
        }

        $unmodifiableMap = clone $map;
        $unmodifiableMap->modifiable = false;

        return $unmodifiableMap;
    }

    public function getIterator(): \Traversable
    {
        foreach ($this->pairs as $pair) {
            yield $pair[0] => $pair[1];
        }
    }

    public function count(): int
    {
        return \count($this->pairs);
    }

    /**
     * The value for the given key, or `null` if $key is not in the map.
     *
     * @return T|null
     */
    public function get(Value $key)
    {
        foreach ($this->pairs as $pair) {
            if ($key->equals($pair[0])) {
                return $pair[1];
            }
        }

        return null;
    }

    /**
     * Associates the key with the given value.
     *
     * If the key was already in the map, its associated value is changed.
     * Otherwise the key/value pair is added to the map.
     *
     * @param T $value
     */
    public function put(Value $key, $value): void
    {
        $this->assertModifiable();

        foreach ($this->pairs as $i => $pair) {
            if ($key->equals($pair[0])) {
                $this->pairs[$i][1] = $value;

                return;
            }
        }

        $this->pairs[] = [$key, $value];
    }

    /**
     * Removes $key and its associated value, if present, from the map.
     *
     * Returns the value associated with `key` before it was removed.
     * Returns `null` if `key` was not in the map.
     *
     * Note that some maps allow `null` as a value,
     * so a returned `null` value doesn't always mean that the key was absent.
     *
     * @return T|null
     */
    public function remove(Value $key)
    {
        $this->assertModifiable();

        foreach ($this->pairs as $i => $pair) {
            if ($key->equals($pair[0])) {
                unset($this->pairs[$i]);

                return $pair[1];
            }
        }

        return null;
    }

    private function assertModifiable(): void
    {
        if (!$this->modifiable) {
            throw new \LogicException('Mutating an unmodifiable Map is not supported. Use Map::of to create a modifiable copy.');
        }
    }
}
