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

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

trait TranslatableRepository
{
    /** @var I18n\Service */
    protected $localizationService;

    /** @var array */
    protected $translationConfiguration;

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

    /** @var bool */
    protected $languageHandlingEnabled = true;

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

    public function injectLocalizationService(I18n\Service $localizationService)
    {
        $this->localizationService = $localizationService;
    }

    public function injectTranslationConfiguration(ConfigurationManager $configurationManager)
    {
        $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.
     *
     * @param callable $block
     */
    public function withoutLanguageHandling(callable $block): void
    {
        $this->languageHandlingEnabled = false;
        $block();
        $this->languageHandlingEnabled = true;
    }

    /**
     * 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);
        $queryBuilder = $this->addLanguageWhereClause($queryBuilder, $this->alias, $this->language);

        if (isset($method[10]) && strpos($method, 'findOneBy') === 0) {
            $propertyName = lcfirst(substr($method, 9));

            $result = $queryBuilder
                ->andWhere(
                    $queryBuilder->expr()->like(
                        $this->alias . '.' . $propertyName,
                        $queryBuilder->expr()->literal($arguments[0])
                    )
                )
                ->setMaxResults(1)
                ->getQuery()
                ->execute();

            $record = null;
            if (\count($result)) {
                $record = $result[0];
            }

            return $record;
        }

        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);
    }

    public function setLanguage(string $language = null)
    {
        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.
     *
     * @return QueryBuilder
     */
    public function createQueryBuilder($alias, $indexBy = null)
    {
        return $this->addLanguageWhereClause(
            parent::createQueryBuilder($alias, $indexBy),
            $alias,
            $this->getLanguage()
        );
    }

    /** @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 addLanguageWhereClause(QueryBuilder $query, string $alias, string $language = null): QueryBuilder
    {
        if (!$language || !$this->languageHandlingEnabled) {
            return $query;
        }

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

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

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

    protected function getLanguage()
    {
        return $this->language ?? $this->localizationService->getConfiguration()->getCurrentLocale()->getLanguage();
    }
}
