<?php
namespace Newland\Toubiz\Sync\Neos\Domain\Repository;

/*
 * This file is part of the "toubiz-sync-neos" package.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 */

use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Persistence\Doctrine\Repository;
use Neos\Flow\Persistence\QueryResultInterface;
use Newland\Toubiz\Sync\Neos\Domain\Filter\FilterInterface;
use Newland\Toubiz\Sync\Neos\Domain\Model\AbstractEntity;
use Newland\Toubiz\Sync\Neos\Service\ArticleClientFilterService;
use Newland\Toubiz\Sync\Neos\Service\SortingService;
use Webit\DoctrineORM\QueryBuilder\Iterator\QueryBuilderIterator;

/**
 * Abstract repository.
 *
 * @Flow\Scope("singleton")
 *
 * @method AbstractEntity|null findOneByOriginalId(string $originalId)
 */
abstract class AbstractRepository extends Repository
{

    /**
     * @var ArticleClientFilterService
     * @Flow\Inject()
     */
    protected $articleClientFilterService;

    /**
     * @var SortingService
     * @Flow\Inject()
     */
    protected $sortingService;

    protected $alias = '';

    /**
     * @param FilterInterface $filter
     * @param QueryBuilder $query
     * @return QueryBuilder
     */
    abstract protected function applyFilter(FilterInterface $filter, QueryBuilder $query): QueryBuilder;

    /**
     * Counts items and pages for given filter.
     *
     * @param FilterInterface $filter
     * @param int $perPage
     * @return array
     */
    public function countByFilter(FilterInterface $filter, $perPage)
    {
        $query = $this->queryForFilter($filter);

        // Reset pagination parameters.
        $query->setMaxResults(null);
        $query->setFirstResult(0);

        $query->select('COUNT(DISTINCT ' . $this->getTableAlias() . ') as total');
        $result = $query->getQuery()->execute()[0];
        $pages = (int) floor($result['total'] / $perPage) + ($result['total'] % $perPage === 0 ? 0 : 1);
        return [
            'items' => (int) $result['total'],
            'pages' => $pages === 0 ? 1 : $pages,
        ];
    }

    /**
     * Find entities matching given filter.
     *
     * @param FilterInterface $filter
     * @return QueryResultInterface
     */
    public function findByFilter(FilterInterface $filter)
    {
        return $this->queryForFilter($filter)
            ->getQuery()
            ->execute();
    }

    /**
     * @param FilterInterface $filter
     * @return AbstractEntity|null
     */
    public function findOneByFilter(FilterInterface $filter): ?AbstractEntity
    {
        return $this->queryForFilter($filter)
                ->getQuery()
                ->setMaxResults(1)
                ->execute()[0] ?? null;
    }

    /**
     * Note: This method changes the return type from it's parent from Entity[] to \Iterator<Entity>
     *
     * @return \Iterator
     */
    public function findAll()
    {
        return new QueryBuilderIterator(
            $this->createQueryBuilder($this->getTableAlias()),
            100
        );
    }

    public function queryForFilter(FilterInterface $filter): QueryBuilder
    {
        return $this->applyFilter(
            $filter,
            $this->createQueryBuilder($this->getTableAlias())->distinct(true)
        );
    }

    public function forEachFilter(FilterInterface $filter, callable $block, int $pageSize = 50): void
    {
        $offset = 0;
        $max = (int) $this->countByFilter($filter, 1)['items'];

        $query = $this->queryForFilter($filter)->getQuery();
        $query->setMaxResults($pageSize);

        while ($offset < $max) {
            $query->setFirstResult($offset);

            foreach ($query->execute() as $entity) {
                $block($entity);
            }

            $offset += $pageSize;
        }
    }

    /**
     * Removes all records from given array of ids.
     *
     * @param array $ids
     * @return void
     */
    public function removeByIds(array $ids)
    {
        $query = $this->createQueryBuilder($this->getTableAlias());
        $query->delete()
            ->andWhere($this->getTableAlias() . '.Persistence_Object_Identifier IN(:ids)')
            ->setParameter('ids', $ids);

        $query->getQuery()->execute();
    }

    /**
     * Applies basic filter functions (from the abstract filter) onto
     * the given query builder.
     *
     * @param FilterInterface $filter
     * @param QueryBuilder $query
     * @return void
     */
    protected function applyBasicFilter(FilterInterface $filter, QueryBuilder $query)
    {
        foreach ($filter->getLeftJoins() as $field => $table) {
            $query->leftJoin($field, $table);
        }

        foreach ($filter->getInnerJoins() as $field => $table) {
            $query->innerJoin($field, $table);
        }

        foreach ($filter->getOrderBy() as $field => $order) {
            $query->addOrderBy($field, $order);
        }

        foreach ($filter->getGroupBy() as $field) {
            $query->addGroupBy($field);
        }

        $limit = $filter->getLimit();
        if ($limit !== null) {
            $query->setMaxResults($limit);
        }

        $offset = $filter->getOffset();
        if ($offset !== null) {
            $query->setFirstResult($offset);
        }

        $excludes = array_map(
            function (AbstractEntity $entity) {
                return $entity->getOriginalId();
            },
            $filter->getExcludes()
        );
        if (!empty($excludes)) {
            $field = $this->getTableAlias() . '.originalId';
            $query->andWhere($query->expr()->notIn($field, $excludes));
        }

        foreach ($filter->getOrderByRelation() as $order) {
            $query->innerJoin($order['relation'], $order['alias'])
                ->groupBy('category.Persistence_Object_Identifier')
                ->orderBy('COUNT(' . $order['alias'] . '.Persistence_Object_Identifier)', $order['direction']);
        }
    }

    /**
     * Helper for getting the table alias used inside queries.
     *
     * This may be a temporary solution!
     *
     * @return string
     */
    protected function getTableAlias(): string
    {
        $parts = explode('\\', get_class($this));
        $name = array_slice($parts, -1)[0];
        return lcfirst(str_replace('Repository', '', $name));
    }

    /**
     * Ensures sorting defined on the backend
     *
     * @return array
     */
    public function getSorted(FilterInterface $filter, string $fieldName, array $sorting): array
    {

        $objects = $this->findByFilter($filter);

        return $this->sortingService->sortObjects(
            (array) $objects,
            $fieldName,
            $sorting
        );
    }

    public function getAllIdentifiers(): array
    {
        $query = $this->createQueryBuilder('entity');
        $query->select('entity.Persistence_Object_Identifier as id');
        return array_map(
            function (array $row) {
                return $row['id'];
            },
            $query->getQuery()->execute([], Query::HYDRATE_ARRAY)
        );
    }

    /**
     * Simple abstraction over the `add` and `update` methods that figures out
     * if the given entity has been persisted before.
     *
     * @param object $entity
     */
    public function store($entity): void
    {
        if ($this->persistenceManager->isNewObject($entity)) {
            $this->add($entity);
        } else {
            $this->update($entity);
        }
    }
}
