<?php
/**
 * core.framework
 *
 * @category  Core
 * @package   Core_QueryLanguage
 * @copyright Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license   proprietary
 */

/**
 * @category  Core
 * @package   Core_QueryLanguage
 * @subpackage Core_QueryLanguage_Adapter
 * @copyright Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license   proprietary
 */
class Core_QueryLanguage_Adapter_Doctrine implements Core_QueryLanguage_Adapter_Interface
{
    /**
     *
     * @var Core_QueryLanguage_Query
     */
    protected $_query;

    /**
     *
     * @var Doctrine_Query
     */
    protected $_doctrineQuery;

    /**
     *
     * @var string
     */
    protected $_entityAlias;

    /**
     *
     * @param Doctrine_Query $query
     *
     * @return \Core_QueryLanguage_Adapter_Doctrine
     */
    public function setInitialQuery(Doctrine_Query $query)
    {
        $this->_doctrineQuery = $query;

        return $this;
    }

    /**
     *
     * @param Core_QueryLanguage_Query $query
     *
     * @return \Core_QueryLanguage_Adapter_Doctrine
     */
    public function setQuery(Core_QueryLanguage_Query $query)
    {
        $this->_query = $query;

        return $this;
    }

    /**
     *
     * @return Doctrine_Query
     * @throws LogicException
     */
    public function getQuery()
    {
        if (null === $this->_query) {
            throw new LogicException('Error while getting query, QueryLanguage Query not set');
        }

        foreach ($this->_getSelectParts() as $part) {
            $this->_getDoctrineQuery()->addSelect($part);
        }

        foreach ($this->_getOrderByParts() as $part) {
            $this->_getDoctrineQuery()->addOrderBy($part);
        }

        if (null !== $this->_query->getOffset()) {
            $this->_getDoctrineQuery()->offset($this->_query->getOffset());
        }

        if (null !== $this->_query->getLimit()) {
            $this->_getDoctrineQuery()->limit($this->_query->getLimit());
        }

        foreach ($this->_getJoins() as $join) {
            switch ($join['type']) {
                case 'left':
                    $this->_getDoctrineQuery()->leftJoin($join['relation']);

                    break;

                case 'inner':
                default:
                    $this->_getDoctrineQuery()->innerJoin($join['relation']);

                    break;
            }
        }

        foreach ($this->_getWhereParts() as $part) {
            switch ($part['type']) {
                case Core_QueryLanguage_Query::OPERATOR_CONTAINS:
                case Core_QueryLanguage_Query::OPERATOR_DOES_NOT_CONTAIN:

                    if (is_array($part['value'])) {
                        $this->_andWhereIn($part, $part['type'] === Core_QueryLanguage_Query::OPERATOR_DOES_NOT_CONTAIN);
                    } else {
                        // Creates LIKE or NOT LIKE clause
                        $statement = sprintf('%s %s :%s',
                            $part['field'],
                            $part['type'] === Core_QueryLanguage_Query::OPERATOR_CONTAINS ? 'LIKE' : 'NOT LIKE',
                            $part['field_alias']
                        );
                        $this->_getDoctrineQuery()->andWhere(
                            $statement,
                            array($part['field_alias'] => '%'. $part['value'] .'%')
                        );
                    }

                    break;

                case Core_QueryLanguage_Query::OPERATOR_EQUAL:
                case Core_QueryLanguage_Query::OPERATOR_NOT_EQUAL:
                    if (is_array($part['value'])) {
                        // Creates IN or NOT IN clause
                        $this->_andWhereIn($part, $part['type'] === Core_QueryLanguage_Query::OPERATOR_NOT_EQUAL);

                        break;
                    }

                    // no break
                case Core_QueryLanguage_Query::OPERATOR_GREATER:
                case Core_QueryLanguage_Query::OPERATOR_GREATER_OR_EQUAL:
                case Core_QueryLanguage_Query::OPERATOR_LOWER:
                case Core_QueryLanguage_Query::OPERATOR_LOWER_OR_EQUAL:
                    if (is_array($part['value'])) {
                        throw new LogicException('Error while generating query, operator "'. $part['type'].'" does not support array values');
                    }

                    $statement = sprintf('%s %s :%s',
                            $part['field'],
                            $part['type'],
                            $part['field_alias']
                        );
                    $this->_getDoctrineQuery()->andWhere(
                        $statement,
                        array($part['field_alias'] => $part['value'])
                    );

                    break;
            }
        }

        return $this->_getDoctrineQuery();
    }

    protected function _andWhereIn($part, $not = false)
    {
        // Creates IN or NOT IN clause
        $keys = array();
        foreach ($part['value'] as $k => $dummy) {
            $keys[] = ':'. $part['field_alias'] .'_'. $k;
        }
        $value = array_combine($keys, $part['value']);

        $this->_getDoctrineQuery()->andWhereIn($part['field'], $value, $not);
    }

    /**
     * @codeCoverageIgnore
     */
    protected function _getDoctrineQuery()
    {
        if (null === $this->_doctrineQuery) {
            $this->_doctrineQuery = new Doctrine_Query;
        }

        return $this->_doctrineQuery;
    }

    /**
     *
     * @return array
     */
    protected function _getSelectParts($delimiter = '_')
    {
        $select = $this->_query->getSelect();
        if (empty($select)) {
            return array();
        }

        $recursiveIterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($select));
        $result = array();
        foreach ($recursiveIterator as $leafValue) {
            $path = array();
            foreach (range(0, $recursiveIterator->getDepth()) as $depth) {
                $path[] = $recursiveIterator->getSubIterator($depth)->key();
            }
            array_pop($path);
            if (!empty($path)) {
                $pathString = implode($delimiter, $path) .'.';
            } else {
                $pathString = '';
            }

            $result[] = $pathString . $leafValue;
        }

        return $result;
    }

    /**
     *
     * @return array
     */
    protected function _getOrderByParts()
    {
        $order = $this->_query->getOrder();
        if (empty($order)) {
            return array();
        }

        $parts = array();

        foreach ($order as $field => $sort) {
            $parts[] = $field .' '. strtoupper($sort);
        }

        return $parts;
    }

    /**
     *
     * @return array
     */
    protected function _getJoins()
    {
        $selectParts = $this->_getSelectParts('.');
        $whereParts  = $this->_getWhereParts();

        if (empty($selectParts) && empty($whereParts)) {
            return array();
        }

        $whereParts = array_column($whereParts, 'field');

        $joins      = $this->_processRelations($selectParts, 'left');
        $joins      += $this->_processRelations($whereParts, 'inner');


        return $joins;
    }

    /**
     *
     * @param array $relations
     *
     * @return array
     */
    protected function _processRelations(array $parts, $type)
    {
        $joins  = array();
        foreach ($parts as $part) {
            if (strpos($part, '.') === false) {
                continue;
            }

            $relations = explode('.', $part);

            // Remove last item - it is the field, not the Relation
            array_pop($relations);

            $parent = $this->_getAlias() .'.';
            foreach ($relations as $relation) {
                $parentRelation = $parent . $relation;

                $joins[$parentRelation] = array(
                    'type'      => $type,
                    'relation'  => $parentRelation .' AS '. $this->_getRelationAlias($parentRelation),
                );
                $parent = $relation .'.';
            }
        }

        return $joins;
    }

    /**
     *
     * @param string $relation
     *
     * @return string
     */
    protected function _getRelationAlias($relation)
    {
        return preg_replace('/^'. $this->_getAlias() .'\_/', '', implode('_', explode('.', $relation)));
    }

    /**
     *
     * @return array
     */
    protected function _getWhereParts($delimiter = '.')
    {
        $query = $this->_query->getQuery();
        if (empty($query)) {
            return array();
        }

        $recursiveIterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($query));
        $result = array();
        foreach ($recursiveIterator as $leafValue) {
            $path = array();
            foreach (range(0, $recursiveIterator->getDepth()) as $depth) {
                $key = $recursiveIterator->getSubIterator($depth)->key();

                if ($this->_query->isOperator($key)) {
                    $type = $key;
                    $result[implode('.', $path)] = array(
                        'field'         => implode($delimiter, $path),
                        'field_alias'   => implode('_', $path),
                        'type'          => $type,
                        'value'         => $recursiveIterator->getSubIterator($depth)->current()
                    );
                } else {
                    $path[] = $key;
                }
            }
        }

        return $result;
    }

    /**
     *
     * @return string
     * @throws RuntimeException
     */
    protected function _getAlias()
    {
        if (null === $this->_entityAlias) {
            $from = $this->_getDoctrineQuery()->getDqlPart('from');

            if (empty($from)) {
                throw new RuntimeException('Error while getting alias, DQL part "from" is empty');
            }

            $parts = explode(' ', $from[0]);
            $this->_entityAlias = array_pop($parts);
        }

        return $this->_entityAlias;
    }
}
