<?php

namespace ManiaControl\Players;


use ManiaControl\ManiaControl;

/**
 * Player Data Manager
 *
 * @author    ManiaControl Team <mail@maniacontrol.com>
 * @copyright 2014 ManiaControl Team
 * @license   http://www.gnu.org/licenses/ GNU General Public License, Version 3
 */
class PlayerDataManager {
	const TABLE_PLAYERDATAMETADATA = 'mc_playerdata_metadata';
	const TABLE_PLAYERDATA         = 'mc_playerdata';
	const TYPE_STRING              = 'string';
	const TYPE_INT                 = 'int';
	const TYPE_REAL                = 'real';
	const TYPE_BOOL                = 'bool';
	const TYPE_ARRAY               = 'array';

	/*
	 * Private Properties
	 */
	private $maniaControl = null;
	private $arrayDelimiter = ';;';
	private $metaData = array();
	private $storedData = array();

	/**
	 * Construct player manager
	 *
	 * @param \ManiaControl\ManiaControl $maniaControl
	 */
	public function __construct(ManiaControl $maniaControl) {
		$this->maniaControl = $maniaControl;
		$this->initTables();

		// Store Stats MetaData
		$this->storeMetaData();
	}

	/**
	 * Initialize necessary database tables
	 *
	 * @return bool
	 */
	private function initTables() {
		$mysqli      = $this->maniaControl->database->mysqli;
		$defaultType = "'" . self::TYPE_STRING . "'";
		$typeSet     = $defaultType . ",'" . self::TYPE_INT . "','" . self::TYPE_REAL . "','" . self::TYPE_BOOL . "','" . self::TYPE_ARRAY . "'";
		$query       = "CREATE TABLE IF NOT EXISTS `" . self::TABLE_PLAYERDATAMETADATA . "` (
				`dataId` int(11) NOT NULL AUTO_INCREMENT,
				`class` varchar(100) NOT NULL,
				`dataName` varchar(100) NOT NULL,
				`type` set({$typeSet}) NOT NULL DEFAULT {$defaultType},
				`defaultValue` varchar(150) NOT NULL,
				`description` varchar(150) NOT NULL,
				PRIMARY KEY (`dataId`),
				UNIQUE KEY `name` (`class`, `dataName`)
				) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='Player-Data MetaData' AUTO_INCREMENT=1;";
		$statement   = $mysqli->prepare($query);
		if ($mysqli->error) {
			trigger_error($mysqli->error, E_USER_ERROR);
			return false;
		}
		$statement->execute();
		if ($statement->error) {
			trigger_error($statement->error, E_USER_ERROR);
			return false;
		}
		$statement->close();

		$query     = "CREATE TABLE IF NOT EXISTS `" . self::TABLE_PLAYERDATA . "` (
				`index` int(11) NOT NULL AUTO_INCREMENT,
				`serverIndex` int(11) NOT NULL,
				`playerId` int(11) NOT NULL,
				`dataId` int(11) NOT NULL,
				`value` varchar(150) NOT NULL,
				`changed` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
				PRIMARY KEY (`index`),
				UNIQUE KEY `unique` (`dataId`,`playerId`,`serverIndex`)
				) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='Player Data' AUTO_INCREMENT=1;";
		$statement = $mysqli->prepare($query);
		if ($mysqli->error) {
			trigger_error($mysqli->error, E_USER_ERROR);
			return false;
		}
		$statement->execute();
		if ($statement->error) {
			trigger_error($statement->error, E_USER_ERROR);
			return false;
		}
		$statement->close();
		return true;
	}

	/**
	 * Store Meta Data from the Database in the Ram
	 */
	private function storeMetaData() {
		$mysqli = $this->maniaControl->database->mysqli;

		$query  = "SELECT * FROM `" . self::TABLE_PLAYERDATAMETADATA . "`;";
		$result = $mysqli->query($query);
		if (!$result) {
			trigger_error($mysqli->error);
			return;
		}

		while ($row = $result->fetch_object()) {
			$this->metaData[$row->class . $row->dataName] = $row;
		}
		$result->close();
	}

	/**
	 * Destroys the stored PlayerData (Method get called by PlayerManager, so don't call it anywhere else)
	 *
	 * @param Player $player
	 */
	public function destroyPlayerData(Player $player) {
		unset($this->storedData[$player->index]);
	}

	/**
	 * Defines the Player-Data MetaData
	 *
	 * @param $object
	 * @param $dataName
	 * @param $default
	 * @param $dataDescription (optional)
	 * @return bool
	 */
	public function defineMetaData($object, $dataName, $default, $dataDescription = '') {
		$mysqli    = $this->maniaControl->database->mysqli;
		$className = $this->getClassName($object);

		$query     = "INSERT INTO `" . self::TABLE_PLAYERDATAMETADATA . "` (
				`class`,
				`dataName`,
				`type`,
				`defaultValue`,
				`description`
				) VALUES (
				?, ?, ?, ?, ?
				) ON DUPLICATE KEY UPDATE
				`type` = VALUES(`type`),
				`defaultValue` = VALUES(`defaultValue`),
				`description` = VALUES(`description`);";
		$statement = $mysqli->prepare($query);
		if ($mysqli->error) {
			trigger_error($mysqli->error);
			return false;
		}
		$type = $this->getType($default);

		$statement->bind_param('sssss', $className, $dataName, $type, $default, $dataDescription);
		$statement->execute();
		if ($statement->error) {
			trigger_error($statement->error);
			$statement->close();
			return false;
		}
		$statement->close();
		return true;
	}

	/**
	 * Get Class Name of a Parameter
	 *
	 * @param mixed $param
	 * @return string
	 */
	private function getClassName($param) {
		if (is_object($param)) {
			return get_class($param);
		}
		if (is_string($param)) {
			return $param;
		}
		trigger_error('Invalid class param. ' . $param);
		return (string)$param;
	}

	/**
	 * Get Type of a Parameter
	 *
	 * @param mixed $param
	 * @return string
	 */
	private function getType($param) {
		if (is_int($param)) {
			return self::TYPE_INT;
		}
		if (is_real($param)) {
			return self::TYPE_REAL;
		}
		if (is_bool($param)) {
			return self::TYPE_BOOL;
		}
		if (is_string($param)) {
			return self::TYPE_STRING;
		}
		if (is_array($param)) {
			return self::TYPE_ARRAY;
		}
		trigger_error('Unsupported setting type. ' . print_r($param, true));
		return null;
	}

	/**
	 * Gets the Player Data
	 *
	 * @param  mixed   $object
	 * @param   string $dataName
	 * @param Player   $player
	 * @param     int  $serverIndex
	 * @return mixed|null
	 */
	public function getPlayerData($object, $dataName, Player $player, $serverIndex = -1) {
		$className = $this->getClassName($object);

		$meta = $this->metaData[$className . $dataName];

		//Check if data is already in the ram
		if (isset($this->storedData[$player->index])) {
			if (isset($this->storedData[$player->index][$meta->dataId])) {
				return $this->storedData[$player->index][$meta->dataId];
			}
		}

		$mysqli        = $this->maniaControl->database->mysqli;
		$dataQuery     = "SELECT `value` FROM `" . self::TABLE_PLAYERDATA . "`
				WHERE `dataId` = ?
				AND `playerId` = ?
				AND `serverIndex` = ?;";
		$dataStatement = $mysqli->prepare($dataQuery);
		if ($mysqli->error) {
			trigger_error($mysqli->error);
			return null;
		}
		$dataStatement->bind_param('iii', $meta->dataId, $player->index, $serverIndex);
		$dataStatement->execute();
		if ($dataStatement->error) {
			trigger_error($dataStatement->error);
			return null;
		}
		$dataStatement->store_result();
		if ($dataStatement->num_rows <= 0) {
			$this->setPlayerData($object, $dataName, $player, $meta->defaultValue, $serverIndex);
			return $meta->default;
		}
		$dataStatement->bind_result($value);
		$dataStatement->fetch();
		$dataStatement->free_result();
		$dataStatement->close();
		$data = $this->castSetting($meta->type, $value);

		//Store setting in the ram
		if (!isset($this->storedData[$player->index])) {
			$this->storedData[$player->index] = array();
		}
		$this->storedData[$player->index][$meta->dataId] = $data;
		return $data;
	}

	/**
	 * Set a PlayerData to a specific defined statMetaData
	 *
	 * @param    mixed  $object
	 * @param    string $dataName
	 * @param Player    $player
	 * @param     mixed $value
	 * @param     int   $serverIndex (let it empty if its global)
	 * @return bool
	 */
	public function setPlayerData($object, $dataName, Player $player, $value, $serverIndex = -1) {
		$className = $this->getClassName($object);
		if (!$player) {
			return false;
		}

		$dataId = $this->getMetaDataId($className, $dataName);
		if (!$dataId) {
			return false;
		}

		$mysqli    = $this->maniaControl->database->mysqli;
		$query     = "INSERT INTO `" . self::TABLE_PLAYERDATA . "` (
				`serverIndex`,
				`playerId`,
				`dataId`,
				`value`
				) VALUES (
				?, ?, ?, ?
				) ON DUPLICATE KEY UPDATE
				`value` = VALUES(`value`);";
		$statement = $mysqli->prepare($query);
		if ($mysqli->error) {
			trigger_error($mysqli->error);
			return false;
		}
		$statement->bind_param('iiis', $serverIndex, $player->index, $dataId, $value);
		$statement->execute();
		if ($statement->error) {
			trigger_error($statement->error);
			$statement->close();
			return false;
		}
		$statement->close();

		//FIXME store changed value
		if (isset($this->storedData[$player->index]) && isset($this->storedData[$player->index][$dataId])) {
			unset($this->storedData[$player->index][$dataId]);
		}

		return true;
	}

	/**
	 * Return the Id of the MetaData
	 *
	 * @param string $className
	 * @param string $statName
	 * @return int
	 */
	private function getMetaDataId($className, $statName) {
		if (isset($this->metaData[$className . $statName])) {
			$stat = $this->metaData[$className . $statName];
			return (int)$stat->dataId;
		}
		return null;
	}

	/**
	 * Cast a Setting to the given Type
	 *
	 * @param string $type
	 * @param mixed  $value
	 * @return mixed
	 */
	private function castSetting($type, $value) {
		if ($type === self::TYPE_INT) {
			return (int)$value;
		}
		if ($type === self::TYPE_REAL) {
			return (float)$value;
		}
		if ($type === self::TYPE_BOOL) {
			return (bool)$value;
		}
		if ($type === self::TYPE_STRING) {
			return (string)$value;
		}
		if ($type === self::TYPE_ARRAY) {
			return explode($this->arrayDelimiter, $value);
		}
		trigger_error('Unsupported setting type. ' . print_r($type, true));
		return $value;
	}
}