/* 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/>.
 *
 * Copyright 2020 Google
 *
 */

#include "common/debug-channels.h"
#include "common/error.h"
#include "common/events.h"
#include "common/formats/ini-file.h"
#include "common/stream.h"
#include "common/system.h"
#include "common/file.h"
#include "common/keyboard.h"
#include "common/macresman.h"
#include "common/util.h"
#include "common/compression/deflate.h"
#include "common/config-manager.h"
#include "common/translation.h"

#include "engines/advancedDetector.h"
#include "engines/util.h"

#include "graphics/surface.h"

#include "video/smk_decoder.h"

#include "hadesch/pod_file.h"
#include "hadesch/hadesch.h"
#include "hadesch/video.h"
#include "hadesch/pod_image.h"

#include "common/memstream.h"
#include "common/formats/winexe_pe.h"
#include "common/substream.h"
#include "common/md5.h"
#include "graphics/wincursor.h"
#include "graphics/cursorman.h"
#include "graphics/maccursor.h"

namespace Hadesch {

HadeschEngine *g_vm;

static const uint32 cursorids[] = {
	127, 128, 129, // normal and active
	125, 134, 135, 136, 137, 138, 139, 140, 141, 142, // waiting
	130, // up arrow
	131, // left arrow
	132, // down arrow
	133, // right arrow
	// 143:cross, 146:cross
	146
};

HadeschEngine::HadeschEngine(OSystem *system, const ADGameDescription *desc)
	: Engine(system), _desc(desc), _rnd("hadesch"), _cheatsEnabled(false) {

	g_vm = this;
	_sceneStartTime = _system->getMillis();
	_currentTime = 0;
	_isQuitting = false;
	_isRestoring = false;

	_subtitleID = 0;

	debug("HadeschEngine::ctor");
}

HadeschEngine::~HadeschEngine() {
	debug("HadeschEngine::dtor");
	for (unsigned i = 0; i < _winCursors.size(); i++) {
		delete _winCursors[i];
		_winCursors[i] = nullptr;
	}
	for (unsigned i = 0; i < _macCursors.size(); i++) {
		delete _macCursors[i];
		_macCursors[i] = nullptr;
	}

#ifdef USE_TRANSLATION
	delete _transMan;
#endif
}

void HadeschEngine::setVideoRoom(Common::SharedPtr<VideoRoom> room,
				 Common::SharedPtr<Handler> handler,
				 RoomId roomId) {
	assert(!_isInOptions || _isRestoring);
	_sceneVideoRoom = room;
	_sceneHandler = handler;
	_currentTime = 0;
	_sceneStartTime = _system->getMillis();
	if (!_isRestoring) {
		_persistent._previousRoomId = _persistent._currentRoomId;
		_persistent._currentRoomId = roomId;
	}
	_sceneTimers.clear();
}

Common::Point HadeschEngine::getMousePos() {
	return _mousePos;
}

void HadeschEngine::handleEvent(EventHandlerWrapper event) {
	event();
}

void HadeschEngine::newGame() {
	_persistent = Persistent();
	_persistent._quest = kCreteQuest;
	moveToRoom(kWallOfFameRoom);
}

struct WiseFile {
	uint start;
	uint end;
	uint uncompressedLength;
};

// Some variants use wise installer. Wise installer use raw zlib compressed files
// Rather than parsing out the wise structures, we just store offsets for
// the files we care about
static const struct {
	const char *md5;
	uint setupLength;
	WiseFile _wdPod;
	WiseFile _hadeschExe;
} setups[] = {
	{
		// Russian, Windows
		"9ddf1b0b271426b9d023dbf3edbb1caa",
		7491209,
		{0xB8DA2, 0x7246CB, 8691909},
		{0x4109C, 0xB4628, 1007616}
	}
};

Common::MemoryReadStream *readWiseFile(Common::File &setupFile, const struct WiseFile &wiseFile) {
	// -4 to skip CRC
	byte *compressedBuffer = new byte[wiseFile.end - wiseFile.start - 4];
	byte *uncompressedBuffer = new byte[wiseFile.uncompressedLength];
	setupFile.seek(wiseFile.start);
	setupFile.read(compressedBuffer, wiseFile.end - wiseFile.start - 4);

	uint dstLen = wiseFile.uncompressedLength;
	if (!Common::inflateZlibHeaderless(uncompressedBuffer, &dstLen,
					   compressedBuffer, wiseFile.end - wiseFile.start - 4) || dstLen != wiseFile.uncompressedLength) {
		debug("wise inflate failed");
		delete[] compressedBuffer;
		delete[] uncompressedBuffer;
		return nullptr;
	}

	delete[] compressedBuffer;
	return new Common::MemoryReadStream(uncompressedBuffer, wiseFile.uncompressedLength);
}

Common::ErrorCode HadeschEngine::loadWindowsCursors(const Common::ScopedPtr<Common::PEResources>& exe) {
	for (unsigned i = 0; i < ARRAYSIZE(cursorids); i++) {
		Graphics::WinCursorGroup *group = Graphics::WinCursorGroup::createCursorGroup(exe.get(), cursorids[i]);

		if (!group) {
			debug("Cannot find cursor group %d", cursorids[i]);
			return Common::kUnsupportedGameidError;
		}

		_cursors.push_back(group->cursors[0].cursor);
		_winCursors.push_back(group);
	}

	return Common::kNoError;
}

Common::ErrorCode HadeschEngine::loadCursors() {
	debug("HadeschEngine: loading cursors");

	const char *const winPaths[] = {
		"HADESCH.EXE",
		"WIN95/HADESCH.EXE"
	};

	const char *const macPaths[] = {
		"Hades_-_Copy_To_Hard_Drive/Hades_Challenge/Hades_Challenge_PPC",
		"Hades - Copy To Hard Drive/Hades Challenge/Hades Challenge PPC"
	};

	for (uint j = 0; j < ARRAYSIZE(macPaths); ++j) {
		Common::ScopedPtr<Common::PEResources> exe = Common::ScopedPtr<Common::PEResources>(new Common::PEResources());
		if (exe->loadFromEXE(winPaths[j])) {
			return loadWindowsCursors(exe);
		}
	}

	for (uint j = 0; j < ARRAYSIZE(macPaths); ++j) {
	  	Common::MacResManager resMan = Common::MacResManager();
		if (!resMan.open(macPaths[j])) {
			continue;
		}

		for (unsigned i = 0; i < ARRAYSIZE(cursorids); i++) {
			Common::ScopedPtr<Common::SeekableReadStream> stream = Common::ScopedPtr<Common::SeekableReadStream>(
				resMan.getResource(MKTAG('c','r','s','r'), cursorids[i]));
			if (!stream) {
				debug("Couldn't load cursor %d", cursorids[i]);
				return Common::kUnsupportedGameidError;
			}
			Graphics::MacCursor *macCursor = new Graphics::MacCursor();
			macCursor->readFromStream(*stream);
			_cursors.push_back(macCursor);
			_macCursors.push_back(macCursor);
		}
		return Common::kNoError;
	}

	Common::File setupFile;
	if (setupFile.open("Setup.exe")) {
		uint len = setupFile.size();
		Common::String md5 = Common::computeStreamMD5AsString(setupFile, len);
		for (uint chosenSetup = 0; chosenSetup < ARRAYSIZE(setups); chosenSetup++) {
			if (setups[chosenSetup].setupLength == len && setups[chosenSetup].md5 == md5) {
				Common::MemoryReadStream *wdPod = readWiseFile(setupFile, setups[0]._wdPod);
				if (!wdPod) {
					debug("wd.pod inflate failed");
					return Common::kUnsupportedGameidError;
				}
				_wdPodFile = Common::SharedPtr<PodFile>(new PodFile("WD.POD"));
				_wdPodFile->openStore(Common::SharedPtr<Common::SeekableReadStream>(wdPod));

				Common::MemoryReadStream *hadeschExe = readWiseFile(setupFile, setups[0]._hadeschExe);
				if (!hadeschExe) {
					debug("hadesch.exe inflate failed");
					return Common::kUnsupportedGameidError;
				}

				Common::ScopedPtr<Common::PEResources> exe = Common::ScopedPtr<Common::PEResources>(new Common::PEResources());
				if (exe->loadFromEXE(hadeschExe)) {
					Common::ErrorCode status = loadWindowsCursors(exe);
					return status;
				}
			}
		}
	}

	debug("Cannot open hadesch.exe");
	return Common::kUnsupportedGameidError;
}

static const char *roomCheats[] = {
	"",
	"in",
	"mo",
	"ht",
	"se",
	"at",
	"mi",
	"me",
	"ar",
	"tr",
	"ca",
	"pr",
	"th",
	"cp",
	"mp",
	"dw",
	"mn",
	"vt",
	"nr",
	"htr",
	"ff",
	"mm",
	"hc",
	"cr",
	"op"
};

static const char *itemCheats[] = {
	"",
	"",
	"straw",
	"stone",
	"bricks",
	"message",
	"key",
	"decree",
	"wood",
	"statue1",
	"statue2",
	"statue3",
	"statue4",
	"statue",
	"coin",
	"potion",
	"shield",
	"sword",
	"bag",
	"helmet",
	"sandals",
	"torch"
};

bool HadeschEngine::handleGenericCheat(const Common::String &cheat) {
	if (cheat == "cheatsoff") {
		_cheatsEnabled = false;
		return true;
	}

	for (int i = kIntroRoom; i < kOptionsRoom; i++)
		if (cheat == roomCheats[i]) {
			moveToRoom((RoomId) i);
			getVideoRoom()->disableMouse();
			return true;
		}

	if (cheat == "commandson") {
		getVideoRoom()->enableMouse();
		return true;
	}

	if (cheat == "commandsoff") {
		getVideoRoom()->disableMouse();
		return true;
	}

	if (cheat == "cretequest") {
		_persistent._quest = kCreteQuest;
		_persistent._roomVisited[kWallOfFameRoom] = true;
		return true;
	}

	if (cheat == "troyquest") {
		_persistent._quest = kTroyQuest;
		_persistent._roomVisited[kWallOfFameRoom] = true;
		_persistent._roomVisited[kMinotaurPuzzle] = true;

		_persistent._powerLevel[0] = MAX(_persistent._powerLevel[0], 1);
		return true;
	}

	if (cheat == "medusaquest") {
		_persistent._quest = kMedusaQuest;
		_persistent._roomVisited[kWallOfFameRoom] = true;
		_persistent._roomVisited[kMinotaurPuzzle] = true;
		_persistent._roomVisited[kTrojanHorsePuzzle] = true;

		for (int i = 0; i < 2; i++)
			_persistent._powerLevel[i] = MAX(_persistent._powerLevel[i], 1);
		return true;
	}

	if (cheat == "rescuephilquest") {
		_persistent._quest = kRescuePhilQuest;
		_persistent._roomVisited[kWallOfFameRoom] = true;
		_persistent._roomVisited[kMedusaPuzzle] = true;
		_persistent._roomVisited[kTrojanHorsePuzzle] = true;
		_persistent._roomVisited[kMinotaurPuzzle] = true;

		for (int i = 0; i < 3; i++)
			_persistent._powerLevel[i] = MAX(_persistent._powerLevel[i], 1);

		return true;
	}

	// TODO: "noquest", "op", "click*", "memory", "showhotspots", "hidehotspots", "showcursor",
	// giveall, takeall, givekeyanddecree, takekeyanddecree

	for (int i = kStraw; i <= kTorch; i++)
		if (cheat == Common::String("give") + itemCheats[i]) {
			_heroBelt->placeToInventory((InventoryItem)i);
			return true;
		}

	for (int i = kStraw; i <= kTorch; i++)
		if (cheat == Common::String("take") + itemCheats[i]) {
			_heroBelt->removeFromInventory((InventoryItem)i);
			return true;
		}

	if (cheat == "hero") {
		_persistent._gender = kMale;
		return true;
	}

	if (cheat == "heroine") {
		_persistent._gender = kFemale;
		return true;
	}

	if (cheat.matchString("powerstrength#")) {
		_persistent._powerLevel[kPowerStrength] = cheat.substr(13).asUint64();
		return true;
	}

	if (cheat.matchString("powerstealth#")) {
		_persistent._powerLevel[kPowerStealth] = cheat.substr(12).asUint64();
		return true;
	}

	if (cheat.matchString("powerwisdom#")) {
		_persistent._powerLevel[kPowerWisdom] = cheat.substr(11).asUint64();
		return true;
	}

	if (cheat.matchString("powerall#")) {
		int val = cheat.substr(8).asUint64();
		_persistent._powerLevel[kPowerWisdom] = val;
		_persistent._powerLevel[kPowerStealth] = val;
		_persistent._powerLevel[kPowerStrength] = val;
		return true;
	}

	return false;
}

void HadeschEngine::resetOptionsRoom() {
	_optionsRoom = Common::SharedPtr<VideoRoom>(new VideoRoom("", "", "OPAssets.txt"));
}

void HadeschEngine::enterOptions() {
	_isInOptions = true;
	_optionsEnterTime = _system->getMillis();
	_sceneVideoRoom->pause();
	resetOptionsRoom();
	_optionsHandler = makeOptionsHandler();
	_optionsHandler->prepareRoom();
}

void HadeschEngine::enterOptionsCredits() {
	if (_isInOptions) {
		_sceneStartTime += _system->getMillis() - _optionsEnterTime;
	}
	_isInOptions = true;
	_optionsEnterTime = _system->getMillis();
	_optionsRoom = Common::SharedPtr<VideoRoom>(new VideoRoom("CREDITS", "CR", ""));
	_optionsHandler = makeCreditsHandler(true);
	_optionsHandler->prepareRoom();
}

void HadeschEngine::exitOptions() {
	_isInOptions = false;
	_sceneStartTime += _system->getMillis() - _optionsEnterTime;
	_optionsHandler.reset();
	_optionsRoom.reset();
	_sceneVideoRoom->unpause();
}

Common::U32String HadeschEngine::translate(const Common::String &str) {
#ifdef USE_TRANSLATION
	return _transMan->getTranslation(str);
#else
	return str.decode();
#endif
}

Common::Error HadeschEngine::run() {
	debug("HadeschEngine::run");

#ifdef USE_TRANSLATION
	_transMan = new Common::TranslationManager("hadesch_translations.dat");
	_transMan->setLanguage(TransMan.getCurrentLanguage());
#endif

	const Common::FSNode gameDataDir(ConfMan.getPath("path"));
	SearchMan.addSubDirectoryMatching(gameDataDir, "WIN9x");

	Common::ErrorCode err = loadCursors();
	if (err != Common::kNoError)
		return err;

	if (!_wdPodFile) {
		const char *const wdpodpaths[] = {
			"WIN9x/WORLD/WD.POD", "WIN95/WORLD/WD.POD", "WD.POD",
			"Hades_-_Copy_To_Hard_Drive/Hades_Challenge/World/wd.pod",
			"Hades - Copy To Hard Drive/Hades Challenge/World/wd.pod"
		};
		debug("HadeschEngine: loading wd.pod");
		for (uint i = 0; i < ARRAYSIZE(wdpodpaths); ++i) {
			Common::SharedPtr<Common::SeekableReadStream> stream(Common::MacResManager::openFileOrDataFork(wdpodpaths[i]));
			if (stream) {
				_wdPodFile = Common::SharedPtr<PodFile>(new PodFile("WD.POD"));
				_wdPodFile->openStore(stream);
				break;
			}
		}
	}

	if (!_wdPodFile) {
		debug("Cannot find WD.POD");
		return Common::kUnsupportedGameidError;
	}

	_cdScenesPath = "";

	// It's tempting to use SearchMan for this but it
	// doesn't work because we need to access subdirs based
	// on cdScenePath
	const char *const scenepaths[] = {"CDAssets/", "Scenes/"};
	for (uint i = 0; i < ARRAYSIZE(scenepaths); ++i) {
		Common::ScopedPtr<Common::SeekableReadStream> stream(Common::MacResManager::openFileOrDataFork(Common::Path(scenepaths[i]).appendInPlace("OLYMPUS/OL.POD")));
		if (stream) {
			_cdScenesPath = scenepaths[i];
			break;
		}
	}

	if (_cdScenesPath.empty()) {
		debug("Cannot find OL.POD");
		return Common::kUnsupportedGameidError;
	}

	debug("HadeschEngine: intro");
	initGraphics(kVideoWidth, kVideoHeight);

	_heroBelt = Common::SharedPtr<HeroBelt>(new HeroBelt());
	_gfxContext = Common::SharedPtr<GfxContext8Bit>(new GfxContext8Bit(2 * kVideoWidth + 10, kVideoHeight + 50));
	_isInOptions = false;
	_lastFallbackSound = -1;

	ConfMan.registerDefault("subtitles", "false");
	ConfMan.registerDefault("sfx_volume", 192);
	ConfMan.registerDefault("music_volume", 192);
	ConfMan.registerDefault("speech_volume", 192);
	ConfMan.registerDefault("mute", "false");
	ConfMan.registerDefault("speech_mute", "false");
	ConfMan.registerDefault("talkspeed", 60);
	_mixer->setVolumeForSoundType(_mixer->kMusicSoundType, ConfMan.getInt("music_volume"));
	_mixer->setVolumeForSoundType(_mixer->kSFXSoundType, ConfMan.getInt("sfx_volume"));
	_mixer->setVolumeForSoundType(_mixer->kSpeechSoundType, ConfMan.getInt("speech_volume"));

	if (!ConfMan.getBool("subtitles")) {
		_subtitleDelayPerChar = -1;
	}else {
		int talkSpeed = ConfMan.getInt("talkspeed");

		if (!talkSpeed)
			talkSpeed = 0;

		_subtitleDelayPerChar = 4500 / ConfMan.getInt("talkspeed");
	}

	debug("HadeschEngine: moving to main loop");
	_nextRoom.clear();
	int loadSlot = ConfMan.getInt("save_slot");
	if (loadSlot >= 0) {
		loadGameState(loadSlot);
	} else {
		_nextRoom.push_back(kIntroRoom);
	}

	while (true) {
		if (_isRestoring) {
			moveToRoomReal(_persistent._currentRoomId);
			_isRestoring = false;
			CursorMan.showMouse(true);
			CursorMan.replaceCursor(_cursors[3]);
		} else if (!_nextRoom.empty()) {
			moveToRoomReal(_nextRoom.remove_at(0));
			CursorMan.showMouse(true);
			CursorMan.replaceCursor(_cursors[3]);
		}

		if (_isQuitting) {
			return Common::kNoError;
		}
		Common::Event event;
		bool stopVideo = false;
		while (_eventMan->pollEvent(event)) {
			switch (event.type) {
			case Common::EVENT_QUIT:
			case Common::EVENT_RETURN_TO_LAUNCHER:
				return Common::kNoError;
			case Common::EVENT_LBUTTONDOWN: {
				if(getVideoRoom()->isMouseEnabled() && getVideoRoom()->isHeroBeltEnabled()
				   && _heroBelt->isPositionOverHeroBelt(event.mouse)) {
					debug("handling belt click");
					_heroBelt->handleClick(event.mouse);
					break;
				}
				const Common::String &q = getVideoRoom()->mapClick(event.mouse);
				debug("handling click on <%s>", q.c_str());
				if (getVideoRoom()->isHeroBeltEnabled() && _heroBelt->isHoldingItem()) {
					if (!getCurrentHandler()->handleClickWithItem(q, _heroBelt->getHoldingItem())) {
						fallbackClick();
					}
				} else {
					getCurrentHandler()->handleAbsoluteClick(event.mouse);
					getCurrentHandler()->handleClick(q);
				}
			}
				break;
			case Common::EVENT_LBUTTONUP:
				getCurrentHandler()->handleUnclick(getVideoRoom()->mapClick(event.mouse), event.mouse);
				break;
			case Common::EVENT_KEYDOWN:
				// TODO: make equivalents for mobile devices. Keyboard is
				// used for 4 things:
				//
				// * Skipping cutscenes (press space or escape. In original: only space).
				// * Entering name.
				//      Original requires a non-empty name. We allow an
				//      empty name.
				// * Optional save name
				// * Cheats
				if (event.kbd.keycode == Common::KEYCODE_SPACE || event.kbd.keycode == Common::KEYCODE_ESCAPE)
					stopVideo = true;
				if ((event.kbd.ascii >= 'a' && event.kbd.ascii <= 'z')
				    || (event.kbd.ascii >= '0' && event.kbd.ascii <= '9')) {
					_cheat += event.kbd.ascii;
				}
				if (event.kbd.keycode == Common::KEYCODE_RETURN) {
					Common::String cheat = _cheat;
					_cheat = "";
					if (cheat == "qazxcdewsrfvbnhytg") {
						debug("Cheats enabled");
						_cheatsEnabled = true;
						break;
					}
					if (_cheatsEnabled) {
						if (handleGenericCheat(cheat))
							break;
						if (getCurrentHandler()->handleCheat(cheat))
							break;
					}
				}
				getCurrentHandler()->handleKeypress(event.kbd.ascii);
				break;
			default:
				break;
			}
		}

		if (_isInOptions) {
			_currentTime = _system->getMillis() - _optionsEnterTime;
		} else {
			_currentTime = _system->getMillis() - _sceneStartTime;
			for (Common::List<Timer>::iterator it = _sceneTimers.begin();
			     it != _sceneTimers.end();) {
				if ((it->period_count != 0 && it->next_time < _currentTime)
				    || (it->skippable && stopVideo)) {
					it->next_time = _currentTime + it->period;
					if (it->period_count != -1) {
						it->period_count--;
					}
					handleEvent(it->event);
				}

				if (it->period_count == 0) {
					it = _sceneTimers.erase(it);
				} else
					it++;
			}
		}

		Common::String oldhotzone = getVideoRoom()->getHotZone();
		_mousePos = _eventMan->getMousePos();
		getVideoRoom()->computeHotZone(_currentTime, _mousePos);

		Common::String newhotzone = getVideoRoom()->getHotZone();

		if (oldhotzone != newhotzone) {
			getCurrentHandler()->handleMouseOut(oldhotzone);
			getCurrentHandler()->handleMouseOver(newhotzone);
		}

		getCurrentHandler()->frameCallback();

		getVideoRoom()->nextFrame(_gfxContext, _currentTime, stopVideo);

		if (getVideoRoom()->getDragged()) {
			CursorMan.showMouse(true);
			CursorMan.replaceCursor(getVideoRoom()->getDragged());
		} else if (getVideoRoom()->isHeroBeltEnabled()
		    && _heroBelt->isHoldingItem()) {
			const Graphics::Cursor *cursor = _heroBelt->getHoldingItemCursor(
				getVideoRoom()->getCursorAnimationFrame(_currentTime));
			CursorMan.showMouse(true);
			CursorMan.replaceCursor(cursor);
		} else {
			int cursor = getVideoRoom()->getCursor();
			if (cursor >= 0) {
				CursorMan.showMouse(true);
				CursorMan.replaceCursor(_cursors[cursor]);
			} else {
				CursorMan.showMouse(true);
				CursorMan.replaceCursor(_cursors[3]);
			}
		}

		_system->updateScreen();
		_system->delayMillis(15);
	}

	return Common::kNoError;
}

Common::RandomSource &HadeschEngine::getRnd() {
	return _rnd;
}

bool HadeschEngine::hasFeature(EngineFeature f) const {
	return
		f == kSupportsReturnToLauncher ||
		f == kSupportsLoadingDuringRuntime ||
		f == kSupportsSavingDuringRuntime ||
		f == kSupportsChangingOptionsDuringRuntime;
}

static const TranscribedSound fallbackSounds[] = {
	{"G0080nA0", _hs("Ho, not there")},
	{"G0080nB0", _hs("Nope")},
	{"G0080nC0", _hs("Time out, kid. That doesn't work there")},
	{"G0080nD0", _hs("How about something else?")},
	{"G0080nE0", _hs("Eh-Eh")},
	{"G0080nF0", _hs("Not that one")},
	{"G0080nG0", _hs("Are you kidding? Try something else")},
	{"G0080nH0", _hs("Enough already. Give something else a try")},
	{"G0080nI0", _hs("Something else might work better")},
	{"G0080nJ0", _hs("You can come up with a better answer than that")},
};

void HadeschEngine::fallbackClick() {
	int soundId = -1;
	if (_lastFallbackSound < 0)
		soundId = getRnd().getRandomNumberRng(0, ARRAYSIZE(fallbackSounds) - 1);
	else {
		soundId = getRnd().getRandomNumberRng(0, ARRAYSIZE(fallbackSounds) - 2);
		if (soundId >= _lastFallbackSound)
			soundId++;
	}

	_lastFallbackSound = soundId;
	Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
	room->playSpeech(fallbackSounds[soundId]);
}

Common::Error HadeschEngine::loadGameStream(Common::SeekableReadStream *stream) {
	Common::Serializer s(stream, nullptr);
	if (!_persistent.syncGameStream(s))
		return Common::kUnknownError;

	_isRestoring = true;

	return Common::kNoError;
}

Common::Error HadeschEngine::saveGameStream(Common::WriteStream *stream, bool isAutosave) {
	Common::Serializer s(nullptr, stream);
	if (isAutosave)
		_persistent._slotDescription = "Autosave";
	if(_persistent._currentRoomId == 0)
		return Common::kUnknownError;
	bool res = _persistent.syncGameStream(s);
	_persistent._slotDescription = "";
	return res ? Common::kNoError
		: Common::kUnknownError;
}

const Common::String &HadeschEngine::getCDScenesPath() const {
	return _cdScenesPath;
}

void HadeschEngine::addTimer(EventHandlerWrapper eventId, int32 start_time, int period, int repeat, bool skippable) {
	struct Timer timer;
	assert(!_isInOptions);
	timer.next_time = start_time + period;
	timer.period_count = repeat;
	timer.period = period;
	timer.event = eventId;
	timer.skippable = skippable;
	_sceneTimers.push_back(timer);
}

void HadeschEngine::addTimer(EventHandlerWrapper eventId, int period, int repeat) {
	addTimer(eventId, _currentTime, period, repeat, false);
}

void HadeschEngine::addSkippableTimer(EventHandlerWrapper eventId, int period, int repeat) {
	addTimer(eventId, _currentTime, period, repeat, true);
}

void HadeschEngine::cancelTimer(int eventId) {
	assert(!_isInOptions);
	for (Common::List<Timer>::iterator it = _sceneTimers.begin();
	     it != _sceneTimers.end();) {
		if (it->event == eventId) {
			it = _sceneTimers.erase(it);
		} else
			it++;
	}
}

Common::SharedPtr<Handler> HadeschEngine::getCurrentHandler() {
	return _isInOptions ? _optionsHandler : _sceneHandler;
}

Common::SharedPtr<VideoRoom> HadeschEngine::getVideoRoom() {
	return _isInOptions ? _optionsRoom : _sceneVideoRoom;
}

void HadeschEngine::moveToRoomReal(RoomId id) {
	if (_sceneVideoRoom)
		_sceneVideoRoom->finish();

	_heroBelt->reset();

	switch (id) {
	case kWallOfFameRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("WALLFAME", "WF", "HTAssets.txt")),
			     makeWallOfFameHandler(), id);
		break;
	case kArgoRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("ARGO", "AR", "ARAssets.txt")),
			     makeArgoHandler(), id);
		break;
	case kCreteRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("CRETE", "CR", "")),
			     makeCreteHandler(), id);
			     break;
	case kOlympusRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("OLYMPUS", "OL", "MOAssets.txt")),
			     makeOlympusHandler(), id);
		break;
	case kMinosPalaceRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("MINOS", "MI", "MIAssets.txt")),
			     makeMinosHandler(), id);
		break;
	case kDaedalusRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("DAEDALUS", "DA", "DWAssets.txt")),
			     makeDaedalusHandler(), id);
		break;
	case kSeriphosRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("SERIPHOS", "SE", "")),
			     makeSeriphosHandler(), id);
		break;
	case kMedIsleRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("MEDISLE", "MI", "")),
			     makeMedIsleHandler(), id);
		break;
	case kMedusaPuzzle:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("MEDUSA", "ME", "")),
			     makeMedusaHandler(), id);
		break;
	case kTroyRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("TROY", "TR", "")),
			     makeTroyHandler(), id);
		break;
	case kTrojanHorsePuzzle:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("TROJAN", "TH", "")),
			     makeTrojanHandler(), id);
		break;
	case kMinotaurPuzzle:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("MINOTAUR", "MM", "")),
			     makeMinotaurHandler(), id);
			     break;
	case kQuiz:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("HADESCH", "HC", "HcAssets.txt")),
			     makeQuizHandler(), id);
		break;
	case kCatacombsRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("CATACOMB", "CA", "CaAssets.txt")),
			     makeCatacombsHandler(), id);
		break;
	case kPriamRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("PRIAM", "PR", "PrAssets.txt")),
			     makePriamHandler(), id);
		break;
	case kAthenaRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("ATHENA", "AT", "")),
			     makeAthenaHandler(), id);
		break;
	case kVolcanoRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("VOLCANO", "VO", "VTAssets.txt")),
			     makeVolcanoHandler(), id);
		break;
	case kRiverStyxRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("NEARR", "NR", "NRAssets.txt")),
			     makeRiverStyxHandler(), id);
		break;
	case kHadesThroneRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("THRONE", "TH", "HTRAsset.txt")),
			     makeHadesThroneHandler(), id);
		break;
	case kCreditsRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("CREDITS", "CR", "")),
			     makeCreditsHandler(false), id);
		break;
	case kIntroRoom:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("INTRO", "IN", "")),
			     makeIntroHandler(), id);
		break;
	case kFerrymanPuzzle:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("FERRY", "FF", "")),
			     makeFerryHandler(), id);
		break;
	case kMonsterPuzzle:
		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("MONSTER", "MM", "")),
			     makeMonsterHandler(), id);
		break;
	default:
		debug("unknown room %d", id);
		assert(0);
		return;
	}

	_sceneHandler->prepareRoom();

	_persistent._roomVisited[id] = true;
}

int HadeschEngine::genSubtitleID() {
	return _subtitleID++;
}

int HadeschEngine::firstAvailableSlot() {
	for (unsigned slot = 3; ; slot++) {
		SaveStateDescriptor desc = getMetaEngine()->querySaveMetaInfos(_targetName.c_str(), slot);
		if (desc.getSaveSlot() == -1 && !desc.getWriteProtectedFlag())
			return slot;
	}
}

void HadeschEngine::quit() {
	_isQuitting = true;
}

bool HadeschEngine::hasAnySaves() {
	Common::SaveFileManager *saveFileMan = getSaveFileManager();
	Common::StringArray filenames;
	Common::String pattern(getMetaEngine()->getSavegameFilePattern(_targetName.c_str()));

	filenames = saveFileMan->listSavefiles(pattern);

	return !filenames.empty();
}

Common::Array<HadeschSaveDescriptor> HadeschEngine::getHadeschSavesList() {
	Common::SaveFileManager *saveFileMan = getSaveFileManager();
	Common::StringArray filenames;
	Common::String pattern(getMetaEngine()->getSavegameFilePattern(_targetName.c_str()));

	filenames = saveFileMan->listSavefiles(pattern);

	Common::Array<HadeschSaveDescriptor> saveList;
	for (const auto &file : filenames) {
		// Obtain the last 2 digits of the filename, since they correspond to the save slot
		int slotNum = atoi(file.c_str() + file.size() - 3);

		if (slotNum >= 0) {
			Common::ScopedPtr<Common::InSaveFile> in(saveFileMan->openForLoading(file));
			if (!in) {
				continue;
			}

			Common::Serializer s(in.get(), nullptr);

			saveList.push_back(HadeschSaveDescriptor(s, slotNum));
		}
	}

	// Sort saves based on slot number.
	// TODO: change it to chronological save id
	Common::sort(saveList.begin(), saveList.end(), HadeschSaveDescriptorSlotComparator());
	return saveList;
}

void HadeschEngine::deleteSave(int slot) {
	getMetaEngine()->removeSaveState(_targetName.c_str(), slot);
}

void EventHandlerWrapper::operator()() const {
  	if (_handler && _eventId == -1)
		debug("handling anon event");
	else if (_eventId != 20001 && _eventId != 14006 && _eventId != 15266)
		debug("handling event %d", _eventId);
	if (_handler)
		_handler->operator()();
	if (_eventId > 0)
		g_vm->getCurrentHandler()->handleEvent(_eventId);
}

bool EventHandlerWrapper::operator==(int b) const {
	return _eventId == b;
}

uint32 HadeschEngine::getSubtitleDelayPerChar() const {
	return _subtitleDelayPerChar;
}

} // End of namespace Hadesch
