<?php
/**
 * Horde_Core_TagBrowser:: class provides logic for dealing with tag browsing.
 *
 * Copyright 2011-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (GPL). If you
 * did not receive this file, see http://www.horde.org/licenses/gpl.
 *
 * @author  Michael J Rubinsky <mrubinsk@horde.org>
 * @category Horde
 * @license  http://www.horde.org/licenses/gpl GPL
 * @package  Core
 */
abstract class Horde_Core_TagBrowser
{
    /**
     * The application this browser is for.
     *
     * @var string
     */
    protected $_app;

    /**
     * Array of tag_name => tag_id hashes for the current search.
     * Tags are always added to the search by name and stored by name=>id.
     *
     * @var array
     */
    protected $_tags = array();

    /**
     * Total count of matches.
     *
     * @var integer
     */
    protected $_totalCount = null;

    /**
     * The user whose resources we are searching.
     *
     * @var string
     */
    protected $_owner = '';

    /**
     * Dirty flag
     *
     * @var boolean
     */
    protected $_dirty = false;

    /**
     * Results cache. Holds the results of the current search.
     *
     * @var array
     */
    protected $_results = array();

    /**
     * The Tagger object.
     *
     * @var Horde_Core_Tagger
     */
    protected $_tagger;

    /**
     * Const'r
     *
     * @param Horde_Core_Tagger $tagger  The tagger object.
     * @param array $tags                Tags to add to initially search on.
     * @param string $owner              Restrict to resources owned by owner.
     */
    public function __construct(
        Horde_Core_Tagger $tagger,
        $tags = null,
        $owner = null)
    {
        $this->_tagger = $tagger;
        if (!empty($tags)) {
            $this->_tags = $this->_tagger->getTagIds($tags);
        } else {
            $this->_tags = $GLOBALS['session']->get(
                $this->_app,
                'browsetags',
                Horde_Session::TYPE_ARRAY);
        }

        $this->_owner = empty($owner) ? $GLOBALS['registry']->getAuth() : $owner;
    }

    /**
     * Saves current state to the session.
     */
    public function save()
    {
        $GLOBALS['session']->set($this->_app, 'browsetags', $this->_tags);
        $this->_dirty = false;
    }

    /**
     * Add a tag to the cumulative tag search
     *
     * @param string $tag  The tag name to add.
     */
    public function addTag($tag)
    {
        $tag_id = (int)current($this->_tagger->getTagIds($tag));
        if (empty($tag_id)) {
            return;
        }
        if (array_search($tag_id, $this->_tags) === false) {
            $this->_tags[$tag] = $tag_id;
            $this->_dirty = true;
        }
    }

    /**
     * Remove a tag from the cumulative search
     *
     * @param string $tag  The tag name to remove.
     */
    public function removeTag($tag)
    {
        if (!empty($this->_tags[$tag])) {
            unset($this->_tags[$tag]);
            $this->_dirty = true;
        }
    }

    /**
     * Get the list of currently choosen tags
     *
     * @return array  A hash of the currently selected tag_name => tag_id.
     */
    public function getTags()
    {
        return $this->_tags;
    }

    /**
     * Get the total number of tags included in this search.
     *
     * @return integer  The number of tags used in the current search.
     */
    public function tagCount()
    {
        return count($this->_tags);
    }

    /**
     * Get the total number of resources that match.
     *
     * @return integer  The count of matching resources.
     */
    public function count()
    {
        if (!is_array($this->_tags) || !count($this->_tags)) {
            return 0;
        }

        $count = count($this->_results);
        $this->_totalCount = $count;

        return $count;
    }

    /**
     * Get a list of tags related to this search. Concrete tagger classes
     * can override the _getRelatedTagsWith* methods if they can perform
     * them more efficiently.
     *
     * @param array $default_results  A default list of object ids to use to
     *                                fetch tags from. Used when the current
     *                                search results are empty.
     *
     * @todo H6 - standardize the cloud array keys (total vs count etc..)
     * @return array An array  tag_id => {tag_name, total}
     */
    public function getRelatedTags($default_results = null)
    {
        // If we have search results already, we can use the more efficient
        // cloud methods to obtain the result counts. Otherwise, we have to
        // search for each tag to get the result count.
        if (empty($this->_results) && !isset($default_results)) {
            $results = $this->_getRelatedTagsWithNoResults();
        } else {
            $results = $this->_getRelatedTagsWithResults($default_results);
        }

        // Get the results sorted by available totals for this user
        uasort($results, array($this, '_sortTagInfo'));
        return $results;
    }

    /**
     * Default implementation for getRelatedTags
     *
     * @param array $default_results  A default list of object ids to use to
     *                                fetch tags from. Used when the current
     *                                search results are empty.
     *
     * @return array An array of tag_id => [tag_name, total].
     */
    protected function _getRelatedTagsWithResults($default_results = null)
    {
        // No results, and an empty default_results set is provided. We have
        // no objects to browse, don't attempt to look for their tags.
        if (empty($this->_results) && isset($default_results) && empty($default_results)) {
            return array();
        }

        $results = array();
        $tags = $this->_tagger->browseTags($this->getTags(), null);
        $result_data = empty($this->_results) ? (!empty($default_results) ? $default_results : array()) : $this->_results;
        if (!empty($result_data) && empty($result_data[0])) {
            // Multiple types.
            $counts = array();
            foreach ($result_data as $data) {
                $counts = array_merge($counts, $this->_tagger->getTagCountsByObjects($data));
            }
        } else {
            $counts = $this->_tagger->getTagCountsByObjects($result_data);
        }
        $tag_ids = array_keys($tags);
        foreach ($counts as $result) {
            // Remove the tags we already included.
            if (in_array($result['tag_id'], $tag_ids)) {
                $results[$result['tag_id']] = array('tag_name' => $result['tag_name'], 'total' => $result['count']);
            }
        }
        return $results;
    }

    /**
     * Default implementation for getting related tags when we don't have
     * any current search in effect. This is very inefficent and should only
     * be used as a very last resort. Better to have concrete classes provide
     * the full result set. See _getRelatedTagsWithResults().
     *
     * @return array An array of tag_id => [tag_name, total]
     */
    protected function _getRelatedTagsWithNoResults()
    {
        $results = array();
        $tags = $this->_tagger->browseTags($this->getTags(), null);
        $class = get_class($this);
        $search = new $class($this->_tagger, null, $this->_owner);
        foreach ($tags as $id => $tag) {
            $search->addTag($tag);
            $search->runSearch();
            $count = $search->count();
            if ($count > 0) {
                $results[$id] = array('tag_name' => $tag, 'total' => $count);
            }
            $search->removeTag($tag);
        }

        return $results;
    }

    /**
     * Perform, and cache the search.
     *
     */
    public function runSearch()
    {
        $this->_results = $this->_runSearch();
    }

    /**
     * Clears the session cache of tags currently included in the search.
     */
    public function clearSearch()
    {
        $GLOBALS['session']->remove($this->_app, 'browsetags');
        $this->_tags = array();
    }

    /**
     * Helper for uasort.  Sorts the results by count.
     *
     */
    protected function _sortTagInfo($a, $b)
    {
        return $a['total']  <  $b['total'];
    }

    /**
     * Default implementation for runSearch.
     *
     * @return array  An array of search results. Either a one dimensional
     *                array containing local object uids, or a multi dimensional
     *                array of object_type => array_of_uids, ....
     */
    protected function _runSearch()
    {
        if (!empty($this->_owner)) {
            $filter = array('user' => $this->_owner);
        } else {
            $filter = array();
        }
        if (empty($this->_results) || $this->_dirty) {
            return $this->_tagger->search($this->_tags, $filter);
        }
    }

    /**
     * Fetch the matching resources that should appear on the current page
     *
     * @param integer $page     The page to get slice for.
     * @param integer $perpage  The number of objects per page.
     *
     * @return array  An array of result objects.
     */
    abstract public function getSlice($page = 0, $perpage = null);

    /**
     * Get breadcrumb style navigation html for choosen tags
     *
     * @return  Return information useful for building a tag trail.
     */
    abstract public function getTagTrail();

}
