<?php
/**
 * @brief		Sphinx Search Query
 * @author		<a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
 * @copyright	(c) 2001 - SVN_YYYY Invision Power Services, Inc.
 * @license		http://www.invisionpower.com/legal/standards/
 * @package		IPS Social Suite
 * @since		26 Aug 2014
 * @version		SVN_VERSION_NUMBER
*/

namespace IPS\Content\Search\Sphinx;

/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
	header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
	exit;
}

/**
 * Sphinx Search Query
 */
class _Query extends \IPS\Content\Search\Query
{
	const SUPPORTS_JOIN_FILTERS = FALSE;
	
	/**
	 * @brief	The index table
	 */
	protected $table;
		
	/**
     * @brief       The WHERE clause
     */
    protected $where = array();
    
    /**
     * @brief       The WHERE clause for hidden/unhidden
     */
    protected $hiddenClause = NULL;
    
     /**
     * @brief       The ORDER BY clause
     */
    protected $order = NULL;
    
    /**
     * @brief       The offset
     */
    protected $offset = 0;
	
	/**
	 * Constructor
	 *
	 * @param	\IPS\Member	$member	The member performing the search
	 * @return	void
	 */
	public function __construct( \IPS\Member $member )
	{		
		\IPS\Db::i( 'sphinx', array(
			'sql_host'		=> \IPS\Settings::i()->search_sphinx_server,
			'sql_user'		=> NULL,
			'sql_pass'		=> NULL,
			'sql_database'	=> NULL,
			'sql_port'		=> (int) \IPS\Settings::i()->search_sphinx_port,
			'sql_socket'	=> NULL,
			'sql_utf8mb4'	=> \IPS\Settings::i()->sql_utf8mb4
		) );
		
		$this->table = \IPS\Settings::i()->sphinx_prefix . 'ips';
		
		parent::__construct( $member );
	}
	
    /**
	 * Filter by content type
	 *
	 * @param	string		$class 				The type of content to search (including all comment/review classes)
	 * @param	array|NULL	$containers			An array of container IDs to filter by, or NULL to not filter by containers
	 * @param	int|NULL	$minimumComments	The minimum number of comments
	 * @param	int|NULL	$minimumReviews		The minimum number of reviews
	 * @param	int|NULL	$minimumViews		The minimum number of views
	 * @return	\IPS\Content\Search\Query	(for daisy chaining)
	 */
	public function filterByContent( $class, $containers = NULL, $minimumComments = NULL, $minimumReviews = NULL, $minimumViews = NULL )
	{				
		/* Classes */
		$classes = array( \IPS\Content\Search\Sphinx\Index::_getClassId( $class ) );
		if ( isset( $class::$commentClass ) )
		{
			$classes[] = \IPS\Content\Search\Sphinx\Index::_getClassId( $class::$commentClass );
		}
		if ( isset( $class::$reviewClass ) )
		{
			$classes[] = \IPS\Content\Search\Sphinx\Index::_getClassId( $class::$reviewClass );
		}
		$this->where[] = 'index_class_id IN(' . implode( ',', $classes ) . ')';
		
		/* Containers */
		if ( $containers !== NULL )
		{
			$this->where[] = 'index_container_id IN(' . implode( ',', array_map( 'intval', $containers ) ) . ' )';
		}
		
		/* We don't support anything else */
		if ( $minimumComments !== NULL or $minimumReviews !== NULL or $minimumViews !== NULL )
		{
			throw new \UnexpectedValueException;
		}
		
		return $this;
	}
	
	/**
	 * Filter by content type to exclude
	 *
	 * @param	array		$classes 		The types of content to exclude (not including all comment/review classes)
	 * @param	bool		$allowedContiners		If TRUE, $class should be an array of allowed containers for that class - e.g. array( 'IPS\forums\Topic' => array( 1, 2 ) - otherwise, a simple array - e.g. array( 'IPS\forums\Topic' )
	 * @return	\IPS\Content\Search\Query	(for daisy chaining)
	 */
	public function filterByExcludeContent( array $classes, $allowedContiners=FALSE )
	{
		if ( $allowedContiners )
		{
			$where = array();
			foreach ( $classes as $class => $allowed )
			{
				if ( count( $allowed ) )
				{
					$where[] = '( index_class<>' . \IPS\Content\Search\Sphinx\Index::_getClassId( $class ) . ' OR index_container_id IN( ' . implode( ',', $allowed ) . ' ) )';
				}
				else
				{
					$excludeAll[] = $class;
				}
			}
			
			$finalWhere = array();
			if ( count( $excludeAll ) )
			{
				$finalWhere[] = 'index_class NOT IN('. implode( ',', array_map( array( 'IPS\Content\Search\Sphinx\Index', '_getClassId' ), $excludeAll ) ) . ')';
			}
			if ( count( $where ) )
			{
				$finalWhere[] = implode( ' OR ', $where );
			}
			
			$this->where[] = array_merge( array( implode( ' AND ', $finalWhere ) ) );
		}
		else
		{
			$this->where[] = 'index_class NOT IN('. implode( ',', array_map( array( 'IPS\Content\Search\Sphinx\Index', '_getClassId' ), $classes ) ) . ')';
		}
		return $this;
	}
		
	/**
	 * Filter by content item
	 *
	 * @param	\IPS\Content\Item	$item		The item
	 * @return	\IPS\Content\Search\Query	(for daisy chaining)
	 */
	public function filterByItem( \IPS\Content\Item $item )
	{
		$class = get_class( $item );
		
		$classes = array();
		if ( isset( $class::$commentClass ) )
		{
			$classes[] = \IPS\Content\Search\Sphinx\Index::_getClassId( $class::$commentClass );
		}
		if ( isset( $class::$reviewClass ) )
		{
			$classes[] = \IPS\Content\Search\Sphinx\Index::_getClassId( $class::$reviewClass );
		}
		$this->where[] = 'index_class_id IN(' . implode( ',', $classes ) . ')';
		
		$idColumn = $item::$databaseColumnId;
		$this->where[] = "index_item_id={$item->$idColumn}";
		
		return $this;
	}
	
	/**
	 * Filter by author
	 *
	 * @param	\IPS\Member	$author		The author
	 * @return	\IPS\Content\Search\Query	(for daisy chaining)
	 */
	public function filterByAuthor( \IPS\Member $author )
	{
		$this->where[] = "index_author={$author->member_id}";
		return $this;
	}
		
	/**
	 * Filter by start date
	 *
	 * @param	\IPS\DateTime|NULL	$start		The start date (only results AFTER this date will be returned)
	 * @param	\IPS\DateTime|NULL	$end		The end date (only results BEFORE this date will be returned)
	 * @return	\IPS\Content\Search\Query	(for daisy chaining)
	 */
	public function filterByCreateDate( \IPS\DateTime $start = NULL, \IPS\DateTime $end = NULL )
	{
		if ( $start )
		{
			$this->where[] = 'index_date_created>' . $start->getTimestamp();
		}
		if ( $end )
		{
			$this->where[] = 'index_date_created<' . $end->getTimestamp();
		}
		return $this;
	}
	
	/**
	 * Filter by last updated date
	 *
	 * @param	\IPS\DateTime|NULL	$start		The start date (only results AFTER this date will be returned)
	 * @param	\IPS\DateTime|NULL	$end		The end date (only results BEFORE this date will be returned)
	 * @return	\IPS\Content\Search\Query	(for daisy chaining)
	 */
	public function filterByLastUpdatedDate( \IPS\DateTime $start = NULL, \IPS\DateTime $end = NULL )
	{
		if ( $start )
		{
			$this->where[] = 'index_date_updated>' . $start->getTimestamp();
		}
		if ( $end )
		{
			$this->where[] = 'index_date_updated<' . $end->getTimestamp();
		}
		return $this;
	}
	
	/**
	 * Set hidden status
	 *
	 * @param	int|array	$statuses	The statuses (array of HIDDEN_ constants)
	 * @return	\IPS\Content\Search\Query	(for daisy chaining)
	 */
	public function setHiddenFilter( $statuses )
	{
		if ( is_null( $statuses ) )
		{
			$this->hiddenClause = NULL;
		}
		if ( is_array( $statuses ) )
		{
			$this->hiddenClause = 'index_hidden IN(' . implode( ',', array_map( 'intval', $statuses ) ) . ' )';
		}
		else
		{
			$this->hiddenClause = 'index_hidden=' . intval( $statuses );
		}
				
		return $this;
	}
	
	/**
	 * Set page
	 *
	 * @param	int		$page	The page number
	 * @return	\IPS\Content\Search\Query	(for daisy chaining)
	 */
	public function setPage( $page )
	{
		$page			= $page ?: 1;
		$this->offset	= ( $page - 1 ) * $this->resultsToGet;
		
		return $this;
	}
	
	/**
	 * Set order
	 *
	 * @param	int		$order	Order (see ORDER_ constants)
	 * @return	\IPS\Content\Search\Query	(for daisy chaining)
	 */
	public function setOrder( $order )
	{
		switch ( $order )
		{
			case static::ORDER_NEWEST_UPDATED:
				$this->order = 'index_date_updated DESC';
				break;
			
			case static::ORDER_NEWEST_CREATED:
				$this->order = 'index_date_created DESC';
				break;
			
			case static::ORDER_RELEVANCY:
				$this->order = NULL;
				break;
		}
		
		return $this;
	}
	
	/**
	 * Search
	 *
	 * @param	string|null	$term	The term to search for
	 * @param	array|null	$tags	The tags to search for
	 * @param	int			$method	\IPS\Content\Search\Index::i()->TERM_OR_TAGS or \IPS\Content\Search\Index::i()->TERM_AND_TAGS
	 * @return	\IPS\Content\Search\Results
	 */
	public function search( $term = NULL, $tags = NULL, $method = 1 )
	{
		/* Build the match term */
		if ( $term !== NULL )
		{
			$match[] = "( @(index_title,index_content) {$this->escape( $term )} )";
		}
		if ( $tags !== NULL )
		{
			$match[] = "( @index_tags " . implode( ' | ', array_map( array( $this, 'escape' ), $tags ) ) . ' )';
		}
		if ( !empty( $match ) )
		{
			array_unshift( $this->where, 'MATCH( \'' . implode( ( $method === static::TERM_OR_TAGS ? ' | ' : ' ' ), $match ) . '\' )' );
		}
		
		/* Only get stuff we have permission for */
		$permissions = array( '\'' . $this->escape( '*' ) . '\'' );
		foreach ( $this->permissionArray() as $id )
		{
			$permissions[] = '\'' . $this->escape( $id ) . '\'';
		}
		$permCheck = 'IN(index_permissions,' . implode( ',', $permissions ) . ') AS perm_check';
		$this->where[] = 'perm_check=1';
		
		/* Add the hidden clause */
		if ( $this->hiddenClause )
		{
			$this->where[] = $this->hiddenClause;
		}
		
		/* Query */
		$query = \IPS\Db::i('sphinx')->query( "SELECT *, {$permCheck} FROM {$this->table} WHERE " . implode( ' AND ', $this->where ) . ( $this->order ? " ORDER BY {$this->order}" : '' ) . " LIMIT {$this->offset}, {$this->resultsToGet}" );
		
		/* Get count */
		$meta = \IPS\Db::i('sphinx')->query('SHOW META');
		while ( $row = $meta->fetch_assoc() )
		{
			if ( $row['Variable_name'] === 'total' )
			{
				$count = $row['Value'];
			}
			break;
		}
		
		/* Convert to array */
		$results = array();
		while ( $row = $query->fetch_assoc() )
		{
			$results[] = $row;
		}
						
		/* Return */
		return new \IPS\Content\Search\Results( $results, $count );
	}
	
	/**
	 * Escape string for SphinxQL query
	 *
	 * @param	string	$term	Search term
	 * @return	string
	 */
	protected function escape( $term )
	{
		return str_replace( array( '\\', '(',')','|','-','!','@','~','"','&', '/', '^', '$', '=', "'", "\x00", "\n", "\r", "\x1a" ), array( '\\\\', '\\\(','\\\)','\\\|','\\\-','\\\!','\\\@','\\\~','\\\"', '\\\&', '\\\/', '\\\^', '\\\$', '\\\=', "\\'", "\\x00", "\\n", "\\r", "\\x1a" ), $term );
	}
}