<?php
namespace Beu;

use FML\Controls\Frame;
use FML\Controls\Quads\Quad_BgsPlayerCard;
use FML\ManiaLink;
use FML\Script\Features\Paging;
use ManiaControl\ManiaControl;
use ManiaControl\Plugins\Plugin;
use ManiaControl\Players\PlayerManager;
use ManiaControl\Callbacks\CallbackListener;
use ManiaControl\Callbacks\Callbacks;
use ManiaControl\Callbacks\Structures\TrackMania\OnWayPointEventStructure;
use ManiaControl\Commands\CommandListener;
use ManiaControl\Manialinks\LabelLine;
use ManiaControl\Manialinks\ManialinkManager;
use ManiaControl\Players\Player;
use ManiaControl\Callbacks\TimerListener;

use ManiaControl\Manialinks\ManialinkPageAnswerListener;
use ManiaControl\Maps\Map;
use ManiaControl\Utils\Formatter;

/**
 * ClimbTheMap
 *
 * @author	Beu
 * @license   http://www.gnu.org/licenses/ GNU General Public License, Version 3
 */
class ClimbTheMap implements ManialinkPageAnswerListener, TimerListener, CommandListener, CallbackListener, Plugin {
	/*
	* Constants
	*/
	const PLUGIN_ID			= 192;
	const PLUGIN_VERSION	= 1.4;
	const PLUGIN_NAME		= 'ClimbTheMap';
	const PLUGIN_AUTHOR		= 'Beu';

	const DB_CLIMBTHEMAP    = "ClimbTheMap";

	const MLID_ALTITUDE_RECORDS         = "ClimbTheMap.AltitudeRecords";

	// Callbacks
	const CB_UPDATEPBS					= 'Trackmania.ClimbTheMap.UpdatePBs';
	const CB_REQUESTPB					= 'Trackmania.ClimbTheMap.RequestPB';

	// Methods
	const M_SETPLAYERSPB				= 'Trackmania.ClimbTheMap.SetPlayersPB';
	const M_SETWR						= 'Trackmania.ClimbTheMap.SetWR';

	// Actions
	const A_SHOW_ALTITUDE_RECORDS		= 'Trackmania.ClimbTheMap.ShowAltitudeRecords';
	

	/*
	* Private properties
	*/
	/** @var ManiaControl $maniaControl */
	private $maniaControl	= null;
	private $manialink		= "";
	private $wraltitude		= 0;
	private $wrtime			= 0;

	/**
	 * @see \ManiaControl\Plugins\Plugin::prepare()
	 */
	public static function prepare(ManiaControl $maniaControl) {
	}

	/**
	 * @see \ManiaControl\Plugins\Plugin::getId()
	 */
	public static function getId() {
		return self::PLUGIN_ID;
	}

	/**
	 * @see \ManiaControl\Plugins\Plugin::getName()
	 */
	public static function getName() {
		return self::PLUGIN_NAME;
	}

	/**
	 * @see \ManiaControl\Plugins\Plugin::getVersion()
	 */
	public static function getVersion() {
		return self::PLUGIN_VERSION;
	}

	/**
	 * @see \ManiaControl\Plugins\Plugin::getAuthor()
	 */
	public static function getAuthor() {
		return self::PLUGIN_AUTHOR;
	}

	/**
	 * @see \ManiaControl\Plugins\Plugin::getDescription()
	 */
	public static function getDescription() {
		return "[TM2020 only] Used to save the altitude records for the ClimbTheMap game mode";
	}

	/**
	 * @see \ManiaControl\Plugins\Plugin::load()
	 */
	public function load(ManiaControl $maniaControl) {
		$this->maniaControl = $maniaControl;

		$this->initTables();

		$this->maniaControl->getCallbackManager()->registerCallbackListener(Callbacks::AFTERINIT, $this, 'handleAfterInit');
		$this->maniaControl->getCallbackManager()->registerCallbackListener(Callbacks::MP_STARTROUNDSTART, $this, 'handleStartRound');
		$this->maniaControl->getCallbackManager()->registerCallbackListener(Callbacks::TM_ONFINISHLINE, $this, 'handleFinishCallback');

		$this->maniaControl->getCallbackManager()->registerScriptCallbackListener(self::CB_UPDATEPBS, $this, 'handleUpdatePBs');
		$this->maniaControl->getCallbackManager()->registerScriptCallbackListener(self::CB_REQUESTPB, $this, 'handleRequestPB');

		$this->maniaControl->getManialinkManager()->registerManialinkPageAnswerListener(self::A_SHOW_ALTITUDE_RECORDS, $this, 'handleShowAltitudeRecords');
		$this->maniaControl->getCommandManager()->registerCommandListener('records', $this, 'handleShowAltitudeRecords', false);

		$this->maniaControl->getTimerManager()->registerTimerListening($this, 'handle1Minute', 60000);
	}

	private function initTables() {
		$mysqli = $this->maniaControl->getDatabase()->getMysqli();

		$query = 'CREATE TABLE IF NOT EXISTS `' . self::DB_CLIMBTHEMAP . '` (
			`index` INT UNSIGNED NOT NULL AUTO_INCREMENT,
			`mapIndex` INT(11) NOT NULL,
			`login` varchar(36) NOT NULL,
			`altitude` INT(11) NOT NULL,
			`time` int(11) DEFAULT -1,
			`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
				PRIMARY KEY (`index`),
				UNIQUE KEY `map_player` (`mapIndex`,`login`)
			) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;';
		$mysqli->query($query);
		if ($mysqli->error) {
			trigger_error($mysqli->error, E_USER_ERROR);
		}
	}

	public function handleAfterInit() {
		$this->handleStartRound();
	}

	public function handleStartRound() {
		$map = $this->maniaControl->getMapManager()->getCurrentMap();
		$logins = [];

		foreach ($this->maniaControl->getPlayerManager()->getPlayers() as $player) {
			$logins[] = $player->login;
		}

		// Send PB
		$pbs = $this->getPlayersPB($map->index, $logins);
		if (count($pbs) > 0) {
			$this->maniaControl->getClient()->triggerModeScriptEvent(self::M_SETPLAYERSPB, [json_encode($pbs)]);
		}

		// Send WR
		$wr = $this->getWR($map->index);
		if ($wr !== null) {
			$this->wraltitude = $wr[1];
			$this->wrtime = $wr[2];
			$this->maniaControl->getClient()->triggerModeScriptEvent(self::M_SETWR, [$wr[0], strval($wr[1]), strval($wr[2])]);
		} else {
			$this->wraltitude = 0;
		}
	}

	public function handleFinishCallback(OnWayPointEventStructure $structure) {
		$map = $this->maniaControl->getMapManager()->getCurrentMap();
		if ($map === null) return;
		$mapIndex = $map->index;
		$login = $structure->getLogin();
		$time = $structure->getRaceTime();

		$mysqli = $this->maniaControl->getDatabase()->getMysqli();
		$stmt = $mysqli->prepare("INSERT INTO `" . self::DB_CLIMBTHEMAP . "` (`mapIndex`, `login`, `time`, `altitude`) 
			VALUES (?, ?, ?, -1) ON DUPLICATE KEY UPDATE
			`time` = IF(`time` < 0 OR `time` > VALUES(`time`),
				VALUES(`time`),
				`time`);"); 
		$stmt->bind_param('isi', $mapIndex, $login, $time);
		$stmt->execute();

		// Reset manialink cache
		$this->manialink = "";
	}

	public function handleUpdatePBs(array $data) {
		$json = json_decode($data[1][0]);
		if ($json !== null) {
			$map = $this->maniaControl->getMapManager()->getCurrentMap();
			$mapIndex = -1;
			if ($map !== null) $mapIndex = $map->index;

			$mysqli = $this->maniaControl->getDatabase()->getMysqli();
			$mysqli->begin_transaction();

			$stmt = $mysqli->prepare("INSERT INTO `" . self::DB_CLIMBTHEMAP . "` (`mapIndex`, `login`, `altitude`) 
				VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE
				`altitude` = GREATEST(VALUES(`altitude`), `altitude`);"); 
			$stmt->bind_param('iss', $mapIndex, $login, $altitude);
			foreach ($json as $login => $altitude) {
				$stmt->execute();
			}
			$mysqli->commit();

			// Reset manialink cache
			$this->manialink = "";
		}
	}

	/**
	 * Handle when a player connects
	 * Can't use the C++ Callback because the it's received by Maniacontrol before that the Maniascript initialized the Player
	 * 
	 * @param Player $player
	 */
	public function handleRequestPB(array $data) {
		$login = $data[1][0];
		$map = $this->maniaControl->getMapManager()->getCurrentMap();
		if ($map === null) return;

		// Send PB
		$pbs = $this->getPlayersPB($map->index, [$login]);
		if (count($pbs) > 0) {
			$this->maniaControl->getClient()->triggerModeScriptEvent(self::M_SETPLAYERSPB, [json_encode($pbs)]);
		}
	}

	public function handle1Minute() {
		$map = $this->maniaControl->getMapManager()->getCurrentMap();
		if ($map === null) return;

		$wr = $this->getWR($map->index);

		// Update WR if done on an another server
		if ($wr !== null && ($this->wraltitude !== $wr[1] || $this->wrtime !== $wr[2])) {
			$this->wraltitude = $wr[1];
			$this->wrtime = $wr[2];
			$this->maniaControl->getClient()->triggerModeScriptEvent(self::M_SETWR, [$wr[0], strval($wr[1]), strval($wr[2])]);

			// Reset manialink cache
			$this->manialink = "";
		}
	}


	private function getPlayersPB(int $mapIndex, array $logins) {
		if (count($logins) === 0) return [];
		$return = [];
		$mysqli = $this->maniaControl->getDatabase()->getMysqli();

		$stmt = $mysqli->prepare('SELECT login,altitude FROM `' . self::DB_CLIMBTHEMAP . '` WHERE `mapIndex` = ? and login IN (' .  str_repeat('?,', count($logins) - 1) . '?' . ' )'); 
		$stmt->bind_param('i' . str_repeat('s', count($logins)), $mapIndex, ...$logins); // bind array at once
		if (!$stmt->execute()) {
			trigger_error('Error executing MySQL query: ' . $stmt->error);
		}
		$result = $stmt->get_result(); // get the mysqli result
		if ($result !== false) {
			foreach ($result->fetch_all(MYSQLI_ASSOC) as $data) {
				$return[$data["login"]] = $data["altitude"];
			}
		}

		return $return;
	}

	private function getWR(int $mapIndex) {
		$mysqli = $this->maniaControl->getDatabase()->getMysqli();

		$stmt = $mysqli->prepare('SELECT `login`,`altitude`,`time` FROM `' . self::DB_CLIMBTHEMAP . '`
			WHERE `mapIndex` = ?
			ORDER BY (CASE WHEN `time` > 0 THEN `time` ELSE 9999999999 END) ASC,
			`altitude` DESC,
			`date` ASC
			LIMIT 1;');
		$stmt->bind_param('i', $mapIndex);
		if (!$stmt->execute()) {
			trigger_error('Error executing MySQL query: ' . $stmt->error);
		}
		$result = $stmt->get_result();
        if ($result !== false) {
            $data = $result->fetch_assoc();
			if ($data !== null) {

				$player = $this->maniaControl->getPlayerManager()->getPlayer($data["login"]);
				if ($player !== null) {
					return [$player->nickname, $data["altitude"], $data["time"]];
				}	
			}

        }
        return null;
	}

	public function getRecords(Map $map) {
		if ($map === null) return [];

		$mapIndex = $map->index;

		$mysqli = $this->maniaControl->getDatabase()->getMysqli();

		$query = 'SELECT ctm.index,ctm.login,p.nickname,ctm.altitude,ctm.time,ctm.date FROM `' . self::DB_CLIMBTHEMAP . '` ctm
		LEFT JOIN `' . PlayerManager::TABLE_PLAYERS . '` p
		ON ctm.login = p.login
		WHERE `mapIndex` = ?
		ORDER BY (CASE WHEN `time` > 0 THEN `time` ELSE 9999999999 END) ASC,
		`altitude` DESC,
		`date` ASC
		LIMIT 500';

		$stmt = $mysqli->prepare($query ); 
		$stmt->bind_param('i', $mapIndex);
		if (!$stmt->execute()) {
			trigger_error('Error executing MySQL query: ' . $stmt->error);
		}
		$result = $stmt->get_result(); // get the mysqli result
		if ($result !== false) {
			return $result->fetch_all(MYSQLI_ASSOC);
		}
		return [];
	}

	public function handleShowAltitudeRecords(array $callback, Player $player) {
		$this->maniaControl->getManialinkManager()->displayWidget($this->getManialink(), $player, self::MLID_ALTITUDE_RECORDS);
	}

	private function getManialink() {
		if ($this->manialink !== "") return $this->manialink;

		$width  = $this->maniaControl->getManialinkManager()->getStyleManager()->getListWidgetsWidth();
		$height = $this->maniaControl->getManialinkManager()->getStyleManager()->getListWidgetsHeight();

		// get PlayerList
		$records = $this->getRecords($this->maniaControl->getMapManager()->getCurrentMap());

		// create manialink
		$maniaLink = new ManiaLink(ManialinkManager::MAIN_MLID);
		$script    = $maniaLink->getScript();
		$paging    = new Paging();
		$script->addFeature($paging);

		// Main frame
		$frame = $this->maniaControl->getManialinkManager()->getStyleManager()->getDefaultListFrame($script, $paging);
		$maniaLink->addChild($frame);

		// Start offsets
		$posX = -$width / 2;
		$posY = $height / 2;

		// Headline
		$headFrame = new Frame();
		$frame->addChild($headFrame);
		$headFrame->setY($posY - 5);

		$labelLine = new LabelLine($headFrame);
		$labelLine->addLabelEntryText('Rank', $posX + 5);
		$labelLine->addLabelEntryText('Nickname', $posX + 18);
		$labelLine->addLabelEntryText('Altitude', $posX + $width * 0.5);
		$labelLine->addLabelEntryText('Time', $posX + $width * 0.6);
		$labelLine->addLabelEntryText('Date (UTC)', $posX + $width * 0.75);
		$labelLine->render();

		$index     = 0;
		$posY      = $height / 2 - 10;
		$pageFrame = null;

		$pageMaxCount = floor(($height - 5 - 10) / 4);

		foreach ($records as $record) {
			if ($index % $pageMaxCount === 0) {
				$pageFrame = new Frame();
				$frame->addChild($pageFrame);
				$posY = $height / 2 - 10;
				$paging->addPageControl($pageFrame);
			}

			$recordFrame = new Frame();
			$pageFrame->addChild($recordFrame);

			if ($index % 2 === 0) {
				$lineQuad = new Quad_BgsPlayerCard();
				$recordFrame->addChild($lineQuad);
				$lineQuad->setSize($width, 4);
				$lineQuad->setSubStyle($lineQuad::SUBSTYLE_BgPlayerCardBig);
				$lineQuad->setZ(-0.001);
			}

			$labelLine = new LabelLine($recordFrame);
			$labelLine->addLabelEntryText($index + 1, $posX + 5, 13);
			$labelLine->addLabelEntryText($record["nickname"], $posX + 18, 52);
			$labelLine->addLabelEntryText($record["altitude"], $posX + $width * 0.5, 31);
			if ($record["time"] > 0) {
				$labelLine->addLabelEntryText(Formatter::formatTime($record["time"]), $posX + $width * 0.6, 30);
			}
			$labelLine->addLabelEntryText($record["date"], $posX + $width * 0.75, 30);
			$labelLine->render();

			$recordFrame->setY($posY);

			$posY -= 4;
			$index++;
		}

		$this->manialink = (string) $maniaLink;
		return $this->manialink;
	}

	/**
	 * Unload the plugin and its Resources
	 */
	public function unload() {
	}
}
