<?php
/*
 *  $Id$
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.phpdoctrine.org>.
 */

/**
 * Doctrine_Pager
 *
 * @author      Guilherme Blanco <guilhermeblanco@hotmail.com>
 * @package     Doctrine
 * @subpackage  Pager
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @version     $Revision$
 * @link        www.phpdoctrine.org
 * @since       0.9
 */
class Doctrine_Pager
{
    /**
     * @var Doctrine_Query $_query      Doctrine_Query object related to the pager
     */
    protected $_query;

    /**
     * @var Doctrine_Query $_countQuery Doctrine_Query object related to the counter of pager
     */
    protected $_countQuery;

    /**
     * @var array $_countQueryParams    Hold the params to be used by Doctrine_Query counter object of pager
     */
    protected $_countQueryParams;

    /**
     * @var integer $_numResults        Number of results found
     */
    protected $_numResults;

    /**
     * @var integer $_maxPerPage        Maximum number of itens per page
     */
    protected $_maxPerPage;

    /**
     * @var integer $page               Current page
     */
    protected $_page;

    /**
     * @var integer $_lastPage          Last page (total of pages)
     */
    protected $_lastPage;

    /**
     * @var boolean $_executed          Pager was initialized (called "execute" at least once)
     */
    protected $_executed;



    /**
     * __construct
     *
     * @param mixed $query     Accepts either a Doctrine_Query object or a string 
     *                        (which does the Doctrine_Query class creation).
     * @param int $page     Current page
     * @param int $maxPerPage     Maximum itens per page
     * @return void
     */
    public function __construct($query, $page, $maxPerPage = 0)
    {
        $this->_setExecuted(false);

        $this->_setQuery($query);
        $this->_setPage($page);

        $this->setMaxPerPage($maxPerPage);
    }

    /**
     * _initialize
     *
     * Initialize Pager object calculating number of results
     *
     * @param $params  Optional parameters to Doctrine_Query::execute
     * @return void
     */
    protected function _initialize($params = array())
    {
        // retrieve the number of items found
        $countQuery = $this->getCountQuery();
        $count = $countQuery->count($this->getCountQueryParams($params));

        $this->_setNumResults($count);
        $this->_setExecuted(true); // _adjustOffset relies of _executed equals true = getNumResults()

        $this->_adjustOffset();
    }

    /**
     * _adjustOffset
     *
     * Adjusts last page of Doctrine_Pager, offset and limit of Doctrine_Query associated
     *
     * @return void
     */
    protected function _adjustOffset()
    {
        // Define new total of pages
        $this->_setLastPage(
            max(1, ceil($this->getNumResults() / $this->getMaxPerPage()))
        );
        $offset = ($this->getPage() - 1) * $this->getMaxPerPage();

        // Assign new offset and limit to Doctrine_Query object
        $p = $this->getQuery();
        $p->offset($offset);
        $p->limit($this->getMaxPerPage());
    }

    /**
     * getExecuted
     *
     * Returns the check if Pager was already executed at least once
     *
     * @return boolen        Pager was executed
     */
    public function getExecuted()
    {
        return $this->_executed;
    }

    /**
     * _setExecuted
     *
     * Defines if Pager was already executed
     *
     * @param $executed       Pager was executed
     * @return void
     */
    protected function _setExecuted($executed)
    {
        $this->_executed = $executed;
    }

    /**
     * getRange
     *
     * Builds and return a Doctrine_Pager_Range_* based on arguments
     *
     * @param string $rangeStyle Pager Range style
     * @param array $options     Custom subclass implementation options.
     *                           Default is a blank array
     * @return Doctrine_Pager_Range Pager Range
     */
    public function getRange($rangeStyle, $options = array())
    {
        $class = 'Doctrine_Pager_Range_' . ucfirst($rangeStyle);

        return new $class($options, $this);
    }

    /**
     * getNumResults
     *
     * Returns the number of results found
     *
     * @return int        the number of results found
     */
    public function getNumResults()
    {
        if ($this->getExecuted()) {
            return $this->_numResults;
        }

        throw new Doctrine_Pager_Exception(
            'Cannot retrieve the number of results of a not yet executed Pager query'
        );
    }

    /**
     * _setNumResults
     *
     * Defines the number of total results on initial query
     *
     * @param $nb       Number of results found on initial query fetch
     * @return void
     */
    protected function _setNumResults($nb)
    {
        $this->_numResults = $nb;
    }

    /**
     * getFirstPage
     *
     * Returns the first page
     *
     * @return int        first page
     */
    public function getFirstPage()
    {
        return 1;
    }

    /**
     * getLastPage
     *
     * Returns the last page (total of pages)
     *
     * @return int        last page (total of pages)
     */
    public function getLastPage()
    {
        if ($this->getExecuted()) {
            return $this->_lastPage;
        }

        throw new Doctrine_Pager_Exception(
            'Cannot retrieve the last page number of a not yet executed Pager query'
        );
    }

    /**
     * _setLastPage
     *
     * Defines the last page (total of pages)
     *
     * @param $page       last page (total of pages)
     * @return void
     */
    protected function _setLastPage($page)
    {
        $this->_lastPage = $page;

        if ($this->getPage() > $page) {
            $this->_setPage($page);
        }
    }

    /**
     * getLastPage
     *
     * Returns the current page
     *
     * @return int        current page
     */
    public function getPage()
    {
        return $this->_page;
    }

    /**
     * getNextPage
     *
     * Returns the next page
     *
     * @return int        next page
     */
    public function getNextPage()
    {
        if ($this->getExecuted()) {
            return min($this->getPage() + 1, $this->getLastPage());
        }

        throw new Doctrine_Pager_Exception(
            'Cannot retrieve the last page number of a not yet executed Pager query'
        );
    }

    /**
     * getPreviousPage
     *
     * Returns the previous page
     *
     * @return int        previous page
     */
    public function getPreviousPage()
    {
        if ($this->getExecuted()) {
            return max($this->getPage() - 1, $this->getFirstPage());
        }

        throw new Doctrine_Pager_Exception(
            'Cannot retrieve the previous page number of a not yet executed Pager query'
        );
    }

    /**
     * getFirstIndice
     *
     * Return the first indice number for the current page
     *
     * @return int First indice number
     */
    public function getFirstIndice()
    {
        return ($this->getPage() - 1) * $this->getMaxPerPage() + 1;
    }

    /**
     * getLastIndice
     *
     * Return the last indice number for the current page
     *
     * @return int Last indice number
     */
    public function getLastIndice()
    {
        return min($this->getNumResults(), ($this->getPage() * $this->getMaxPerPage()));
    }

    /**
     * haveToPaginate
     *
     * Return true if it's necessary to paginate or false if not
     *
     * @return bool        true if it is necessary to paginate, false otherwise
     */
    public function haveToPaginate()
    {
        if ($this->getExecuted()) {
            return $this->getNumResults() > $this->getMaxPerPage();
        }

        throw new Doctrine_Pager_Exception(
            'Cannot know if it is necessary to paginate a not yet executed Pager query'
        );
    }

    /**
     * setPage
     *
     * Defines the current page and automatically adjust offset and limits
     *
     * @param $page       current page
     * @return void
     */
    public function setPage($page)
    {
        $this->_setPage($page);
        $this->_setExecuted(false);
    }

    /**
     * _setPage
     *
     * Defines the current page
     *
     * @param $page       current page
     * @return void
     */
    private function _setPage($page)
    {
        $page = intval($page);
        $this->_page = ($page <= 0) ? 1 : $page;
    }

    /**
     * getLastPage
     *
     * Returns the maximum number of itens per page
     *
     * @return int        maximum number of itens per page
     */
    public function getMaxPerPage()
    {
        return $this->_maxPerPage;
    }

    /**
     * setMaxPerPage
     *
     * Defines the maximum number of itens per page and automatically adjust offset and limits
     *
     * @param $max       maximum number of itens per page
     * @return void
     */
    public function setMaxPerPage($max)
    {
        if ($max > 0) {
            $this->_maxPerPage = $max;
        } else if ($max == 0) {
            $this->_maxPerPage = 25;
        } else {
            $this->_maxPerPage = abs($max);
        }

        $this->_setExecuted(false);
    }

    /**
     * getResultsInPage
     *
     * Returns the number of itens in current page
     *
     * @return int    Number of itens in current page
     */
    public function getResultsInPage()
    {
        $page = $this->getPage();

        if ($page != $this->getLastPage()) {
            return $this->getMaxPerPage();
        }

        $offset = ($this->getPage() - 1) * $this->getMaxPerPage();

        return abs($this->getNumResults() - $offset);
    }

    /**
     * getQuery
     *
     * Returns the Doctrine_Query collector object related to the pager
     *
     * @return Doctrine_Query    Doctrine_Query object related to the pager
     */
    public function getQuery()
    {
        return $this->_query;
    }

    /**
     * _setQuery
     *
     * Defines the collector query to be used by pager
     *
     * @param Doctrine_Query     Accepts either a Doctrine_Query object or a string 
     *                           (which does the Doctrine_Query class creation).
     * @return void
     */
    protected function _setQuery($query)
    {
        if (is_string($query)) {
            $query = Doctrine_Query::create()
                ->parseDqlQuery($query);
        }

        $this->_query = $query;
    }

    /**
     * getCountQuery
     *
     * Returns the Doctrine_Query object that is used to make the count results to pager
     *
     * @return Doctrine_Query     Doctrine_Query object related to the pager
     */
    public function getCountQuery()
    {
        return ($this->_countQuery !== null) ? $this->_countQuery : $this->_query;
    }

    /**
     * setCountQuery
     *
     * Defines the counter query to be used by pager
     *
     * @param Doctrine_Query  Accepts either a Doctrine_Query object or a string 
     *                        (which does the Doctrine_Query class creation).
     * @param array           Optional params to be used by counter Doctrine_Query. 
     *                        If not defined, the params passed to execute method will be used.
     * @return void
     */
    public function setCountQuery($query, $params = null)
    {
        if (is_string($query)) {
            $query = Doctrine_Query::create()
                ->parseDqlQuery($query);
        }

        $this->_countQuery = $query;

        $this->setCountQueryParams($params);

        $this->_setExecuted(false);
    }

    /**
     * getCountQueryParams
     *
     * Returns the params to be used by counter Doctrine_Query
     *
     * @return array     Doctrine_Query counter params
     */
    public function getCountQueryParams($defaultParams = array())
    {
        return ($this->_countQueryParams !== null) ? $this->_countQueryParams : $defaultParams;
    }

    /**
     * setCountQueryParams
     *
     * Defines the params to be used by counter Doctrine_Query
     *
     * @param array       Optional params to be used by counter Doctrine_Query. 
     *                    If not defined, the params passed to execute method will be used.
     * @param boolean     Optional argument that append the query param instead of overriding the existent ones.
     * @return void
     */
    public function setCountQueryParams($params = array(), $append = false)
    {
        if ($append && is_array($this->_countQueryParams)) {
            $this->_countQueryParams = array_merge($this->_countQueryParams, $params);
        } else {
            if ($params !== null && !is_array($params)) {
                $params = array($params);
            }

            $this->_countQueryParams = $params;
        }

        $this->_setExecuted(false);
    }

    /**
     * execute
     *
     * Executes the query, populates the collection and then return it
     *
     * @param $params               Optional parameters to Doctrine_Query::execute
     * @param $hydrationMode        Hydration Mode of Doctrine_Query::execute returned ResultSet.
     * @return Doctrine_Collection  The root collection
     */
    public function execute($params = array(), $hydrationMode = null)
    {
        if ( !$this->getExecuted()) {
            $this->_initialize($params);
        }
        
        return $this->getQuery()->execute($params, $hydrationMode);
    }
}