/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "common/scummsys.h"
#include "common/config-manager.h"
#include "common/debug-channels.h"
#include "common/events.h"
#include "common/system.h"
#include "common/savefile.h"
#include "engines/util.h"
#include "graphics/paletteman.h"
#include "m4/m4.h"
#include "m4/adv_r/adv_control.h"
#include "m4/adv_r/adv_file.h"
#include "m4/adv_r/conv_io.h"
#include "m4/graphics/gr_sprite.h"
#include "m4/gui/hotkeys.h"
#include "m4/platform/sound/digi.h"
#include "m4/platform/sound/midi.h"
#include "m4/detection.h"
#include "m4/console.h"
#include "m4/metaengine.h"

namespace M4 {

#define SAVEGAME_VERSION 1

M4Engine *g_engine;

M4Engine::M4Engine(OSystem *syst, const M4GameDescription *gameDesc) : Engine(syst),
	_gameDescription(gameDesc), _randomSource("M4") {
	g_engine = this;
}

M4Engine::~M4Engine() {
}

void M4Engine::initializePath(const Common::FSNode &gamePath) {
	Engine::initializePath(gamePath);
	SearchMan.addSubDirectoryMatching(gamePath, "goodstuf");
	SearchMan.addSubDirectoryMatching(gamePath, "resource");
}

uint32 M4Engine::getFeatures() const {
	return _gameDescription->desc.flags;
}

Common::String M4Engine::getGameId() const {
	return _gameDescription->desc.gameId;
}

int M4Engine::getGameType() const {
	return _gameDescription->gameType;
}

Common::Language M4Engine::getLanguage() const {
	return _gameDescription->desc.language;
}

int M4Engine::isDemo() const {
	if ((getFeatures() & ADGF_DEMO) == 0)
		return GStyle_Game;
	else if (_gameDescription->features & kFeaturesNonInteractiveDemo)
		return GStyle_NonInteractiveDemo;
	else
		return GStyle_Demo;
}

Common::Error M4Engine::run() {
	// Initialize 320x200 paletted graphics mode
	initGraphics(640, 480);
	syncSoundSettings();

	// Instantiate globals and setup
	Vars *vars = createVars();

	if (vars->init()) {
		// Set the console
		setupConsole();

		// Check for launcher savegame to load
		_useOriginalSaveLoad = ConfMan.getBool("original_menus");
		if (ConfMan.hasKey("save_slot")) {
			_G(kernel).restore_slot = ConfMan.getInt("save_slot");
			_G(game).previous_room = KERNEL_RESTORING_GAME;
		}

		// Run game here
		m4_inflight();
	}

	delete vars;
	return Common::kNoError;
}

#define KEEP_PLAYING (_G(kernel).going && !shouldQuit())

void M4Engine::m4_inflight() {
	g_vars->getHotkeys()->add_hot_keys();
	_G(kernel).going = true;

	while (KEEP_PLAYING) {
		if (_G(game).previous_room == KERNEL_RESTORING_GAME) {
			midi_stop();
			int slot = _G(kernel).restore_slot;
			if (!kernel_load_game(slot))
				error("Could not restore save slot %d", slot);
		}

		// Start up next section
		_G(between_rooms) = true;
		global_section_constructor();		// Sets the active section
		_G(kernel).going = kernel_section_startup();
		section_init();

		while (_G(game).new_section == _G(game).section_id && KEEP_PLAYING) {
			m4SceneLoad();
			m4RunScene();
			m4EndScene();
		}
	}
}

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

	const int volume = ConfMan.getBool("sfx_mute") ? 0 : CLIP(ConfMan.getInt("sfx_volume"), 0, 255);
	_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType, volume);
}

bool M4Engine::canLoadGameStateCurrently(Common::U32String *msg) {
	return g_vars && INTERFACE_VISIBLE && player_commands_allowed();
}

bool M4Engine::canSaveGameStateCurrently(Common::U32String *msg) {
	return g_vars && INTERFACE_VISIBLE && player_commands_allowed();
}

void M4Engine::showSaveScreen() {
	saveGameDialog();
}

void M4Engine::showLoadScreen(LoadDialogSource source) {
	loadGameDialog();
}

Common::Error M4Engine::loadGameState(int slot) {
	// Don't load savegame immediately, just set the slot for the engine's
	// kernel to take care of in the outer game loop
	_G(kernel).restore_slot = slot;
	_G(game).new_room = KERNEL_RESTORING_GAME;
	_G(game).new_section = KERNEL_RESTORING_GAME;
	_G(game).previous_room = KERNEL_RESTORING_GAME;

	return Common::kNoError;
}

Common::Error M4Engine::loadGameStateDoIt(int slot) {
	Common::InSaveFile *save = getOriginalSave(slot);

	if (save) {
		// Skip original description
		const int descSize = save->readUint32LE();
		save->seek(descSize + 45, SEEK_CUR);

		const int thumbSize = save->readUint32LE();
		save->seek(thumbSize, SEEK_CUR);

		// We're now at data section, handle it
		Common::Serializer s(save, nullptr);
		s.setVersion(1);
		Common::Error result = syncGame(s);

		delete save;

		return result;

	}

	return Engine::loadGameState(slot);
}

Common::InSaveFile *M4Engine::getOriginalSave(int slot) const {
	Common::InSaveFile *save = g_system->getSavefileManager()->openForLoading(
		getSaveStateName(slot));
	char name[16];

	if (save) {
		if (save->seek(-44, SEEK_END) && save->read(name, 7) == 7 &&
				!strncmp(name, "MIRROR", 7)) {
			save->seek(0);
			return save;
		}

		if (save->seek(-44, SEEK_END) && save->read(name, 7) == 7 &&
				!strncmp(name, "FAUCET ", 7)) {
			save->seek(0);
			return save;
		}

		delete save;
	}

	return nullptr;
}

Common::Error M4Engine::saveGameStream(Common::WriteStream *stream, bool isAutosave) {
	stream->writeByte(SAVEGAME_VERSION);

	Common::Serializer s(nullptr, stream);
	return syncGame(s);
}

Common::Error M4Engine::loadGameStream(Common::SeekableReadStream *stream) {
	const byte version = stream->readByte();
	if (version > SAVEGAME_VERSION)
		error("Tried to load unsupported savegame version");

	Common::Serializer s(stream, nullptr);
	s.setVersion(version);
	return syncGame(s);
}

Common::Error M4Engine::syncGame(Common::Serializer &s) {
	if (s.isSaving())
		_G(kernel).pause = false;

	// To match the original, we sync a set of fields from kernel and game
	if (getGameType() == GType_Riddle)
		s.syncAsByte(_G(kernel).going);

	s.syncAsByte(_G(kernel).pause);
	for (int i = 0; i < KERNEL_SCRATCH_SIZE; ++i)
		s.syncAsUint32LE(_G(kernel).scratch[i]);

	s.syncAsByte(_G(kernel).unused);
	s.syncAsSint16LE(_G(kernel).last_save);
	s.syncAsSint16LE(_G(game).room_id);
	s.syncAsSint16LE(_G(game).new_room);
	s.syncAsSint16LE(_G(game).previous_section);
	s.syncAsSint16LE(_G(game).section_id);
	s.syncAsSint16LE(_G(game).new_section);
	s.syncAsSint16LE(_G(game).previous_room);

	if (getGameType() == GType_Riddle) {
		// TODO: Determinate if the extra fields need to be synced
		s.skip(22);

	} else {
		s.syncAsByte(_G(kernel).restore_game);
		s.syncAsSint32LE(_G(game).digi_overall_volume_percent);
		s.syncAsSint32LE(_G(game).midi_overall_volume_percent);
		s.syncAsByte(_G(kernel).camera_pan_instant);
	}

	_G(player).syncGame(s);

	_G(player_info).syncGame(s);

	syncFlags(s);

	player_been_sync(s);

	_G(conversations).syncGame(s);
	_G(inventory)->syncGame(s);

	Room *room = s.isSaving() ? g_engine->_activeRoom :
		g_engine->getRoom(_G(game).room_id);
	room->syncGame(s);

	if (s.isLoading()) {
		// set up variables so everyone knows we've teleported
		_G(kernel).restore_game = true;
		_G(between_rooms) = true;
		_G(game).previous_room = KERNEL_RESTORING_GAME;

		digi_set_overall_volume(_G(game).digi_overall_volume_percent);
		midi_set_overall_volume(_G(game).midi_overall_volume_percent);
		interface_show();
	}

	return Common::kNoError;
}

bool M4Engine::autosaveExists() const {
	const Common::String slotName = getSaveStateName(getAutosaveSlot());
	Common::InSaveFile *saveFile = g_system->getSavefileManager()->openForLoading(slotName);
	const bool result = saveFile != nullptr;
	delete saveFile;

	return result;
}

SaveStateList M4Engine::listSaves() const {
	return getMetaEngine()->listSaves(_targetName.c_str());
}

bool M4Engine::savesExist() const {
	return !listSaves().empty();
}

bool M4Engine::loadSaveThumbnail(int slotNum, M4sprite *thumbnail) const {
	const SaveStateDescriptor desc = getMetaEngine()->querySaveMetaInfos(_targetName.c_str(), slotNum);
	if (!desc.isValid())
		return false;

	// Gert the thumbnail
	const Graphics::Surface *surf = desc.getThumbnail();
	assert(surf->format.bytesPerPixel == 2);

	// Set up output sprite
	thumbnail->w = surf->w;
	thumbnail->h = surf->h;
	thumbnail->encoding = NO_COMPRESS;

	thumbnail->sourceHandle = (MemHandle)malloc(sizeof(void *));
	if (!thumbnail->sourceHandle)
		error("loadSaveThumbnail - Unable to allocate sourceHandle");

	byte *data = (byte *)malloc(surf->w * surf->h);
	*thumbnail->sourceHandle = (void *)data;
	thumbnail->sourceOffset = 0;
	thumbnail->data = data;

	byte pal[Graphics::PALETTE_SIZE];
	byte r, g, b;

	g_system->getPaletteManager()->grabPalette(pal, 0, Graphics::PALETTE_COUNT);

	// Translate the 16-bit thumbnail to paletted
	for (int y = 0; y < surf->h; ++y) {
		const uint16 *srcLine = (const uint16 *)surf->getBasePtr(0, y);
		byte *destLine = data + surf->w * y;

		for (int x = 0; x < surf->w; ++x, ++srcLine, ++destLine) {
			int minProximity = 0xffff;
			surf->format.colorToRGB(*srcLine, r, g, b);

			const byte *palP = pal;
			for (int palIdx = 0; palIdx < Graphics::PALETTE_COUNT; ++palIdx, palP += 3) {
				const int proximity = ABS((int)r - (int)palP[0]) +
				                      ABS((int)g - (int)palP[1]) +
				                      ABS((int)b - (int)palP[2]);

				if (proximity < minProximity && destLine) {
					minProximity = proximity;
					*destLine = (byte)palIdx;
				}
			}
		}
	}

	return true;
}

bool M4Engine::saveGameFromMenu(int slotNum, const Common::String &desc,
		Graphics::Surface &thumbnail) {
	M4MetaEngine *metaEngine = static_cast<M4MetaEngine *>(getMetaEngine());
	metaEngine->_thumbnail = &thumbnail;

	bool result = saveGameState(slotNum, desc).getCode() == Common::kNoError;

	metaEngine->_thumbnail = nullptr;
	return result;
}

} // End of namespace M4
