/* ResidualVM - A 3D game interpreter
 *
 * ResidualVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the AUTHORS
 * 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 2
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include "common/debug-channels.h"
#include "common/events.h"
#include "common/error.h"
#include "common/config-manager.h"
#include "common/file.h"
#include "common/util.h"
#include "common/textconsole.h"
#include "common/translation.h"

#include "gui/debugger.h"
#include "gui/error.h"

#include "engines/engine.h"

#include "engines/myst3/archive.h"
#include "engines/myst3/console.h"
#include "engines/myst3/database.h"
#include "engines/myst3/effects.h"
#include "engines/myst3/myst3.h"
#include "engines/myst3/nodecube.h"
#include "engines/myst3/nodeframe.h"
#include "engines/myst3/scene.h"
#include "engines/myst3/state.h"
#include "engines/myst3/cursor.h"
#include "engines/myst3/inventory.h"
#include "engines/myst3/script.h"
#include "engines/myst3/menu.h"
#include "engines/myst3/movie.h"
#include "engines/myst3/sound.h"
#include "engines/myst3/ambient.h"
#include "engines/myst3/transition.h"

#include "image/jpeg.h"

#include "graphics/conversion.h"
#include "graphics/renderer.h"
#include "graphics/yuv_to_rgb.h"

#include "math/vector2d.h"

namespace Myst3 {

Myst3Engine::Myst3Engine(OSystem *syst, const Myst3GameDescription *version) :
		Engine(syst), _system(syst), _gameDescription(version),
		_db(0), _console(0), _scriptEngine(0),
		_state(0), _node(0), _scene(0), _archiveNode(0),
		_cursor(0), _inventory(0), _gfx(0), _menu(0),
		_rnd(0), _sound(0), _ambient(0),
		_inputSpacePressed(false), _inputEnterPressed(false),
		_inputEscapePressed(false), _inputTildePressed(false),
		_inputEscapePressedNotConsumed(false),
		_interactive(false),
		_menuAction(0), _projectorBackground(0),
		_shakeEffect(0), _rotationEffect(0), _backgroundSoundScriptLastRoomId(0),
		_transition(0), _frameLimiter(0), _inventoryManualHide(false) {
	DebugMan.addDebugChannel(kDebugVariable, "Variable", "Track Variable Accesses");
	DebugMan.addDebugChannel(kDebugSaveLoad, "SaveLoad", "Track Save/Load Function");
	DebugMan.addDebugChannel(kDebugScript, "Script", "Track Script Execution");
	DebugMan.addDebugChannel(kDebugNode, "Node", "Track Node Changes");

	// Add subdirectories to the search path to allow running from a full HDD install
	const Common::FSNode gameDataDir(ConfMan.get("path"));
	SearchMan.addSubDirectoryMatching(gameDataDir, "bin");
	SearchMan.addSubDirectoryMatching(gameDataDir, "M3Data");
	SearchMan.addSubDirectoryMatching(gameDataDir, "M3Data/TEXT");
	SearchMan.addSubDirectoriesMatching(gameDataDir, "EXILE Disc ?/Data", true);

	// Win DVD version directories
	SearchMan.addSubDirectoryMatching(gameDataDir, "English");
	SearchMan.addSubDirectoryMatching(gameDataDir, "Data");

	// Mac DVD version directories
	SearchMan.addSubDirectoryMatching(gameDataDir, "Exile DVD");
	SearchMan.addSubDirectoryMatching(gameDataDir, "Exile DVD/data");

	// PS2 version directories
	SearchMan.addSubDirectoryMatching(gameDataDir, "GAMEDATA");
	SearchMan.addSubDirectoryMatching(gameDataDir, "GAMEDATA/WORLD");
	SearchMan.addSubDirectoryMatching(gameDataDir, "GAMEDATA/WORLD/SOUND");
	SearchMan.addSubDirectoryMatching(gameDataDir, "MYST3BIN");
	SearchMan.addSubDirectoryMatching(gameDataDir, "MYST3BIN/DISCS");
	SearchMan.addSubDirectoryMatching(gameDataDir, "MYST3BIN/DISCS/DATA");
	SearchMan.addSubDirectoryMatching(gameDataDir, "MYST3BIN/M3DATA");
	SearchMan.addSubDirectoryMatching(gameDataDir, "MYST3BIN/M3DATA/TEXT");
	SearchMan.addSubDirectoryMatching(gameDataDir, "MYST3BIN/M3DATA/TEXT/NTSC");
	SearchMan.addSubDirectoryMatching(gameDataDir, "MYST3BIN/M3DATA/TEXT/PAL");
}

Myst3Engine::~Myst3Engine() {
	DebugMan.clearAllDebugChannels();

	closeArchives();

	delete _menu;
	delete _inventory;
	delete _cursor;
	delete _scene;
	delete _archiveNode;
	delete _db;
	delete _scriptEngine;
	delete _console;
	delete _state;
	delete _rnd;
	delete _sound;
	delete _ambient;
	delete _frameLimiter;
	delete _gfx;
}

bool Myst3Engine::hasFeature(EngineFeature f) const {
	// The TinyGL renderer does not support arbitrary resolutions for now
	Common::String rendererConfig = ConfMan.get("renderer");
	Graphics::RendererType desiredRendererType = Graphics::parseRendererTypeCode(rendererConfig);
	Graphics::RendererType matchingRendererType = Graphics::getBestMatchingAvailableRendererType(desiredRendererType);
	bool softRenderer = matchingRendererType == Graphics::kRendererTypeTinyGL;

	return
		(f == kSupportsRTL) ||
		(f == kSupportsLoadingDuringRuntime) ||
		(f == kSupportsArbitraryResolutions && !softRenderer);
}

Common::Error Myst3Engine::run() {
	if (!checkDatafiles()) {
		// An error message has already been displayed
		return Common::kUserCanceled;
	}

	_gfx = createRenderer(_system);
	_gfx->init();
	_gfx->clear();

	_frameLimiter = new FrameLimiter(_system, ConfMan.getInt("engine_speed"));
	_sound = new Sound(this);
	_ambient = new Ambient(this);
	_rnd = new Common::RandomSource("sprint");
	_console = new Console(this);
	_scriptEngine = new Script(this);
	_db = new Database(getPlatform(), getGameLanguage(), getGameLocalizationType());
	_state = new GameState(getPlatform(), _db);
	_scene = new Scene(this);
	if (getPlatform() == Common::kPlatformXbox) {
		_menu = new AlbumMenu(this);
	} else {
		_menu = new PagingMenu(this);
	}
	_archiveNode = new Archive();

	_system->showMouse(false);

	openArchives();

	_cursor = new Cursor(this);
	_inventory = new Inventory(this);

	settingsInitDefaults();
	syncSoundSettings();

	// Init the font
	Graphics::Surface *font = loadTexture(1206);
	_gfx->initFont(font);
	font->free();
	delete font;

	if (ConfMan.hasKey("save_slot")) {
		// Load game from specified slot, if any
		loadGameState(ConfMan.getInt("save_slot"));
	} else {
		if (getPlatform() == Common::kPlatformXbox) {
			// Play the logo videos
			loadNode(1, 1101, 11);
		}

		// Game init script, loads the menu
		loadNode(1, 101, 1);
	}

	while (!shouldQuit()) {
		runNodeBackgroundScripts();
		processInput(true);
		updateCursor();

		if (_menuAction) {
			_menu->updateMainMenu(_menuAction);
			_menuAction = 0;
		}

		drawFrame();
	}

	unloadNode();

	_archiveNode->close();
	_gfx->freeFont();

	// Make sure the mouse is unlocked
	_system->lockMouse(false);

	return Common::kNoError;
}

bool Myst3Engine::addArchive(const Common::String &file, bool mandatory) {
	Archive *archive = new Archive();
	bool opened = archive->open(file.c_str(), 0);

	if (opened) {
		_archivesCommon.push_back(archive);
	} else {
		delete archive;
		if (mandatory)
			error("Unable to open archive %s", file.c_str());
	}

	return opened;
}

void Myst3Engine::openArchives() {
	// The language of the menus is always the same as the executable
	// The English CD version can only display English text
	// The non English CD versions can display their localized language and English
	// The DVD version can display 6 different languages

	Common::String menuLanguage;
	Common::String textLanguage;

	switch (getGameLanguage()) {
	case Common::NL_NLD:
		menuLanguage = "DUTCH";
		break;
	case Common::FR_FRA:
		menuLanguage = "FRENCH";
		break;
	case Common::DE_DEU:
		menuLanguage = "GERMAN";
		break;
	case Common::HE_ISR:
		menuLanguage = "HEBREW";
		break;
	case Common::IT_ITA:
		menuLanguage = "ITALIAN";
		break;
	case Common::ES_ESP:
		menuLanguage = "SPANISH";
		break;
	case Common::JA_JPN:
		menuLanguage = "JAPANESE";
		break;
	case Common::PL_POL:
		menuLanguage = "POLISH";
		break;
	case Common::EN_ANY:
	case Common::RU_RUS:
	default:
		menuLanguage = "ENGLISH";
		break;
	}

	if (getGameLocalizationType() == kLocMulti6) {
		switch (ConfMan.getInt("text_language")) {
		case kDutch:
			textLanguage = "DUTCH";
			break;
		case kFrench:
			textLanguage = "FRENCH";
			break;
		case kGerman:
			textLanguage = "GERMAN";
			break;
		case kItalian:
			textLanguage = "ITALIAN";
			break;
		case kSpanish:
			textLanguage = "SPANISH";
			break;
		case kEnglish:
		default:
			textLanguage = "ENGLISH";
			break;
		}
	} else if (getGameLanguage() == Common::HE_ISR) {
		textLanguage = "ENGLISH"; // The Hebrew version does not have a "HEBREW.m3t" file
	} else {
		if (getGameLocalizationType() == kLocMonolingual || ConfMan.getInt("text_language")) {
			textLanguage = menuLanguage;
		} else {
			textLanguage = "ENGLISH";
		}
	}

	if (getGameLocalizationType() != kLocMonolingual && getPlatform() != Common::kPlatformXbox && textLanguage == "ENGLISH") {
		textLanguage = "ENGLISHjp";
	}

	if (getPlatform() == Common::kPlatformXbox) {
		menuLanguage += "X";
		textLanguage += "X";
	}

	// Load all the override files in the search path
	Common::ArchiveMemberList overrides;
	SearchMan.listMatchingMembers(overrides, "*.m3o");
	for (Common::ArchiveMemberList::const_iterator it = overrides.begin(); it != overrides.end(); it++) {
		addArchive(it->get()->getName(), false);
	}

	addArchive(textLanguage + ".m3t", true);

	if (getGameLocalizationType() != kLocMonolingual || getPlatform() == Common::kPlatformXbox || getGameLanguage() == Common::HE_ISR) {
		addArchive(menuLanguage + ".m3u", true);
	}

	addArchive("RSRC.m3r", true);
}

void Myst3Engine::closeArchives() {
	for (uint i = 0; i < _archivesCommon.size(); i++)
		delete _archivesCommon[i];

	_archivesCommon.clear();
}

bool Myst3Engine::checkDatafiles() {
	if (!SearchMan.hasFile("OVER101.m3o")) {
		warning("Unable to open the update game archive 'OVER101.m3o'");
		static const char *updateMessage =
				_("This version of Myst III has not been updated with the latest official patch.\n"
						  "Please install the official update corresponding to your game's language.\n"
						  "The updates can be downloaded from:\n"
						  "http://www.residualvm.org/downloads/");
		warning("%s", updateMessage);
		GUI::displayErrorDialog(updateMessage);
		return false;
	}

	return true;
}

HotSpot *Myst3Engine::getHoveredHotspot(NodePtr nodeData, uint16 var) {
	_state->setHotspotHovered(false);
	_state->setHotspotActiveRect(0);

	if (_state->getViewType() == kCube) {
		float pitch, heading;
		_cursor->getDirection(pitch, heading);

		for (uint j = 0; j < nodeData->hotspots.size(); j++) {
			int32 hitRect = nodeData->hotspots[j].isPointInRectsCube(pitch, heading);
			if (hitRect >= 0 && nodeData->hotspots[j].isEnabled(_state, var)) {
				if (nodeData->hotspots[j].rects.size() > 1) {
					_state->setHotspotHovered(true);
					_state->setHotspotActiveRect(hitRect);
				}
				return &nodeData->hotspots[j];
			}
		}
	} else {
		// get the mouse position in original game window coordinates
		Common::Point mouse = _cursor->getPosition(false);
		mouse = _scene->scalePoint(mouse);

		for (uint j = 0; j < nodeData->hotspots.size(); j++) {
			int32 hitRect = nodeData->hotspots[j].isPointInRectsFrame(_state, mouse);
			if (hitRect >= 0 && nodeData->hotspots[j].isEnabled(_state, var)) {
				if (nodeData->hotspots[j].rects.size() > 1) {
					_state->setHotspotHovered(true);
					_state->setHotspotActiveRect(hitRect);
				}
				return &nodeData->hotspots[j];
			}
		}
	}

	return 0;
}

void Myst3Engine::updateCursor() {
	if (!_inventory->isMouseInside()) {
		_inventoryManualHide = false;
	}

	if (isInventoryVisible() && _inventory->isMouseInside()) {
		_inventory->updateCursor();
		return;
	}

	NodePtr nodeData = _db->getNodeData(_state->getLocationNode(), _state->getLocationRoom(), _state->getLocationAge());

	_state->setHotspotIgnoreClick(true);
	HotSpot *hovered = getHoveredHotspot(nodeData);
	_state->setHotspotIgnoreClick(false);

	if (hovered) {
		_cursor->changeCursor(hovered->cursor);
	} else {
		_cursor->changeCursor(8);
	}
}

void Myst3Engine::processInput(bool interactive) {
	_interactive = interactive;

	if (_state->hasVarGamePadUpPressed()) {
		// Reset the gamepad directions once they had a chance to be read by the scripts
		// This combined with keyboard repeat ensures the menu does not scroll too fast
		_state->setGamePadUpPressed(false);
		_state->setGamePadDownPressed(false);
		_state->setGamePadLeftPressed(false);
		_state->setGamePadRightPressed(false);
	}

	bool shouldInteractWithHoveredElement = false;

	// Process events
	Common::Event event;
	while (getEventManager()->pollEvent(event)) {
		if (_state->hasVarGamePadUpPressed()) {
			processEventForGamepad(event);
		}

		if (event.type == Common::EVENT_MOUSEMOVE) {
			if (_state->getViewType() == kCube
					&& _cursor->isPositionLocked()) {
				_scene->updateCamera(event.relMouse);
			}

			_cursor->updatePosition(event.mouse);

		} else if (event.type == Common::EVENT_LBUTTONDOWN) {
			shouldInteractWithHoveredElement = true;
		} else if (event.type == Common::EVENT_RBUTTONDOWN) {
			// Skip the event when in non-interactive mode
			if (!interactive)
				continue;
			// Nothing to do if not in cube view
			if (_state->getViewType() != kCube)
				continue;
			// Don't unlock if the cursor is transparent
			if (!_state->getCursorTransparency())
				continue;

			bool cursorLocked = _cursor->isPositionLocked();
			_cursor->lockPosition(!cursorLocked);

		} else if (event.type == Common::EVENT_KEYDOWN) {
			// Save file name input
			if (_menu->handleInput(event.kbd)) {
				continue;
			}

			switch (event.kbd.keycode) {
			case Common::KEYCODE_ESCAPE:
				_inputEscapePressed = true;
				_inputEscapePressedNotConsumed = true;
				break;
			case Common::KEYCODE_RETURN:
			case Common::KEYCODE_KP_ENTER:
				if (event.kbd.hasFlags(Common::KBD_ALT)) {
					_gfx->toggleFullscreen();
				} else {
					_inputEnterPressed = true;
					shouldInteractWithHoveredElement = true;
				}
				break;
			case Common::KEYCODE_SPACE:
				_inputSpacePressed = true;
				break;
			case Common::KEYCODE_BACKQUOTE: // tilde, used to trigger the easter eggs
				_inputTildePressed = true;
				break;
			case Common::KEYCODE_F5:
				// Open main menu
				if (_cursor->isVisible() && interactive) {
					if (_state->getLocationRoom() != 901)
						_menu->goToNode(100);
				}
				break;
			case Common::KEYCODE_d:
				if (event.kbd.flags & Common::KBD_CTRL) {
					_console->attach();
					_console->onFrame();
				}
				break;
			default:
				break;
			}
		} else if (event.type == Common::EVENT_KEYUP) {
			switch (event.kbd.keycode) {
			case Common::KEYCODE_ESCAPE:
				_inputEscapePressed = false;
				_inputEscapePressedNotConsumed = false;
				break;
			case Common::KEYCODE_RETURN:
			case Common::KEYCODE_KP_ENTER:
				_inputEnterPressed = false;
				break;
			case Common::KEYCODE_SPACE:
				_inputSpacePressed = false;
				break;
			case Common::KEYCODE_BACKQUOTE:
				_inputTildePressed = false;
				break;
			default:
				break;
			}
		} else if (event.type == Common::EVENT_SCREEN_CHANGED) {
			_gfx->computeScreenViewport();
			_cursor->updatePosition(_eventMan->getMousePos());
			_inventory->reflow();
		}
	}

	// The input state variables need to be set before calling the scripts
	updateInputState();

	if (shouldInteractWithHoveredElement && interactive) {
		interactWithHoveredElement();
	}

	// Open main menu
	// This is not checked directly in the event handling code
	// because menu open requests done while in lookOnly mode
	// need to be honored after leaving the inner script loop,
	// especially when the script loop was cancelled due to pressing
	// escape.
	if (_inputEscapePressedNotConsumed && interactive) {
		_inputEscapePressedNotConsumed = false;
		if (_cursor->isVisible() && _state->hasVarMenuEscapePressed()) {
			if (_state->getLocationRoom() != 901)
				_menu->goToNode(100);
			else
				_state->setMenuEscapePressed(1);
		}
	}
}

void Myst3Engine::processEventForGamepad(const Common::Event &event) {
	if (event.type == Common::EVENT_LBUTTONDOWN) {
		_state->setGamePadActionPressed(true);
	} else if (event.type == Common::EVENT_LBUTTONUP) {
		_state->setGamePadActionPressed(false);
	} else if (event.type == Common::EVENT_KEYDOWN) {
		switch (event.kbd.keycode) {
		case Common::KEYCODE_RETURN:
		case Common::KEYCODE_KP_ENTER:
			_state->setGamePadActionPressed(true);
			break;
		case Common::KEYCODE_UP:
			_state->setGamePadUpPressed(true);
			break;
		case Common::KEYCODE_DOWN:
			_state->setGamePadDownPressed(true);
			break;
		case Common::KEYCODE_LEFT:
			_state->setGamePadLeftPressed(true);
			break;
		case Common::KEYCODE_RIGHT:
			_state->setGamePadRightPressed(true);
			break;
		case Common::KEYCODE_ESCAPE:
			_state->setGamePadCancelPressed(true);
			break;
		default:
			break;
		}
	} else if (event.type == Common::EVENT_KEYUP) {
		switch (event.kbd.keycode) {
		case Common::KEYCODE_RETURN:
		case Common::KEYCODE_KP_ENTER:
			_state->setGamePadActionPressed(false);
			break;
		case Common::KEYCODE_ESCAPE:
			_state->setGamePadCancelPressed(false);
			break;
		default:
			break;
		}
	}
}

void Myst3Engine::updateInputState() {
	_state->setInputMousePressed(inputValidatePressed());
	_state->setInputTildePressed(_inputTildePressed);
	_state->setInputSpacePressed(_inputSpacePressed);
	_state->setInputEscapePressed(_inputEscapePressed);
}

void Myst3Engine::resetInput() {
	_inputSpacePressed = false;
	_inputEnterPressed = false;
	_inputEscapePressed = false;
	_inputEscapePressedNotConsumed = false;
	_inputTildePressed = false;

	updateInputState();

	if (_state->hasVarGamePadUpPressed()) {
		_state->setGamePadUpPressed(false);
		_state->setGamePadDownPressed(false);
		_state->setGamePadLeftPressed(false);
		_state->setGamePadRightPressed(false);
		_state->setGamePadActionPressed(false);
		_state->setGamePadCancelPressed(false);
	}
}

void Myst3Engine::interactWithHoveredElement() {
	if (isInventoryVisible() && _inventory->isMouseInside()) {
		uint16 hoveredInventory = _inventory->hoveredItem();
		if (hoveredInventory > 0) {
			_inventory->useItem(hoveredInventory);
		} else {
			if (isWideScreenModEnabled()) {
				_inventoryManualHide = true;
			}
		}
		return;
	}

	NodePtr nodeData = _db->getNodeData(_state->getLocationNode(), _state->getLocationRoom(), _state->getLocationAge());
	HotSpot *hovered = getHoveredHotspot(nodeData);

	if (hovered) {
		_scriptEngine->run(&hovered->script);
		return;
	}

	// Bad click
	_sound->playEffect(697, 5);
}

void Myst3Engine::drawFrame(bool noSwap) {
	_sound->update();
	_gfx->clear();

	if (_state->getViewType() == kCube) {
		float pitch = _state->getLookAtPitch();
		float heading = _state->getLookAtHeading();
		float fov = _state->getLookAtFOV();

		// Apply the rotation effect
		if (_rotationEffect) {
			_rotationEffect->update();

			heading += _rotationEffect->getHeadingOffset();
			_state->lookAt(pitch, heading);
		}

		// Apply the shake effect
		if (_shakeEffect) {
			_shakeEffect->update();
			pitch += _shakeEffect->getPitchOffset();
			heading += _shakeEffect->getHeadingOffset();
		}

		_gfx->setupCameraPerspective(pitch, heading, fov);
	}

	if (_node) {
		_node->update();
		_gfx->renderDrawable(_node, _scene);
	}

	for (int i = _movies.size() - 1; i >= 0 ; i--) {
		_movies[i]->update();
		_gfx->renderDrawable(_movies[i], _scene);
	}

	if (_state->getViewType() == kMenu) {
		_gfx->renderDrawable(_menu, _scene);
	}

	for (uint i = 0; i < _drawables.size(); i++) {
		_gfx->renderDrawable(_drawables[i], _scene);
	}

	if (_state->getViewType() != kMenu) {
		float pitch = _state->getLookAtPitch();
		float heading = _state->getLookAtHeading();
		SunSpot flare = computeSunspotsIntensity(pitch, heading);
		if (flare.intensity >= 0)
			_scene->drawSunspotFlare(flare);
	}

	if (isInventoryVisible()) {
		_gfx->renderWindow(_inventory);
	}

	// Draw overlay 2D movies
	for (int i = _movies.size() - 1; i >= 0 ; i--) {
		_gfx->renderDrawableOverlay(_movies[i], _scene);
	}

	for (uint i = 0; i < _drawables.size(); i++) {
		_gfx->renderDrawableOverlay(_drawables[i], _scene);
	}

	// Draw spot subtitles
	if (_node) {
		_gfx->renderDrawableOverlay(_node, _scene);
	}

	bool cursorVisible = _cursor->isVisible();

	if (getPlatform() == Common::kPlatformXbox) {
		// The cursor is not drawn in the Xbox version menus and journals
		cursorVisible &= !(_state->getLocationRoom() == 901 || _state->getLocationRoom() == 902);
	}

	if (cursorVisible)
		_gfx->renderDrawable(_cursor, _scene);

	_gfx->flipBuffer();

	if (!noSwap) {
		_frameLimiter->delayBeforeSwap();
		_system->updateScreen();
		_state->updateFrameCounters();
		_frameLimiter->startFrame();
	}
}

bool Myst3Engine::isInventoryVisible() {
	if (_state->getViewType() == kMenu)
		return false;

	if (_node && _node->hasSubtitlesToDraw())
		return false;

	if (_inventoryManualHide) {
		return false;
	}

	// Only draw the inventory when the mouse is inside its area
	if (isWideScreenModEnabled() && !_inventory->isMouseInside()) {
		return false;
	}

	return true;
}

void Myst3Engine::setupTransition() {
	delete _transition;
	_transition = new Transition(this);
}

void Myst3Engine::drawTransition(TransitionType transitionType) {
	if (_transition) {
		_interactive = false; // Don't allow loading while drawing transitions
		_transition->draw(transitionType);
		delete _transition;
		_transition = nullptr;
	}
}

void Myst3Engine::goToNode(uint16 nodeID, TransitionType transitionType) {
	uint16 node = _state->getLocationNextNode();
	if (node == 0)
		node = nodeID;

	uint16 room = _state->getLocationNextRoom();
	uint16 age = _state->getLocationNextAge();

	setupTransition();

	ViewType sourceViewType = _state->getViewType();
	if (sourceViewType == kCube) {
		// The lookat direction in the next node should be
		// the direction of the mouse cursor
		float pitch, heading;
		_cursor->getDirection(pitch, heading);
		_state->lookAt(pitch, heading);
	}

	loadNode(node, room, age);

	_state->setLocationNextNode(0);
	_state->setLocationNextRoom(0);
	_state->setLocationNextAge(0);

	if (_state->getAmbiantPreviousFadeOutDelay() > 0) {
		_ambient->playCurrentNode(100, _state->getAmbiantPreviousFadeOutDelay());
	}

	drawTransition(transitionType);
}

void Myst3Engine::loadNode(uint16 nodeID, uint32 roomID, uint32 ageID) {
	unloadNode();

	_scriptEngine->run(&_db->getNodeInitScript());

	if (nodeID)
		_state->setLocationNode(_state->valueOrVarValue(nodeID));

	if (roomID)
		_state->setLocationRoom(_state->valueOrVarValue(roomID));
	else
		roomID = _state->getLocationRoom();

	if (ageID)
		_state->setLocationAge(_state->valueOrVarValue(ageID));
	else
		ageID = _state->getLocationAge();

	_db->cacheRoom(roomID, ageID);

	Common::String newRoomName = _db->getRoomName(roomID, ageID);
	if ((!_archiveNode || _archiveNode->getRoomName() != newRoomName) && !_db->isCommonRoom(roomID, ageID)) {

		Common::String nodeFile = Common::String::format("%snodes.m3a", newRoomName.c_str());

		_archiveNode->close();
		if (!_archiveNode->open(nodeFile.c_str(), newRoomName.c_str())) {
			error("Unable to open archive %s", nodeFile.c_str());
		}
	}

	runNodeInitScripts();

	// These effects can only be created after running the node init scripts
	_shakeEffect = ShakeEffect::create(this);
	_rotationEffect = RotationEffect::create(this);

	// WORKAROUND: In Narayan, the scripts in node NACH 9 test on var 39
	// without first reinitializing it leading to Saavedro not always giving
	// Releeshan to the player when he is trapped between both shields.
	if (nodeID == 9 && roomID == 801)
		_state->setVar(39, 0);
}

void Myst3Engine::unloadNode() {
	if (!_node)
		return;

	// Delete all movies
	removeMovie(0);

	// Remove all sunspots
	for (uint i = 0; i < _sunspots.size(); i++)
		delete _sunspots[i];

	_sunspots.clear();

	// Clean up the effects
	delete _shakeEffect;
	_shakeEffect = nullptr;
	_state->setShakeEffectAmpl(0);
	delete _rotationEffect;
	_rotationEffect = nullptr;

	delete _node;
	_node = nullptr;
}

void Myst3Engine::runNodeInitScripts() {
	NodePtr nodeData = _db->getNodeData(
			_state->getLocationNode(),
			_state->getLocationRoom(),
			_state->getLocationAge());

	NodePtr nodeDataInit = _db->getNodeData(
			32765,
			_state->getLocationRoom(),
			_state->getLocationAge());

	if (nodeDataInit)
		runScriptsFromNode(32765);

	if (!nodeData)
		error("Node %d unknown in the database", _state->getLocationNode());

	for (uint j = 0; j < nodeData->scripts.size(); j++) {
		if (_state->evaluate(nodeData->scripts[j].condition)) {
			_scriptEngine->run(&nodeData->scripts[j].script);
		}
	}

	// Mark the node as a reachable zip destination
	_state->markNodeAsVisited(
			_state->getLocationNode(),
			_state->getLocationRoom(),
			_state->getLocationAge());
}

void Myst3Engine::runNodeBackgroundScripts() {
	NodePtr nodeDataRoom = _db->getNodeData(32765, _state->getLocationRoom(), _state->getLocationAge());

	if (nodeDataRoom) {
		for (uint j = 0; j < nodeDataRoom->hotspots.size(); j++) {
			if (nodeDataRoom->hotspots[j].condition == -1) {
				if (!_scriptEngine->run(&nodeDataRoom->hotspots[j].script))
					break;
			}
		}
	}

	NodePtr nodeData = _db->getNodeData(_state->getLocationNode(), _state->getLocationRoom(), _state->getLocationAge());

	for (uint j = 0; j < nodeData->hotspots.size(); j++) {
		if (nodeData->hotspots[j].condition == -1) {
			if (!_scriptEngine->run(&nodeData->hotspots[j].script))
				break;
		}
	}
}

void Myst3Engine::loadNodeCubeFaces(uint16 nodeID) {
	_state->setViewType(kCube);

	_cursor->lockPosition(true);
	updateCursor();

	_node = new NodeCube(this, nodeID);
}

void Myst3Engine::loadNodeFrame(uint16 nodeID) {
	_state->setViewType(kFrame);

	_cursor->lockPosition(false);
	updateCursor();

	_node = new NodeFrame(this, nodeID);
}

void Myst3Engine::loadNodeMenu(uint16 nodeID) {
	_state->setViewType(kMenu);

	_cursor->lockPosition(false);
	updateCursor();

	_node = new NodeFrame(this, nodeID);
}

void Myst3Engine::runScriptsFromNode(uint16 nodeID, uint32 roomID, uint32 ageID) {
	if (roomID == 0)
		roomID = _state->getLocationRoom();

	if (ageID == 0)
		ageID = _state->getLocationAge();

	NodePtr nodeData = _db->getNodeData(nodeID, roomID, ageID);

	for (uint j = 0; j < nodeData->scripts.size(); j++) {
		if (_state->evaluate(nodeData->scripts[j].condition)) {
			if (!_scriptEngine->run(&nodeData->scripts[j].script))
				break;
		}
	}
}

void Myst3Engine::runBackgroundSoundScriptsFromNode(uint16 nodeID, uint32 roomID, uint32 ageID) {
	if (_state->getSoundScriptsSuspended())
		return;

	if (roomID == 0)
		roomID = _state->getLocationRoom();

	if (ageID == 0)
		ageID = _state->getLocationAge();

	NodePtr nodeData = _db->getNodeData(nodeID, roomID, ageID);

	if (!nodeData) return;

	// Stop previous music when changing room
	if (_backgroundSoundScriptLastRoomId != roomID) {
		if (_backgroundSoundScriptLastRoomId != 0
				&& _backgroundSoundScriptLastRoomId != 901
				&& _backgroundSoundScriptLastRoomId != 902
				&& roomID != 0 && roomID != 901 && roomID != 902) {
			_sound->stopMusic(_state->getSoundScriptFadeOutDelay());

			if (nodeData->backgroundSoundScripts.size() != 0) {
				_state->setSoundScriptsPaused(1);
				_state->setSoundScriptsTimer(0);
			}
		}

		_backgroundSoundScriptLastRoomId = roomID;
	}


	for (uint j = 0; j < nodeData->backgroundSoundScripts.size(); j++) {
		if (_state->evaluate(nodeData->backgroundSoundScripts[j].condition)) {
			if (!_scriptEngine->run(&nodeData->backgroundSoundScripts[j].script))
				break;
		}
	}
}

void Myst3Engine::runAmbientScripts(uint32 node) {
	uint32 room = _ambient->_scriptRoom;
	uint32 age = _ambient->_scriptAge;

	if (room == 0)
		room = _state->getLocationRoom();

	if (age == 0)
		age = _state->getLocationAge();

	NodePtr nodeData = _db->getNodeData(node, room, age);

	if (!nodeData) return;

	for (uint j = 0; j < nodeData->soundScripts.size(); j++)
		if (_state->evaluate(nodeData->soundScripts[j].condition))
			_scriptEngine->run(&nodeData->soundScripts[j].script);
}

void Myst3Engine::loadMovie(uint16 id, uint16 condition, bool resetCond, bool loop) {
	ScriptedMovie *movie;

	if (!_state->getMovieUseBackground()) {
		movie = new ScriptedMovie(this, id);
	} else {
		movie = new ProjectorMovie(this, id, _projectorBackground);
		_projectorBackground = 0;
		_state->setMovieUseBackground(0);
	}

	movie->setCondition(condition);
	movie->setDisableWhenComplete(resetCond);
	movie->setLoop(loop);

	if (_state->getMovieScriptDriven()) {
		movie->setScriptDriven(_state->getMovieScriptDriven());
		_state->setMovieScriptDriven(0);
	}

	if (_state->getMovieStartFrameVar()) {
		movie->setStartFrameVar(_state->getMovieStartFrameVar());
		_state->setMovieStartFrameVar(0);
	}

	if (_state->getMovieEndFrameVar()) {
		movie->setEndFrameVar(_state->getMovieEndFrameVar());
		_state->setMovieEndFrameVar(0);
	}

	if (_state->getMovieStartFrame()) {
		movie->setStartFrame(_state->getMovieStartFrame());
		_state->setMovieStartFrame(0);
	}

	if (_state->getMovieEndFrame()) {
		movie->setEndFrame(_state->getMovieEndFrame());
		_state->setMovieEndFrame(0);
	}

	if (_state->getMovieNextFrameGetVar()) {
		movie->setNextFrameReadVar(_state->getMovieNextFrameGetVar());
		_state->setMovieNextFrameGetVar(0);
	}

	if (_state->getMovieNextFrameSetVar()) {
		movie->setNextFrameWriteVar(_state->getMovieNextFrameSetVar());
		_state->setMovieNextFrameSetVar(0);
	}

	if (_state->getMoviePlayingVar()) {
		movie->setPlayingVar(_state->getMoviePlayingVar());
		_state->setMoviePlayingVar(0);
	}

	if (_state->getMovieOverridePosition()) {
		movie->setPosU(_state->getMovieOverridePosU());
		movie->setPosV(_state->getMovieOverridePosV());
		_state->setMovieOverridePosition(0);
	}

	if (_state->getMovieUVar()) {
		movie->setPosUVar(_state->getMovieUVar());
		_state->setMovieUVar(0);
	}

	if (_state->getMovieVVar()) {
		movie->setPosVVar(_state->getMovieVVar());
		_state->setMovieVVar(0);
	}

	if (_state->getMovieOverrideCondition()) {
		movie->setCondition(_state->getMovieOverrideCondition());
		_state->setMovieOverrideCondition(0);
	}

	if (_state->getMovieConditionBit()) {
		movie->setConditionBit(_state->getMovieConditionBit());
		_state->setMovieConditionBit(0);
	}

	if (_state->getMovieForce2d()) {
		movie->setForce2d(_state->getMovieForce2d());
		_state->setMovieForce2d(0);
	}

	if (_state->getMovieVolume1()) {
		movie->setVolume(_state->getMovieVolume1());
		_state->setMovieVolume1(0);
	} else {
		movie->setVolume(_state->getMovieVolume2());
	}

	if (_state->getMovieVolumeVar()) {
		movie->setVolumeVar(_state->getMovieVolumeVar());
		_state->setMovieVolumeVar(0);
	}

	if (_state->getMovieSoundHeading()) {
		movie->setSoundHeading(_state->getMovieSoundHeading());
		_state->setMovieSoundHeading(0);
	}

	if (_state->getMoviePanningStrenght()) {
		movie->setSoundAttenuation(_state->getMoviePanningStrenght());
		_state->setMoviePanningStrenght(0);
	}

	if (_state->getMovieAdditiveBlending()) {
		movie->setAdditiveBlending(true);
		_state->setMovieAdditiveBlending(0);
	}

	if (_state->getMovieTransparency()) {
		movie->setTransparency(_state->getMovieTransparency());
		_state->setMovieTransparency(0);
	} else {
		movie->setTransparency(100);
	}

	if (_state->getMovieTransparencyVar()) {
		movie->setTransparencyVar(_state->getMovieTransparencyVar());
		_state->setMovieTransparencyVar(0);
	}

	_movies.push_back(movie);
}

void Myst3Engine::playSimpleMovie(uint16 id, bool fullframe, bool refreshAmbientSounds) {
	SimpleMovie movie(this, id);

	if (!movie.isVideoLoaded()) {
		// The video was not loaded and it was optional, just do nothing
		return;
	}

	if (_state->getMovieSynchronized()) {
		movie.setSynchronized(_state->getMovieSynchronized());
		_state->setMovieSynchronized(0);
	}

	if (_state->getMovieStartFrame()) {
		movie.setStartFrame(_state->getMovieStartFrame());
		_state->setMovieStartFrame(0);
	}

	if (_state->getMovieEndFrame()) {
		movie.setEndFrame(_state->getMovieEndFrame());
		_state->setMovieEndFrame(0);
	}

	if (_state->getMovieVolume1()) {
		movie.setVolume(_state->getMovieVolume1());
		_state->setMovieVolume1(0);
	} else {
		movie.setVolume(_state->getMovieVolume2());
	}

	if (fullframe) {
		movie.setForce2d(_state->getViewType() == kCube);
		movie.setForceOpaque(true);
		movie.setPosU(0);
		movie.setPosV(_state->getViewType() == kMenu ? Renderer::kTopBorderHeight : 0);
	}

	movie.play();

	if (refreshAmbientSounds) {
		movie.refreshAmbientSounds();
	}

	_drawables.push_back(&movie);

	while (!shouldQuit() && !movie.endOfVideo()) {
		movie.update();

		// Process events
		processInput(false);

		// Handle skipping
		if (_inputSpacePressed || _inputEscapePressed) {
			// Consume the escape key press so the menu does not open
			_inputEscapePressedNotConsumed = false;
			break;
		}

		drawFrame();
	}

	_drawables.pop_back();

	// Reset the movie script so that the next movie will not try to run them
	// when the user has skipped this one before the script is triggered.
	_state->setMovieScriptStartFrame(0);
	_state->setMovieScript(0);
	_state->setMovieAmbiantScriptStartFrame(0);
	_state->setMovieAmbiantScript(0);
}

void Myst3Engine::removeMovie(uint16 id) {
	if (id == 0) {
		for (uint i = 0; i < _movies.size(); i++)
			delete _movies[i];

		_movies.clear();
		return;
	} else {
		for (uint i = 0; i < _movies.size(); i++)
			if (_movies[i]->getId() == id) {
				delete _movies[i];
				_movies.remove_at(i);
				break;
			}
	}
}

void Myst3Engine::setMovieLooping(uint16 id, bool loop) {
	for (uint i = 0; i < _movies.size(); i++) {
		if (_movies[i]->getId() == id) {
			// Enable or disable looping
			_movies[i]->setLoop(loop);
			_movies[i]->setDisableWhenComplete(!loop);
			break;
		}
	}
}

void Myst3Engine::addSpotItem(uint16 id, int16 condition, bool fade) {
	assert(_node);

	_node->loadSpotItem(id, condition, fade);
}

SpotItemFace *Myst3Engine::addMenuSpotItem(uint16 id, int16 condition, const Common::Rect &rect) {
	assert(_node);

	SpotItemFace *face = _node->loadMenuSpotItem(condition, rect);

	_menu->setSaveLoadSpotItem(id, face);

	return face;
}

void Myst3Engine::loadNodeSubtitles(uint32 id) {
	assert(_node);

	_node->loadSubtitles(id);
}

const DirectorySubEntry *Myst3Engine::getFileDescription(const Common::String &room, uint32 index, uint16 face,
                                                         DirectorySubEntry::ResourceType type) {
	Common::String archiveRoom = room;
	if (archiveRoom == "") {
		archiveRoom = _db->getRoomName(_state->getLocationRoom(), _state->getLocationAge());
	}

	const DirectorySubEntry *desc = 0;

	// Search common archives
	uint i = 0;
	while (!desc && i < _archivesCommon.size()) {
		desc = _archivesCommon[i]->getDescription(archiveRoom, index, face, type);
		i++;
	}

	// Search currently loaded node archive
	if (!desc && _archiveNode)
		desc = _archiveNode->getDescription(archiveRoom, index, face, type);

	return desc;
}

DirectorySubEntryList Myst3Engine::listFilesMatching(const Common::String &room, uint32 index, uint16 face,
                                                     DirectorySubEntry::ResourceType type) {
	Common::String archiveRoom = room;
	if (archiveRoom == "") {
		archiveRoom = _db->getRoomName(_state->getLocationRoom(), _state->getLocationAge());
	}

	for (uint i = 0; i < _archivesCommon.size(); i++) {
		DirectorySubEntryList list = _archivesCommon[i]->listFilesMatching(archiveRoom, index, face, type);
		if (!list.empty()) {
			return list;
		}
	}

	return _archiveNode->listFilesMatching(archiveRoom, index, face, type);
}

Graphics::Surface *Myst3Engine::loadTexture(uint16 id) {
	const DirectorySubEntry *desc = getFileDescription("GLOB", id, 0, DirectorySubEntry::kRawData);

	if (!desc)
		error("Texture %d does not exist", id);

	Common::MemoryReadStream *data = desc->getData();

	uint32 magic = data->readUint32LE();
	if (magic != MKTAG('.', 'T', 'E', 'X'))
		error("Wrong texture format %d", id);

	data->readUint32LE(); // unk 1
	uint32 width = data->readUint32LE();
	uint32 height = data->readUint32LE();
	data->readUint32LE(); // unk 2
	data->readUint32LE(); // unk 3

#ifdef SCUMM_BIG_ENDIAN
	Graphics::PixelFormat onDiskFormat = Graphics::PixelFormat(4, 8, 8, 8, 8, 0, 24, 16, 8);
#else
	Graphics::PixelFormat onDiskFormat = Graphics::PixelFormat(4, 8, 8, 8, 8, 8, 16, 24, 0);
#endif

	Graphics::Surface *s = new Graphics::Surface();
	s->create(width, height, onDiskFormat);

	data->read(s->getPixels(), height * s->pitch);
	delete data;

	s->convertToInPlace(Texture::getRGBAPixelFormat());

	return s;
}

Graphics::Surface *Myst3Engine::decodeJpeg(const DirectorySubEntry *jpegDesc) {
	Common::MemoryReadStream *jpegStream = jpegDesc->getData();

	Image::JPEGDecoder jpeg;
	if (!jpeg.loadStream(*jpegStream))
		error("Could not decode Myst III JPEG");
	delete jpegStream;

	const Graphics::Surface *bitmap = jpeg.getSurface();
	assert(bitmap->format == Graphics::PixelFormat(4, 8, 8, 8, 0, 24, 16, 8, 0));

	// Convert the surface to RGBA if needed.
	// RGBA being the order of the bytes in memory, not in a 32 bits word.
	Graphics::PixelFormat rgbaFormat = Texture::getRGBAPixelFormat();
	Graphics::Surface *rgbaSurface = new Graphics::Surface();

#ifdef SCUMM_BIG_ENDIAN
	assert(rgbaFormat == Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0));
	rgbaSurface->copyFrom(*bitmap);
	rgbaSurface->format = rgbaFormat;
#else
	assert(rgbaFormat == Graphics::PixelFormat(4, 8, 8, 8, 8, 0, 8, 16, 24));

	rgbaSurface->create(bitmap->w, bitmap->h, rgbaFormat);

	// Use SWAP_BYTES_32 as an optimization. This path is hot.
	for (uint y = 0; y < bitmap->h; y++) {
		const uint32 *srcRow = (const uint32 *) bitmap->getBasePtr(0, y);
		uint32 *dstRow = (uint32 *) rgbaSurface->getBasePtr(0, y);
		for (uint x = 0; x < bitmap->w; x++) {
			*dstRow++ = SWAP_BYTES_32(*srcRow++);
		}
	}
#endif

	return rgbaSurface;
}

int16 Myst3Engine::openDialog(uint16 id) {
	Dialog *dialog;

	if (getPlatform() == Common::kPlatformXbox) {
		dialog = new GamepadDialog(this, id);
	} else {
		dialog = new ButtonsDialog(this, id);
	}

	_drawables.push_back(dialog);

	int16 result = -2;

	while (result == -2 && !shouldQuit()) {
		result = dialog->update();
		drawFrame();
	}

	_drawables.pop_back();

	delete dialog;

	return result;
}

void Myst3Engine::dragSymbol(uint16 var, uint16 id) {
	DragItem drag(this, id);

	_drawables.push_back(&drag);

	_cursor->changeCursor(2);
	_state->setVar(var, -1);

	NodePtr nodeData = _db->getNodeData(_state->getLocationNode(), _state->getLocationRoom(), _state->getLocationAge());

	while (inputValidatePressed() && !shouldQuit()) {
		processInput(false);

		HotSpot *hovered = getHoveredHotspot(nodeData, var);
		drag.setFrame(hovered ? 2 : 1);

		drawFrame();
	}

	_state->setVar(var, 1);
	_drawables.pop_back();

	HotSpot *hovered = getHoveredHotspot(nodeData, var);
	if (hovered) {
		_cursor->setVisible(false);
		_scriptEngine->run(&hovered->script);
		_cursor->setVisible(true);
	}
}

void Myst3Engine::dragItem(uint16 statusVar, uint16 movie, uint16 frame, uint16 hoverFrame, uint16 itemVar) {
	DragItem drag(this, movie);

	_drawables.push_back(&drag);

	_cursor->changeCursor(2);
	_state->setVar(statusVar, 0);
	_state->setVar(itemVar, 1);

	NodePtr nodeData = _db->getNodeData(_state->getLocationNode(), _state->getLocationRoom(), _state->getLocationAge());

	while (inputValidatePressed() && !shouldQuit()) {
		processInput(false);

		HotSpot *hovered = getHoveredHotspot(nodeData, itemVar);
		drag.setFrame(hovered ? hoverFrame : frame);

		drawFrame();
	}

	_drawables.pop_back();

	HotSpot *hovered = getHoveredHotspot(nodeData, itemVar);
	if (hovered) {
		_cursor->setVisible(false);
		_scriptEngine->run(&hovered->script);
		_cursor->setVisible(true);
	} else {
		_state->setVar(statusVar, 1);
		_state->setVar(itemVar, 0);
	}
}

bool Myst3Engine::canLoadGameStateCurrently() {
	// Loading from the GMM is only possible when the game is interactive
	// This is to prevent loading from inner loops. Loading while
	// in an inner loop can cause the exit condition to never happen,
	// or can unload required resources.
	return _interactive;
}

Common::Error Myst3Engine::loadGameState(int slot) {
	Common::StringArray filenames = Saves::list(_saveFileMan, getPlatform());
	return loadGameState(filenames[slot], kTransitionNone);
}

Common::Error Myst3Engine::loadGameState(Common::String fileName, TransitionType transition) {
	Common::InSaveFile *saveFile = _saveFileMan->openForLoading(fileName);
	if (!saveFile) {
		return _saveFileMan->getError();
	}

	if (!_state->load(saveFile)) {
		delete saveFile;
		return Common::kUnknownError;
	}
	delete saveFile;

	_inventory->loadFromState();

	_state->setLocationNextAge(_state->getMenuSavedAge());
	_state->setLocationNextRoom(_state->getMenuSavedRoom());
	_state->setLocationNextNode(_state->getMenuSavedNode());
	_state->setMenuSavedAge(0);
	_state->setMenuSavedRoom(0);
	_state->setMenuSavedNode(0);

	_sound->stopMusic(15);
	_state->setSoundScriptsSuspended(0);
	_sound->playEffect(696, 60);

	goToNode(0, transition);
	return Common::kNoError;
}

void Myst3Engine::animateDirectionChange(float targetPitch, float targetHeading, uint16 scriptTicks) {
	float startPitch = _state->getLookAtPitch();
	float startHeading = _state->getLookAtHeading();

	if (startPitch == targetPitch && startHeading == targetHeading)
		return; // Fast path

	float pitchDistance = targetPitch - startPitch;
	float headingDistance = targetHeading - startHeading;

	// Make sure to use the shortest direction
	while (ABS(headingDistance) > 180) {
		if (headingDistance > 0) {
			headingDistance -= 360;
		} else {
			headingDistance += 360;
		}
	}

	// Compute animation duration in frames
	float numTicks;
	if (scriptTicks) {
		numTicks = scriptTicks;
	} else {
		numTicks = sqrt(pitchDistance * pitchDistance + headingDistance * headingDistance)
				* 30.0f / _state->getCameraMoveSpeed();

		if (numTicks > 0.0f)
			numTicks += 10.0f;
	}

	uint startTick = _state->getTickCount();

	// Draw animation
	if (numTicks != 0.0f) {
		while (1) {
			uint elapsedTicks = _state->getTickCount() - startTick;
			if (elapsedTicks >= numTicks || shouldQuit())
				break;

			float step;
			if (numTicks >= 15) {
				// Fast then slow movement
				if (elapsedTicks > numTicks / 2.0f)
					step = 1.0f - (numTicks - elapsedTicks) * (numTicks - elapsedTicks)
								/ (numTicks / 2.0f * numTicks / 2.0f) / 2.0f;
				else
					step = elapsedTicks * elapsedTicks / (numTicks / 2.0f * numTicks / 2.0f) / 2.0f;

			} else {
				// Constant speed movement
				step = elapsedTicks / numTicks;
			}

			float nextPitch = startPitch + pitchDistance * step;
			float nextHeading = startHeading + headingDistance * step;

			_state->lookAt(nextPitch, nextHeading);
			drawFrame();
		}
	}

	_state->lookAt(targetPitch, targetHeading);
	drawFrame();
}

void Myst3Engine::getMovieLookAt(uint16 id, bool start, float &pitch, float &heading) {
	const DirectorySubEntry *desc = getFileDescription("", id, 0, DirectorySubEntry::kMovie);

	if (!desc)
		desc = getFileDescription("", id, 0, DirectorySubEntry::kMultitrackMovie);

	if (!desc)
		error("Movie %d does not exist", id);

	Math::Vector3d v;
	if (start)
		v = desc->getVideoData().v1;
	else
		v = desc->getVideoData().v2;

	Math::Vector2d horizontalProjection(v.x(), v.z());
	horizontalProjection.normalize();

	pitch = 90 - Math::Angle::arcCosine(v.y()).getDegrees();
	heading = Math::Angle::arcCosine(horizontalProjection.getY()).getDegrees();

	if (horizontalProjection.getX() < 0.0) {
		heading = 360 - heading;
	}
}

void Myst3Engine::playMovieGoToNode(uint16 movie, uint16 node) {
	uint16 room = _state->getLocationNextRoom();
	uint16 age = _state->getLocationNextAge();

	if (_state->getLocationNextNode() != 0) {
		node = _state->getLocationNextNode();
	}

	if (_state->getViewType() == kCube && !_state->getCameraSkipAnimation()) {
		float startPitch, startHeading;
		getMovieLookAt(movie, true, startPitch, startHeading);
		animateDirectionChange(startPitch, startHeading, 0);
	}
	_state->setCameraSkipAnimation(0);

	loadNode(node, room, age);

	playSimpleMovie(movie, true, true);

	_state->setLocationNextNode(0);
	_state->setLocationNextRoom(0);
	_state->setLocationNextAge(0);

	if (_state->getViewType() == kCube) {
		float endPitch, endHeading;
		getMovieLookAt(movie, false, endPitch, endHeading);
		_state->lookAt(endPitch, endHeading);
	}

	setupTransition();
}

void Myst3Engine::playMovieFullFrame(uint16 movie) {
	if (_state->getViewType() == kCube && !_state->getCameraSkipAnimation()) {
		float startPitch, startHeading;
		getMovieLookAt(movie, true, startPitch, startHeading);
		animateDirectionChange(startPitch, startHeading, 0);
	}
	_state->setCameraSkipAnimation(0);

	playSimpleMovie(movie, true, false);

	if (_state->getViewType() == kCube) {
		float endPitch, endHeading;
		getMovieLookAt(movie, false, endPitch, endHeading);
		_state->lookAt(endPitch, endHeading);
	}

	setupTransition();
}

bool Myst3Engine::inputValidatePressed() {
	return _inputEnterPressed ||
			_inputSpacePressed ||
			getEventManager()->getButtonState() & Common::EventManager::LBUTTON;
}

bool Myst3Engine::inputEscapePressed() {
	return _inputEscapePressed;
}

bool Myst3Engine::inputSpacePressed() {
	return _inputSpacePressed;
}

bool Myst3Engine::inputTilePressed() {
	return _inputTildePressed;
}

void Myst3Engine::addSunSpot(uint16 pitch, uint16 heading, uint16 intensity,
		uint16 color, uint16 var, bool varControlledIntensity, uint16 radius) {

	SunSpot *s = new SunSpot();

	s->pitch = pitch;
	s->heading = heading;
	s->intensity = intensity * 2.55;
	s->color = (color & 0xF) | 16
			* ((color & 0xF) | 16
			* (((color >> 4) & 0xF) | 16
			* (((color >> 4) & 0xF) | 16
			* (((color >> 8) & 0xF) | 16
			* (((color >> 8) & 0xF))))));
	s->var = var;
	s->variableIntensity = varControlledIntensity;
	s->radius = radius;

	_sunspots.push_back(s);
}

SunSpot Myst3Engine::computeSunspotsIntensity(float pitch, float heading) {
	SunSpot result;
	result.intensity = -1;
	result.color = 0;
	result.radius = 0;

	for (uint i = 0; i < _sunspots.size(); i++) {
		SunSpot *s = _sunspots[i];

		uint32 value = _state->getVar(s->var);

		// Skip disabled items
		if (value == 0) continue;

		float distance = _scene->distanceToZone(s->heading, s->pitch, s->radius, heading, pitch);

		if (distance > result.radius) {
			result.radius = distance;
			result.color = s->color;
			result.intensity = s->intensity;
			result.variableIntensity = s->variableIntensity;

			if (result.variableIntensity) {
				result.radius = value * distance / 100;
			}
		}
	}

	return result;
}

void Myst3Engine::settingsInitDefaults() {
	int defaultLanguage = _db->getGameLanguageCode();

	int defaultTextLanguage;
	if (getGameLocalizationType() == kLocMulti6)
		defaultTextLanguage = defaultLanguage;
	else
		defaultTextLanguage = getGameLanguage() != Common::EN_ANY;

	ConfMan.registerDefault("overall_volume", Audio::Mixer::kMaxMixerVolume);
	ConfMan.registerDefault("music_volume", Audio::Mixer::kMaxMixerVolume / 2);
	ConfMan.registerDefault("music_frequency", 75);
	ConfMan.registerDefault("audio_language", defaultLanguage);
	ConfMan.registerDefault("text_language", defaultTextLanguage);
	ConfMan.registerDefault("water_effects", true);
	ConfMan.registerDefault("transition_speed", 50);
	ConfMan.registerDefault("mouse_speed", 50);
	ConfMan.registerDefault("zip_mode", false);
	ConfMan.registerDefault("subtitles", false);
	ConfMan.registerDefault("vibrations", true); // Xbox specific
}

void Myst3Engine::settingsLoadToVars() {
	_state->setWaterEffects(ConfMan.getBool("water_effects"));
	_state->setTransitionSpeed(ConfMan.getInt("transition_speed"));
	_state->setMouseSpeed(ConfMan.getInt("mouse_speed"));
	_state->setZipModeEnabled(ConfMan.getBool("zip_mode"));
	_state->setSubtitlesEnabled(ConfMan.getBool("subtitles"));

	if (getPlatform() != Common::kPlatformXbox) {
		_state->setOverallVolume(CLIP<uint>(ConfMan.getInt("overall_volume") * 100 / 256, 0, 100));
		_state->setMusicVolume(CLIP<uint>(ConfMan.getInt("music_volume") * 100 / 256, 0, 100));
		_state->setMusicFrequency(ConfMan.getInt("music_frequency"));
		_state->setLanguageAudio(ConfMan.getInt("audio_language"));
		_state->setLanguageText(ConfMan.getInt("text_language"));
	} else {
		_state->setVibrationEnabled(ConfMan.getBool("vibrations"));
	}
}

void Myst3Engine::settingsApplyFromVars() {
	int32 oldTextLanguage = ConfMan.getInt("text_language");

	ConfMan.setInt("transition_speed", _state->getTransitionSpeed());
	ConfMan.setInt("mouse_speed", _state->getMouseSpeed());
	ConfMan.setBool("zip_mode", _state->getZipModeEnabled());
	ConfMan.setBool("subtitles", _state->getSubtitlesEnabled());

	if (getPlatform() != Common::kPlatformXbox) {
		ConfMan.setInt("overall_volume", _state->getOverallVolume() * 256 / 100);
		ConfMan.setInt("music_volume", _state->getMusicVolume() * 256 / 100);
		ConfMan.setInt("music_frequency", _state->getMusicFrequency());
		ConfMan.setInt("audio_language", _state->getLanguageAudio());
		ConfMan.setInt("text_language", _state->getLanguageText());
		ConfMan.setBool("water_effects", _state->getWaterEffects());

		// The language changed, reload the correct archives
		if (_state->getLanguageText() != oldTextLanguage) {
			closeArchives();
			openArchives();
		}
	} else {
		ConfMan.setBool("vibrations", _state->getVibrationEnabled());
	}

	// Mouse speed may have changed, refresh it
	_scene->updateMouseSpeed();

	syncSoundSettings();
}

void Myst3Engine::syncSoundSettings() {
	Engine::syncSoundSettings();

	uint soundOverall = ConfMan.getInt("overall_volume");
	uint soundVolumeMusic = ConfMan.getInt("music_volume");

	_mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, soundOverall);
	_mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, soundVolumeMusic * soundOverall / 256);
}

bool Myst3Engine::isWideScreenModEnabled() const {
	return ConfMan.getBool("widescreen_mod");
}

void Myst3Engine::pauseEngineIntern(bool pause) {
	Engine::pauseEngineIntern(pause);

	if (!_state || !_cursor) {
		// This method may be called before the engine is fully initialized
		return;
	}

	for (uint i = 0; i < _movies.size(); i++) {
		_movies[i]->pause(pause);
	}

	_state->pauseEngine(pause);

	// Unlock the mouse so that the cursor is visible when the GMM opens
	if (_state->getViewType() == kCube && _cursor->isPositionLocked()) {
		_system->lockMouse(!pause);
	}
}

} // end of namespace Myst3
