/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "common/events.h"

#include "hypno/grammar.h"
#include "hypno/hypno.h"

#include "backends/keymapper/keymapper.h"

namespace Hypno {

extern int parse_mis(const char *);

const char *sceneVariables[] = {
	"GS_NONE",
	"GS_SCTEXT",
	"GS_AMBIENT",
	"GS_MUSIC",
	"GS_VOLUME",
	"GS_MOUSESPEED",
	"GS_MOUSEON",
	"GS_LEVELCOMPLETE",
	"GS_LEVELWON",
	"GS_CHEATS",
	"GS_SWITCH0",
	"GS_SWITCH1",
	"GS_SWITCH2",
	"GS_SWITCH3",
	"GS_SWITCH4",
	"GS_SWITCH5",
	"GS_SWITCH6",
	"GS_SWITCH7",
	"GS_SWITCH8",
	"GS_SWITCH9",
	"GS_SWITCH10",
	"GS_SWITCH11",
	"GS_SWITCH12",
	"GS_COMBATJSON",
	"GS_COMBATLEVEL",
	"GS_PUZZLELEVEL",
	nullptr
};

void HypnoEngine::loadSceneLevel(const Common::String &current, const Common::String &next, const Common::String &prefix) {
	debugC(1, kHypnoDebugParser, "Parsing %s", current.c_str());
	Common::Path name = convertPath(current);

	Common::File test;
	if (!test.open(name))
		error("Failed to open %s", name.toString().c_str());

	const uint32 fileSize = test.size();
	char *buf = (char *)malloc(fileSize + 1);
	test.read(buf, fileSize);
	test.close();
	buf[fileSize] = '\0';
	debugC(1, kHypnoDebugParser, "%s", buf);
	parse_mis(buf);
	Scene *level = new Scene();
	level->prefix = prefix;
	level->levelIfWin = next;
	level->hots = *g_parsedHots;
	_levels[name.toString('/')] = level;
	free(buf);
}

void HypnoEngine::loadSceneLevel(const char *buf, const Common::String &name, const Common::String &next, const Common::String &prefix) {
	debugC(1, kHypnoDebugParser, "Parsing %s", name.c_str());
	debugC(1, kHypnoDebugParser, "%s", buf);
	parse_mis(buf);
	Scene *level = new Scene();
	level->prefix = prefix;
	level->levelIfWin = next;
	level->hots = *g_parsedHots;
	_levels[name] = level;
}

void HypnoEngine::resetSceneState() {
	uint32 i = 0;
	while (sceneVariables[i]) {
		// Preserve difficulty level variables
		if (sceneVariables[i] != Common::String("GS_COMBATLEVEL") && sceneVariables[i] != Common::String("GS_PUZZLELEVEL"))
			_sceneState[sceneVariables[i]] = 0;
		i++;
	}
	_intros.clear();
}

bool HypnoEngine::checkSceneCompleted() {
	return _sceneState["GS_LEVELCOMPLETE"] || _sceneState["GS_LEVELWON"];
}

bool HypnoEngine::checkLevelWon() {
	return _sceneState["GS_LEVELWON"];
}

// Hotspots

void HypnoEngine::clickedHotspot(Common::Point mousePos) {
	Hotspots *hots = stack.back();
	Hotspot selected(MakeHotspot);
	bool found = false;
	int rs = 100000000;
	int cs = 0;
	for (Hotspots::const_iterator it = hots->begin(); it != hots->end(); ++it) {
		const Hotspot h = *it;
		cs = h.rect.width() * h.rect.height();
		if (h.rect.contains(mousePos)) {
			if (cs < rs) {
				selected = h;
				found = true;
				rs = cs;
			}
		}
	}
	if (selected.type == MakeMenu) {
		if (isDemo()) {
			_nextLevel = "sixdemo/mis/demo.mis";
			resetSceneState();
		} else // TODO: remove when proper escape to main menu is implemented
			openMainMenuDialog();
		return;
	}

	if (!found)
		return;

	if (selected.smenu) {
		if (selected.smenu->empty())
			error("Invalid menu selected");
		_nextHotsToAdd = selected.smenu;
	}

	_videosPlaying.clear();
	_nextParallelVideoToPlay.clear();
	_nextSequentialVideoToPlay.clear();

	bool cont = true;
	for (Actions::const_iterator itt = selected.actions.begin(); itt != selected.actions.end() && cont; ++itt) {
		Action *action = *itt;
		switch (action->type) {
		case ChangeLevelAction:
			runChangeLevel((ChangeLevel *)action);
			break;

		case EscapeAction:
			runEscape();
			break;

		case CutsceneAction:
			runCutscene((Cutscene *)action);
			break;

		case PlayAction:
			runPlay((Play *)action);
			break;

		case SoundAction:
			runSound((Sound *)action);
			break;

		case WalNAction:
			runWalN((WalN *)action);
			break;

		case GlobalAction:
			cont = runGlobal((Global *)action);
			break;

		case TalkAction:
			runTalk((Talk *)action);
			break;

		case SaveAction:
			runSave((Save *)action);
			break;

		case LoadAction:
			runLoad((Load *)action);
			break;

		case LoadCheckpointAction:
			runLoadCheckpoint((LoadCheckpoint *)action);
			break;

		case QuitAction:
			runQuit((Quit *)action);
			break;

		case AmbientAction:
			runAmbient((Ambient *)action);
			break;

		case PaletteAction:
			runPalette((Palette *)action);
			break;

		case SwapPointerAction:
			runSwapPointer((SwapPointer *)action);
			break;

		default:
			break;
		}
	}
}

bool HypnoEngine::hoverHotspot(Common::Point mousePos) {
	Hotspots *hots = stack.back();
	Hotspot selected(MakeHotspot);
	bool found = false;
	int rs = 100000000;
	for (Hotspots::const_iterator it = hots->begin(); it != hots->end(); ++it) {
		const Hotspot h = *it;
		if (h.type != MakeHotspot)
			continue;

		int cs = h.rect.width() * h.rect.height();
		if (h.rect.contains(mousePos)) {
			if (cs < rs) {
				selected = h;
				found = true;
				rs = cs;
			}
		}
	}
	if (found) {
		for (Actions::const_iterator itt = selected.actions.begin(); itt != selected.actions.end(); ++itt) {
			Action *action = *itt;
			switch (action->type) {
			case MiceAction:
				runMice((Mice *)action);
				break;
			default:
				break;
			}
		}
		return true;
	}
	return false;
}

Common::String HypnoEngine::findNextLevel(const Transition *trans) { error("Function \"%s\" not implemented", __FUNCTION__); }

void HypnoEngine::runTransition(Transition *trans) {
	Common::String nextLevel = findNextLevel(trans);
	if (!trans->frameImage.empty()) {
		// This is only used in Wetlands, and therefore, resolution should be 320x200
		changeScreenMode("320x200");
		debugC(1, kHypnoDebugScene, "Rendering %s frame in transaction", trans->frameImage.c_str());
		loadImage(trans->frameImage, 0, 0, false, true, trans->frameNumber);
		drawScreen();
		Common::String *ptr = new Common::String(nextLevel);
		if (!startAlarm(2 * 1000000, ptr)) // 2 seconds
			error("Failed to install alarm");
	} else
		_nextLevel = nextLevel;
}

void HypnoEngine::runScene(Scene *scene) {
	changeScreenMode(scene->resolution);
	_refreshConversation = false;
	Common::Event event;
	Common::Point mousePos;
	Common::Keymapper *keymapper = g_system->getEventManager()->getKeymapper();
	Common::List<uint32> videosToRemove;
	bool enableLoopingVideos = true;
	int32 lastCountdown = 0;
	// These variables are always resetted
	_sceneState["GS_LEVELCOMPLETE"] = 0;
	_sceneState["GS_LEVELWON"] = 0;

	stack.clear();
	_nextHotsToAdd = &scene->hots;
	defaultCursor();

	while (!shouldQuit() && _nextLevel.empty()) {

		if (_timerStarted && _videosPlaying.empty() && !_nextHotsToRemove) {

			if (lastCountdown == _countdown) {
			} else if (_countdown > 0) {
				uint32 c = 251; // red
				if (stack.size() > 0)
					runMenu(stack.back());
				uint32 minutes = _countdown / 60;
				uint32 seconds = _countdown % 60;
				drawString("console", Common::String::format("TIME: %d:%d", minutes, seconds), 80, 10, 60, c);
				drawScreen();
			} else {
				assert(!scene->levelIfLose.empty());
				_nextLevel = scene->levelIfLose;
				debugC(1, kHypnoDebugScene, "Finishing level with timeout and jumping to %s", _nextLevel.c_str());
				resetSceneState();
				removeTimers();
				_defaultCursorIdx = 0;
				continue;
			}
			lastCountdown = _countdown;
		}

		disableGameKeymaps();
		keymapper->getKeymap("cutscene")->setEnabled(true);

		while (g_system->getEventManager()->pollEvent(event)) {
			mousePos = g_system->getEventManager()->getMousePos();
			// Events
			switch (event.type) {
			case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
				if (event.customType == kActionSkipCutscene) {
					for (Videos::iterator it = _videosPlaying.begin(); it != _videosPlaying.end(); ++it) {
						if (it->decoder) {
							skipVideo(*it);
							if (it->scaled) {
								runMenu(stack.back());
								drawScreen();
							}
						}
					}
					_videosPlaying.clear();

					if (!_conversation.empty())
						_refreshConversation = true;
				}

				break;

			case Common::EVENT_QUIT:
			case Common::EVENT_RETURN_TO_LAUNCHER:
				break;

			case Common::EVENT_RBUTTONDOWN:
				if (stack.empty())
					break;
				if (!_conversation.empty()) {
					rightClickedConversation(mousePos);
					break;
				}
				break;

			case Common::EVENT_LBUTTONDOWN:
				if (stack.empty())
					break;
				if (!_conversation.empty()) {
					leftClickedConversation(mousePos);
					break;
				}
				if (!_nextHotsToAdd && !_nextHotsToRemove /*&& _videosPlaying.empty()*/) {
					clickedHotspot(mousePos);
					drawScreen();
				}
				break;

			case Common::EVENT_MOUSEMOVE:
				// Reset cursor to default
				// changeCursor("default");
				// The following functions will return true
				// if the cursor is changed

				if (!_conversation.empty() && !hoverConversation(mousePos))
					defaultCursor();

				if (stack.empty() || !_conversation.empty() || !_videosPlaying.empty())
					break;

				if (!hoverHotspot(mousePos))
					defaultCursor();
				break;

			default:
				break;
			}
		}

		keymapper->getKeymap("cutscene")->setEnabled(false);
		enableGameKeymaps();

		if (_refreshConversation && !_conversation.empty() &&
			_nextSequentialVideoToPlay.empty() &&
			_nextParallelVideoToPlay.empty() &&
			_videosPlaying.empty()) {
			showConversation();
			runMenu(stack.back(), true);
			drawScreen();
			_refreshConversation = false;
		}

		// Movies
		if (!_nextParallelVideoToPlay.empty()) {
			for (Videos::iterator it = _nextParallelVideoToPlay.begin(); it != _nextParallelVideoToPlay.end(); ++it) {
				playVideo(*it);
				if (it->loop)
					_videosLooping.push_back(*it);
				else
					_videosPlaying.push_back(*it);
			}
			_nextParallelVideoToPlay.clear();
		}

		if (!_nextSequentialVideoToPlay.empty() && _videosPlaying.empty()) {
			MVideo *it = _nextSequentialVideoToPlay.begin();
			playVideo(*it);
			if (it->loop)
				_videosLooping.push_back(*it);
			else
				_videosPlaying.push_back(*it);
			_nextSequentialVideoToPlay.remove_at(0);
		}

		for (Videos::iterator it = _videosLooping.begin(); it != _videosLooping.end(); ++it) {
			if (it->decoder && _conversation.empty()) {
				if (it->decoder->endOfVideo()) {
					if (it->loop && enableLoopingVideos) {
						it->decoder->rewind();
						it->decoder->start();
					}
				} else if (it->decoder->needsUpdate()) {
					updateScreen(*it);
				}
			}
		}

		uint32 i = 0;
		videosToRemove.clear();
		for (Videos::iterator it = _videosPlaying.begin(); it != _videosPlaying.end(); ++it) {

			if (it->decoder) {
				if (it->decoder->endOfVideo()) {
					if (it->scaled ||
					(  it->decoder->getWidth() == _screenW
					&& it->decoder->getHeight() == _screenH
					&& it->decoder->getCurFrame() > 0)) {
						runMenu(stack.back());
						drawScreen();
					}
					it->decoder->close();
					delete it->decoder;
					it->decoder = nullptr;
					videosToRemove.push_back(i);
				} else if (it->decoder->needsUpdate()) {
					updateScreen(*it);
				}
			}
			i++;
		}
		if (!videosToRemove.empty()) {

			for (Common::List<uint32>::iterator it = videosToRemove.begin(); it != videosToRemove.end(); ++it) {
				debugC(1, kHypnoDebugScene, "removing %d from %d size", *it, _videosPlaying.size());
				_videosPlaying.remove_at(*it);
			}
			debugC(1, kHypnoDebugScene, "Something to play: %d", _videosPlaying.size());
			// Nothing else to play
			if (_videosPlaying.empty() && _nextSequentialVideoToPlay.empty() && !checkSceneCompleted()) {
				if (!_conversation.empty())
					_refreshConversation = true;
			}
		}

		if (checkSceneCompleted() || checkLevelWon()) {
			if (!checkLevelWon() && stack.size() > 1) {
				debugC(1, kHypnoDebugScene, "Executing escape instead of ending the scene");
				runEscape();
				_sceneState["GS_LEVELCOMPLETE"] = 0;
				continue;
			}

			// Make sure all the videos are played before we finish
			enableLoopingVideos = false;
			if (_conversation.empty() &&
				_videosPlaying.empty() &&
				_nextSequentialVideoToPlay.empty() &&
				_nextParallelVideoToPlay.empty()) {

				if (_nextLevel.empty()) {
					assert(!scene->levelIfWin.empty());
					_nextLevel = scene->levelIfWin;
				}

				if (checkLevelWon()) {
					debugC(1, kHypnoDebugScene, "Resetting level variables");
					resetSceneState();
					_checkpoint = _nextLevel;
					_defaultCursorIdx = 0;
				}
				_sceneState["GS_LEVELCOMPLETE"] = 0;

				debugC(1, kHypnoDebugScene, "Finishing level and jumping to %s", _nextLevel.c_str());
				continue;
			}
		}

		if (!_videosPlaying.empty() || !_videosLooping.empty() || !_nextSequentialVideoToPlay.empty()) {
			drawScreen();
			continue;
		}

		if (_nextHotsToRemove) {
			debugC(1, kHypnoDebugScene, "Removing a hotspot list!");
			stack.pop_back();
			runMenu(stack.back());
			_nextHotsToRemove = nullptr;
			drawScreen();
		} else if (_nextHotsToAdd) {
			debugC(1, kHypnoDebugScene, "Adding a hotspot list!");
			stack.push_back(_nextHotsToAdd);
			runMenu(stack.back());
			_nextHotsToAdd = nullptr;
			drawScreen();
		}

		if (!isMusicActive() && !scene->music.empty()) {
			playMusic(scene->music, scene->musicRate);
		}

		g_system->updateScreen();
		g_system->delayMillis(30);
	}

	// Deallocate videos
	for (Videos::iterator it = _videosLooping.begin(); it != _videosLooping.end(); ++it) {
		if (it->decoder)
			skipVideo(*it);
	}

	for (Videos::iterator it = _videosPlaying.begin(); it != _videosPlaying.end(); ++it) {
		if (it->decoder)
			skipVideo(*it);
	}

	for (Videos::iterator it = _nextParallelVideoToPlay.begin(); it != _nextParallelVideoToPlay.end(); ++it) {
		if (it->decoder)
			skipVideo(*it);
	}

	for (Videos::iterator it = _nextSequentialVideoToPlay.begin(); it != _nextSequentialVideoToPlay.end(); ++it) {
		if (it->decoder)
			skipVideo(*it);
	}

	for (Videos::iterator it = _escapeSequentialVideoToPlay.begin(); it != _escapeSequentialVideoToPlay.end(); ++it) {
		if (it->decoder)
			skipVideo(*it);
	}

	_nextParallelVideoToPlay.clear();
	_nextSequentialVideoToPlay.clear();
	_escapeSequentialVideoToPlay.clear();
	_conversation.clear();

	if (!_keepTimerDuringScenes)
		removeTimers();
}

void HypnoEngine::showConversation() { error("Function \"%s\" not implemented", __FUNCTION__); }
void HypnoEngine::endConversation() { error("Function \"%s\" not implemented", __FUNCTION__); }
void HypnoEngine::rightClickedConversation(const Common::Point &mousePos) { error("Function \"%s\" not implemented", __FUNCTION__); }
void HypnoEngine::leftClickedConversation(const Common::Point &mousePos) { error("Function \"%s\" not implemented", __FUNCTION__); }
bool HypnoEngine::hoverConversation(const Common::Point &mousePos) { error("Function \"%s\" not implemented", __FUNCTION__); }

} // End of namespace Hypno
