<?php
namespace Newland\Toubiz\Sync\Neos\Translation;

use Doctrine\ORM\Query\Expr\Base;
use Doctrine\ORM\QueryBuilder;
use Neos\Flow\Configuration\ConfigurationManager;
use Neos\Flow\Property\Exception\InvalidPropertyException;
use Newland\Toubiz\Api\Constants\Language;

trait TranslatableRepository
{
    /** @var array */
    protected $translationConfiguration;

    /** @var bool */
    protected $strictLanguageHandling = false;

    /** @var string|null */
    protected $language;

    public function injectLanguage(CurrentLanguageService $languageService): void
    {
        $languageService->onInitialLanguageAvailable(function (string $language) {
            $this->language = $language;
        });
    }

    public function injectTranslationConfiguration(ConfigurationManager $configurationManager): void
    {
        $this->translationConfiguration = (array) $configurationManager->getConfiguration(
            ConfigurationManager::CONFIGURATION_TYPE_SETTINGS,
            'Newland.Toubiz.Sync.Neos.translation'
        );
        $this->strictLanguageHandling = $this->translationConfiguration['strict'] ?? false;
    }

    /**
     * Enables or disables strict language handling.
     * Pass `null` in order to reset strict language handling to the configured default.
     *
     * @param bool|null $strict
     */
    public function setStrictLanguageHandling(?bool $strict): void
    {
        if ($strict === null) {
            $strict = $this->translationConfiguration['strict'] ?? false;
        }
        $this->strictLanguageHandling = $strict;
    }

    /**
     * Disables language handling for the given block.
     *
     * @return mixed
     */
    public function withoutLanguageHandling(callable $block)
    {
        $language = $this->language;
        $this->language = null;
        $result = $block();
        $this->language = $language;
        return $result;
    }

    /**
     * Temporarily sets the language for the duration of the given block.
     * This method is preferred over `setLanguage` as it does not taint the repository for
     * further usage.
     *
     * Note: Setting the language to `null` will not disable language handling - it will result in
     *       the language not being changed. If you want to temporarily disable language handling
     *       then you'll have to investigate `withoutLanguageHandling`.
     *
     * @param string|null $language
     * @param callable $block
     * @return mixed
     */
    public function withLanguage(?string $language, callable $block)
    {
        if ($language === null) {
            return $block();
        }

        $before = $this->language;
        $this->setLanguage($language);
        $result = $block();
        $this->language = $before;
        return $result;
    }

    public function getLanguage(): ?string
    {
        return $this->language;
    }

    /**
     * Override core method to support languages.
     *
     * @see \Neos\Flow\Persistence\Doctrine\Repository::__call
     * @param mixed $method
     * @param mixed $arguments
     * @return mixed|null
     */
    public function __call($method, $arguments)
    {
        if (empty($this->language)) {
            parent::__call($method, $arguments);
        }

        if (empty($this->alias)) {
            throw new InvalidPropertyException('Property alias must be set on repository.', 1556545106);
        }

        $queryBuilder = $this->createQueryBuilder($this->alias);
        if (isset($method[10]) && strpos($method, 'findOneBy') === 0) {
            $propertyName = lcfirst(substr($method, 9));
            return $this->findOneBy([ $propertyName => $arguments[0] ]);
        }

        if (isset($method[8]) && strpos($method, 'countBy') === 0) {
            $propertyName = lcfirst(substr($method, 7));

            return $queryBuilder
                ->andWhere(
                    $queryBuilder->expr()->like(
                        $this->alias . '.' . $propertyName,
                        $queryBuilder->expr()->literal($arguments[0])
                    )
                )
                ->getQuery()
                ->execute()
                ->count();
        }

        if (isset($method[7]) && strpos($method, 'findBy') === 0) {
            $propertyName = lcfirst(substr($method, 6));

            return $queryBuilder
                ->andWhere(
                    $queryBuilder->expr()->like(
                        $this->alias . '.' . $propertyName,
                        $queryBuilder->expr()->literal($arguments[0])
                    )
                )
                ->getQuery()
                ->execute();
        }

        trigger_error('Call to undefined method ' . get_class($this) . '::' . $method, E_USER_ERROR);
    }

    /**
     * Sets the language of records selected by the current repository.
     * Languages passed to this method must be a valid languages in the `Language` enum.
     * If the language is set to `null` then the language handling of the repository
     * is disabled - it will select records of all languages.
     *
     * Consider using the `withLanguage` method which only sets the language of the repository for
     * a single closure instead of permanently tainting it in order to prevent accidentally setting
     * a language which affects all future calls.
     *
     * @param string|null $language
     */
    public function setLanguage(string $language = null): void
    {
        if ($language !== null) {
            Language::throwIfInvalid($language);
        }
        $this->language = $language;
    }

    /**
     * Creates a new QueryBuilder instance that is prepopulated for this entity name.
     *
     * @param string $alias
     * @param string $indexBy The index for the from.
     */
    public function createQueryBuilder($alias, $indexBy = null): QueryBuilderWithFixedExpression
    {
        $queryBuilder = new QueryBuilderWithFixedExpression($this->_em);

        $languageExpression = $this->getLanguageExpression($queryBuilder, $alias);
        if ($languageExpression) {
            $queryBuilder->addFixedExpression($languageExpression);
        }

        $queryBuilder->select($alias)->from($this->_entityName, $alias, $indexBy);
        return $queryBuilder;
    }

    /** @inheritdoc */
    public function findByIdentifier($identifier)
    {
        $query = $this->createQueryBuilder('entity');
        $query->andWhere(
            $query->expr()->eq('entity.Persistence_Object_Identifier', ':identifier')
        );
        $query->setMaxResults(1);
        return $query->getQuery()->execute([ 'identifier' => $identifier ])[0] ?? null;
    }

    /** @inheritdoc */
    public function findOneBy(array $criteria, array $orderBy = null)
    {
        return $this->findByCriteriaArrayQuery($criteria, $orderBy, 1, 0)
                ->getQuery()
                ->execute()[0] ?? null;
    }

    /** @inheritdoc */
    public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
    {
        return $this->findByCriteriaArrayQuery($criteria, $orderBy, $limit, $offset)
            ->getQuery()
            ->execute();
    }

    private function findByCriteriaArrayQuery(
        array $criteria,
        ?array $orderBy,
        ?int $limit,
        ?int $offset
    ): QueryBuilder {
        $query = $this->createQueryBuilder('entity');

        foreach ($criteria as $field => $value) {
            $queryField = sprintf('entity.%s', $field);
            $placeholder = sprintf(':%s', $field);
            $query->andWhere($query->expr()->eq($queryField, $placeholder));
            $query->setParameter($placeholder, $value);
        }

        foreach ($orderBy ?? [] as $field => $order) {
            $queryField = sprintf('entity.%s', $field);
            $query->addOrderBy($queryField, $order);
        }

        if ($limit) {
            $query->setMaxResults($limit);
        }

        if ($offset) {
            $query->setFirstResult($offset);
        }

        return $query;
    }


    protected function getLanguageExpression(QueryBuilder $query, string $alias): ?Base
    {
        if ($this->language === null) {
            return null;
        }

        $languageField = sprintf('%s.language', $alias);
        $where = [
            $query->expr()->eq($languageField, $query->expr()->literal($this->language)),
        ];

        if (!$this->strictLanguageHandling) {
            $where[] = $query->expr()->isNull($languageField);
        }

        return $query->expr()->orX(...$where);
    }

    /**
     * @template T
     * @param T $entity
     * @return array<string, T>
     */
    public function findLanguageVariants($entity): array
    {
        $grouping = $entity->getLanguageGrouping();
        if (!$grouping) {
            return [ ];
        }

        $entities = $this->withoutLanguageHandling(function () use ($grouping) {
            return $this->createQueryBuilder('entity')
                ->where('entity.languageGrouping = :grouping')
                ->andWhere('entity.language IS NOT NULL')
                ->setParameter('grouping', $grouping)
                ->groupBy('entity.language')
                ->getQuery()
                ->execute();
        });

        $variants = [];
        foreach ($entities as $variant) {
            $variants[$variant->getLanguage()] = $variant;
        }

        return $variants;
    }

    /**
     * @template T
     * @param T $entity
     * @return T|null
     */
    public function translateInto($entity, string $targetLanguage)
    {
        if ($entity->getLanguage() === null) {
            return $entity;
        }

        $grouping = $entity->getLanguageGrouping();
        if (!$grouping) {
            return null;
        }

        return $this->withLanguage($targetLanguage, function () use ($grouping) {
            return $this->createQueryBuilder('entity')
                ->where('entity.languageGrouping = :grouping')
                ->setParameter('grouping', $grouping)
                ->setMaxResults(1)
                ->getQuery()
                ->execute()[0] ?? null;
        });
    }
}
