<?php

declare(strict_types=1);

namespace Intervention\Image;

use Intervention\Image\Interfaces\CollectionInterface;
use ArrayIterator;
use Countable;
use Traversable;
use IteratorAggregate;

/**
 * @implements IteratorAggregate<int|string, mixed>
 */
class Collection implements CollectionInterface, IteratorAggregate, Countable
{
    /**
     * Create new collection object.
     *
     * @param array<int|string, mixed> $items
     */
    public function __construct(protected array $items = [])
    {
        //
    }

    /**
     * Static constructor.
     *
     * @param array<int|string, mixed> $items
     * @return self<int|string, mixed>
     */
    public static function create(array $items = []): self
    {
        return new self($items);
    }

    /**
     * {@inheritdoc}
     *
     * @see CollectionInterface::has()
     */
    public function has(int|string $key): bool
    {
        return array_key_exists($key, $this->items);
    }

    /**
     * {@inheritdoc}
     *
     * @see CollectionInterface::set()
     */
    public function set(int|string $key, mixed $item): self
    {
        $this->items[$key] = $item;

        return $this;
    }

    /**
     * Returns Iterator.
     *
     * @return Traversable<int|string, mixed>
     */
    public function getIterator(): Traversable
    {
        return new ArrayIterator($this->items);
    }

    /**
     * {@inheritdoc}
     *
     * @see CollectionInterface::toArray()
     */
    public function toArray(): array
    {
        return $this->items;
    }

    /**
     * Count items in collection.
     */
    public function count(): int
    {
        return count($this->items);
    }

    /**
     * Append new item to collection.
     *
     * @return CollectionInterface<int|string, mixed>
     */
    public function push(mixed $item): CollectionInterface
    {
        $this->items[] = $item;

        return $this;
    }

    /**
     * Return first item in collection.
     */
    public function first(): mixed
    {
        if (count($this->items) === 0) {
            return null;
        }

        return reset($this->items);
    }

    /**
     * Returns last item in collection.
     */
    public function last(): mixed
    {
        if (count($this->items) === 0) {
            return null;
        }

        return end($this->items);
    }

    /**
     * Return item at given position starting at 0.
     */
    public function at(int $key = 0, mixed $default = null): mixed
    {
        if ($this->count() == 0) {
            return $default;
        }

        $positions = array_values($this->items);
        if (!array_key_exists($key, $positions)) {
            return $default;
        }

        return $positions[$key];
    }

    /**
     * {@inheritdoc}
     *
     * @see CollectionInterface::get()
     */
    public function get(int|string $query, mixed $default = null): mixed
    {
        if ($this->count() == 0) {
            return $default;
        }

        if (is_int($query) && array_key_exists($query, $this->items)) {
            return $this->items[$query];
        }

        if (is_string($query) && !str_contains($query, '.')) {
            return array_key_exists($query, $this->items) ? $this->items[$query] : $default;
        }

        $query = explode('.', (string) $query);

        $result = $default;
        $items = $this->items;
        foreach ($query as $key) {
            if (!is_array($items) || !array_key_exists($key, $items)) {
                $result = $default;
                break;
            }

            $result = $items[$key];
            $items = $result;
        }

        return $result;
    }

    /**
     * {@inheritdoc}
     *
     * @see CollectionInterface::map()
     */
    public function map(callable $callback): self
    {

        return new self(
            array_map(
                fn(mixed $item) => $callback($item),
                $this->items,
            )
        );
    }

    /**
     * {@inheritdoc}
     *
     * @see CollectionInterface::map()
     */
    public function filter(callable $callback): self
    {
        return new self(
            array_filter(
                $this->items,
                fn(mixed $item) => $callback($item),
            )
        );
    }

    /**
     * {@inheritdoc}
     *
     * @see CollectionInterface::clear()
     */
    public function clear(): CollectionInterface
    {
        $this->items = [];

        return $this;
    }

    /**
     * {@inheritdoc}
     *
     * @see CollectionInterface::slice()
     */
    public function slice(int $offset, ?int $length = null): CollectionInterface
    {
        $this->items = array_slice($this->items, $offset, $length);

        return $this;
    }
}
