<?php
namespace Newland\NeosCommon\Domain\Repository;

use Newland\NeosCommon\Domain\Model\Topic;
use Newland\NeosCommon\Exceptions\InvalidRepositoryException;
use Newland\NeosCommon\Utility\CacheAware;
use function Safe\json_encode;

class TopicRepository
{
    use CacheAware;

    protected function cacheName(): string
    {
        return 'Newland_NeosCommon-Topics';
    }

    /**
     * Types of topics this repository handles.
     * For all types in this array there must be a `findAll{Type}` and `findBy{Type}`
     * method.
     *
     * @var string[]
     */
    protected $types = [];

    public function __construct()
    {
        $this->throwIfTypeMethodsMissing();
    }

    public function getTypes(): array
    {
        return $this->types;
    }

    /** @return Topic[] */
    public function findAllForType(string $type, array $options): array
    {
        $method = 'findAll' . ucfirst($type);
        $key = md5($method . json_encode($options));
        return $this->cache($key, function () use ($method, $options) {
            return $this->{$method}($options);
        });
    }

    /** @return Topic[] */
    public function findAll(array $options = []): array
    {
        $topics = [];
        foreach ($this->types as $type) {
            $topics = array_merge($topics, $this->findAllForType($type, $options));
        }

        return $this->sortTopics($topics);
    }

    /**
     * @param string $identifier
     * @return Topic|null
     */
    public function findByIdentifier(string $identifier)
    {
        if (strpos($identifier, ':') === false) {
            return null;
        }

        list($type, $id) = explode(':', $identifier, 2);

        $method = 'findBy' . ucfirst($type);
        if (!method_exists($this, $method)) {
            return null;
        }

        return $this->{$method}($id);
    }

    /**
     * @param array $identifiers
     * @param bool $sort
     * @return Topic[]
     */
    public function findByIdentifiers(array $identifiers, bool $sort = false): iterable
    {
        $topics = [];

        foreach ($identifiers as $identifier) {
            $topic = $this->findByIdentifier($identifier);
            if ($topic) {
                $topics[] = $topic;
            }
        }
        if ($sort) {
            $topics = $this->sortTopics($topics);
        }
        return $topics;
    }

    protected function sortTopics(array $topics): array
    {
        usort(
            $topics,
            function ($topic1, $topic2) {
                return strnatcasecmp($topic1->getTitle(), $topic2->getTitle());
            }
        );
        return $topics;
    }

    private function throwIfTypeMethodsMissing(): void
    {
        foreach ($this->types as $type) {
            $method = 'findAll' . ucfirst($type);
            if (!method_exists($this, $method)) {
                throw new InvalidRepositoryException(
                    static::class . ' has configured types ' . implode(', ', $this->types) .
                    'but is missing method ' . $method
                );
            }

            $method = 'findBy' . ucfirst($type);
            if (!method_exists($this, $method)) {
                throw new InvalidRepositoryException(
                    static::class . ' has configured types ' . implode(', ', $this->types) .
                    'but is missing method ' . $method
                );
            }
        }
    }
}
