/* 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 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/file.h"
#include "common/fs.h"
#include "common/config-manager.h"
#include "common/system.h"
#include "common/events.h"

#include "audio/mixer.h"

#include "saga/saga.h"

#include "saga/resource.h"
#include "saga/gfx.h"
#include "saga/render.h"
#include "saga/actor.h"
#include "saga/animation.h"
#include "saga/console.h"
#include "saga/events.h"
#include "saga/font.h"
#include "saga/interface.h"
#include "saga/isomap.h"
#include "saga/puzzle.h"
#include "saga/script.h"
#include "saga/scene.h"
#include "saga/sndres.h"
#include "saga/sprite.h"
#include "saga/sound.h"
#include "saga/music.h"
#include "saga/palanim.h"
#include "saga/objectmap.h"

namespace Saga {

#define MAX_TIME_DELTA 100

SagaEngine::SagaEngine(OSystem *syst, const SAGAGameDescription *gameDesc)
	: Engine(syst), _gameDescription(gameDesc), _rnd("saga") {

	_framesEsc = 0;

	_globalFlags = 0;
	memset(_ethicsPoints, 0, sizeof(_ethicsPoints));
	_spiritualBarometer = 0;

	_soundVolume = 0;
	_musicVolume = 0;
	_speechVolume = 0;
	_subtitlesEnabled = false;
	_voicesEnabled = false;
	_voiceFilesExist = false;
	_readingSpeed = 0;

	_copyProtection = false;
	_musicWasPlaying = false;
	_hasITESceneSubstitutes = false;

	_sndRes = NULL;
	_sound = NULL;
	_music = NULL;
	_anim = NULL;
	_render = NULL;
	_isoMap = NULL;
	_gfx = NULL;
	_script = NULL;
	_actor = NULL;
	_font = NULL;
	_sprite = NULL;
	_scene = NULL;
	_interface = NULL;
	_console = NULL;
	_events = NULL;
	_palanim = NULL;
	_puzzle = NULL;
	_resource = NULL;

	_previousTicks = 0;

	_saveFilesCount = 0;

	_leftMouseButtonPressed = _rightMouseButtonPressed = false;
	_mouseClickCount = 0;

	_gameNumber = 0;

	_frameCount = 0;

	const Common::FSNode gameDataDir(ConfMan.get("path"));

	// The Linux version of Inherit the Earth puts all data files in an
	// 'itedata' sub-directory, except for voices.rsc
	SearchMan.addSubDirectoryMatching(gameDataDir, "itedata");

	// The Windows version of Inherit the Earth puts various data files in
	// other subdirectories.
	SearchMan.addSubDirectoryMatching(gameDataDir, "graphics");
	SearchMan.addSubDirectoryMatching(gameDataDir, "music");
	SearchMan.addSubDirectoryMatching(gameDataDir, "sound");

	// Location of Miles audio files (sample.ad and sample.opl) in IHNM
	SearchMan.addSubDirectoryMatching(gameDataDir, "drivers");

	// The Multi-OS version puts the voices file in the root directory of
	// the CD. The rest of the data files are in game/itedata
	SearchMan.addSubDirectoryMatching(gameDataDir, "game/itedata");

	// Mac CD Wyrmkeep
	SearchMan.addSubDirectoryMatching(gameDataDir, "patch");

	// Dinotopia
	SearchMan.addSubDirectoryMatching(gameDataDir, "smack");

	// FTA2
	SearchMan.addSubDirectoryMatching(gameDataDir, "video");

	_displayClip.left = _displayClip.top = 0;
}

SagaEngine::~SagaEngine() {
	if (_scene != NULL) {
		if (_scene->isSceneLoaded()) {
			_scene->endScene();
		}
	}

	if (getGameId() == GID_ITE) {
		delete _isoMap;
		_isoMap = NULL;

		delete _puzzle;
		_puzzle = NULL;
	}

	delete _sndRes;
	_sndRes = NULL;

	delete _events;
	_events = NULL;

	if (!isSaga2()) {
		delete _font;
		_font = NULL;

		delete _sprite;
		_sprite = NULL;
	}

	delete _anim;
	_anim = NULL;

	delete _script;
	_script = NULL;

	if (!isSaga2()) {
		delete _interface;
		_interface = NULL;
	}

	delete _actor;
	_actor = NULL;

	delete _palanim;
	_palanim = NULL;

	delete _scene;
	_scene = NULL;

	delete _render;
	_render = NULL;

	delete _music;
	_music = NULL;

	delete _sound;
	_sound = NULL;

	delete _gfx;
	_gfx = NULL;

	delete _console;
	_console = NULL;

	delete _resource;
	_resource = NULL;
}

Common::Error SagaEngine::run() {
	setTotalPlayTime(0);

	// Assign default values to the config manager, in case settings are missing
	ConfMan.registerDefault("talkspeed", "255");
	ConfMan.registerDefault("subtitles", "true");

	_musicVolume = ConfMan.getInt("music_volume");
	_subtitlesEnabled = ConfMan.getBool("subtitles");
	_readingSpeed = getTalkspeed();
	_copyProtection = ConfMan.getBool("copy_protection");
	_musicWasPlaying = false;
	_isIHNMDemo = Common::File::exists("music.res");
	_hasITESceneSubstitutes = Common::File::exists("boarhall.bbm");

	if (_readingSpeed > 3)
		_readingSpeed = 0;

	switch (getGameId()) {
		case GID_ITE:
			_resource = new Resource_RSC(this);
			break;
#ifdef ENABLE_IHNM
		case GID_IHNM:
			_resource = new Resource_RES(this);
			break;
#endif
#ifdef ENABLE_SAGA2
		case GID_DINO:
		case GID_FTA2:
			_resource = new Resource_HRS(this);
			break;
#endif
	}

	// Detect game and open resource files
	if (!initGame()) {
		GUIErrorMessage("Error loading game resources.");
		return Common::kUnknownError;
	}

	// Initialize engine modules
	// TODO: implement differences for SAGA2
	_sndRes = new SndRes(this);
	_events = new Events(this);

	if (!isSaga2()) {
		_font = new Font(this);
		_sprite = new Sprite(this);
		_script = new SAGA1Script(this);
	} else {
		_script = new SAGA2Script(this);
	}

	_anim = new Anim(this);
	_interface = new Interface(this); // requires script module
	_scene = new Scene(this);
	_actor = new Actor(this);
	_palanim = new PalAnim(this);

	if (getGameId() == GID_ITE) {
		_isoMap = new IsoMap(this);
		_puzzle = new Puzzle(this);
	}

	// System initialization

	_previousTicks = _system->getMillis();

	// Initialize graphics
	_gfx = new Gfx(this, _system, getDisplayInfo().width, getDisplayInfo().height);

	// Graphics driver should be initialized before console
	_console = new Console(this);

	// Graphics should be initialized before music
	_music = new Music(this, _mixer);
	_render = new Render(this, _system);
	if (!_render->initialized()) {
		return Common::kUnknownError;
	}

	// Initialize system specific sound
	_sound = new Sound(this, _mixer);

	if (!isSaga2()) {
		_interface->converseClear();
		_script->setVerb(_script->getVerbType(kVerbWalkTo));
	}

	_music->setVolume(_musicVolume, 1);

	if (!isSaga2()) {
		_gfx->initPalette();
	}

	if (_voiceFilesExist) {
		if (getGameId() == GID_IHNM) {
			if (!ConfMan.hasKey("voices")) {
				_voicesEnabled = true;
				ConfMan.setBool("voices", true);
			} else {
				_voicesEnabled = ConfMan.getBool("voices");
			}
		} else {
			_voicesEnabled = true;
		}
	}

	syncSoundSettings();

	int msec = 0;

	_previousTicks = _system->getMillis();

	if (ConfMan.hasKey("start_scene")) {
		_scene->changeScene(ConfMan.getInt("start_scene"), 0, kTransitionNoFade);
	} else if (ConfMan.hasKey("boot_param")) {
		if (getGameId() == GID_ITE)
			_interface->addToInventory(_actor->objIndexToId(0));	// Magic hat
		_scene->changeScene(ConfMan.getInt("boot_param"), 0, kTransitionNoFade);
	} else if (ConfMan.hasKey("save_slot")) {
		// Init the current chapter to 8 (character selection) for IHNM
		if (getGameId() == GID_IHNM)
			_scene->changeScene(-2, 0, kTransitionFade, 8);

		// First scene sets up palette
		_scene->changeScene(getStartSceneNumber(), 0, kTransitionNoFade);
		_events->handleEvents(0); // Process immediate events

		if (getGameId() == GID_ITE)
			_interface->setMode(kPanelMain);
		else
			_interface->setMode(kPanelChapterSelection);

		char *fileName = calcSaveFileName(ConfMan.getInt("save_slot"));
		load(fileName);
		syncSoundSettings();
	} else {
		_framesEsc = 0;
		_scene->startScene();
	}

	uint32 currentTicks;

	while (!shouldQuit()) {
		_console->onFrame();

		if (_render->getFlags() & RF_RENDERPAUSE) {
			// Freeze time while paused
			_previousTicks = _system->getMillis();
		} else {
			currentTicks = _system->getMillis();
			// Timer has rolled over after 49 days
			if (currentTicks < _previousTicks)
				msec = 0;
			else {
				msec = currentTicks - _previousTicks;
				_previousTicks = currentTicks;
			}
			if (msec > MAX_TIME_DELTA) {
				msec = MAX_TIME_DELTA;
			}

			// Since Puzzle and forced text are actorless, we do them here
			if ((getGameId() == GID_ITE && _puzzle->isActive()) || _actor->isForcedTextShown()) {
				_actor->handleSpeech(msec);
			} else if (!_scene->isInIntro()) {
				if (_interface->getMode() == kPanelMain ||
						_interface->getMode() == kPanelConverse ||
						_interface->getMode() == kPanelCutaway ||
						_interface->getMode() == kPanelNull ||
						_interface->getMode() == kPanelChapterSelection)
					_actor->direct(msec);
			}

			_events->handleEvents(msec);
			_script->executeThreads(msec);
		}
		// Per frame processing
		_render->drawScene();
		_system->delayMillis(10);
	}

	return Common::kNoError;
}

void SagaEngine::loadStrings(StringsTable &stringsTable, const ByteArray &stringsData) {
	uint16 stringsCount;
	size_t offset;
	size_t prevOffset = 0;
	Common::Array<size_t> tempOffsets;
	uint ui;

	if (stringsData.empty()) {
		error("SagaEngine::loadStrings() Error loading strings list resource");
	}


	ByteArrayReadStreamEndian scriptS(stringsData, isBigEndian()); //TODO: get endianess from context

	offset = scriptS.readUint16();
	stringsCount = offset / 2;
	ui = 0;
	scriptS.seek(0);
	tempOffsets.resize(stringsCount);
	while (ui < stringsCount) {
		offset = scriptS.readUint16();
		// In some rooms in IHNM, string offsets can be greater than the maximum value than a 16-bit integer can hold
		// We detect this by checking the previous offset, and if it was bigger than the current one, an overflow
		// occurred (since the string offsets are sequential), so we're adding the missing part of the number
		// Fixes bug #1895205 - "IHNM: end game text/caption error"
		if (prevOffset > offset)
			offset += 65536;
		prevOffset = offset;
		if (offset == stringsData.size()) {
			stringsCount = ui;
			tempOffsets.resize(stringsCount);
			break;
		}
		if (offset > stringsData.size()) {
			// This case should never occur, but apparently it does in the Italian fan
			// translation of IHNM
			warning("SagaEngine::loadStrings wrong strings table");
			stringsCount = ui;
			tempOffsets.resize(stringsCount);
			break;
		}
		tempOffsets[ui] = offset;
		ui++;
	}

	prevOffset = scriptS.pos();
	int32 left = scriptS.size() - prevOffset;
	if (left < 0) {
		error("SagaEngine::loadStrings() Error loading strings buffer");
	}

	stringsTable.buffer.resize(left);
	if (left > 0) {
		scriptS.read(&stringsTable.buffer.front(), left);
	}

	stringsTable.strings.resize(tempOffsets.size());
	for (ui = 0; ui < tempOffsets.size(); ui++) {
		offset = tempOffsets[ui] - prevOffset;
		if (offset >= stringsTable.buffer.size()) {
			error("SagaEngine::loadStrings() Wrong offset");
		}
		stringsTable.strings[ui] = &stringsTable.buffer[offset];

		debug(9, "string[%i]=%s", ui, stringsTable.strings[ui]);
	}
}

const char *SagaEngine::getObjectName(uint16 objectId) const {
	ActorData *actor;
	ObjectData *obj;
	const HitZone *hitZone;

	// Disable the object names in IHNM when the chapter is 8
	if (getGameId() == GID_IHNM && _scene->currentChapterNumber() == 8)
		return "";

	switch (objectTypeId(objectId)) {
	case kGameObjectObject:
		obj = _actor->getObj(objectId);
		if (getGameId() == GID_ITE)
			return _script->_mainStrings.getString(obj->_nameIndex);
		return _actor->_objectsStrings.getString(obj->_nameIndex);
	case kGameObjectActor:
		actor = _actor->getActor(objectId);
		return _actor->_actorsStrings.getString(actor->_nameIndex);
	case kGameObjectHitZone:
		hitZone = _scene->_objectMap->getHitZone(objectIdToIndex(objectId));

		if (hitZone == NULL)
			return "";

		return _scene->_sceneStrings.getString(hitZone->getNameIndex());
	}
	warning("SagaEngine::getObjectName name not found for 0x%X", objectId);
	return NULL;
}

const char *SagaEngine::getTextString(int textStringId) {
	const char *string;
	int lang = 0;

	switch (getLanguage()) {
		case Common::DE_DEU:
			lang = 1;
			break;
		case Common::IT_ITA:
			lang = 2;
			break;
		case Common::ES_ESP:
			lang = 3;
			break;
		case Common::RU_RUS:
			lang = 4;
			break;
		case Common::FR_FRA:
			lang = 5;
			break;
		default:
			lang = 0;
			break;
	}

	string = ITEinterfaceTextStrings[lang][textStringId];
	if (!string)
		string = ITEinterfaceTextStrings[0][textStringId];

	return string;
}

void SagaEngine::getExcuseInfo(int verb, const char *&textString, int &soundResourceId) {
	textString = NULL;

	if (verb == _script->getVerbType(kVerbOpen)) {
		textString = getTextString(kTextNoPlaceToOpen);
		soundResourceId = 239;		// Boar voice 0
	}
	if (verb == _script->getVerbType(kVerbClose)) {
		textString = getTextString(kTextNoOpening);
		soundResourceId = 241;		// Boar voice 2
	}
	if (verb == _script->getVerbType(kVerbUse)) {
		textString = getTextString(kTextDontKnow);
		soundResourceId = 244;		// Boar voice 5
	}
	if (verb == _script->getVerbType(kVerbLookAt)) {
		textString = getTextString(kTextNothingSpecial);
		soundResourceId = 245;		// Boar voice 6
	}
	if (verb == _script->getVerbType(kVerbPickUp)) {
		textString = getTextString(kTextICantPickup);
		soundResourceId = 246;		// Boar voice 7
	}
}

ColorId SagaEngine::KnownColor2ColorId(KnownColor knownColor) {
	ColorId colorId = kITEColorTransBlack;

	if (getGameId() == GID_ITE) {
		switch (knownColor) {
		case(kKnownColorTransparent):
			colorId = kITEColorTransBlack;
			break;
		case (kKnownColorBrightWhite):
			colorId = kITEColorBrightWhite;
			break;
		case (kKnownColorWhite):
			colorId = kITEColorWhite;
			break;
		case (kKnownColorBlack):
			colorId = kITEColorBlack;
			break;
		case (kKnownColorSubtitleTextColor):
			colorId = (ColorId)255;
			break;
		case (kKnownColorVerbText):
			colorId = kITEColorBlue;
			break;
		case (kKnownColorVerbTextShadow):
			colorId = kITEColorBlack;
			break;
		case (kKnownColorVerbTextActive):
			colorId = (ColorId)96;
			break;

		default:
			error("SagaEngine::KnownColor2ColorId unknown color %i", knownColor);
		}
#ifdef ENABLE_IHNM
	} else if (getGameId() == GID_IHNM) {
		// The default colors in the Spanish, version of IHNM are shifted by one
		// Fixes bug #1848016 - "IHNM: Wrong Subtitles Color (Spanish)". This
		// also applies to the German and French versions (bug #7064 - "IHNM:
		// text mistake in german version").
		int offset = (getFeatures() & GF_IHNM_COLOR_FIX) ? 1 : 0;

		switch (knownColor) {
		case(kKnownColorTransparent):
			colorId = (ColorId)(249 - offset);
			break;
		case (kKnownColorBrightWhite):
			colorId = (ColorId)(251 - offset);
			break;
		case (kKnownColorWhite):
			colorId = (ColorId)(251 - offset);
			break;
		case (kKnownColorBlack):
			colorId = (ColorId)(249 - offset);
			break;
		case (kKnownColorVerbText):
			colorId = (ColorId)(253 - offset);
			break;
		case (kKnownColorVerbTextShadow):
			colorId = (ColorId)(15 - offset);
			break;
		case (kKnownColorVerbTextActive):
			colorId = (ColorId)(252 - offset);
			break;

		default:
			error("SagaEngine::KnownColor2ColorId unknown color %i", knownColor);
		}
#endif
	}
	return colorId;
}

void SagaEngine::setTalkspeed(int talkspeed) {
	ConfMan.setInt("talkspeed", (talkspeed * 255 + 3 / 2) / 3);
}

int SagaEngine::getTalkspeed() const {
	return (ConfMan.getInt("talkspeed") * 3 + 255 / 2) / 255;
}

GUI::Debugger *SagaEngine::getDebugger() {
	return _console;
}

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

	_subtitlesEnabled = ConfMan.getBool("subtitles");
	_readingSpeed = getTalkspeed();

	if (_readingSpeed > 3)
		_readingSpeed = 0;

	_musicVolume = ConfMan.getInt("music_volume");
	_music->setVolume(_musicVolume, 1);
	_sound->setVolume();
}

void SagaEngine::pauseEngineIntern(bool pause) {
	if (!_render || !_music)
		return;

	bool engineIsPaused = (_render->getFlags() & RF_RENDERPAUSE);
	if (engineIsPaused == pause)
		return;

	if (pause) {
		_render->setFlag(RF_RENDERPAUSE);
		if (_music->isPlaying() && !_music->hasDigitalMusic()) {
			_music->pause();
			_musicWasPlaying = true;
		} else {
			_musicWasPlaying = false;
		}
	} else {
		_render->clearFlag(RF_RENDERPAUSE);
		if (_musicWasPlaying) {
			_music->resume();
		}
	}

	_mixer->pauseAll(pause);
}

} // End of namespace Saga
