/* 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 "engines/myst3/state.h"
#include "engines/myst3/database.h"

#include "common/savefile.h"

namespace Myst3 {

GameState::StateData::StateData() {
	version = GameState::kSaveVersion;
	gameRunning = true;
	currentFrame = 0;
	nextSecondsUpdate = 0;
	secondsPlayed = 0;
	dword_4C2C44 = 0;
	dword_4C2C48 = 0;
	dword_4C2C4C = 0;
	dword_4C2C50 = 0;
	dword_4C2C54 = 0;
	dword_4C2C58 = 0;
	dword_4C2C5C = 0;
	dword_4C2C60 = 0;
	currentNodeType = 0;
	lookatPitch = 0;
	lookatHeading = 0;
	lookatFOV = 0;
	pitchOffset = 0;
	headingOffset = 0;
	limitCubeCamera = 0;
	minPitch = 0;
	maxPitch = 0;
	minHeading = 0;
	maxHeading = 0;
	dword_4C2C90 = 0;

	for (uint i = 0; i < 2048; i++)
		vars[i] = 0;

	vars[0] = 0;
	vars[1] = 1;

	inventoryCount = 0;

	for (uint i = 0; i < 7; i++)
		inventoryList[i] = 0;

	for (uint i = 0; i < 64; i++)
		zipDestinations[i] = 0;

	saveDay = 0;
	saveMonth = 0;
	saveYear = 0;
	saveHour = 0;
	saveMinute = 0;
}

GameState::GameState(Myst3Engine *vm):
		_vm(vm) {

#define VAR(var, x, unk) _varDescriptions.setVal(var, VarDescription(var, #x, unk));

	VAR(14, CursorTransparency, false)

	VAR(47, ProjectorAngleX, false)
	VAR(48, ProjectorAngleY, false)
	VAR(49, ProjectorAngleZoom, false)
	VAR(50, ProjectorAngleBlur, false)
	VAR(51, DraggedWeight, false)

	VAR(57, DragEnded, false)
	VAR(58, DragLeverSpeed, false)
	VAR(59, DragPositionFound, false)
	VAR(60, DragLeverPositionChanged, false)

	VAR(61, LocationAge, false)
	VAR(62, LocationRoom, false)
	VAR(63, LocationNode, false)
	VAR(64, BookSavedAge, false)
	VAR(65, BookSavedRoom, false)
	VAR(66, BookSavedNode, false)
	VAR(67, MenuSavedAge, false)
	VAR(68, MenuSavedRoom, false)
	VAR(69, MenuSavedNode, false)

	VAR(70, SecondsCountdown, false)
	VAR(71, FrameCountdown, false)

	// Counters, unused by the game scripts
	VAR(76, CounterUnk76, false)
	VAR(77, CounterUnk77, false)
	VAR(78, CounterUnk78, false)

	VAR(79, SweepEnabled, false)
	VAR(80, SweepValue, false)
	VAR(81, SweepStep, false)
	VAR(82, SweepMin, false)
	VAR(83, SweepMax, false)

	VAR(84, InputMousePressed, false)
	VAR(88, InputEscapePressed, false)
	VAR(89, InputTildePressed, false)
	VAR(90, InputSpacePressed, false)

	VAR(92, HotspotActiveRect, false)

	VAR(93, WaterEffectRunning, false)
	VAR(94, WaterEffectActive, false)
	VAR(95, WaterEffectSpeed, false)
	VAR(96, WaterEffectAttenuation, false)
	VAR(97, WaterEffectFrequency, false)
	VAR(98, WaterEffectAmpl, false)
	VAR(99, WaterEffectMaxStep, false)
	VAR(100, WaterEffectAmplOffset, false)

	VAR(101, LavaEffectActive, false)
	VAR(102, LavaEffectSpeed, false)
	VAR(103, LavaEffectAmpl, false)
	VAR(104, LavaEffectStepSize, false)

	VAR(105, MagnetEffectActive, false)
	VAR(106, MagnetEffectSpeed, false)
	VAR(107, MagnetEffectUnk1, false)
	VAR(108, MagnetEffectUnk2, false)
	VAR(109, MagnetEffectSound, false)
	VAR(110, MagnetEffectNode, false)
	VAR(111, MagnetEffectUnk3, false)

	VAR(112, ShakeEffectAmpl, false)
	VAR(113, ShakeEffectFramePeriod, false)
	VAR(114, RotationEffectSpeed, false)
	VAR(115, SunspotIntensity, false)
	VAR(116, SunspotColor, false)
	VAR(117, SunspotRadius, false)

	VAR(119, AmbiantFadeOutDelay, false)
	VAR(120, AmbiantPreviousFadeOutDelay, false)
	VAR(121, AmbientOverrideFadeOutDelay, false)
	VAR(122, SoundScriptsSuspended, false)

	VAR(124, SoundNextMultipleSounds, false)
	VAR(125, SoundNextIsChoosen, false)
	VAR(126, SoundNextId, false)
	VAR(127, SoundNextIsLast, false)
	VAR(128, SoundScriptsTimer, false)
	VAR(129, SoundScriptsPaused, false)
	VAR(130, SoundScriptFadeOutDelay, false)

	VAR(131, CursorLocked, false)
	VAR(132, CursorHidden, false)

	VAR(136, CameraPitch, false)
	VAR(137, CameraHeading, false)
	VAR(140, CameraMinPitch, false)
	VAR(141, CameraMaxPitch, false)

	VAR(142, MovieOverrideStartFrame, false)
	VAR(143, MovieOverrideEndFrame, false)
	VAR(144, MovieVolume1, false)
	VAR(145, MovieVolume2, false)
	VAR(146, MovieOverrideSubtitles, false)

	VAR(149, MovieConditionBit, false)
	VAR(150, MoviePreloadToMemory, false)
	VAR(151, MovieScriptDriven, false)
	VAR(152, MovieNextFrameSetVar, false)
	VAR(153, MovieNextFrameGetVar, false)
	VAR(154, MovieStartFrameVar, false)
	VAR(155, MovieEndFrameVar, false)
	VAR(156, MovieForce2d, false)
	VAR(157, MovieVolumeVar, false)
	VAR(158, MovieSoundHeading, false)
	VAR(159, MoviePanningStrenght, false)
	VAR(160, MovieSynchronized, false)

	// We ignore this, and never skip frames
	VAR(161, MovieNoFrameSkip, false)

	// Only play the audio track. This is used in TOHO 3 only.
	// Looks like it works fine without any specific implementation
	VAR(162, MovieAudioOnly, false)

	VAR(163, MovieOverrideCondition, false)
	VAR(164, MovieUVar, false)
	VAR(165, MovieVVar, false)
	VAR(166, MovieOverridePosition, false)
	VAR(167, MovieOverridePosU, false)
	VAR(168, MovieOverridePosV, false)
	VAR(169, MovieScale, false)
	VAR(170, MovieAdditiveBlending, false)
	VAR(171, MovieTransparency, false)
	VAR(172, MovieTransparencyVar, false)
	VAR(173, MoviePlayingVar, false)
	VAR(174, MovieStartSoundId, false)
	VAR(175, MovieStartSoundVolume, false)
	VAR(176, MovieStartSoundHeading, false)
	VAR(177, MovieStartSoundAttenuation, false)

	VAR(178, MovieUseBackground, false)
	VAR(179, CameraSkipAnimation, false)
	VAR(180, MovieAmbiantScriptStartFrame, false)
	VAR(181, MovieAmbiantScript, false)
	VAR(182, MovieScriptStartFrame, false)
	VAR(183, MovieScript, false)

	VAR(185, CameraMoveSpeed, false)

	// We always allow missing SpotItem data
	VAR(186, SpotItemAllowMissing, false)

	VAR(187, TransitionSound, false)
	VAR(188, TransitionSoundVolume, false)
	VAR(189, LocationNextNode, false)
	VAR(190, LocationNextRoom, false)
	VAR(191, LocationNextAge, false)

	VAR(195, BallPosition, false)
	VAR(196, BallFrame, false)
	VAR(197, BallLeverLeft, false)
	VAR(198, BallLeverRight, false)

	VAR(228, BallDoorOpen, false)

	VAR(243, ProjectorX, false)
	VAR(244, ProjectorY, false)
	VAR(245, ProjectorZoom, false)
	VAR(246, ProjectorBlur, false)
	VAR(247, ProjectorAngleXOffset, false)
	VAR(248, ProjectorAngleYOffset, false)
	VAR(249, ProjectorAngleZoomOffset, false)
	VAR(250, ProjectorAngleBlurOffset, false)

	VAR(277, JournalAtrusState, false)
	VAR(279, JournalSaavedroState, false)
	VAR(280, JournalSaavedroClosed, false)
	VAR(281, JournalSaavedroOpen, false)
	VAR(282, JournalSaavedroLastPage, false)
	VAR(283, JournalSaavedroChapter, false)
	VAR(284, JournalSaavedroPageInChapter, false)

	VAR(329, TeslaAllAligned, false)
	VAR(330, TeslaTopAligned, false)
	VAR(331, TeslaMiddleAligned, false)
	VAR(332, TeslaBottomAligned, false)
	VAR(333, TeslaMovieStart, false)

	// Amateria ambient sound / movie counters (XXXX 1001 and XXXX 1002)
	VAR(406, AmateriaSecondsCounter, false)
	VAR(407, AmateriaFramesCounter, false)

	VAR(444, ResonanceRingsSolved, false)

	VAR(460, PinballRemainingPegs, false)

	VAR(480, BookStateTomahna, false)
	VAR(481, BookStateReleeshahn, false)

	VAR(489, SymbolCode2Solved, false)
	VAR(495, SymbolCode1AllSolved, false)
	VAR(496, SymbolCode1CurrentSolved, false)
	VAR(497, SymbolCode1TopSolved, false)
	VAR(502, SymbolCode1LeftSolved, false)
	VAR(507, SymbolCode1RightSolved, false)

	VAR(540, SoundVoltaicUnk540, false)
	VAR(587, SoundEdannaUnk587, false)
	VAR(627, SoundAmateriaUnk627, false)
	VAR(930, SoundAmateriaUnk930, false)
	VAR(1031, SoundEdannaUnk1031, false)
	VAR(1146, SoundVoltaicUnk1146, false)

	VAR(1322, ZipModeEnabled, false)
	VAR(1323, SubtitlesEnabled, false)
	VAR(1324, WaterEffects, false)
	VAR(1325, TransitionSpeed, false)
	VAR(1326, MouseSpeed, false)
	VAR(1327, DialogResult, false)

	VAR(1337, MenuEscapePressed, false)
	VAR(1338, MenuNextAction, false)
	VAR(1339, MenuLoadBack, false)
	VAR(1340, MenuSaveBack, false)
	VAR(1341, MenuSaveAction, false)
	VAR(1342, MenuOptionsBack, false)

	VAR(1350, MenuSaveLoadPageLeft, false)
	VAR(1351, MenuSaveLoadPageRight, false)
	VAR(1352, MenuSaveLoadSelectedItem, false)
	VAR(1353, MenuSaveLoadCurrentPage, false)

	VAR(1374, OverallVolume, false)
	VAR(1377, MusicVolume, false)
	VAR(1380, MusicFrequency, false)
	VAR(1393, LanguageAudio, false)
	VAR(1394, LanguageText, false)

	VAR(1395, HotspotIgnoreClick, false)
	VAR(1396, HotspotHovered, false)
	VAR(1397, SpotSubtitle, false)

	// Override node from which effect masks are loaded
	// This is only used in LEIS x75, but is useless
	// since all the affected nodes have the same effect masks
	VAR(1398, EffectsOverrideMaskNode, false)

	VAR(1399, DragLeverLimited, false)
	VAR(1400, DragLeverLimitMin, false)
	VAR(1401, DragLeverLimitMax, false)

	// Mouse unk
	VAR(6, Unk6, true)

	// Backup var for opcodes 245, 246 => find usage
	VAR(13, Unk13, true)

	// ???
	VAR(147, MovieUnk147, true)
	VAR(148, MovieUnk148, true)

	// Menu stuff does not look like it's too useful
	VAR(1361, Unk1361, true)
	VAR(1362, Unk1362, true)
	VAR(1363, Unk1363, true)

#undef VAR

	newGame();
}

GameState::~GameState() {
}

void GameState::syncFloat(Common::Serializer &s, float &val,
		Common::Serializer::Version minVersion, Common::Serializer::Version maxVersion) {
	static const float precision = 10000.0;

	if (s.isLoading()) {
		int32 saved = 0;
		s.syncAsSint32LE(saved, minVersion, maxVersion);
		val = saved / precision;
	} else {
		int32 toSave = static_cast<int32>(val * precision);
		s.syncAsSint32LE(toSave, minVersion, maxVersion);
	}
}

void GameState::StateData::syncWithSaveGame(Common::Serializer &s) {
	if (!s.syncVersion(kSaveVersion))
		error("This savegame (v%d) is too recent (max %d) please get a newer version of ResidualVM", s.getVersion(), kSaveVersion);

	s.syncAsUint32LE(gameRunning);
	s.syncAsUint32LE(currentFrame);
	s.syncAsUint32LE(nextSecondsUpdate);
	s.syncAsUint32LE(secondsPlayed);
	s.syncAsUint32LE(dword_4C2C44);
	s.syncAsUint32LE(dword_4C2C48);
	s.syncAsUint32LE(dword_4C2C4C);
	s.syncAsUint32LE(dword_4C2C50);
	s.syncAsUint32LE(dword_4C2C54);
	s.syncAsUint32LE(dword_4C2C58);
	s.syncAsUint32LE(dword_4C2C5C);
	s.syncAsUint32LE(dword_4C2C60);
	s.syncAsUint32LE(currentNodeType);

	// The original engine (v148) saved the raw IEE754 data,
	// we save decimal data as fixed point instead to be achieve portability
	if (s.getVersion() < 149) {
		s.syncBytes((byte*) &lookatPitch, sizeof(float));
		s.syncBytes((byte*) &lookatHeading, sizeof(float));
		s.syncBytes((byte*) &lookatFOV, sizeof(float));
		s.syncBytes((byte*) &pitchOffset, sizeof(float));
		s.syncBytes((byte*) &headingOffset, sizeof(float));
	} else {
		syncFloat(s, lookatPitch);
		syncFloat(s, lookatHeading);
		syncFloat(s, lookatFOV);
		syncFloat(s, pitchOffset);
		syncFloat(s, headingOffset);
	}

	s.syncAsUint32LE(limitCubeCamera);

	if (s.getVersion() < 149) {
		s.syncBytes((byte*) &minPitch, sizeof(float));
		s.syncBytes((byte*) &maxPitch, sizeof(float));
		s.syncBytes((byte*) &minHeading, sizeof(float));
		s.syncBytes((byte*) &maxHeading, sizeof(float));
	} else {
		syncFloat(s, minPitch);
		syncFloat(s, maxPitch);
		syncFloat(s, minHeading);
		syncFloat(s, maxHeading);
	}

	s.syncAsUint32LE(dword_4C2C90);

	for (uint i = 0; i < 2048; i++)
		s.syncAsSint32LE(vars[i]);

	s.syncAsUint32LE(inventoryCount);

	for (uint i = 0; i < 7; i++)
		s.syncAsUint32LE(inventoryList[i]);

	for (uint i = 0; i < 64; i++)
		s.syncAsUint32LE(zipDestinations[i]);

	s.syncAsByte(saveDay, 149);
	s.syncAsByte(saveMonth, 149);
	s.syncAsUint16LE(saveYear, 149);
	s.syncAsByte(saveHour, 149);
	s.syncAsByte(saveMinute, 149);
	s.syncString(saveDescription, 149);

	if (s.isLoading()) {
		thumbnail = Common::SharedPtr<Graphics::Surface>(new Graphics::Surface(), Graphics::SharedPtrSurfaceDeleter());
		thumbnail->create(kThumbnailWidth, kThumbnailHeight, Graphics::PixelFormat(4, 8, 8, 8, 8, 16, 8, 0, 24));
	}
	assert(thumbnail && thumbnail->w == kThumbnailWidth && thumbnail->h == kThumbnailHeight);

	s.syncBytes((byte *)thumbnail->getPixels(), kThumbnailWidth * kThumbnailHeight * 4);
}

void GameState::newGame() {
	_data = StateData();
}

bool GameState::load(const Common::String &file) {
	Common::InSaveFile *saveFile = _vm->getSaveFileManager()->openForLoading(file);
	Common::Serializer s = Common::Serializer(saveFile, 0);
	_data.syncWithSaveGame(s);
	delete saveFile;

	_data.gameRunning = true;

	return true;
}

bool GameState::save(Common::OutSaveFile *saveFile) {
	Common::Serializer s = Common::Serializer(0, saveFile);

	// Update save creation info
	TimeDate t;
	g_system->getTimeAndDate(t);
	_data.saveYear = t.tm_year + 1900;
	_data.saveMonth = t.tm_mon + 1;
	_data.saveDay = t.tm_mday;
	_data.saveHour = t.tm_hour;
	_data.saveMinute = t.tm_min;

	_data.gameRunning = false;
	_data.syncWithSaveGame(s);
	_data.gameRunning = true;

	return true;
}

Graphics::Surface *GameState::getSaveThumbnail() const {
	return _data.thumbnail.get();
}

void GameState::setSaveThumbnail(Graphics::Surface *thumb) {
	if (_data.thumbnail.get() == thumb)
		return;

	_data.thumbnail = Common::SharedPtr<Graphics::Surface>(thumb, Graphics::SharedPtrSurfaceDeleter());
}

Common::Array<uint16> GameState::getInventory() {
	Common::Array<uint16> items;

	for (uint i = 0; i < _data.inventoryCount; i++)
		items.push_back(_data.inventoryList[i]);

	return items;
}

void GameState::updateInventory(const Common::Array<uint16> &items) {
	for (uint i = 0; i < 7; i++)
		_data.inventoryList[i] = 0;

	for (uint i = 0; i < items.size(); i++)
		_data.inventoryList[i] = items[i];

	_data.inventoryCount = items.size();
}

void GameState::checkRange(uint16 var) {
	if (var < 1 || var > 2047)
		error("Variable out of range %d", var);
}

int32 GameState::getVar(uint16 var) {
	checkRange(var);

	return _data.vars[var];
}

void GameState::setVar(uint16 var, int32 value) {
	checkRange(var);

	if (_varDescriptions.contains(var)) {
		const VarDescription &d = _varDescriptions.getVal(var);
		if (d.unknown)
			debugC(kDebugScript, "A script is writing to the unimplemented engine-mapped var %d (%s)", var, d.name);
	}

	_data.vars[var] = value;
}

bool GameState::evaluate(int16 condition) {
	uint16 unsignedCond = abs(condition);
	uint16 var = unsignedCond & 2047;

	switch (var) {
	case 84:
		setInputMousePressed(_vm->inputValidatePressed());
		break;
	case 88:
		setInputEscapePressed(_vm->inputEscapePressed());
		break;
	case 89:
		setInputTildePressed(_vm->inputTilePressed());
		break;
	case 90:
		setInputSpacePressed(_vm->inputSpacePressed());
		break;
	default:
		break;
	}

	int32 varValue = getVar(var);
	int32 targetValue = (unsignedCond >> 11) - 1;

	if (targetValue >= 0) {
		if (condition >= 0)
			return varValue == targetValue;
		else
			return varValue != targetValue;
	} else {
		if (condition >= 0)
			return varValue != 0;
		else
			return varValue == 0;
	}
}

int32 GameState::valueOrVarValue(int16 value) {
	if (value < 0)
		return getVar(-value);

	return value;
}

int32 GameState::engineGet(uint16 var) {
	if (!_varDescriptions.contains(var))
		error("The engine is trying to access an undescribed var (%d)", var);

	return _data.vars[var];
}

void GameState::engineSet(uint16 var, int32 value) {
	if (!_varDescriptions.contains(var))
		error("The engine is trying to access an undescribed var (%d)", var);

	_data.vars[var] = value;
}

const Common::String GameState::describeVar(uint16 var) {
	if (_varDescriptions.contains(var)) {
		const VarDescription &d = _varDescriptions.getVal(var);

		return Common::String::format("v%s", d.name);
	} else {
		return Common::String::format("v%d", var);
	}
}

const Common::String GameState::describeCondition(int16 condition) {
	uint16 unsignedCond = abs(condition);
	uint16 var = unsignedCond & 2047;
	int16 value = (unsignedCond >> 11) - 1;

	return Common::String::format("c[%s %s %d]",
			describeVar(var).c_str(),
			(condition >= 0 && value >= 0) || (condition < 0 && value < 0) ? "==" : "!=",
			value >= 0 ? value : 0);
}

void GameState::limitCubeCamera(float minPitch, float maxPitch, float minHeading, float maxHeading) {
	_data.limitCubeCamera = true;
	_data.minPitch = minPitch;
	_data.maxPitch = maxPitch;
	_data.minHeading = minHeading;
	_data.maxHeading = maxHeading;
}

void GameState::updateFrameCounters() {
	_data.currentFrame++;

	if (!_data.gameRunning)
		return;

	int32 frameCountdown = getFrameCountdown();
	if (frameCountdown > 0)
		setFrameCountdown(--frameCountdown);

	if (getAmateriaFramesCounter() > 0)
		setAmateriaFramesCounter(getAmateriaFramesCounter() - 1);

	uint32 currentTime = g_system->getMillis();
	if (currentTime > _data.nextSecondsUpdate || ABS<int32>(_data.nextSecondsUpdate - currentTime) > 2000) {
		_data.secondsPlayed++;
		_data.nextSecondsUpdate = currentTime + 1000;

		int32 secondsCountdown = getSecondsCountdown();
		if (secondsCountdown > 0)
			setSecondsCountdown(--secondsCountdown);

		if (getAmateriaSecondsCounter() > 0)
			setAmateriaSecondsCounter(getAmateriaSecondsCounter() - 1);

		if (getSoundScriptsTimer() > 0)
			setSoundScriptsTimer(getSoundScriptsTimer() - 1);
	}

	if (getSweepEnabled()) {
		if (getSweepValue() + getSweepStep() > getSweepMax()) {
			setSweepValue(getSweepMax());

			if (getSweepStep() > 0) {
				setSweepStep(-getSweepStep());
			}
		} else if (getSweepValue() + getSweepStep() < getSweepMin()) {
			setSweepValue(getSweepMin());

			if (getSweepStep() < 0) {
				setSweepStep(-getSweepStep());
			}
		} else {
			setSweepValue(getSweepValue() + getSweepStep());
		}
	}
}

bool GameState::isZipDestinationAvailable(uint16 node, uint16 room) {
	int32 zipBitIndex = _vm->_db->getNodeZipBitIndex(node, room);

	int32 arrayIndex = zipBitIndex / 32;
	assert(arrayIndex < 64);

	return (_data.zipDestinations[arrayIndex] & (1 << (zipBitIndex % 32))) != 0;
}

void GameState::markNodeAsVisited(uint16 node, uint16 room) {
	int32 zipBitIndex = _vm->_db->getNodeZipBitIndex(node, room);

	int32 arrayIndex = zipBitIndex / 32;
	assert(arrayIndex < 64);

	_data.zipDestinations[arrayIndex] |= 1 << (zipBitIndex % 32);
}

} // End of namespace Myst3
