<?php

/**************************************************************
* This file is part of Remository
* Copyright (c) 2006-17 Martin Brampton
* 
* License: GNU General Public License version 2 or later; see LICENSE.txt
* 
* For support and other information, visit http://remository.com
* To contact Martin Brampton, write to martin@remository.com
*
* Remository started life as the psx-dude script by psx-dude@psx-dude.net
* It was enhanced by Matt Smith up to version 2.10
* Since then development has been primarily by Martin Brampton,
* with contributions from other people gratefully accepted
*/

defined('_JEXEC') or die;

class booleanSearchParser {
	protected $tokens = array('');
	protected $tokentext = array(false);
	protected $track = array();
	protected $tid = 0;
	protected $next = 0;

	public function parse ($text) {
		$inquote = false;
		while ($text != ($text = str_replace('  ', ' ', $text)));
		for ($i=0; $i<strlen($text); $i++) {
			$char = $text[$i];
			if ('"' == $char) {
				if ($inquote) $this->tid = array_pop($this->track);
				else $this->startNext(true);
				$inquote = !$inquote;
			}
			elseif (!$inquote AND '(' == $char) $this->startNext(false);
			elseif (!$inquote AND ')' == $char) {
				if (count($this->track)) $this->tid = array_pop($this->track);
			}
			else $this->tokens[$this->tid] .= $char;
		}
		$this->addAnds();
		$this->addBrackets();
		foreach ($this->tokens as $i=>$token) if (!$this->tokentext[$i]) $this->tokens[$i] = $this->convertLogic($token);
		$text = $this->recombine();
		$text = $this->finishOff($text);
		return $text;
	}

	private function startNext ($isText) {
		$this->next++;
		$this->tokentext[$this->next] = $isText;
		$this->tokens[$this->tid] .= "#$this->next#";
		array_push($this->track,$this->tid);
		$this->tid = $this->next;
		$this->tokens[$this->tid] = '';
	}

	private function addAnds () {
		$logic = array('NOT', 'OR', 'AND');
		foreach ($this->tokens as $i=>$token) {
			if ($this->tokentext[$i]) continue;
			$anded = array();
			$words = explode (' ',$token);
			foreach ($words as $sub=>$word) {
				if (!in_array($word, $logic) AND $sub > 0 AND !in_array($words[$sub-1], $logic)) array_push($anded, 'AND');
				array_push($anded, $word);
			}
			$this->tokens[$i] = implode(' ', $anded);
		}
	}

	private function addBrackets () {
		$priority = 'OR';
		$nonpriority = 'AND';
		foreach ($this->tokens as $i=>$token) {
			$pmode = false;
			$nmode = false;
			$opened = false;
			$words = explode(' ', $token);
			foreach ($words as $j=>$word) {
				if ($priority == $word) {
					if ($nmode AND $j>0) {
						$words[$j-1] = '('.$words[$j-1];
						$opened = true;
					}
					$pmode = true;
					$nmode = false;
				}
				if ($nonpriority == $word) {
					if ($pmode AND $j>0) $words[$j-1] = $words[$j-1].')';
					if (!$opened) $words[0] = '('.$words[0];
					$pmode = false;
					$nmode = true;
				}
			}
			if ($pmode AND $opened) $words[j] .= ')';
			$this->tokens[$i] = implode(' ', $words);
		}
	}

	private function convertLogic ($text) {
		$regexes = array(
			'/\bOR\b/',
			'/\bAND\b/',
			'/\bNOT\b/'
		);
		$replace = array(
			' ',
			'+',
			'-'
		);

		$text = preg_replace($regexes, $replace, $text);

		$newword = true;
		$parsed = $nonblank = '';
		$depth = 0;
		for ($i=0; $i<strlen($text); $i++) {
			$char = $text[$i];
			if (' ' != $char) $nonblank .= $char;
			if ('(' == $char) {
				$newword = true;
				$start[$depth] = strlen($parsed);
				$parsed .= ' (';
				$depth++;
			}
			elseif (')' == $char) {
				$newword = true;
				$parsed .= $char;
				if (isset($start[$depth])) unset($start[$depth]);
				$depth--;
			}
			elseif (' ' == $char) {
				$newword = true;
				$parsed .= $char;
			}
			elseif ('+' == $char) {
				$newword = true;
				if (isset($start[$depth])) $parsed[$start[$depth]] = '+';
				$parsed .= '+';
			}
			else {
				if ($newword) {
					$start[$depth] = strlen($parsed);
					$parsed .= ' '.$char;
				}
				else $parsed .= $char;
				$newword = false;
			}
		}
		return $parsed;
	}

	private function recombine () {
		$text = isset($this->tokens[0]) ? $this->tokens[0] : '';
		for ($i=1; $i<count($this->tokens); $i++) {
			if ($this->tokentext[$i]) $text = str_replace("#$i#", '"'.$this->tokens[$i].'"', $text);
			else $text = str_replace("#$i#", '('.$this->tokens[$i].')', $text);
		}
		return $text;
	}

	private function finishOff ($parsed) {
		$mess = array('  ', '+ +', '++', '( ', ' )', '+ ', '- ');
		$tidy = array(' ', '+', '+', '(', ')', '+', '-');
		while ($parsed != ($parsed = str_replace($mess, $tidy, $parsed)));
		return $parsed;
	}

}

class remositoryClassificationHandler {
	// Search parameters
	protected $userstuff = array();
	protected $matchtype = 'all';
	protected $allclassify = array();
	protected $allvisible = array();
	protected $typelink = array();

	// Constructor
	private function __construct () {
		$sql = "SELECT * FROM #__downloads_classify WHERE published = 1 ORDER BY type";
		$result = remositoryRepository::doSQLget($sql, 'remositoryClassification');
		if ($result) foreach ($result as $item) {
			$this->allclassify[$item->type][$item->id] = $item;
			if (!$item->hidden) $this->allvisible[$item->type][$item->id] = $item->id;
			$this->typelink[$item->id] = $item->type;
		}
		$this->setNewUserStuff();
		$this->userstuff['pagecount'] = (int) _ITEMS_PER_PAGE;
	}

	// private function to reset the user stuff
	private function setNewUserStuff () {
		$this->userstuff['searchtext'] = '';
		$this->userstuff['categories'] = array();
		$this->userstuff['submitter'] = 0;
		$this->userstuff['newsearch'] = true;
		$this->userstuff['clstack'] = array();
		$this->userstuff['onlyfree'] = false;
	}

	// public static function
	public static function getInstance ($new=false) {
		static $instance;
		if (empty($instance)) {
			$instance = new remositoryClassificationHandler();
			$instance->loadCookie();
		}
		if ($new) $instance->setNewUserStuff();
		return $instance;
	}

	private function loadCookie () {
		$this->userstuff = array();
		if (!empty($_COOKIE['remositoryClassification'])) {
			$cookiestuff = $_COOKIE['remositoryClassification'];
			if (strlen($cookiestuff) > 8) {
				$datastuff = substr($cookiestuff,8);
				if (substr($cookiestuff,0,8) == substr(md5($datastuff),12,8)) {
					$this->userstuff = @unserialize(base64_decode($datastuff));
				}
			}
		}
		if (0 == count($this->userstuff)) {
			$this->setNewUserStuff();
			$this->setAllCategories();
		}
	}

	// Appears not to be used
	function isNew () {
		return $this->userstuff['newsearch'];
	}

	// public function save - write this object to a cookie
	public function save () {
		$this->userstuff['newsearch'] = false;
		$cookietext = base64_encode(serialize($this->userstuff));
		$validator = substr(md5($cookietext),12,8);
		setcookie('remositoryClassification', $validator.$cookietext, 0, '/');
	}

	// public function to save the search text
	public function setSearchText ($text) {
		$this->userstuff['searchtext'] = $text;
	}

	public function getSearchText () {
		return $this->userstuff['searchtext'];
	}

	// public function - returns array of as yet unselected classifications
	public function unselected () {
		$result = array();
		foreach ($this->getUnselectedClassify() as $type=>$group) {
			$result[$type][0] = "No additional $type filters are available";
			foreach ($group as $id=>$item) {
				if (!in_array($id, $this->userstuff['clstack'])) $result[$type][$id] = $item;
			}
		}
		return $result;
	}

	// public function - add classification to active list
	public function addClassify ($id) {
		if (('*' == $id OR !is_null($this->getType(intval($id)))) AND !in_array($id, $this->userstuff['clstack'])) {
			array_push($this->userstuff['clstack'], $id);
			if ('*' != $id) remositoryRepository::doSQL("UPDATE #__downloads_classify SET frequency=frequency+1 WHERE id=$id");
		}
	}

	// public function - remove classification from active list
	public function removeClassify ($id) {
		$key = array_search($id, $this->userstuff['clstack']);
		if (false !== $key AND null !== $key) unset($this->userstuff['clstack'][$key]);
	}

	// public function - set "only free" flag to true or false
	public function setOnlyFree ($bool) {
		$this->userstuff['onlyfree'] = $bool ? true : false;
	}

	// public function - get "only free" flag
	public function getOnlyFree () {
		return $this->userstuff['onlyfree'];
	}

	// public function - get classifications stack
	public function getClassifyStack () {
		return $this->userstuff['clstack'];
	}

	// public function - get a single classification given ID
	public function getClassify ($id) {
		$type = $this->getType($id);
		if (!is_null($type)) return $this->allclassify[$type][$id];
		return null;
	}

	// public function - get all classifications for a type
	public function getClassifyByType ($type) {
		if (isset($this->allvisible[$type]) AND count($this->allvisible[$type])) {
			foreach ($this->allvisible[$type] as $id) $result[] = $this->allclassify[$type][$id];
			return $result;
		}
		else return array();
	}

	// private function - get the type from a classification ID
	private function getType ($id) {
		return isset($this->typelink[$id]) ? $this->typelink[$id] : null;
	}

	// public function - set categories to give array of category IDs
	public function setCategories ($cats) {
		if (is_array($cats)) $this->userstuff['categories'] = $cats;
		$this->save();
	}

	// public function - set all available categories
	public function setAllCategories () {
		$cmanager = remositoryContainerManager::getInstance();
		$this->userstuff['categories'] = $cmanager->getCategoryIDs();
	}

	public function areAllCategoriesSet () {
		$cmanager = remositoryContainerManager::getInstance();
		$stored = $cmanager->getCategoryIDs();
		if (count(array_diff($stored, $this->userstuff['categories']))) return false;
		else return true;
	}

	// public function - set HTML tag if category is active
	public function checkCategory ($id) {
		if (in_array($id, $this->userstuff['categories'])) return 'checked="checked"';
		else return '';
	}

	// public function - return number of active categories
	public function countCategories () {
		return count($this->userstuff['categories']);
	}

	// public function - get sole category - if there is only one selected
	public function getSoleCategory () {
		if (1 == count($this->userstuff['categories'])) {
			$cats = array_values($this->userstuff['categories']);
			return new remositoryContainer($cats[0]);
		}
		else return null;
	}

	// public function - get current count of items per page
	public function getPageCount () {
		return empty($this->userstuff['pagecount']) ? (int) _ITEMS_PER_PAGE : $this->userstuff['pagecount'];
	}

	// public function - set current count of items per page
	public function setPageCount ($n) {
		$this->userstuff['pagecount'] = intval($n);
	}

	// public function - get the array of files that fits the current criteria
	public function &getFileArray ($remUser, $orderby, $func, &$pagecontrol) {
		if ($this->countCategories()) {
			$sql = $this->getSelectedSQL(true);
			$interface = remositoryInterface::getInstance();
			$database = $interface->getDB();
			$database->setQuery($sql);
			$total = intval($database->loadResult());
			$page = remositoryRepository::getParam($_REQUEST, 'page', 1);
			$pagecount = remositoryRepository::getParam($_REQUEST, 'pagecount', 0);
			if ($pagecount AND $pagecount != $this->getPageCount()) {
				$page = 1;
				$this->setPageCount($pagecount);
			}
			// Fourth parameter should be _ITEMS_PER_PAGE
			$pagecontrol = new remositoryPage ($total, $remUser, $this->getPageCount(), $page, "&func=$func&orderby=$orderby" );
			$sql = $this->getSelectedSQL(false, $orderby, $pagecontrol->startItem(), $pagecontrol->itemsperpage);
			$file_array = remositoryRepository::doSQLget($sql, 'remositoryFile');
		}
		else {
			$file_array = array();
			$pagecontrol = null;
		}
		return $file_array;
	}

	// private function - gets the search text clause for SQL
	private function searchCondition ($text) {
		$interface = remositoryInterface::getInstance();
		$database = $interface->getDB();
		$parser = new booleanSearchParser;
		$parsed = $parser->parse($text);
		$parsed = $database->escape($parsed);
		// Don't need text files for Nucleus Reseach
	    	// return "\n (MATCH (t.filetext) AGAINST ('$parsed' IN BOOLEAN MODE) OR MATCH (f.filetitle,f.description,f.smalldesc,f.fileauthor,f.custom_1) AGAINST ('$parsed' IN BOOLEAN MODE))";
	    	return "\n (MATCH (f.filetitle,f.description,f.smalldesc,f.fileauthor,f.custom_1) AGAINST ('$parsed' IN BOOLEAN MODE))";
	}

	// public function - return the SQL to do a search (for either count of results or actual files)
	public function getSelectedSQL ($count=true, $orderby=_REM_DEFAULT_ORDERING, $limitstart=0, $limit=0) {
		$sorter = array ('', ' ORDER BY id', ' ORDER BY filetitle', ' ORDER BY downloads DESC', ' ORDER BY custom_5 DESC', ' ORDER BY u.username');
		if (!isset($sorter[$orderby]) OR $orderby == 0) $orderby = _REM_DEFAULT_ORDERING;
		if ($count) $results = 'count(f.id)';
		// Suppress ratings - not needed by Nucleus Research
		// else $results = 'f.*, AVG(l.value) AS vote_value, COUNT(l.value) AS vote_count';
		else $results = 'f.*';
		if ($this->userstuff['submitter']) $results .= ', u.username';
		// $sql = "SELECT $results ".$this->detailedSQL(!$count, ($this->userstuff['submitter'] OR (5 == $orderby)), false);
		// Remove possibility of ratings - not needed for Nucleus Research
		$sql = "SELECT $results ".$this->detailedSQL(false, ($this->userstuff['submitter'] OR (5 == $orderby)), false);
		if (!$count) {
			$sql .= ' GROUP BY f.id';
			$sql .= $sorter[$orderby];
		}
		if ($limit) $sql .= " LIMIT $limitstart,$limit";
		return $sql;
	}

	// private function - get all classifications that are linked to current search set
	private function getUnselectedClassify () {
		$sql = "SELECT cl.* ".$this->detailedSQL(false, false, true);
		$sql .= ' GROUP BY cl.id ';
		$result = remositoryRepository::doSQLget($sql, 'remositoryClassification');
		$unselected = array();
		if ($result) foreach ($result as $item) {
			if (!$item->hidden) $unselected[$item->type][$item->id] = $item;
		}
		return $unselected;
	}

	// private function - work out tables and conditions for SQL statement
	private function detailedSQL ($addRatings, $addSubmitters, $joinClassify) {
		$interface = remositoryInterface::getInstance();
		$cmanager = remositoryContainerManager::getInstance();
		foreach ($this->userstuff['categories'] as $id) {
			$list = $cmanager->listGivenAndDescendants($id);
			if ($list) {
				$sql = "FROM #__downloads_files AS f";
				$partlist[] = "f.containerid IN($list)";
			}
		}
		if (isset($partlist)) $where[] = '('.implode(' OR ', $partlist).')';
		if (empty($sql)) {
			$sql = "FROM #__downloads_files AS f ";
			$where[] = "f.metatype = 0";
		}
		// Change for multiple repositories
		// $where[] = 'f.repnum = '.max(1,remositoryRepository::getParam($_REQUEST, 'repnum', 1));
		$stack = $this->userstuff['clstack'];
		$sub = array_search('*', $stack);
		if (false !== $sub) unset($stack[$sub]);
		if ($classify = implode(',', $stack)) {
			$classcount = count(explode(',', $classify));
			$sql .= " INNER JOIN (SELECT file_id, COUNT(file_id) AS num FROM #__downloads_file_classify "
			." WHERE classify_id IN ($classify) GROUP BY file_id) AS fc ON f.id = fc.file_id ";
			$where[] = "fc.num >= $classcount";
		}
		if ($joinClassify) {
			$sql .= " INNER JOIN #__downloads_file_classify AS fcl ON fcl.file_id = f.id ";
			$sql .= " INNER JOIN #__downloads_classify AS cl ON cl.id = fcl.classify_id AND cl.published = 1";
		}
		if ($this->userstuff['submitter']) $where[] = "f.submittedby = $this->userstuff['submitter']";
		if ($this->userstuff['onlyfree']) $where[] = "f.custom_4 != 0";
		if ($addRatings) $sql .= ' LEFT JOIN #__downloads_log AS l ON l.type=3 AND l.fileid=f.id ';
		if ($addSubmitters) $sql .= ' LEFT JOIN #__users AS u ON u.id=f.submittedby';
		if ($this->userstuff['searchtext']) {
			// Don't need to match against text files for Nucleus Research
			// $sql .= ' LEFT JOIN #__downloads_text AS t ON t.fileid = f.id';
			$searchtext = $interface->getEscaped($this->userstuff['searchtext']);
			$where[] = $this->searchCondition($searchtext);
		}
		if (isset($where)) $sql .= ' WHERE '.implode(' AND ',$where);
		$user = $interface->getUser();
		$visibility = remositoryAbstract::visibilitySQL ($user);
		if ($visibility) $sql .= ' AND '.$visibility;
		return $sql;
	}

}
