<?php declare(strict_types=1);

namespace Grav\Framework\Relationships;

use ArrayIterator;
use Grav\Framework\Compat\Serializable;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use Grav\Framework\Contracts\Relationships\ToManyRelationshipInterface;
use Grav\Framework\Relationships\Traits\RelationshipTrait;
use function count;
use function is_callable;

/**
 * Class ToManyRelationship
 *
 * @template T of IdentifierInterface
 * @template P of IdentifierInterface
 * @template-implements ToManyRelationshipInterface<T,P>
 */
class ToManyRelationship implements ToManyRelationshipInterface
{
    /** @template-use RelationshipTrait<T> */
    use RelationshipTrait;
    use Serializable;

    /** @var IdentifierInterface[] */
    protected $identifiers = [];

    /**
     * ToManyRelationship constructor.
     * @param string $name
     * @param IdentifierInterface $parent
     * @param iterable<IdentifierInterface> $identifiers
     */
    public function __construct(IdentifierInterface $parent, string $name, array $options, iterable $identifiers = [])
    {
        $this->parent = $parent;
        $this->name = $name;

        $this->parseOptions($options);
        $this->addIdentifiers($identifiers);

        $this->modified = false;
    }

    /**
     * @return string
     * @phpstan-pure
     */
    public function getCardinality(): string
    {
        return 'to-many';
    }

    /**
     * @return int
     * @phpstan-pure
     */
    public function count(): int
    {
        return count($this->identifiers);
    }

    /**
     * @return array
     */
    public function fetch(): array
    {
        $list = [];
        foreach ($this->identifiers as $identifier) {
            if (is_callable([$identifier, 'getObject'])) {
                $identifier = $identifier->getObject();
            }
            $list[] = $identifier;
        }

        return $list;
    }

    /**
     * @param string $id
     * @param string|null $type
     * @return bool
     * @phpstan-pure
     */
    public function has(string $id, ?string $type = null): bool
    {
        return $this->getIdentifier($id, $type) !== null;
    }

    /**
     * @param positive-int $pos
     * @return IdentifierInterface|null
     */
    public function getNthIdentifier(int $pos): ?IdentifierInterface
    {
        $items = array_keys($this->identifiers);
        $key = $items[$pos - 1] ?? null;
        if (null === $key) {
            return null;
        }

        return $this->identifiers[$key] ?? null;
    }

    /**
     * @param string $id
     * @param string|null $type
     * @return IdentifierInterface|null
     * @phpstan-pure
     */
    public function getIdentifier(string $id, ?string $type = null): ?IdentifierInterface
    {
        if (null === $type) {
            $type = $this->getType();
        }

        if ($type === 'media' && !str_contains($id, '/')) {
            $name = $this->name;
            $id = $this->parent->getType() . '/' . $this->parent->getId() . '/'. $name . '/' . $id;
        }

        $key = "{$type}/{$id}";

        return $this->identifiers[$key] ?? null;
    }

    /**
     * @param string $id
     * @param string|null $type
     * @return T|null
     */
    public function getObject(string $id, ?string $type = null): ?object
    {
        $identifier = $this->getIdentifier($id, $type);
        if ($identifier && is_callable([$identifier, 'getObject'])) {
            $identifier = $identifier->getObject();
        }

        return $identifier;
    }

    /**
     * @param IdentifierInterface $identifier
     * @return bool
     */
    public function addIdentifier(IdentifierInterface $identifier): bool
    {
        return $this->addIdentifiers([$identifier]);
    }

    /**
     * @param IdentifierInterface|null $identifier
     * @return bool
     */
    public function removeIdentifier(?IdentifierInterface $identifier = null): bool
    {
        return !$identifier || $this->removeIdentifiers([$identifier]);
    }

    /**
     * @param iterable<IdentifierInterface> $identifiers
     * @return bool
     */
    public function addIdentifiers(iterable $identifiers): bool
    {
        foreach ($identifiers as $identifier) {
            $type = $identifier->getType();
            $id = $identifier->getId();
            $key = "{$type}/{$id}";

            $this->identifiers[$key] = $this->checkIdentifier($identifier);
            $this->modified = true;
        }

        return true;
    }

    /**
     * @param iterable<IdentifierInterface> $identifiers
     * @return bool
     */
    public function replaceIdentifiers(iterable $identifiers): bool
    {
        $this->identifiers = [];
        $this->modified = true;

        return $this->addIdentifiers($identifiers);
    }

    /**
     * @param iterable<IdentifierInterface> $identifiers
     * @return bool
     */
    public function removeIdentifiers(iterable $identifiers): bool
    {
        foreach ($identifiers as $identifier) {
            $type = $identifier->getType();
            $id = $identifier->getId();
            $key = "{$type}/{$id}";

            unset($this->identifiers[$key]);
            $this->modified = true;
        }

        return true;
    }

    /**
     * @return iterable<IdentifierInterface>
     * @phpstan-pure
     */
    public function getIterator(): iterable
    {
        return new ArrayIterator($this->identifiers);
    }

    /**
     * @return array
     */
    public function jsonSerialize(): array
    {
        $list = [];
        foreach ($this->getIterator() as $item) {
            $list[] = $item->jsonSerialize();
        }

        return $list;
    }

    /**
     * @return array
     */
    public function __serialize(): array
    {
        return [
            'parent' => $this->parent,
            'name' => $this->name,
            'type' => $this->type,
            'options' => $this->options,
            'modified' => $this->modified,
            'identifiers' => $this->identifiers,
        ];
    }

    /**
     * @param array $data
     * @return void
     */
    public function __unserialize(array $data): void
    {
        $this->parent = $data['parent'];
        $this->name = $data['name'];
        $this->type = $data['type'];
        $this->options = $data['options'];
        $this->modified = $data['modified'];
        $this->identifiers = $data['identifiers'];
    }
}
