<?php

namespace ManiaControl\Maps;

use FML\Controls\Frame;
use FML\Controls\Label;
use FML\Controls\Labels\Label_Text;
use FML\Controls\Quads\Quad_BgsPlayerCard;
use FML\Controls\Quads\Quad_Icons64x64_1;
use FML\Controls\Quads\Quad_UIConstruction_Buttons;
use FML\Controls\Quads\Quad_UIConstructionBullet_Buttons;
use FML\ManiaLink;
use FML\Script\Features\Paging;
use ManiaControl\Logger;
use ManiaControl\ManiaControl;
use ManiaControl\Manialinks\ManialinkManager;
use ManiaControl\Manialinks\ManialinkPageAnswerListener;
use ManiaControl\Players\Player;
use Maniaplanet\DedicatedServer\Xmlrpc\AlreadyInListException;
use Maniaplanet\DedicatedServer\Xmlrpc\FileException;
use Maniaplanet\DedicatedServer\Xmlrpc\InvalidMapException;

/**
 * Maps Directory Browser
 *
 * @author    ManiaControl Team <mail@maniacontrol.com>
 * @copyright 2014 ManiaControl Team
 * @license   http://www.gnu.org/licenses/ GNU General Public License, Version 3
 */
class DirectoryBrowser implements ManialinkPageAnswerListener {
	/*
	 * Constants
	 */
	const ACTION_SHOW          = 'MapsDirBrowser.Show';
	const ACTION_NAVIGATE_UP   = 'MapsDirBrowser.NavigateUp';
	const ACTION_NAVIGATE_ROOT = 'MapsDirBrowser.NavigateRoot';
	const ACTION_OPEN_FOLDER   = 'MapsDirBrowser.OpenFolder.';
	const ACTION_INSPECT_FILE  = 'MapsDirBrowser.InspectFile.';
	const ACTION_ADD_FILE      = 'MapsDirBrowser.AddFile.';
	const ACTION_ERASE_FILE    = 'MapsDirBrowser.EraseFile.';
	const WIDGET_NAME          = 'MapsDirBrowser.Widget';
	const CACHE_FOLDER_PATH    = 'FolderPath';

	/*
	 * Private properties
	 */
	/** @var ManiaControl $maniaControl */
	private $maniaControl = null;

	/**
	 * Construct a new directory browser instance
	 *
	 * @param ManiaControl $maniaControl
	 */
	public function __construct(ManiaControl $maniaControl) {
		$this->maniaControl = $maniaControl;

		// ManiaLink Actions
		$this->maniaControl->getManialinkManager()
		                   ->registerManialinkPageAnswerListener(self::ACTION_SHOW, $this, 'handleActionShow');
		$this->maniaControl->getManialinkManager()
		                   ->registerManialinkPageAnswerListener(self::ACTION_NAVIGATE_UP, $this, 'handleNavigateUp');
		$this->maniaControl->getManialinkManager()
		                   ->registerManialinkPageAnswerListener(self::ACTION_NAVIGATE_ROOT, $this, 'handleNavigateRoot');
		$this->maniaControl->getManialinkManager()
		                   ->registerManialinkPageAnswerRegexListener($this->buildActionRegex(self::ACTION_OPEN_FOLDER), $this, 'handleOpenFolder');
		$this->maniaControl->getManialinkManager()
		                   ->registerManialinkPageAnswerRegexListener($this->buildActionRegex(self::ACTION_INSPECT_FILE), $this, 'handleInspectFile');
		$this->maniaControl->getManialinkManager()
		                   ->registerManialinkPageAnswerRegexListener($this->buildActionRegex(self::ACTION_ADD_FILE), $this, 'handleAddFile');
		$this->maniaControl->getManialinkManager()
		                   ->registerManialinkPageAnswerRegexListener($this->buildActionRegex(self::ACTION_ERASE_FILE), $this, 'handleEraseFile');
	}

	/**
	 * Build the regex to register for the given action
	 *
	 * @param string $actionName
	 * @return string
	 */
	private function buildActionRegex($actionName) {
		return '/' . $actionName . '*/';
	}

	/**
	 * Handle 'Show' action
	 *
	 * @param array  $actionCallback
	 * @param Player $player
	 */
	public function handleActionShow(array $actionCallback, Player $player) {
		$this->showManiaLink($player);
	}

	/**
	 * Build and show the Browser ManiaLink to the given Player
	 *
	 * @param Player $player
	 * @param mixed  $nextFolder
	 */
	public function showManiaLink(Player $player, $nextFolder = null) {
		$oldFolderPath  = $player->getCache($this, self::CACHE_FOLDER_PATH);
		$isInMapsFolder = false;
		if (!$oldFolderPath) {
			$oldFolderPath  = $this->maniaControl->getServer()
			                                     ->getDirectory()
			                                     ->getMapsFolder();
			$isInMapsFolder = true;
		}
		$folderPath = $oldFolderPath;
		if (is_string($nextFolder)) {
			$newFolderPath = realpath($oldFolderPath . $nextFolder);
			if ($newFolderPath) {
				$folderPath = $newFolderPath . DIRECTORY_SEPARATOR;
				$folderName = basename($newFolderPath);
				switch ($folderName) {
					case 'Maps':
						$mapsDir        = dirname($this->maniaControl->getServer()
						                                             ->getDirectory()
						                                             ->getMapsFolder());
						$folderDir      = dirname($folderPath);
						$isInMapsFolder = ($mapsDir === $folderDir);
						break;
					case 'UserData':
						$dataDir   = dirname($this->maniaControl->getServer()
						                                        ->getDirectory()
						                                        ->getGameDataFolder());
						$folderDir = dirname($folderPath);
						if ($dataDir === $folderDir) {
							// Prevent navigation out of maps directory
							return;
						}
						break;
				}
			}
		}
		$player->setCache($this, self::CACHE_FOLDER_PATH, $folderPath);

		$maniaLink = new ManiaLink(ManialinkManager::MAIN_MLID);
		$script    = $maniaLink->getScript();
		$paging    = new Paging();
		$script->addFeature($paging);
		$frame = $this->maniaControl->getManialinkManager()
		                            ->getStyleManager()
		                            ->getDefaultListFrame($script, $paging);
		$maniaLink->add($frame);

		$width     = $this->maniaControl->getManialinkManager()
		                                ->getStyleManager()
		                                ->getListWidgetsWidth();
		$height    = $this->maniaControl->getManialinkManager()
		                                ->getStyleManager()
		                                ->getListWidgetsHeight();
		$index     = 0;
		$posY      = $height / 2 - 10;
		$pageFrame = null;

		$navigateRootQuad = new Quad_Icons64x64_1();
		$frame->add($navigateRootQuad);
		$navigateRootQuad->setPosition($width * -0.47, $height * 0.45)
		                 ->setSize(4, 4)
		                 ->setSubStyle($navigateRootQuad::SUBSTYLE_ToolRoot);

		$navigateUpQuad = new Quad_Icons64x64_1();
		$frame->add($navigateUpQuad);
		$navigateUpQuad->setPosition($width * -0.44, $height * 0.45)
		               ->setSize(4, 4)
		               ->setSubStyle($navigateUpQuad::SUBSTYLE_ToolUp);

		if (!$isInMapsFolder) {
			$navigateRootQuad->setAction(self::ACTION_NAVIGATE_ROOT);
			$navigateUpQuad->setAction(self::ACTION_NAVIGATE_UP);
		}

		$directoryLabel = new Label_Text();
		$frame->add($directoryLabel);
		$dataFolder    = $this->maniaControl->getServer()
		                                    ->getDirectory()
		                                    ->getGameDataFolder();
		$directoryText = substr($folderPath, strlen($dataFolder));
		$directoryLabel->setPosition($width * -0.41, $height * 0.45)
		               ->setSize($width * 0.85, 4)
		               ->setHAlign($directoryLabel::LEFT)
		               ->setText($directoryText)
		               ->setTextSize(2);

		$tooltipLabel = new Label();
		$frame->add($tooltipLabel);
		$tooltipLabel->setPosition($width * -0.48, $height * -0.44)
		             ->setSize($width * 0.8, 5)
		             ->setHAlign($tooltipLabel::LEFT)
		             ->setTextSize(1)
		             ->setText('tooltip');

		$mapFiles = $this->scanMapFiles($folderPath);

		if (is_array($mapFiles)) {
			if (empty($mapFiles)) {
				$emptyLabel = new Label();
				$frame->add($emptyLabel);
				$emptyLabel->setY(20)
				           ->setTextColor('aaa')
				           ->setText('No files found.')
				           ->setTranslate(true);
			} else {
				$canAddMaps   = $this->maniaControl->getAuthenticationManager()
				                                   ->checkPermission($player, MapManager::SETTING_PERMISSION_ADD_MAP);
				$canEraseMaps = $this->maniaControl->getAuthenticationManager()
				                                   ->checkPermission($player, MapManager::SETTING_PERMISSION_ERASE_MAP);

				foreach ($mapFiles as $filePath => $fileName) {
					$shortFilePath = substr($filePath, strlen($folderPath));

					if ($index % 15 === 0) {
						// New Page
						$pageFrame = new Frame();
						$frame->add($pageFrame);
						$posY = $height / 2 - 10;
						$paging->addPage($pageFrame);
					}

					// Map Frame
					$mapFrame = new Frame();
					$pageFrame->add($mapFrame);
					$mapFrame->setY($posY);

					if ($index % 2 === 0) {
						// Striped background line
						$lineQuad = new Quad_BgsPlayerCard();
						$mapFrame->add($lineQuad);
						$lineQuad->setZ(-1)
						         ->setSize($width, 4)
						         ->setSubStyle($lineQuad::SUBSTYLE_BgPlayerCardBig);
					}

					// File name Label
					$nameLabel = new Label_Text();
					$mapFrame->add($nameLabel);
					$nameLabel->setX($width * -0.48)
					          ->setSize($width * 0.79, 4)
					          ->setHAlign($nameLabel::LEFT)
					          ->setStyle($nameLabel::STYLE_TextCardRaceRank)
					          ->setTextSize(1)
					          ->setText($fileName);

					if (is_dir($filePath)) {
						// Folder
						$nameLabel->setAction(self::ACTION_OPEN_FOLDER . substr($shortFilePath, 0, -1))
						          ->addTooltipLabelFeature($tooltipLabel, 'Open folder ' . $fileName);
					} else {
						// File
						$nameLabel->setAction(self::ACTION_INSPECT_FILE . $fileName)
						          ->addTooltipLabelFeature($tooltipLabel, 'Inspect file ' . $fileName);

						if ($canAddMaps) {
							// 'Add' button
							$addButton = new Quad_UIConstructionBullet_Buttons();
							$mapFrame->add($addButton);
							$addButton->setX($width * 0.42)
							          ->setSize(4, 4)
							          ->setSubStyle($addButton::SUBSTYLE_NewBullet)
							          ->setAction(self::ACTION_ADD_FILE . $fileName)
							          ->addTooltipLabelFeature($tooltipLabel, 'Add map ' . $fileName);
						}

						if ($canEraseMaps) {
							// 'Erase' button
							$eraseButton = new Quad_UIConstruction_Buttons();
							$mapFrame->add($eraseButton);
							$eraseButton->setX($width * 0.46)
							            ->setSize(4, 4)
							            ->setSubStyle($eraseButton::SUBSTYLE_Erase)
							            ->setAction(self::ACTION_ERASE_FILE . $fileName)
							            ->addTooltipLabelFeature($tooltipLabel, 'Erase file ' . $fileName);
						}
					}

					$posY -= 4;
					$index++;
				}
			}
		} else {
			$errorLabel = new Label();
			$frame->add($errorLabel);
			$errorLabel->setY(20)
			           ->setTextColor('f30')
			           ->setText('No access to the directory.')
			           ->setTranslate(true);
		}

		$this->maniaControl->getManialinkManager()
		                   ->displayWidget($maniaLink, $player, self::WIDGET_NAME);
	}

	/**
	 * Scan the given directory for Map files
	 *
	 * @param string $directory
	 * @return array|bool
	 */
	protected function scanMapFiles($directory) {
		if (!is_readable($directory) || !is_dir($directory)) {
			return false;
		}
		$mapFiles = array();
		$dirFiles = scandir($directory);
		foreach ($dirFiles as $fileName) {
			if (substr($fileName, 0, 1) === '.') {
				continue;
			}
			$fullFileName = $directory . $fileName;
			if (!is_readable($fullFileName)) {
				continue;
			}
			if (is_dir($fullFileName)) {
				$mapFiles[$fullFileName . DIRECTORY_SEPARATOR] = $fileName . DIRECTORY_SEPARATOR;
				continue;
			} else {
				if ($this->isMapFileName($fileName)) {
					$mapFiles[$fullFileName] = $fileName;
				}
			}
		}
		return $mapFiles;
	}

	/**
	 * Check if the given file name represents a Map file
	 *
	 * @param string $fileName
	 * @return bool
	 */
	protected function isMapFileName($fileName) {
		$mapFileNameEnding = '.map.gbx';
		$fileNameEnding    = strtolower(substr($fileName, -strlen($mapFileNameEnding)));
		return ($fileNameEnding === $mapFileNameEnding);
	}

	/**
	 * Handle 'NavigateRoot' action
	 *
	 * @param array  $actionCallback
	 * @param Player $player
	 */
	public function handleNavigateRoot(array $actionCallback, Player $player) {
		$player->destroyCache($this, self::CACHE_FOLDER_PATH);
		$this->showManiaLink($player);
	}

	/**
	 * Handle 'NavigateUp' action
	 *
	 * @param array  $actionCallback
	 * @param Player $player
	 */
	public function handleNavigateUp(array $actionCallback, Player $player) {
		$this->showManiaLink($player, '..');
	}

	/**
	 * Handle 'OpenFolder' page action
	 *
	 * @param array  $actionCallback
	 * @param Player $player
	 */
	public function handleOpenFolder(array $actionCallback, Player $player) {
		$actionName = $actionCallback[1][2];
		$folderName = substr($actionName, strlen(self::ACTION_OPEN_FOLDER));
		$this->showManiaLink($player, $folderName);
	}

	/**
	 * Handle 'InspectFile' page action
	 *
	 * @param array  $actionCallback
	 * @param Player $player
	 */
	public function handleInspectFile(array $actionCallback, Player $player) {
		$actionName = $actionCallback[1][2];
		$fileName   = substr($actionName, strlen(self::ACTION_INSPECT_FILE));
		// TODO: show inspect file view
		var_dump($fileName);
	}

	/**
	 * Handle 'AddFile' page action
	 *
	 * @param array  $actionCallback
	 * @param Player $player
	 */
	public function handleAddFile(array $actionCallback, Player $player) {
		$actionName = $actionCallback[1][2];
		$fileName   = substr($actionName, strlen(self::ACTION_ADD_FILE));
		$folderPath = $player->getCache($this, self::CACHE_FOLDER_PATH);
		$filePath   = $folderPath . $fileName;

		$mapsFolder       = $this->maniaControl->getServer()
		                                       ->getDirectory()
		                                       ->getMapsFolder();
		$relativeFilePath = substr($filePath, strlen($mapsFolder));

		// Check for valid map
		try {
			$this->maniaControl->getClient()
			                   ->checkMapForCurrentServerParams($relativeFilePath);
		} catch (InvalidMapException $exception) {
			$this->maniaControl->getChat()
			                   ->sendException($exception, $player);
			return;
		} catch (FileException $exception) {
			$this->maniaControl->getChat()
			                   ->sendException($exception, $player);
			return;
		}

		// Add map to map list
		try {
			$this->maniaControl->getClient()
			                   ->insertMap($relativeFilePath);
		} catch (AlreadyInListException $exception) {
			$this->maniaControl->getChat()
			                   ->sendException($exception, $player);
			return;
		}
		$map = $this->maniaControl->getMapManager()
		                          ->fetchMapByFileName($relativeFilePath);
		if (!$map) {
			$this->maniaControl->getChat()
			                   ->sendError('Error occurred.', $player);
			return;
		}

		// Message
		$message = $player->getEscapedNickname() . ' added ' . $map->getEscapedName() . '!';
		$this->maniaControl->getChat()
		                   ->sendSuccess($message);
		Logger::logInfo($message, true);

		// Queue requested Map
		$this->maniaControl->getMapManager()
		                   ->getMapQueue()
		                   ->addMapToMapQueue($player, $map);
	}

	/**
	 * Handle 'EraseFile' page action
	 *
	 * @param array  $actionCallback
	 * @param Player $player
	 */
	public function handleEraseFile(array $actionCallback, Player $player) {
		$actionName = $actionCallback[1][2];
		$fileName   = substr($actionName, strlen(self::ACTION_ERASE_FILE));
		$folderPath = $player->getCache($this, self::CACHE_FOLDER_PATH);
		$filePath   = $folderPath . $fileName;
		if (@unlink($filePath)) {
			$this->maniaControl->getChat()
			                   ->sendSuccess("Erased {$fileName}!");
			$this->showManiaLink($player);
		} else {
			$this->maniaControl->getChat()
			                   ->sendError("Couldn't erase {$fileName}!");
		}
	}
}