<?php
/**
 * Copyright 2001-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file LICENSE for license information (BSD). If you did not
 * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
 *
 * @author  Michael J Rubinsky <mrubinsk@horde.org>
 * @category Horde
 * @license  http://www.horde.org/licenses/gpl GPL
 * @package Nag
 */
/**
 * Nag_Search:: Interface for performing task searches.
 *
 * Copyright 2001-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file LICENSE for license information (BSD). If you did not
 * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
 *
 * @author  Michael J Rubinsky <mrubinsk@horde.org>
 * @category Horde
 * @license  http://www.horde.org/licenses/gpl GPL
 * @package Nag
 */
class Nag_Search implements Serializable
{
    /**
     * Search bit masks
     */
    const MASK_NAME      = 1;
    const MASK_DESC      = 2;
    const MASK_TAGS      = 4;
    const MASK_ALL       = 7;

    /**
     * Search criteria
     *
     * @var array
     */
    protected $_search;

    /**
     * The search mask
     *
     * @var integer
     */
    protected $_mask;

    /**
     * The completed/view value.
     *
     * @var integer
     */
    protected $_completed;

    /**
     * The tasks lists to search.
     *
     * @var array
     */
    protected $_tasklists;

    /**
     * Duedate criteria
     *
     * @var array
     */
    protected $_due;

    /**
     * Tag search criteria
     *
     * @var array
     */
    protected $_tags;

    /**
     * Constructor
     *
     * @param string $search  The search string.
     * @param integer $mask   A bitmask to indicate the fields to search.
     * @param array $options  Additional options:
     *   - completed: (integer) Which tasks to include. A NAG::VIEW_* constant.
     *                DEFAULT: Nag::VIEW_INCOMPLETE
     *
     *   - due: (array)  An array describing the due date portion of the search.
     *          EXAMPLE: array('5', 'tomorrow') would be all tasks due within 5
     *          days of tomorrow.
     *          DEFAULT: No date filters.
     *
     *   - tags: (array) An array of tags to filter on.
     *   - tasklists: (array) An array of tasklist ids to filter on.
     *                DEFAULT: The current display_tasklists value is used.
     *
     * @return Nag_Search
     */
    public function __construct($search, $mask, array $options = array())
    {
        $options = array_merge(
            array(
                'completed' => 0,
                'due' => array(),
                'tags' => array(),
                'tasklists' => $GLOBALS['display_tasklists']),
            $options);

        $this->_search = $search;
        $this->_mask = $mask;
        $this->_completed = $options['completed'];
        $this->_due = $options['due'];
        $this->_tags = $options['tags'];
        $this->_tasklists = $options['tasklists'];
    }

    /**
     * Get a result slice.
     *
     * @param integer $page     The page number
     * @param integer $perpage  The number of results per page.
     *
     * @return Nag_Task  The result list.
     */
    public function getSlice($page = 0, $perpage = 0)
    {
        return $this->_search($page, $perpage);
    }

    /**
     * Perform the search
     *
     * @param integer $page     The page number
     * @param integer $perpage  The number of results per page.
     *
     * @return Nag_Task
     */
    protected function _search($page, $perpage)
    {
        global $injector, $prefs;

        if (!empty($this->_due)) {
            $parser = Horde_Date_Parser::factory(array('locale' => $GLOBALS['prefs']->getValue('language')));
            $date = $parser->parse($this->_due[1]);
            $date->mday += $this->_due[0];
            $date = $date->timestamp();
        } else {
            $date = false;
        }

        // Get the full, sorted task list.
        $tasks = Nag::listTasks(array(
            'tasklists' => $this->_tasklists,
            'completed' => $this->_completed,
            'include_history' => false)
        );
        if (!empty($this->_search)) {
            $pattern = '/' . preg_quote($this->_search, '/') . '/i';
        }
        $search_results = new Nag_Task();
        if (!empty($this->_tags)) {
            $tagged_tasks = $injector->getInstance('Nag_Tagger')->search(
                $this->_tags,
                array('list' => $GLOBALS['display_tasklists'])
            );
        }
        $tasks->reset();
        while ($task = $tasks->each()) {
            // Need to empty the children since they might not be in the results
            $task = clone $task;
            $task->orphan();
            if (!empty($date)) {
                if (empty($task->due) || $task->due > $date) {
                    continue;
                }
            }

            // If we have a search string and it doesn't match name|desc continue
            if (!empty($this->_search) &&
                !($this->_mask & self::MASK_NAME && preg_match($pattern, $task->name)) &&
                !($this->_mask & self::MASK_DESC && preg_match($pattern, $task->desc))) {

                continue;
            }

            // No tags to search? Add it to results. Otherwise, make sure it
            // has the requested tags.
            if (empty($this->_tags) || in_array($task->uid, $tagged_tasks)) {
                $search_results->add($task);
            }
        }

        // Now try to maintain parent/child relationships when they are both
        // in the result set.
        $search_results->reset();
        $search_results_copy = clone $search_results;
        $processed_results = new Nag_Task();

        while ($result = $search_results_copy->each()) {
            if ($result->parent_id &&
                ($parent_task = $search_results->hasTask($result->parent_id))) {
                $parent_task->add($result, true);
                $processed_results->add($parent_task, true);
            } else {
                $result->parent_id = '';
                $processed_results->add($result);
            }
        }

      // Now that we have filtered results, load all tags at once.
        $processed_results->loadTags();
        $processed_results->process();

        return $processed_results;
    }

    /**
     * Populate a Horde_Variables instance with the search values for this
     * search.
     *
     * @param Horde_Variables $vars  The Horde_Variables object.
     */
    public function getVars(Horde_Variables &$vars)
    {
        $vars->set('search_pattern', $this->_search);
        $vars->set('search_tags', implode(',', $this->_tags));
        $vars->set('search_completed', $this->_completed);
        $vars->set('due_within', $this->_due[0]);
        $vars->set('due_of', $this->_due[1]);

        $mask = array();
        if ($this->_mask & self::MASK_NAME) {
            $mask[] = 'search_name';
        }
        if ($this->_mask & self::MASK_DESC) {
            $mask[] = 'search_desc';
        }
        if (!empty($mask)) {
            $vars->set('search_in', $mask);
        }
    }

    /**
     * Serialize method
     *
     * @return array  The unserialized data.
     */
    public function serialize()
    {
        return serialize(array(
            'search' => $this->_search,
            'mask' => $this->_mask,
            'completed' => $this->_completed,
            'due' => $this->_due,
            'tags' => $this->_tags));
    }

    /**
     * Unserialize method
     *
     * @param string $data  The serialized data.
     */
    public function unserialize($data)
    {
        $data = unserialize($data);
        $this->_search = $data['search'];
        $this->_mask = $data['mask'];
        $this->_completed = $data['completed'];
        $this->_due = $data['due'];
        $this->_tags = $data['tags'];
    }

}
