<?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\ToOneRelationshipInterface;
use Grav\Framework\Relationships\Traits\RelationshipTrait;
use function is_callable;

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

    /** @var IdentifierInterface|null */
    protected $identifier = null;

    public function __construct(IdentifierInterface $parent, string $name, array $options, ?IdentifierInterface $identifier = null)
    {
        $this->parent = $parent;
        $this->name = $name;

        $this->parseOptions($options);
        $this->replaceIdentifier($identifier);

        $this->modified = false;
    }

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

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

    /**
     * @return object|null
     */
    public function fetch(): ?object
    {
        $identifier = $this->identifier;
        if (is_callable([$identifier, 'getObject'])) {
            $identifier = $identifier->getObject();
        }

        return $identifier;
    }


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

    /**
     * @param string|null $id
     * @param string|null $type
     * @return IdentifierInterface|null
     * @phpstan-pure
     */
    public function getIdentifier(?string $id = null, ?string $type = null): ?IdentifierInterface
    {
        if ($id && $this->getType() === 'media' && !str_contains($id, '/')) {
            $name = $this->name;
            $id = $this->parent->getType() . '/' . $this->parent->getId() . '/'. $name . '/' . $id;
        }

        $identifier = $this->identifier ?? null;
        if (null === $identifier || ($type && $type !== $identifier->getType()) || ($id && $id !== $identifier->getId())) {
            return null;
        }

        return $identifier;
    }

    /**
     * @param string|null $id
     * @param string|null $type
     * @return T|null
     */
    public function getObject(?string $id = null, ?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
    {
        $this->identifier = $this->checkIdentifier($identifier);
        $this->modified = true;

        return true;
    }

    /**
     * @param IdentifierInterface|null $identifier
     * @return bool
     */
    public function replaceIdentifier(?IdentifierInterface $identifier = null): bool
    {
        if ($identifier === null) {
            $this->identifier = null;
            $this->modified = true;

            return true;
        }

        return $this->addIdentifier($identifier);
    }

    /**
     * @param IdentifierInterface|null $identifier
     * @return bool
     */
    public function removeIdentifier(?IdentifierInterface $identifier = null): bool
    {
        if (null === $identifier || $this->has($identifier->getId(), $identifier->getType())) {
            $this->identifier = null;
            $this->modified = true;

            return true;
        }

        return false;
    }

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

    /**
     * @return array|null
     */
    public function jsonSerialize(): ?array
    {
        return $this->identifier ? $this->identifier->jsonSerialize() : null;
    }

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

    /**
     * @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->identifier = $data['identifier'];
    }
}
