/* 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/debug-channels.h"

#include "backends/audiocd/audiocd.h"
#include "base/plugins.h"
#include "common/config-manager.h"
#include "engines/util.h"
#include "audio/mididrv.h"
#include "audio/mixer.h"

#include "gui/gui-manager.h"
#include "gui/dialog.h"
#include "gui/widget.h"

#include "gob/gob.h"
#include "gob/global.h"
#include "gob/util.h"
#include "gob/dataio.h"
#include "gob/game.h"
#include "gob/sound/sound.h"
#include "gob/init.h"
#include "gob/inter.h"
#include "gob/draw.h"
#include "gob/goblin.h"
#include "gob/map.h"
#include "gob/mult.h"
#include "gob/palanim.h"
#include "gob/scenery.h"
#include "gob/videoplayer.h"
#include "gob/save/saveload.h"

#include "gob/pregob/pregob.h"
#include "gob/pregob/onceupon/abracadabra.h"
#include "gob/pregob/onceupon/babayaga.h"

namespace Gob {

#define MAX_TIME_DELTA 100

const Common::Language GobEngine::_gobToScummVMLang[] = {
	Common::FR_FRA,
	Common::DE_DEU,
	Common::EN_GRB,
	Common::ES_ESP,
	Common::IT_ITA,
	Common::EN_USA,
	Common::NL_NLD,
	Common::KO_KOR,
	Common::HE_ISR,
	Common::PT_BRA,
	Common::JA_JPN
};


class PauseDialog : public GUI::Dialog {
public:
	PauseDialog();

	virtual void reflowLayout();
	virtual void handleKeyDown(Common::KeyState state);

private:
	Common::String _message;
	GUI::StaticTextWidget *_text;
};

PauseDialog::PauseDialog() : GUI::Dialog(0, 0, 0, 0) {
	_backgroundType = GUI::ThemeEngine::kDialogBackgroundSpecial;

	_message = "Game paused. Press Ctrl+p again to continue.";
	_text = new GUI::StaticTextWidget(this, 4, 0, 10, 10,
			_message, Graphics::kTextAlignCenter);
}

void PauseDialog::reflowLayout() {
	const int screenW = g_system->getOverlayWidth();
	const int screenH = g_system->getOverlayHeight();

	int width = g_gui.getStringWidth(_message) + 16;
	int height = g_gui.getFontHeight() + 8;

	_w = width;
	_h = height;
	_x = (screenW - width) / 2;
	_y = (screenH - height) / 2;

	_text->setSize(_w - 8, _h);
}

void PauseDialog::handleKeyDown(Common::KeyState state) {
	// Close on CTRL+p
	if ((state.hasFlags(Common::KBD_CTRL)) && (state.keycode == Common::KEYCODE_p))
		close();
}


GobEngine::GobEngine(OSystem *syst) : Engine(syst), _rnd("gob") {
	DebugMan.addDebugChannel(kDebugFuncOp, "FuncOpcodes", "Script FuncOpcodes debug level");
	DebugMan.addDebugChannel(kDebugDrawOp, "DrawOpcodes", "Script DrawOpcodes debug level");
	DebugMan.addDebugChannel(kDebugGobOp, "GoblinOpcodes", "Script GoblinOpcodes debug level");
	DebugMan.addDebugChannel(kDebugSound, "Sound", "Sound output debug level");
	DebugMan.addDebugChannel(kDebugExpression, "Expression", "Expression parser debug level");
	DebugMan.addDebugChannel(kDebugGameFlow, "Gameflow", "Gameflow debug level");
	DebugMan.addDebugChannel(kDebugFileIO, "FileIO", "File Input/Output debug level");
	DebugMan.addDebugChannel(kDebugSaveLoad, "SaveLoad", "Saving/Loading debug level");
	DebugMan.addDebugChannel(kDebugGraphics, "Graphics", "Graphics debug level");
	DebugMan.addDebugChannel(kDebugVideo, "Video", "IMD/VMD video debug level");
	DebugMan.addDebugChannel(kDebugHotspots, "Hotspots", "Hotspots debug level");
	DebugMan.addDebugChannel(kDebugDemo, "Demo", "Demo script debug level");

	_sound     = 0; _mult     = 0; _game    = 0;
	_global    = 0; _dataIO   = 0; _goblin  = 0;
	_vidPlayer = 0; _init     = 0; _inter   = 0;
	_map       = 0; _palAnim  = 0; _scenery = 0;
	_draw      = 0; _util     = 0; _video   = 0;
	_saveLoad  = 0; _preGob   = 0;

	_pauseStart = 0;

	// Setup mixer
	bool muteSFX   = ConfMan.getBool("mute") || ConfMan.getBool("sfx_mute");
	bool muteMusic = ConfMan.getBool("mute") || ConfMan.getBool("music_mute");

	_mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType,
			muteSFX   ? 0 : ConfMan.getInt("sfx_volume"));
	_mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType,
			muteMusic ? 0 : ConfMan.getInt("music_volume"));

	_copyProtection = ConfMan.getBool("copy_protection");

	_console = new GobConsole(this);
}

GobEngine::~GobEngine() {
	deinitGameParts();

	delete _console;
}

const char *GobEngine::getLangDesc(int16 language) const {
	if ((language < 0) || (language > 10))
		language = 2;
	return Common::getLanguageDescription(_gobToScummVMLang[language]);
}

void GobEngine::validateLanguage() {
	if (_global->_languageWanted != _global->_language) {
		warning("Your game version doesn't support the requested language %s",
				getLangDesc(_global->_languageWanted));

		if (((_global->_languageWanted == 2) && (_global->_language == 5)) ||
		    ((_global->_languageWanted == 5) && (_global->_language == 2)))
			warning("Using %s instead", getLangDesc(_global->_language));
		else
			warning("Using the first language available: %s",
					getLangDesc(_global->_language));

		_global->_languageWanted = _global->_language;
	}
}

void GobEngine::validateVideoMode(int16 videoMode) {
	if ((videoMode != 0x10) && (videoMode != 0x13) &&
		  (videoMode != 0x14) && (videoMode != 0x18))
		error("Video mode 0x%X is not supported", videoMode);
}

EndiannessMethod GobEngine::getEndiannessMethod() const {
	return _endiannessMethod;
}

Endianness GobEngine::getEndianness() const {
	if ((getPlatform() == Common::kPlatformAmiga) ||
	    (getPlatform() == Common::kPlatformMacintosh) ||
	    (getPlatform() == Common::kPlatformAtariST))
		return kEndiannessBE;

	return kEndiannessLE;
}

Common::Platform GobEngine::getPlatform() const {
	return _platform;
}

GameType GobEngine::getGameType() const {
	return _gameType;
}

bool GobEngine::isCD() const {
	return (_features & kFeaturesCD) != 0;
}

bool GobEngine::isEGA() const {
	return (_features & kFeaturesEGA) != 0;
}

bool GobEngine::hasAdLib() const {
	return (_features & kFeaturesAdLib) != 0;
}

bool GobEngine::isSCNDemo() const {
	return (_features & kFeaturesSCNDemo) != 0;
}

bool GobEngine::isBATDemo() const {
	return (_features & kFeaturesBATDemo) != 0;
}

bool GobEngine::is640x480() const {
	return (_features & kFeatures640x480) != 0;
}

bool GobEngine::is800x600() const {
	return (_features & kFeatures800x600) != 0;
}

bool GobEngine::isTrueColor() const {
	return (_features & kFeaturesTrueColor) != 0;
}

bool GobEngine::isDemo() const {
	return (isSCNDemo() || isBATDemo());
}

bool GobEngine::hasResourceSizeWorkaround() const {
	return _resourceSizeWorkaround;
}

bool GobEngine::isCurrentTot(const Common::String &tot) const {
	return _game->_curTotFile.equalsIgnoreCase(tot);
}

const Graphics::PixelFormat &GobEngine::getPixelFormat() const {
	return _pixelFormat;
}

void GobEngine::setTrueColor(bool trueColor) {
	if (isTrueColor() == trueColor)
		return;

	_features = (_features & ~kFeaturesTrueColor) | (trueColor ? kFeaturesTrueColor : 0);

	_video->setSize();

	_pixelFormat = g_system->getScreenFormat();

	Common::Array<SurfacePtr>::iterator surf;
	for (surf = _draw->_spritesArray.begin(); surf != _draw->_spritesArray.end(); ++surf)
		if (*surf)
			(*surf)->setBPP(_pixelFormat.bytesPerPixel);

	if (_draw->_backSurface)
		_draw->_backSurface->setBPP(_pixelFormat.bytesPerPixel);
	if (_draw->_frontSurface)
		_draw->_frontSurface->setBPP(_pixelFormat.bytesPerPixel);
	if (_draw->_cursorSprites)
		_draw->_cursorSprites->setBPP(_pixelFormat.bytesPerPixel);
	if (_draw->_cursorSpritesBack)
		_draw->_cursorSpritesBack->setBPP(_pixelFormat.bytesPerPixel);
	if (_draw->_scummvmCursor)
		_draw->_scummvmCursor->setBPP(_pixelFormat.bytesPerPixel);
	SurfacePtr _scummvmCursor;
}

Common::Error GobEngine::run() {
	Common::Error err;

	err = initGameParts();
	if (err.getCode() != Common::kNoError)
		return err;

	err = initGraphics();
	if (err.getCode() != Common::kNoError)
		return err;

	// On some systems it's not safe to run CD audio games from the CD.
	if (isCD())
		checkCD();

	_system->getAudioCDManager()->open();

	_global->_debugFlag = 1;
	_video->_doRangeClamp = true;

	// WORKAROUND: Some versions check the video mode to detect the system
	if (_platform == Common::kPlatformAmiga)
		_global->_fakeVideoMode = 0x11;
	else if (_platform == Common::kPlatformAtariST)
		_global->_fakeVideoMode = 0x10;
	else
		_global->_fakeVideoMode = 0x13;

	_global->_videoMode = 0x13;
	_global->_useMouse = 1;
	_global->_soundFlags = MIDI_FLAG | SPEAKER_FLAG | BLASTER_FLAG | ADLIB_FLAG;

	if (ConfMan.hasKey("language"))
		_language = Common::parseLanguage(ConfMan.get("language"));

	switch (_language) {
	case Common::FR_FRA:
	case Common::RU_RUS:
		_global->_language = kLanguageFrench;
		break;
	case Common::DE_DEU:
		_global->_language = kLanguageGerman;
		break;
	case Common::EN_ANY:
	case Common::EN_GRB:
	case Common::HU_HUN:
		_global->_language = kLanguageBritish;
		break;
	case Common::ES_ESP:
		_global->_language = kLanguageSpanish;
		break;
	case Common::IT_ITA:
		_global->_language = kLanguageItalian;
		break;
	case Common::EN_USA:
		_global->_language = kLanguageAmerican;
		break;
	case Common::NL_NLD:
		_global->_language = kLanguageDutch;
		break;
	case Common::KO_KOR:
		_global->_language = kLanguageKorean;
		break;
	case Common::HE_ISR:
		_global->_language = kLanguageHebrew;
		break;
	case Common::PT_BRA:
		_global->_language = kLanguagePortuguese;
		break;
	case Common::JA_JPN:
		_global->_language = kLanguageJapanese;
		break;
	default:
		_global->_language = kLanguageBritish;
		break;
	}
	_global->_languageWanted = _global->_language;

	_init->initGame();

	return Common::kNoError;
}

void GobEngine::pauseEngineIntern(bool pause) {
	if (pause) {
		_pauseStart = _system->getMillis();
	} else {
		uint32 duration = _system->getMillis() - _pauseStart;

		_util->notifyPaused(duration);

		_game->_startTimeKey += duration;
		_draw->_cursorTimeKey += duration;
		if (_inter && (_inter->_soundEndTimeKey != 0))
			_inter->_soundEndTimeKey += duration;
	}

	if (_vidPlayer)
		_vidPlayer->pauseAll(pause);
	_mixer->pauseAll(pause);
}

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

	_init->updateConfig();

	if (_sound)
		_sound->adlibSyncVolume();
}

void GobEngine::pauseGame() {
	pauseEngineIntern(true);

	PauseDialog pauseDialog;

	pauseDialog.runModal();

	pauseEngineIntern(false);
}

Common::Error GobEngine::initGameParts() {
	_resourceSizeWorkaround = false;

	// just detect some devices some of which will be always there if the music is not disabled
	_noMusic = MidiDriver::getMusicType(MidiDriver::detectDevice(MDT_PCSPK | MDT_MIDI | MDT_ADLIB)) == MT_NULL ? true : false;

	_endiannessMethod = kEndiannessMethodSystem;

	_global    = new Global(this);
	_util      = new Util(this);
	_dataIO    = new DataIO();
	_palAnim   = new PalAnim(this);
	_vidPlayer = new VideoPlayer(this);
	_sound     = new Sound(this);
	_game      = new Game(this);

	switch (_gameType) {
	case kGameTypeGob1:
		_init     = new Init_v1(this);
		_video    = new Video_v1(this);
		_inter    = new Inter_v1(this);
		_mult     = new Mult_v1(this);
		_draw     = new Draw_v1(this);
		_map      = new Map_v1(this);
		_goblin   = new Goblin_v1(this);
		_scenery  = new Scenery_v1(this);

		// WORKAROUND: The EGA version of Gobliiins claims a few resources are
		//             larger than they actually are. The original happily reads
		//             past the resource structure boundary, but we don't.
		//             To make sure we don't throw an error like we normally do
		//             (which leads to these resources not loading), we enable
		//             this workaround that automatically fixes the resources
		//             sizes.
		//
		//             This glitch is visible in levels
		//             - 03 (ICIGCAA)
		//             - 09 (ICVGCGT)
		//             - 16 (TCVQRPM)
		//             - 20 (NNGWTTO)
		//             See also ScummVM bug report #7162.
		if (isEGA())
			_resourceSizeWorkaround = true;
		break;

	case kGameTypeGeisha:
		_init     = new Init_Geisha(this);
		_video    = new Video_v1(this);
		_inter    = new Inter_Geisha(this);
		_mult     = new Mult_v1(this);
		_draw     = new Draw_v1(this);
		_map      = new Map_v1(this);
		_goblin   = new Goblin_v1(this);
		_scenery  = new Scenery_v1(this);
		_saveLoad = new SaveLoad_Geisha(this, _targetName.c_str());

		_endiannessMethod = kEndiannessMethodAltFile;
		break;

	case kGameTypeFascination:
		_init     = new Init_Fascination(this);
		_video    = new Video_v2(this);
		_inter    = new Inter_Fascination(this);
		_mult     = new Mult_v2(this);
		_draw     = new Draw_Fascination(this);
		_map      = new Map_v2(this);
		_goblin   = new Goblin_v2(this);
		_scenery  = new Scenery_v2(this);
		_saveLoad = new SaveLoad_Fascination(this, _targetName.c_str());
		break;

	case kGameTypeWeen:
	case kGameTypeGob2:
	case kGameTypeCrousti:
		_init     = new Init_v2(this);
		_video    = new Video_v2(this);
		_inter    = new Inter_v2(this);
		_mult     = new Mult_v2(this);
		_draw     = new Draw_v2(this);
		_map      = new Map_v2(this);
		_goblin   = new Goblin_v2(this);
		_scenery  = new Scenery_v2(this);
		_saveLoad = new SaveLoad_v2(this, _targetName.c_str());
		break;

	case kGameTypeBargon:
		_init     = new Init_v2(this);
		_video    = new Video_v2(this);
		_inter    = new Inter_Bargon(this);
		_mult     = new Mult_v2(this);
		_draw     = new Draw_Bargon(this);
		_map      = new Map_v2(this);
		_goblin   = new Goblin_v2(this);
		_scenery  = new Scenery_v2(this);
		_saveLoad = new SaveLoad_v2(this, _targetName.c_str());
		break;

	case kGameTypeLittleRed:
		_init     = new Init_v2(this);
		_video    = new Video_v2(this);
		_inter    = new Inter_LittleRed(this);
		_mult     = new Mult_v2(this);
		_draw     = new Draw_v2(this);
		_map      = new Map_v2(this);
		_goblin   = new Goblin_v2(this);
		_scenery  = new Scenery_v2(this);

		// WORKAROUND: Little Red Riding Hood has a small resource size glitch in the
		//             screen where Little Red needs to find the animals' homes.
		_resourceSizeWorkaround = true;
		break;

	case kGameTypeAJWorld:
		_init     = new Init_v2(this);
		_video    = new Video_v2(this);
		_inter    = new Inter_v2(this);
		_mult     = new Mult_v2(this);
		_draw     = new Draw_v2(this);
		_map      = new Map_v2(this);
		_goblin   = new Goblin_v2(this);
		_scenery  = new Scenery_v2(this);
		_saveLoad = new SaveLoad_AJWorld(this, _targetName.c_str());
		break;

	case kGameTypeGob3:
		_init     = new Init_v3(this);
		_video    = new Video_v2(this);
		_inter    = new Inter_v3(this);
		_mult     = new Mult_v2(this);
		_draw     = new Draw_v2(this);
		_map      = new Map_v2(this);
		_goblin   = new Goblin_v3(this);
		_scenery  = new Scenery_v2(this);
		_saveLoad = new SaveLoad_v3(this, _targetName.c_str(), SaveLoad_v3::kScreenshotTypeGob3);
		break;

	case kGameTypeInca2:
		_init     = new Init_v3(this);
		_video    = new Video_v2(this);
		_inter    = new Inter_Inca2(this);
		_mult     = new Mult_v2(this);
		_draw     = new Draw_v2(this);
		_map      = new Map_v2(this);
		_goblin   = new Goblin_v3(this);
		_scenery  = new Scenery_v2(this);
		_saveLoad = new SaveLoad_Inca2(this, _targetName.c_str());
		break;

	case kGameTypeLostInTime:
		_init     = new Init_v3(this);
		_video    = new Video_v2(this);
		_inter    = new Inter_v3(this);
		_mult     = new Mult_v2(this);
		_draw     = new Draw_v2(this);
		_map      = new Map_v2(this);
		_goblin   = new Goblin_v3(this);
		_scenery  = new Scenery_v2(this);
		_saveLoad = new SaveLoad_v3(this, _targetName.c_str(), SaveLoad_v3::kScreenshotTypeLost);
		break;

	case kGameTypeWoodruff:
		_init     = new Init_v4(this);
		_video    = new Video_v2(this);
		_inter    = new Inter_v4(this);
		_mult     = new Mult_v2(this);
		_draw     = new Draw_v2(this);
		_map      = new Map_v2(this);
		_goblin   = new Goblin_v4(this);
		_scenery  = new Scenery_v2(this);
		_saveLoad = new SaveLoad_v4(this, _targetName.c_str());
		break;

	case kGameTypeDynasty:
		_init     = new Init_v3(this);
		_video    = new Video_v2(this);
		_inter    = new Inter_v5(this);
		_mult     = new Mult_v2(this);
		_draw     = new Draw_v2(this);
		_map      = new Map_v2(this);
		_goblin   = new Goblin_v4(this);
		_scenery  = new Scenery_v2(this);
		_saveLoad = new SaveLoad(this);
		break;

	case kGameTypeUrban:
		_init     = new Init_v6(this);
		_video    = new Video_v6(this);
		_inter    = new Inter_v6(this);
		_mult     = new Mult_v2(this);
		_draw     = new Draw_v2(this);
		_map      = new Map_v2(this);
		_goblin   = new Goblin_v4(this);
		_scenery  = new Scenery_v2(this);
		_saveLoad = new SaveLoad_v6(this, _targetName.c_str());
		break;

	case kGameTypePlaytoons:
	case kGameTypeBambou:
		_init     = new Init_v2(this);
		_video    = new Video_v6(this);
		_inter    = new Inter_Playtoons(this);
		_mult     = new Mult_v2(this);
		_draw     = new Draw_Playtoons(this);
		_map      = new Map_v2(this);
		_goblin   = new Goblin_v4(this);
		_scenery  = new Scenery_v2(this);
		_saveLoad = new SaveLoad_Playtoons(this, _targetName.c_str());
		break;

	case kGameTypeAdibou2:
	case kGameTypeAdi2:
	case kGameTypeAdi4:
		_init     = new Init_v7(this);
		_video    = new Video_v6(this);
		_inter    = new Inter_v7(this);
		_mult     = new Mult_v2(this);
		_draw     = new Draw_v2(this);
		_map      = new Map_v2(this);
		_goblin   = new Goblin_v4(this);
		_scenery  = new Scenery_v2(this);
		_saveLoad = new SaveLoad_v7(this, _targetName.c_str());
		break;

	case kGameTypeAdibou1:
		_init     = new Init_v2(this);
		_video    = new Video_v2(this);
		_inter    = new Inter_v2(this);
		_mult     = new Mult_v2(this);
		_draw     = new Draw_v2(this);
		_map      = new Map_v2(this);
		_goblin   = new Goblin_v2(this);
		_scenery  = new Scenery_v2(this);
		_saveLoad = new SaveLoad_v2(this, _targetName.c_str());
		break;

	case kGameTypeAbracadabra:
		_init     = new Init_v2(this);
		_video    = new Video_v2(this);
		_mult     = new Mult_v2(this);
		_draw     = new Draw_v2(this);
		_map      = new Map_v2(this);
		_goblin   = new Goblin_v2(this);
		_scenery  = new Scenery_v2(this);
		_preGob   = new OnceUpon::Abracadabra(this);
		break;

	case kGameTypeBabaYaga:
		_init     = new Init_v2(this);
		_video    = new Video_v2(this);
		_mult     = new Mult_v2(this);
		_draw     = new Draw_v2(this);
		_map      = new Map_v2(this);
		_goblin   = new Goblin_v2(this);
		_scenery  = new Scenery_v2(this);
		_preGob   = new OnceUpon::BabaYaga(this);
		break;

	default:
		deinitGameParts();
		return Common::kUnsupportedGameidError;
	}

	// Setup mixer
	syncSoundSettings();

	if (_inter)
		_inter->setupOpcodes();

	return Common::kNoError;
}

void GobEngine::deinitGameParts() {
	delete _preGob;    _preGob = 0;
	delete _saveLoad;  _saveLoad = 0;
	delete _mult;      _mult = 0;
	delete _vidPlayer; _vidPlayer = 0;
	delete _game;      _game = 0;
	delete _global;    _global = 0;
	delete _goblin;    _goblin = 0;
	delete _init;      _init = 0;
	delete _inter;     _inter = 0;
	delete _map;       _map = 0;
	delete _palAnim;   _palAnim = 0;
	delete _scenery;   _scenery = 0;
	delete _draw;      _draw = 0;
	delete _util;      _util = 0;
	delete _video;     _video = 0;
	delete _sound;     _sound = 0;
	delete _dataIO;    _dataIO = 0;
}

Common::Error GobEngine::initGraphics() {
	if        (is800x600()) {
		warning("GobEngine::initGraphics(): 800x600 games currently unsupported");
		return Common::kUnsupportedGameidError;
	} else if (is640x480()) {
		_width  = 640;
		_height = 480;
		_mode   = 0x18;
	} else {
		_width  = 320;
		_height = 200;
		_mode   = 0x14;
	}

	Graphics::ModeList modes;
	modes.push_back(Graphics::Mode(_width, _height));
	if (getGameType() == kGameTypeLostInTime) {
		modes.push_back(Graphics::Mode(640, 400));
	}
	initGraphicsModes(modes);

	_video->setSize();

	_pixelFormat = g_system->getScreenFormat();

	_video->_surfWidth    = _width;
	_video->_surfHeight   = _height;
	_video->_splitHeight1 = _height;

	_global->_mouseMaxX = _width;
	_global->_mouseMaxY = _height;

	_global->_primarySurfDesc = SurfacePtr(new Surface(_width, _height, _pixelFormat.bytesPerPixel));

	return Common::kNoError;
}

} // End of namespace Gob