<?php

/**
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package fusionLib
 * @copyright Copyright (c) 2008 - 2016 fusionLib. All rights reserved.
 * @link http://fusionlib.com
 */

/**
 * flSearchManager Class
 *
 * Class to provide the functionality to install, upgrade and remove the database
 * tables required for the search engine system to operate.
 *
 * @package fusionLib
 */
class flSearchManager {

	/**
	 * The database object.
	 *
	 * @var flDb
	 */
	protected $db;

	/**
	 * The base name for database tables.
	 *
	 * @var string
	 */
	protected $tableBase;

	/**
	 * Construct the manager object.
	 *
	 * @param flDb $db The database object to use.
	 * @param string $tableBase The base name for database tables.
	 */
	function __construct($db, $tableBase) {
		$this->db = $db;
		$this->tableBase = $tableBase;
	}

	/**
	 * Initialise the database tables needed for search.
	 *
 	 * @param string $lang The language to load the stemmer for, e.g. en, see ISO 639 for the list of 2 letter codes.
 	 * @param int $minWordLen The minimum length of a word that will be indexed.
 	 * @param int $maxWordLen The maximum length of a word that will be indexed.
	 */
	function initializeTables($lang = 'en', $minWordLen = 2, $maxWordLen = 28) {
		$lang = preg_replace('/_.*$/', '', $lang);

		$this->db->query("
CREATE  TABLE IF NOT EXISTS {$this->tableBase}_docs (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
  `parentid` INT UNSIGNED NOT NULL ,
  `lft` INT UNSIGNED NOT NULL ,
  `rgt` INT UNSIGNED NOT NULL ,
  `lftrgtset` LINESTRING NOT NULL ,
  `norm` FLOAT NOT NULL ,
  `publishedat` DATETIME NULL ,
  `slug` VARCHAR(255) NOT NULL ,
  `accessperm` VARCHAR(32) NOT NULL ,
  PRIMARY KEY (`id`) ,
  INDEX `parentid` (`parentid` ASC) ,
  INDEX `lft` (`lft` ASC) ,
  INDEX `rgt` (`rgt` ASC) ,
  INDEX `slug` (`slug` ASC) ,
  INDEX `publishedat` (`publishedat` ASC) ,
  SPATIAL INDEX `lftrgtset` (`lftrgtset` ASC) )
ENGINE = MyISAM
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_general_ci
");

		$this->db->query("
CREATE  TABLE IF NOT EXISTS {$this->tableBase}_index (
  `wordid` INT UNSIGNED NOT NULL ,
  `id` INT UNSIGNED NOT NULL ,
  `fldid` TINYINT UNSIGNED NOT NULL ,
  `tind` SMALLINT UNSIGNED NOT NULL ,
  PRIMARY KEY (`wordid`, `fldid`, `id`) ,
  INDEX `id` (`id` ASC) )
ENGINE = MyISAM
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_general_ci
");

		$this->db->query("
CREATE  TABLE IF NOT EXISTS {$this->tableBase}_words (
  `wordid` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
  `word` VARCHAR(255) NOT NULL ,
  PRIMARY KEY (`wordid`) ,
  UNIQUE INDEX `word` (`word` ASC) )
ENGINE = MyISAM
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_general_ci
");

		$this->db->query("
CREATE  TABLE IF NOT EXISTS {$this->tableBase}_fields (
  `fldname` VARCHAR(64) NOT NULL ,
  `fldid` TINYINT UNSIGNED NOT NULL ,
  `fldtype` ENUM('text','int','number') DEFAULT 'text'  NOT NULL ,
  PRIMARY KEY (`fldname`) )
ENGINE = MyISAM
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_general_ci
");

		$this->db->query("
CREATE  TABLE IF NOT EXISTS {$this->tableBase}_config (
  `keyname` VARCHAR(32) NOT NULL ,
  `value` MEDIUMTEXT NOT NULL ,
  PRIMARY KEY (`keyname`) )
ENGINE = MyISAM
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_general_ci
");

		$this->db->query("
CREATE  TABLE IF NOT EXISTS {$this->tableBase}_tags (
  `tag` VARCHAR(64) NOT NULL ,
  `id` INT UNSIGNED NOT NULL ,
  PRIMARY KEY (`tag`, `id`) ,
  INDEX `docid` (`id` ASC) )
ENGINE = MyISAM
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_general_ci
");

		$this->db->query("
CREATE  TABLE IF NOT EXISTS {$this->tableBase}_ints (
  `fldid` TINYINT UNSIGNED NOT NULL ,
  `id` INT UNSIGNED NOT NULL ,
  `val` INT NOT NULL ,
  PRIMARY KEY (`fldid`, `id`) ,
  INDEX `docid` (`id` ASC) ,
  INDEX `val` (`val` ASC) )
ENGINE = MyISAM
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_general_ci
");

		$this->db->query("
CREATE  TABLE IF NOT EXISTS {$this->tableBase}_nums (
  `fldid` TINYINT UNSIGNED NOT NULL ,
  `id` INT UNSIGNED NOT NULL ,
  `val` DECIMAL(13, 4) NOT NULL ,
  PRIMARY KEY (`fldid`, `id`) ,
  INDEX `docid` (`id` ASC) ,
  INDEX `val` (`val` ASC) )
ENGINE = MyISAM
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_general_ci
");

		// Look up the language stop words
		if(is_readable(FLLIBPATH . 'Search/stopwords.' . $lang . '.php'))
			include FLLIBPATH . 'Search/stopwords.' . $lang . '.php';
		else
			$stopWords = array();

		// Set the config
		$this->db->insert("{$this->tableBase}_config", array('keyname' => 'minWordLen', 'value' => $minWordLen));
		$this->db->insert("{$this->tableBase}_config", array('keyname' => 'maxWordLen', 'value' => $maxWordLen));
		$this->db->insert("{$this->tableBase}_config", array('keyname' => 'stemmer', 'value' => $lang));
		$this->db->insert("{$this->tableBase}_config", array('keyname' => 'stopWords', 'value' => serialize(array_flip($stopWords))));
	}

	/**
	 * Delete the database tables needed for search.
	 */
	function deleteTables() {
		$this->db->query("DROP TABLE IF EXISTS {$this->tableBase}_docs");
		$this->db->query("DROP TABLE IF EXISTS {$this->tableBase}_index");
		$this->db->query("DROP TABLE IF EXISTS {$this->tableBase}_words");
		$this->db->query("DROP TABLE IF EXISTS {$this->tableBase}_fields");
		$this->db->query("DROP TABLE IF EXISTS {$this->tableBase}_config");
		$this->db->query("DROP TABLE IF EXISTS {$this->tableBase}_tags");
		$this->db->query("DROP TABLE IF EXISTS {$this->tableBase}_ints");
		$this->db->query("DROP TABLE IF EXISTS {$this->tableBase}_nums");
	}

	/**
	 * Set the minimum word length.
	 *
 	 * @param int $minWordLen The minimum length of a word that will be indexed.
	 */
	function setMinWordLen($minWordLen) {
		$this->db->update("{$this->tableBase}_config", array('value' => $minWordLen), 'keyname=?', 'minWordLen');
	}

	/**
	 * Set the maximum word length.
	 *
 	 * @param int $maxWordLen The maximum length of a word that will be indexed.
	 */
	function setMaxWordLen($maxWordLen) {
		$this->db->update("{$this->tableBase}_config", array('value' => $maxWordLen), 'keyname=?', 'maxWordLen');
	}

	/**
	 * Set the stemmer language.
	 *
 	 * @param string $lang The language to load the stemmer for, e.g. en, see ISO 639 for the list of 2 letter codes.
	 * @param bool $reloadStopWords If true the stopwords are to be reloaded.
	 * @return bool true if changed; else false.
	 */
	function setLang($lang, $reloadStopWords = true) {
		$lang = preg_replace('/_.*$/', '', $lang);

		$this->db->update("{$this->tableBase}_config", array('value' => $lang), 'keyname=?', 'stemmer');

		// If language not changed don't replace stop words
		if(!$this->db->affectedRows())
			$reloadStopWords = false;

		if($reloadStopWords) {
			// Look up the language stop words
			if(is_readable(FLLIBPATH . 'Search/stopwords.' . $lang . '.php'))
				include FLLIBPATH . 'Search/stopwords.' . $lang . '.php';
			else
				$stopWords = array();

			$this->db->update("{$this->tableBase}_config", array('value' => serialize(array_flip($stopWords))), 'keyname=?', 'stopWords');
		}
	}

	/**
	 * Create a new search field.
	 *
	 * @param string $name The name of the field to create.
	 * @param string $type The field type, text, int or number.
	 * @return bool true if the field was added or already existed; false if the field could not be added.
	 */
	function addField($name, $type = 'text') {
		$this->db->query("LOCK TABLES {$this->tableBase}_fields WRITE");

		// Get the current defined fields
		$fieldIds = array();
		$fieldNames = array();
		$result = $this->db->query("SELECT fldname,fldid FROM {$this->tableBase}_fields");
		while($row = $result->fetch()) {
			$fieldIds[] = $row['fldid'];
			$fieldNames[] = $row['fldname'];
		}

		// If field doesn't already exist
		$fid = 1;
		if(!in_array($name, $fieldNames)) {

			// Find an unused id
			while($fid < 256 && in_array($fid, $fieldIds))
				$fid++;

			// Add to db
			if($fid < 256)
				$this->db->insert("{$this->tableBase}_fields", array('fldname' => $name, 'fldid' => $fid, 'fldtype' => $type));
		}

		$this->db->query('UNLOCK TABLES');

		return $fid < 256;
	}

	/**
	 * Delete an existing search field and all indexed data in the field.
	 *
	 * @param string $name The name of the field to delete.
	 */
	function deleteField($name) {
		$fid = $this->db->fetchOne("SELECT fldid FROM {$this->tableBase}_fields WHERE fldname=?", $name);
		if($fid !== false) {
			$this->db->delete("{$this->tableBase}_index", 'fldid=?', $fid);
			$this->db->delete("{$this->tableBase}_ints", 'fldid=?', $fid);
			$this->db->delete("{$this->tableBase}_nums", 'fldid=?', $fid);
			$this->db->delete("{$this->tableBase}_fields", 'fldid=?', $fid);
		}
	}

	/**
	 * Get the search configuration.
	 *
	 * @param array $keys The names of the configuration values to get.
	 * @return array The configuration items.
	 */
	function getConfig($keys) {
		$result = $this->db->query("SELECT keyname,value FROM {$this->tableBase}_config WHERE keyname IN(?" . str_repeat(',?', count($keys) - 1) . ')', $keys);
		while($row = $result->fetch())
			$r[$row['keyname']] = $row['value'];
		return $r;
	}

	/**
	 * Set the search configuration.
	 *
	 * @param array $cfg The array of configuration options.
	 */
	function setConfig($cfg) {
		foreach($cfg as $k => $v)
			$this->db->update("{$this->tableBase}_config", array('value' => $v), 'keyname=?', $k);
	}
}
