<?php

namespace AppBundle\Repository;

use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Tools\Pagination\Paginator;

/**
 * BaseRepository
 *
 * @author Bastien Gatellier <contact@bgatellier.fr>
 */
class BaseRepository extends EntityRepository
{
    const COMPARISON_OPERATORS = ['=', '<>', '<', '<=', '>', '>='];
    const ROOT_ALIAS = 'entity';

    /**
     * Find entities paginated.
     * Take into account criteria and ordering.
     * @param  array $criteria
     * @param  array $orderBy
     * @param  int $limit
     * @param  int $offset
     * @param  array $joinProperties
     * @param  int $hydrateMode Doctrine hydrate mode
     * @return Paginator
     * @throws \Exception
     */
    public function paginate(
        array $criteria = [],
        array $orderBy = [],
        int $limit,
        int $offset = 0,
        array $joinProperties = [],
        int $hydrateMode = Query::HYDRATE_OBJECT
    ): Paginator
    {
        $qb = $this->createQueryBuilder(self::ROOT_ALIAS)
            ->setFirstResult($offset)
            ->setMaxResults($limit)
        ;

        if ($criteria) {
            $this->addCriteria($qb, $criteria);
        }

        if ($orderBy) {
            foreach ($orderBy as $field => $value) {
                $qb->addOrderBy(self::ROOT_ALIAS . '.' . $field, $value);
            }
        }

        if ($joinProperties) {
            foreach ($joinProperties as $property) {
                $alias = $property . '_join';

                $qb
                    ->leftJoin(self::ROOT_ALIAS . " . $property", $alias)
                    ->addSelect($alias)
                ;
            }
        }

        $qb->getQuery()->setHydrationMode($hydrateMode);

        return new Paginator($qb);
    }

    /**
     * Add criteria to a query builder
     * @param QueryBuilder &$qb
     * @param array $criteria Array of criteria. Could be a scalar or an array ([comparison operator, value]).
     * @throws \Exception
     */
    public function addCriteria(QueryBuilder &$qb, array $criteria): void
    {
        foreach ($criteria as $expr => $value) {
            if ('NULL' === $value || 'NOT NULL' === $value) {
                $whereClause = "$expr IS $value";
            } else {
                $comparisonOperator = '=';
                $paramName = $expr . '_param';

                if (is_array($value)) {
                    $comparisonOperator = $value[0];
                    $value = $value[1];
                }

                // Add the root alias if no alias is specified
                if (false === strpos($expr, '.')) {
                    $expr = join([self::ROOT_ALIAS, '.', $expr]);
                }

                switch ($comparisonOperator) {
                    case in_array($comparisonOperator, self::COMPARISON_OPERATORS):
                        $whereClause = "$expr $comparisonOperator :$paramName";
                        break;

                    // Compare uppercase values
                    case 'like':
                        $whereClause = $qb->expr()->like($qb->expr()->upper($expr), ":$paramName");
                        $value = '%' . mb_strtoupper($value) . '%';
                        break;
                    
                    default:
                        throw new \Exception(sprintf('The comparison operator %s is not taken into account.', $comparisonOperator), 1);
                        break;
                }

                $qb ->setParameter($paramName, $value);
            }

            $qb->andWhere($whereClause);
        }
    }
}
