/* 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/config-manager.h"
#include "common/debug-channels.h"
#include "common/events.h"
#include "common/gui_options.h"
#include "common/keyboard.h"
#include "common/translation.h"
#include "common/system.h"
#include "backends/keymapper/action.h"
#include "backends/keymapper/keymapper.h"
#include "backends/keymapper/standard-actions.h"
#include "engines/dialogs.h"
#include "graphics/scaler.h"
#include "gui/saveload.h"
#include "gui/message.h"

#include "mohawk/cursors.h"
#include "mohawk/installer_archive.h"
#include "mohawk/resource.h"
#include "mohawk/riven.h"
#include "mohawk/riven_card.h"
#include "mohawk/riven_graphics.h"
#include "mohawk/riven_inventory.h"
#include "mohawk/riven_saveload.h"
#include "mohawk/riven_sound.h"
#include "mohawk/riven_stack.h"
#include "mohawk/riven_stacks/aspit.h"
#include "mohawk/riven_stacks/bspit.h"
#include "mohawk/riven_stacks/gspit.h"
#include "mohawk/riven_stacks/jspit.h"
#include "mohawk/riven_stacks/ospit.h"
#include "mohawk/riven_stacks/pspit.h"
#include "mohawk/riven_stacks/rspit.h"
#include "mohawk/riven_stacks/tspit.h"
#include "mohawk/riven_video.h"
#include "mohawk/dialogs.h"
#include "mohawk/console.h"

// Shared code between detection/engine.
#include "mohawk/riven_metaengine.h"

namespace Mohawk {

MohawkEngine_Riven::MohawkEngine_Riven(OSystem *syst, const MohawkGameDescription *gamedesc) :
		MohawkEngine(syst, gamedesc) {
	_showHotspots = false;
	_activatedPLST = false;
	_activatedSLST = false;
	_gameEnded = false;
	_extrasFile = nullptr;
	_stack = nullptr;
	_gfx = nullptr;
	_video = nullptr;
	_sound = nullptr;
	_rnd = nullptr;
	_scriptMan = nullptr;
	_saveLoad = nullptr;
	_card = nullptr;
	_inventory = nullptr;
	_lastSaveTime = 0;
	_currentLanguage = getLanguage();

	_menuSavedCard = -1;
	_menuSavedStack = -1;

	DebugMan.addDebugChannel(kRivenDebugScript, "Script", "Track Script Execution");
	DebugMan.addDebugChannel(kRivenDebugPatches, "Patches", "Track Script Patching");

	// NOTE: We can never really support CD swapping. All of the music files
	// (*_Sounds.mhk) are stored on disc 1. They are copied to the hard drive
	// during install and used from there. The same goes for the extras.mhk
	// file. The following directories allow Riven to be played directly
	// from the DVD.

	const Common::FSNode gameDataDir(ConfMan.get("path"));
	SearchMan.addSubDirectoryMatching(gameDataDir, "all");
	SearchMan.addSubDirectoryMatching(gameDataDir, "data");
	SearchMan.addSubDirectoryMatching(gameDataDir, "exe");
	SearchMan.addSubDirectoryMatching(gameDataDir, "assets1");
	SearchMan.addSubDirectoryMatching(gameDataDir, "program");
}

MohawkEngine_Riven::~MohawkEngine_Riven() {
	delete _card;
	delete _stack;
	delete _sound;
	delete _video;
	delete _gfx;
	delete _extrasFile;
	delete _saveLoad;
	delete _scriptMan;
	delete _inventory;
	delete _rnd;
}

Common::Error MohawkEngine_Riven::run() {
	MohawkEngine::run();

	if (!_mixer->isReady()) {
		return Common::kAudioDeviceInitFailed;
	}

	// Let's try to open the installer file (it holds extras.mhk)
	// Though, we set a low priority to prefer the extracted version
	if (_installerArchive.open("arcriven.z"))
		SearchMan.add("arcriven.z", &_installerArchive, 0, false);

	_gfx = new RivenGraphics(this);
	_video = new RivenVideoManager(this);
	_sound = new RivenSoundManager(this);
	setDebugger(new RivenConsole(this));
	_saveLoad = new RivenSaveLoad(this, _saveFileMan);
	_scriptMan = new RivenScriptManager(this);
	_inventory = new RivenInventory(this);

	_rnd = new Common::RandomSource("riven");

	// Create the cursor manager
	if (Common::File::exists("rivendmo.exe"))
		_cursor = new PECursorManager("rivendmo.exe");
	else if (Common::File::exists("riven.exe"))
		_cursor = new PECursorManager("riven.exe");
	else // last resort: try the Mac executable
		_cursor = new MacCursorManager("Riven");

	initVars();
	applyGameSettings();

	// Check the user has copied all the required datafiles
	if (!checkDatafiles()) {
		return Common::kNoGameDataFoundError;
	}

	// We need to have a cursor source, or the game won't work
	if (!_cursor->hasSource()) {
		Common::U32String message = _("You're missing a Riven executable. The Windows executable is 'riven.exe' or 'rivendmo.exe'. ");
		message += _("Using the 'arcriven.z' installer file also works. In addition, you can use the Mac 'Riven' executable.");
		GUIErrorMessage(message);
		warning("You're missing a Riven executable. The Windows executable is 'riven.exe' or 'rivendmo.exe'. \
			Using the 'arcriven.z' installer file also works. In addition, you can use the Mac 'Riven' executable.");
		return Common::kNoGameDataFoundError;
	}

	// Open extras.mhk for common images
	_extrasFile = new MohawkArchive();

	// We need extras.mhk for inventory images, marble images, and credits images
	if (!_extrasFile->openFile("extras.mhk")) {
		const char *msg = _s("You're missing 'extras.mhk'. Using the 'arcriven.z' installer file also works.");
		Common::U32String message = _(msg);
		GUIErrorMessage(message);
		warning("%s", msg);
		return Common::kNoGameDataFoundError;
	}

	// Start at main cursor
	_cursor->setCursor(kRivenMainCursor);
	_cursor->showCursor();

	// Let's begin, shall we?
	if (isGameVariant(GF_DEMO)) {
		// Start the demo off with the videos
		changeToStack(kStackAspit);
		changeToCard(6);
	} else if (ConfMan.hasKey("save_slot")) {
		// Load game from launcher/command line if requested
		int gameToLoad = ConfMan.getInt("save_slot");

		// Attempt to load the game.
		Common::Error loadError = _saveLoad->loadGame(gameToLoad);
		if (loadError.getCode() != Common::kNoError) {
			return loadError;
		}
	} else {
		// Otherwise, start us off at aspit's card 1 (the main menu)
		changeToStack(kStackAspit);
		changeToCard(1);
	}


	while (!hasGameEnded())
		doFrame();

	// Attempt to autosave before exiting from the GMM / when closing the window
	saveAutosaveIfEnabled();

	return Common::kNoError;
}

void MohawkEngine_Riven::doFrame() {
	// Update background running things
	uint32 loopStart = _system->getMillis();
	_sound->updateSLST();
	_video->updateMovies();

	if (!_scriptMan->hasQueuedScripts()) {
		_stack->resetAction();
	}

	processInput();

	_stack->onFrame();

	if (!_scriptMan->runningQueuedScripts()) {
		// Don't run queued scripts if we are calling from a queued script
		// otherwise infinite looping will happen.
		_scriptMan->runQueuedScripts();
	}

	_inventory->onFrame();

	// Update the screen once per frame
	_system->updateScreen();
	uint32 loopElapsed = _system->getMillis() - loopStart;

	// Cut down on CPU usage
	if (loopElapsed < 10)
		_system->delayMillis(10 - loopElapsed);
}

void MohawkEngine_Riven::processInput() {
	Common::Event event;
	while (_eventMan->pollEvent(event)) {
		switch (event.type) {
		case Common::EVENT_MOUSEMOVE:
			_stack->onMouseMove(event.mouse);
			break;
		case Common::EVENT_CUSTOM_ENGINE_ACTION_END:
			switch ((RivenAction)event.customType) {
			case kRivenActionInteract:
				_stack->onMouseUp(_eventMan->getMousePos());
				_inventory->checkClick(_eventMan->getMousePos());
				break;
			default:
				_stack->resetAction();
				break;
			}
			break;
		case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
			switch ((RivenAction)event.customType) {
			case kRivenActionInteract:
				_stack->onMouseDown(_eventMan->getMousePos());
				break;
			case kRivenActionPause:
				pauseGame();
				break;
			case kRivenActionOpenOptionsDialog:
				runOptionsDialog();
				break;
			case kRivenActionOpenMainMenu:
				if (isGameVariant(GF_DEMO)) {
					// Return to the main menu in the demo
					if (_stack->getId() != kStackAspit)
						changeToStack(kStackAspit);
					changeToCard(1);
				} else if (!_scriptMan->hasQueuedScripts() && isGameVariant(GF_25TH)) {
					// Check if we haven't jumped to menu
					if (_menuSavedStack == -1) {
						goToMainMenu();
					} else {
						resumeFromMainMenu();
					}
				} else if (!isGameVariant(GF_25TH)) {
					openMainMenuDialog();
				}
				break;
			case kRivenActionPlayIntroVideos:
				// Play the intro videos in the demo
				if (isGameVariant(GF_DEMO)) {
					if (_stack->getId() != kStackAspit)
						changeToStack(kStackAspit);
					changeToCard(6);
				}
				break;
			case kRivenActionLoadGameState:
				if (canLoadGameStateCurrently()) {
					loadGameDialog();
				}
				break;
			case kRivenActionSaveGameState:
				if (canSaveGameStateCurrently()) {
					saveGameDialog();
				}
				break;
			default:
				_stack->onAction((RivenAction)event.customType);
				break;
			}
			break;
		default:
			break;
		}
	}
}

void MohawkEngine_Riven::goToMainMenu() {
	if (isInMainMenu()) {
		return;
	}

	_menuSavedStack = _stack->getId();
	_menuSavedCard = _card->getId();

	_menuThumbnail.reset(new Graphics::Surface());
	createThumbnailFromScreen(_menuThumbnail.get());

	RivenCommand *go = new RivenStackChangeCommand(this, kStackAspit, 1, true, true);
	RivenScriptPtr goScript = _scriptMan->createScriptWithCommand(go);
	_scriptMan->runScript(goScript, true);
}

void MohawkEngine_Riven::resumeFromMainMenu() {
	assert(_menuSavedStack != -1);

	RivenCommand *resume = new RivenStackChangeCommand(this, _menuSavedStack, _menuSavedCard, true, true);
	RivenScriptPtr resumeScript = _scriptMan->createScriptWithCommand(resume);
	_scriptMan->runScript(resumeScript, true);

	_menuSavedStack = -1;
	_menuSavedCard = -1;
	_menuThumbnail.reset();
}

bool MohawkEngine_Riven::isInMainMenu() const {
	static const uint16 kCardIdAspitAtrusJournal = 5;
	return _stack->getId() == kStackAspit && _card->getId() < kCardIdAspitAtrusJournal;
}

bool MohawkEngine_Riven::isGameStarted() const {
	return !isInMainMenu() || _menuSavedStack != -1;
}

void MohawkEngine_Riven::pauseEngineIntern(bool pause) {
	MohawkEngine::pauseEngineIntern(pause);

	if (pause) {
		_video->pauseVideos();
	} else {
		_video->resumeVideos();

		if (_stack) {
			// The mouse may have moved while the game was paused,
			// the mouse cursor needs to be updated.
			_stack->onMouseMove(_eventMan->getMousePos());
		}
	}
}

// Stack/Card-Related Functions

void MohawkEngine_Riven::changeToStack(uint16 stackId) {
	// Don't change stack to the current stack (if the files are loaded)
	if (_stack && _stack->getId() == stackId && !_mhk.empty())
		return;

	// Free resources that may rely on the current stack data being loaded
	if (_card) {
		_card->leave();
		delete _card;
		_card = nullptr;
	}
	_video->removeVideos();
	_sound->stopAllSLST();

	// Clear the graphics cache; images aren't used across stack boundaries
	_gfx->clearCache();

	// Clear the old stack files out
	closeAllArchives();

	// Get the prefix character for the destination stack
	char prefix = RivenStacks::getName(stackId)[0];

	// Load the localization override file if any
	if (isGameVariant(GF_25TH)) {
		loadLanguageDatafile(prefix, stackId);
	}

	// Load files that start with the prefix
	const char **datafiles = listExpectedDatafiles();
	for (int i = 0; datafiles[i] != nullptr; i++) {
		if (datafiles[i][0] == prefix) {
			MohawkArchive *mhk = new MohawkArchive();
			if (mhk->openFile(datafiles[i]))
				_mhk.push_back(mhk);
			else
				delete mhk;
		}
	}

	// Make sure we have loaded files
	if (_mhk.empty())
		error("Could not load stack %s", RivenStacks::getName(stackId));

	delete _stack;
	_stack = constructStackById(stackId);

	// Set the mouse position to the correct value so the mouse
	// cursor can be computed accurately when loading a card.
	_stack->onMouseMove(getEventManager()->getMousePos());
}

void MohawkEngine_Riven::reloadCurrentCard() {
	assert(_stack && _card);

	uint16 cardId = _card->getId();

	closeAllArchives();

	changeToStack(_stack->getId());
	changeToCard(cardId);
}

const char **MohawkEngine_Riven::listExpectedDatafiles() const {
	// The files are in reverse order because of the way the 1.02 patch works.
	// The only "Data3" file is j_Data3.mhk from that patch. Patch files have higher
	// priorities over the regular files and are therefore loaded and checked first.
	static const char *datafilesDVD[] = {
			"a_Data.mhk",                  "a_Sounds.mhk",
			"b_Data.mhk",                  "b_Sounds.mhk",
			"g_Data.mhk",                  "g_Sounds.mhk",
			"j_Data2.mhk", "j_Data1.mhk",  "j_Sounds.mhk",
			"o_Data.mhk",                  "o_Sounds.mhk",
			"p_Data.mhk",                  "p_Sounds.mhk",
			"r_Data.mhk",                  "r_Sounds.mhk",
			"t_Data2.mhk", "t_Data1.mhk",  "t_Sounds.mhk",
			nullptr
	};

	static const char *datafilesCD[] = {
			"a_Data.mhk",                                "a_Sounds.mhk",
			"b_Data1.mhk", "b_Data.mhk",                 "b_Sounds.mhk",
			"g_Data.mhk",                                "g_Sounds.mhk",
			"j_Data3.mhk", "j_Data2.mhk", "j_Data1.mhk", "j_Sounds.mhk",
			"o_Data.mhk",                                "o_Sounds.mhk",
			"p_Data.mhk",                                "p_Sounds.mhk",
			"r_Data.mhk",                                "r_Sounds.mhk",
			"t_Data.mhk",                                "t_Sounds.mhk",
			nullptr
	};

	static const char *datafilesDemo[] = {
			"a_Data.mhk", "a_Sounds.mhk",
			"j_Data.mhk", "j_Sounds.mhk",
			"t_Data.mhk", "t_Sounds.mhk",
			nullptr
	};

	const char **datafiles;
	if (isGameVariant(GF_DEMO)) {
		datafiles = datafilesDemo;
	} else if (isGameVariant(GF_DVD)) {
		datafiles = datafilesDVD;
	} else {
		datafiles = datafilesCD;
	}
	return datafiles;
}

bool MohawkEngine_Riven::checkDatafiles() {
	Common::String missingFiles;

	const char **datafiles = listExpectedDatafiles();
	for (int i = 0; datafiles[i] != nullptr; i++) {
		if (!SearchMan.hasFile(datafiles[i])) {
			if (strcmp(datafiles[i], "j_Data3.mhk") == 0
					|| strcmp(datafiles[i], "b_Data1.mhk") == 0) {
				// j_Data3.mhk and b_Data1.mhk come from the 1.02 patch. They are not required to play.
				continue;
			}

			if (!missingFiles.empty()) {
				missingFiles += ", ";
			}
			missingFiles += datafiles[i];
		}
	}

	if (missingFiles.empty()) {
		return true;
	}

	const char *msg = _s("You are missing the following required Riven data files:\n");
	Common::U32String message = _(msg) + Common::U32String(missingFiles);

	warning("%s%s", msg, missingFiles.c_str());
	GUIErrorMessage(message);

	return false;
}

const RivenLanguage *MohawkEngine_Riven::getLanguageDesc(Common::Language language) {
	const RivenLanguage *languages = MohawkMetaEngine_Riven::listLanguages();

	while (languages->language != Common::UNK_LANG) {
		if (languages->language == language) {
			return languages;
		}

		languages++;
	}

	return nullptr;
}

void MohawkEngine_Riven::loadLanguageDatafile(char prefix, uint16 stackId) {
	Common::Language language = getLanguage();
	const RivenLanguage *languageDesc = getLanguageDesc(language);
	if (!languageDesc) {
		return;
	}

	Common::String languageDatafile = Common::String::format("%c_data_%s.mhk", prefix, languageDesc->archiveSuffix);

	MohawkArchive *mhk = new MohawkArchive();
	if (mhk->openFile(languageDatafile)) {

		if (stackId == kStackOspit && getLanguage() != Common::EN_ANY && getLanguage() != Common::RU_RUS) {
			// WORKAROUND: The international CD versions were repacked for the 25th anniversary release
			// so they share the same resources as the English DVD version. The resource IDs for the DVD
			// version resources have a delta of 1 in their numbering when compared to the CD version
			// resources for Gehn's office. Unfortunately this delta was not compensated when repacking
			// the archives. We need to do it here at run time...
			mhk->offsetResourceIDs(ID_TBMP, 196, 1);
		} else if (stackId == kStackJspit && getLanguage() != Common::EN_ANY && getLanguage() != Common::RU_RUS) {
			// WORKAROUND: Same thing with Gehn's imager in the School in Jungle Island.
			mhk->offsetResourceIDs(ID_TMOV, 342, -2);
		} else if (stackId == kStackGspit && getLanguage() == Common::PL_POL) {
			// WORKAROUND: Same thing for the advertisement easter egg on Garden Island.
			mhk->offsetResourceIDs(ID_TMOV, 148, 2);
		}

		_mhk.push_back(mhk);
	} else {
		delete mhk;
	}
}

RivenStack *MohawkEngine_Riven::constructStackById(uint16 id) {
	switch (id) {
		case kStackAspit:
			return new RivenStacks::ASpit(this);
		case kStackBspit:
			return new RivenStacks::BSpit(this);
		case kStackGspit:
			return new RivenStacks::GSpit(this);
		case kStackJspit:
			return new RivenStacks::JSpit(this);
		case kStackOspit:
			return new RivenStacks::OSpit(this);
		case kStackPspit:
			return new RivenStacks::PSpit(this);
		case kStackRspit:
			return new RivenStacks::RSpit(this);
		case kStackTspit:
			return new RivenStacks::TSpit(this);
		default:
			error("Unknown stack id '%d'", id);
	}
}

// Riven uses some hacks to change stacks for linking books
// Otherwise, script command 27 changes stacks
struct RivenSpecialChange {
	byte startStack;
	uint32 startCardRMAP;
	byte targetStack;
	uint32 targetCardRMAP;
};

static const RivenSpecialChange rivenSpecialChange[] = {
	{ kStackAspit,  0x1f04, kStackOspit,  0x44ad }, // Trap Book
	{ kStackBspit, 0x1c0e7, kStackOspit,  0x2e76 }, // Dome Linking Book
	{ kStackGspit, 0x111b1, kStackOspit,  0x2e76 }, // Dome Linking Book
	{ kStackJspit, 0x28a18, kStackRspit,   0xf94 }, // Tay Linking Book
	{ kStackJspit, 0x26228, kStackOspit,  0x2e76 }, // Dome Linking Book
	{ kStackOspit,  0x5f0d, kStackPspit,  0x3bf0 }, // Return from 233rd Age
	{ kStackOspit,  0x470a, kStackJspit, 0x1508e }, // Return from 233rd Age
	{ kStackOspit,  0x5c52, kStackGspit, 0x10bea }, // Return from 233rd Age
	{ kStackOspit,  0x5d68, kStackBspit, 0x1adfd }, // Return from 233rd Age
	{ kStackOspit,  0x5e49, kStackTspit,   0xe87 }, // Return from 233rd Age
	{ kStackPspit,  0x4108, kStackOspit,  0x2e76 }, // Dome Linking Book
	{ kStackRspit,  0x32d8, kStackJspit, 0x1c474 }, // Return from Tay
	{ kStackTspit, 0x21b69, kStackOspit,  0x2e76 }  // Dome Linking Book
};

void MohawkEngine_Riven::changeToCard(uint16 dest) {
	debug (1, "Changing to card %d", dest);

	// Clear the graphics cache (images typically aren't used
	// on different cards).
	_gfx->clearCache();

	if (!isGameVariant(GF_DEMO)) {
		for (byte i = 0; i < ARRAYSIZE(rivenSpecialChange); i++)
			if (_stack->getId() == rivenSpecialChange[i].startStack && dest == _stack->getCardStackId(
					rivenSpecialChange[i].startCardRMAP)) {
				changeToStack(rivenSpecialChange[i].targetStack);
				dest = _stack->getCardStackId(rivenSpecialChange[i].targetCardRMAP);
			}
	}

	// Clear any timer still floating around
	_stack->removeTimer();

	if (_card) {
		_card->leave();
		delete _card;
	}
	_card = new RivenCard(this, dest);
	_card->enter(true);

	// Now we need to redraw the cursor if necessary and handle mouse over scripts
	_stack->queueMouseCursorRefresh();

	// Finally, install any hardcoded timer
	_stack->installCardTimer();
}

Common::SeekableReadStream *MohawkEngine_Riven::getExtrasResource(uint32 tag, uint16 id) {
	return _extrasFile->getResource(tag, id);
}

Common::Array<uint16> MohawkEngine_Riven::getResourceIDList(uint32 type) const {
	Common::Array<uint16> ids;

	for (uint i = 0; i < _mhk.size(); i++) {
		ids.push_back(_mhk[i]->getResourceIDList(type));
	}

	return ids;
}


void MohawkEngine_Riven::delay(uint32 ms) {
	uint32 startTime = _system->getMillis();

	while (_system->getMillis() < startTime + ms && !hasGameEnded()) {
		doFrame();
	}
}

void MohawkEngine_Riven::startNewGame() {
	// Clear all the state data
	_menuSavedStack = -1;
	_menuSavedCard = -1;
	_menuThumbnail.reset();

	_vars.clear();
	initVars();
	applyGameSettings();

	_zipModeData.clear();

	setTotalPlayTime(0);
}

Common::Error MohawkEngine_Riven::loadGameState(int slot) {
	Common::Error loadError = _saveLoad->loadGame(slot);

	if (loadError.getCode() == Common::kNoError) {
		_menuSavedStack = -1;
		_menuSavedCard = -1;
		_menuThumbnail.reset();
	}

	return loadError;
}

Common::Error MohawkEngine_Riven::saveGameState(int slot, const Common::String &desc, bool isAutosave) {
	if (_menuSavedStack != -1) {
		_vars["CurrentStackID"] = _menuSavedStack;
		_vars["CurrentCardID"] = _menuSavedCard;
	}

	const Graphics::Surface *thumbnail = _menuSavedStack != -1 ? _menuThumbnail.get() : nullptr;
	Common::Error error = _saveLoad->saveGame(slot, desc, thumbnail, isAutosave);

	if (_menuSavedStack != -1) {
		_vars["CurrentStackID"] = 1;
		_vars["CurrentCardID"] = 1;
	}

	return error;
}

Common::Language MohawkEngine_Riven::getLanguage() const {
	Common::Language language = MohawkEngine::getLanguage();

	// The language can be changed at run time in the 25th anniversary edition
	if (language == Common::UNK_LANG) {
		language = Common::parseLanguage(ConfMan.get("language"));
	}

	if (language == Common::UNK_LANG) {
		language = Common::EN_ANY;
	}

	return language;
}

bool MohawkEngine_Riven::canSaveAutosaveCurrently() {
	return canSaveGameStateCurrently() && !_gameEnded;
}

void MohawkEngine_Riven::addZipVisitedCard(uint16 cardId, uint16 cardNameId) {
	Common::String cardName = getStack()->getName(kCardNames, cardNameId);
	if (cardName.empty())
		return;
	ZipMode zip;
	zip.name = cardName;
	zip.id = cardId;
	if (Common::find(_zipModeData.begin(), _zipModeData.end(), zip) == _zipModeData.end())
		_zipModeData.push_back(zip);
}

bool MohawkEngine_Riven::isZipVisitedCard(const Common::String &hotspotName) const {
	bool foundMatch = false;

	if (!hotspotName.empty())
		for (uint16 j = 0; j < _zipModeData.size(); j++)
			if (_zipModeData[j].name == hotspotName) {
				foundMatch = true;
				break;
			}

	return foundMatch;
}

bool MohawkEngine_Riven::canLoadGameStateCurrently() {
	if (isGameVariant(GF_DEMO)) {
		return false;
	}

	if (_scriptMan->hasQueuedScripts() && !isInMainMenu()) {
		return false;
	}

	return true;
}

bool MohawkEngine_Riven::canSaveGameStateCurrently() {
	return canLoadGameStateCurrently() && isGameStarted();
}

bool MohawkEngine_Riven::hasGameEnded() const {
	return _gameEnded || shouldQuit();
}

void MohawkEngine_Riven::setGameEnded() {
	_gameEnded = true;
}

void MohawkEngine_Riven::runOptionsDialog() {
	GUI::ConfigDialog dlg;
	if (runDialog(dlg)) {
		syncSoundSettings();
		applyGameSettings();
	}
}

void MohawkEngine_Riven::applyGameSettings() {
	int transitions = ConfMan.getInt("transition_mode");
	RivenTransitionMode transitionsMode = RivenGraphics::sanitizeTransitionMode(transitions);

	_vars["transitionmode"] = transitionsMode;
	_vars["azip"]           = ConfMan.getBool("zip_mode");
	_vars["waterenabled"]   = ConfMan.getBool("water_effects");

	_gfx->setTransitionMode(transitionsMode);

	Common::Language newLanguage = getLanguage();
	if (_stack && newLanguage != _currentLanguage) {
		_gfx->loadMenuFont();
		reloadCurrentCard();
	}
	_currentLanguage = newLanguage;

	if (_card) {
		_card->initializeZipMode();
	}
}

bool MohawkEngine_Riven::isInteractive() const {
	return !_scriptMan->hasQueuedScripts() && !hasGameEnded();
}

Common::KeymapArray MohawkEngine_Riven::initKeymaps(const char *target) {
	using namespace Common;

	String guiOptions = ConfMan.get("guioptions", target);
	bool is25th = checkGameGUIOption(GAMEOPTION_25TH, guiOptions);
	bool isDemo = checkGameGUIOption(GAMEOPTION_DEMO, guiOptions);

	Keymap *engineKeyMap = new Keymap(Keymap::kKeymapTypeGame, "riven", "Riven");

	Action *act;

	act = new Action(kStandardActionOpenMainMenu, _("Open main menu"));
	act->setCustomEngineActionEvent(kRivenActionOpenMainMenu);
	act->addDefaultInputMapping("JOY_X");
	if (is25th) {
		act->addDefaultInputMapping("ESCAPE");
	} else if (isDemo) {
		act->addDefaultInputMapping("C+r");
	} else {
		act->addDefaultInputMapping("F5");
	}
	engineKeyMap->addAction(act);

	act = new Action(kStandardActionSkip, _("Skip"));
	act->setCustomEngineActionEvent(kRivenActionSkip);
	act->addDefaultInputMapping("ESCAPE");
	act->addDefaultInputMapping("JOY_Y");
	engineKeyMap->addAction(act);

	act = new Action(kStandardActionInteract, _("Interact"));
	act->setCustomEngineActionEvent(kRivenActionInteract);
	act->addDefaultInputMapping("MOUSE_LEFT");
	act->addDefaultInputMapping("JOY_A");
	engineKeyMap->addAction(act);

	act = new Action(kStandardActionLoad, _("Load game state"));
	act->setCustomEngineActionEvent(kRivenActionLoadGameState);
	act->addDefaultInputMapping("C+o");
	engineKeyMap->addAction(act);

	act = new Action(kStandardActionSave, _("Save game state"));
	act->setCustomEngineActionEvent(kRivenActionSaveGameState);
	act->addDefaultInputMapping("C+s");
	engineKeyMap->addAction(act);

	act = new Action(kStandardActionOpenSettings, _("Show options menu"));
	act->setCustomEngineActionEvent(kRivenActionOpenOptionsDialog);
	if (is25th) {
		act->addDefaultInputMapping("F5");
	}
	engineKeyMap->addAction(act);

	act = new Action(kStandardActionPause, _("Pause"));
	act->setCustomEngineActionEvent(kRivenActionPause);
	act->addDefaultInputMapping("SPACE");
	engineKeyMap->addAction(act);

	act = new Action(kStandardActionMoveUp, _("Move forward"));
	act->setCustomEngineActionEvent(kRivenActionMoveForward);
	act->addDefaultInputMapping("UP");
	act->addDefaultInputMapping("JOY_UP");
	engineKeyMap->addAction(act);

	act = new Action("FWDL", _("Move forward left"));
	act->setCustomEngineActionEvent(kRivenActionMoveForwardLeft);
	engineKeyMap->addAction(act);

	act = new Action("FWDR", _("Move forward right"));
	act->setCustomEngineActionEvent(kRivenActionMoveForwardRight);
	engineKeyMap->addAction(act);

	act = new Action(kStandardActionMoveDown, _("Move backwards"));
	act->setCustomEngineActionEvent(kRivenActionMoveBack);
	act->addDefaultInputMapping("DOWN");
	act->addDefaultInputMapping("JOY_DOWN");
	engineKeyMap->addAction(act);

	act = new Action(kStandardActionMoveLeft, _("Turn left"));
	act->setCustomEngineActionEvent(kRivenActionMoveLeft);
	act->addDefaultInputMapping("LEFT");
	act->addDefaultInputMapping("JOY_LEFT");
	engineKeyMap->addAction(act);

	act = new Action(kStandardActionMoveRight, _("Turn right"));
	act->setCustomEngineActionEvent(kRivenActionMoveRight);
	act->addDefaultInputMapping("RIGHT");
	act->addDefaultInputMapping("JOY_RIGHT");
	engineKeyMap->addAction(act);

	act = new Action("LKUP", _("Look up"));
	act->setCustomEngineActionEvent(kRivenActionLookUp);
	act->addDefaultInputMapping("PAGEUP");
	engineKeyMap->addAction(act);

	act = new Action("LKDN", _("Look down"));
	act->setCustomEngineActionEvent(kRivenActionLookDown);
	act->addDefaultInputMapping("PAGEDOWN");
	engineKeyMap->addAction(act);

	if (isDemo) {
		act = new Action("INTV", _("Play intro videos"));
		act->setCustomEngineActionEvent(kRivenActionPlayIntroVideos);
		act->addDefaultInputMapping("C+p");
		engineKeyMap->addAction(act);
	}

	return Keymap::arrayOf(engineKeyMap);
}

bool ZipMode::operator== (const ZipMode &z) const {
	return z.name == name && z.id == id;
}

} // End of namespace Mohawk
