<?php

/**
 * Abstract representation of a search source.
 *
 * @package XenForo_Search
 */
abstract class XenForo_Search_SourceHandler_Abstract
{
	/**
	 * @var Zend_Db_Adapter_Abstract
	 */
	protected $_db = null;

	/**
	 * @var XenForo_Search_Searcher|null
	 */
	protected $_searcher = null;

	/**
	 * Determines if this process is doing a bulk rebuild.
	 *
	 * @var boolean
	 */
	protected $_isRebuild = false;

	/**
	 * Determines if this source handler supports relevance searching. If false,
	 * it will not be made available as a sorting option.
	 *
	 * @return boolean
	 */
	abstract public function supportsRelevance();

	/**
	 * Inserts (or prepares to insert) a new record into the search index. This must
	 * also update (replace) an existing record, if the (type, id) pair already exists.
	 *
	 * @param string $contentType
	 * @param integer $contentId
	 * @param string $title
	 * @param string $message
	 * @param integer $itemDate Time stamp for the content (this will be used in date limits and sorts)
	 * @param integer $userId User that created the content
	 * @param integer $discussionId ID of discussion or other grouping container
	 * @param array $metadata Arbitrary list of other metadata that should be indexed if possible
	 */
	abstract public function insertIntoIndex($contentType, $contentId, $title, $message, $itemDate, $userId, $discussionId = 0, array $metadata = array());

	/**
	 * Updates specific fields in an already existing index record. Metadata cannot
	 * be updated this way.
	 *
	 * @param string $contentType
	 * @param integer $contentId
	 * @param array $fieldUpdates Key-value pairs to change
	 */
	abstract public function updateIndex($contentType, $contentId, array $fieldUpdates);

	/**
	 * Deletes the specified records from the index.
	 * @param string $contentType
	 * @param array $contentIds List of content IDs (of $contentType to delete)
	 */
	abstract public function deleteFromIndex($contentType, array $contentIds);

	/**
	 * Executes a search against the full text index.
	 *
	 * @param string $searchQuery Text to search for
	 * @param boolean $titleOnly If true, only searches text in titles
	 * @param array $processedConstraints Structured constraints
	 * @param array $orderParts Structured ordered by parts
	 * @param string $groupByDiscussionType If grouping, content type of grouped results
	 * @param integer $maxResults
	 * @param XenForo_Search_DataHandler_Abstract $typeHandler Type-specific handler, for joins
	 *
	 * @return array Search results ([] => array(content type, id))
	 */
	abstract public function executeSearch($searchQuery, $titleOnly, array $processedConstraints, array $orderParts,
		$groupByDiscussionType, $maxResults, XenForo_Search_DataHandler_Abstract $typeHandler = null
	);

	/**
	 * Executes a search for content by a specific user. Currently this includes no constraints,
	 * but down the line it may support non-query constraints.
	 *
	 * @param integer $userId
	 * @param integer $maxDate If >0, only messages older than this should be found
	 * @param integer $maxResults
	 *
	 * @return array Search results ([] => array(content type, id))
	 */
	abstract public function executeSearchByUserId($userId, $maxDate, $maxResults);

	/**
	 * Performs a general search. This will usually be across all types of content, but
	 * could be limited but only using standard constraints.
	 *
	 * @param string $searchQuery Text to search for
	 * @param array $constraints Constraints to apply; handled by source handlers
	 * @param string $order Ordering; handled by source handlers
	 * @param integer $maxResults Maximum number of results to return
	 *
	 * @return array Search results: [] => array(content_type => x, content_id => y)
	 */
	public function searchGeneral($searchQuery, array $constraints, $order, $maxResults)
	{
		$titleOnly = isset($constraints['title_only']);
		unset($constraints['title_only']);

		$processedConstraints = $this->processConstraints($constraints);
		$orderClause = $this->getGeneralOrderClause($order);

		return $this->executeSearch($searchQuery, $titleOnly, $processedConstraints, $orderClause, false, $maxResults);
	}

	/**
	 * Performs a type specific search.
	 *
	 * @param XenForo_Search_DataHandler_Abstract $typeHandler Data handler for the type of search
	 * @param string $searchQuery Text to search for
	 * @param array $constraints Constraints to apply; handled by source handlers
	 * @param string $order Ordering; handled by source handlers
	 * @param boolean $groupByDiscussion If true, fold/group the results by the discussion_id value
	 * @param integer $maxResults Maximum number of results to return
	 *
	 * @return array Search results: [] => array(content_type => x, content_id => y)
	 */
	public function searchType(XenForo_Search_DataHandler_Abstract $typeHandler,
		$searchQuery, array $constraints = array(), $order, $groupByDiscussion, $maxResults
	)
	{
		$titleOnly = isset($constraints['title_only']);
		unset($constraints['title_only']);

		$constraints['content'] = $typeHandler->getSearchContentTypes();
		$constraints = $typeHandler->filterConstraints($this, $constraints);
		$processedConstraints = $this->processConstraints($constraints, $typeHandler);

		$orderClause = $typeHandler->getOrderClause($order);
		if (!$orderClause)
		{
			 $orderClause = $this->getGeneralOrderClause($order);
		}

		$groupByDiscussionType = ($groupByDiscussion ? $typeHandler->getGroupByType() : '');

		return $this->executeSearch(
			$searchQuery, $titleOnly, $processedConstraints, $orderClause,
			$groupByDiscussionType, $maxResults, $typeHandler
		);
	}

	/**
	 * Deletes the entire search index or a particular part of it.
	 *
	 * @param string|null $contentType If specified, only deletes the index for this type
	 */
	public function deleteIndex($contentType = null)
	{
	}

	/**
	 * Process search constraints.
	 *
	 * @param array $constraints List of constraints: [constraint name] => limit (may be scalar or array)
	 * @param XenForo_Search_DataHandler_Abstract|null $typeHandler
	 *
	 * @return array Processed constraints. Names as keys, value is array with possible keys:
	 * 		* metadata - metadata value; keys: 0 = name of metadata, 1 = scalar/array allowed value(s) for metadata
	 * 		* query - constraint to limit via query; keys: 0 = table alias, 1 = field, 2 = operator, 3 = scalar/array allowed value(s). Multiple for "=" operator only.
	 * 	Note that the metadata and query keys are assumed to be equivalent. Engines need only use one (depending on engine details).
	 */
	public function processConstraints(array $constraints, XenForo_Search_DataHandler_Abstract $typeHandler = null)
	{
		$processed = array();

		foreach ($constraints AS $constraint => $constraintInfo)
		{
			if ((is_array($constraintInfo) && count($constraintInfo) == 0)
				|| (is_string($constraintInfo) && $constraintInfo === '')
			)
			{
				continue;
			}

			switch ($constraint)
			{
				case 'user':
					$processed[$constraint] = array(
						'metadata' => array('user', $constraintInfo),
						'query' => array('search_index', 'user_id', '=', $constraintInfo)
					);
					break;

				case 'user_content':
					if (!empty($constraints['user']))
					{
						$processed[$constraint] = array(
							'metadata' => array('content', $constraintInfo),
							'query' => array('search_index', 'content_type', '=', $constraintInfo)
						);
					}
					break;

				case 'content':
					$processed[$constraint] = array(
						'metadata' => array('content', $constraintInfo),
						'query' => array('search_index', 'content_type', '=', $constraintInfo)
					);
					break;

				case 'node':
					$nodes = preg_split('/\D/', strval($constraintInfo));
					$processed[$constraint] = array(
						'metadata' => array('node', array_map('intval', $nodes)),
					);
					break;

				case 'tag':
					$tagIds = preg_split('/\D/', strval($constraintInfo));
					foreach ($tagIds AS $tagId)
					{
						// this approach requires all tags to exist
						$processed[$constraint . $tagId] = array(
							'metadata' => array('tag', $tagId),
						);
					}
					break;

				case 'tag_any':
					$nodes = preg_split('/\D/', strval($constraintInfo));
					$processed[$constraint] = array(
						'metadata' => array('tag', array_map('intval', $nodes)),
					);
					break;

				case 'date':
					$processed[$constraint] = array(
						'query' => array('search_index', 'item_date', '>', intval($constraintInfo))
					);
					break;

				case 'date_max':
					$processed[$constraint] = array(
						'query' => array('search_index', 'item_date', '<', intval($constraintInfo))
					);
					break;

				default:
					if ($typeHandler)
					{
						$newConstraint = $typeHandler->processConstraint($this, $constraint, $constraintInfo, $constraints);
						if ($newConstraint)
						{
							$processed[$constraint] = $newConstraint;
						}
					}
			}
		}

		return $processed;
	}

	/**
	 * Gets the general order clauses for a search.
	 *
	 * @param string $order User-requested order
	 *
	 * @return array Structured order clause, array of arrays. Child array keys: 0 = table alias, 1 = field, 2 = dir (asc/desc)
	 */
	public function getGeneralOrderClause($order)
	{
		return array(
			array('search_index', 'item_date', 'desc')
		);
	}

	/**
	 * Triggers an error with the searcher object. An error will prevent the search
	 * from going through.
	 *
	 * @param XenForo_Phrase|string $message Error message
	 * @param string $field Field error applies to
	 */
	public function error($message, $field)
	{
		if ($this->_searcher)
		{
			$this->_searcher->error($message, $field);
		}
	}

	/**
	 * Triggers a warning with the searcher object. This will be shown to the user
	 * on the search results page.
	 *
	 * @param XenForo_Phrase|string $message Warning message
	 * @param string $field Field warning applies to
	 */
	public function warning($message, $field)
	{
		if ($this->_searcher)
		{
			$this->_searcher->warning($message, $field);
		}
	}

	/**
	 * Sets the containing searcher object. This will be used for things
	 * like error tracking.
	 *
	 * @param XenForo_Search_Searcher|null $searcher
	 */
	public function setSearcher(XenForo_Search_Searcher $searcher = null)
	{
		$this->_searcher = $searcher;
	}

	/**
	 * Sets whether this is a bulk rebuild. If true, behavior may be modified to be
	 * less asynchronous.
	 *
	 * @param boolean $rebuild
	 */
	public function setIsRebuild($rebuild)
	{
		$this->_isRebuild = $rebuild;
	}

	/**
	 * When rebuilding, it might be advantageous to bulk update records. This function
	 * must be called to ensure that all records are updated together.
	 */
	public function finalizeRebuildSet()
	{
	}

	/**
	 * @return Zend_Db_Adapter_Abstract
	 */
	protected function _getDb()
	{
		if ($this->_db === null)
		{
			$this->_db = XenForo_Application::getDb();
		}

		return $this->_db;
	}

	/**
	 * Gets the default source handler.
	 *
	 * @return XenForo_Search_SourceHandler_Abstract
	 */
	public static function getDefaultSourceHandler()
	{
		$class = 'XenForo_Search_SourceHandler_MySqlFt';
		XenForo_CodeEvent::fire('search_source_create', array(&$class));
		$class = XenForo_Application::resolveDynamicClass($class);

		return new $class();
	}
}