<?php

namespace Planaday\Utility\DataGrouping;

use ArrayAccess;
use Countable;
use IteratorAggregate;
use Traversable;

/**
 * @template TKey
 * @template TValue
 *
 * @implements ArrayAccess<TKey, TValue>
 * @implements IteratorAggregate<TKey, TValue>
 */
class Map implements ArrayAccess, Countable, IteratorAggregate{
	/** @var TValue[] */
	protected array $contents = [];
	/** @var TKey[] */
	protected array $keys = [];

	/**
	 * @return Traversable<TKey, TValue>
	 */
	public function getIterator(): Traversable {
		foreach ( $this->contents as $key => $value ) {
			yield ($this->keys[$key] ?? $key) => $value;
		}
	}

	/**
	 * @param TKey|int $offset
	 */
	public function offsetExists( mixed $offset ): bool {
		$index = $this->getIndex($offset);
		if ( $index === null ) return false;
		return isset( $this->contents[$index] );
	}

	/**
	 * @param TKey|int $offset
	 *
	 * @return TValue|null
	 */
	public function offsetGet( mixed $offset ): mixed {
		$index = $this->getIndex($offset);
		if ( $index === null ) return null;
		return $this->contents[$index];
	}

	/**
	 * @param TKey|int $offset
	 * @param TValue $value
	 */
	public function offsetSet( mixed $offset, mixed $value ): void {
		$index = $this->getIndex($offset);

		if ( $index !== null ) {
			$this->contents[$index] = $value;
			return;
		}

		$this->keys[] = $offset;
		$this->contents[] = $value;
	}

	/**
	 * @param TKey|int $offset
	 */
	public function offsetUnset( mixed $offset ): void {
		$index = $this->getIndex($offset);
		if ( $index === null ) return;
		unset( $this->contents[$index] );
		unset( $this->keys[$index] );
	}

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

	/**
	 * @param TKey|int $key
	 *
	 * @return int|null
	 */
	public function getIndex( mixed $key ): int|null {
		if ( is_int( $key ) ) {
			return $key;
		}

		$index = array_search( $key, $this->keys, true );

		return $index === false ? null : $index;
	}

	/**
	 * @template TValueFrom
	 * @template TKeyFrom
	 * @param array<TKeyFrom, TValueFrom> $array
	 *
	 * @return self<TKeyFrom, TValueFrom>
	 */
	public static function fromArray( array $array ): self {
		return self::fromIterable( $array );
	}

	/**
	 * @template TValueFrom
	 * @template TKeyFrom
	 * @param iterable<TKeyFrom, TValueFrom> $array
	 *
	 * @return self<TKeyFrom, TValueFrom>
	 */
	public static function fromIterable( iterable $array ): self {
		$map = new self();

		foreach ( $array as $key => $value ) {
			$map[$key] = $value;
		}

		return $map;
	}

	public function getKeys(): array {
		return $this->keys;
	}

	public function getValues(): array {
		return $this->contents;
	}
}