/* 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/>.
 *
 */

#ifdef ENABLE_LOL

#include "kyra/engine/lol.h"
#include "kyra/graphics/screen_lol.h"

#include "common/savefile.h"
#include "common/substream.h"
#include "common/memstream.h"

#include "graphics/scaler.h"

namespace Kyra {

Common::Error LoLEngine::loadGameState(int slot) {
	const uint16 *cdf[] = { _charDefsMan, _charDefsWoman, _charDefsKieran, _charDefsMan, _charDefsAkshel };

	const char *fileName = getSavegameFilename(slot);

	SaveHeader header;
	Common::InSaveFile *saveFile = openSaveForReading(fileName, header);
	if (!saveFile) {
		_txt->printMessage(2, "%s", getLangString(0x425D));
		return Common::kNoError;
	}

	if (header.originalSave)
		warning("Trying to load savegame from original interpreter, while this is possible, it is not officially supported");

	_screen->fadeClearSceneWindow(10);
	completeDoorOperations();
	_screen->fillRect(112, 0, 287, 119, 0, 0);
	_screen->updateScreen();

	Common::SeekableSubReadStreamEndian in(saveFile, saveFile->pos(), saveFile->size(), !header.originalSave, DisposeAfterUse::YES);

	for (int i = 0; i < 4; i++) {
		LoLCharacter *c = &_characters[i];
		c->flags = in.readUint16();
		in.read(c->name, 11);
		c->raceClassSex = in.readByte();
		c->id = in.readSint16();
		c->curFaceFrame = in.readByte();
		c->tempFaceFrame = in.readByte();
		c->screamSfx = in.readByte();
		if (header.originalSave)
			in.skip(4);
		for (int ii = 0; ii < 8; ii++)
			c->itemsMight[ii] = in.readUint16();
		for (int ii = 0; ii < 8; ii++)
			c->protectionAgainstItems[ii] = in.readUint16();
		c->itemProtection = in.readUint16();
		c->hitPointsCur = in.readSint16();
		c->hitPointsMax = in.readUint16();
		c->magicPointsCur = in.readSint16();
		c->magicPointsMax = in.readUint16();
		c->field_41 = in.readByte();
		c->damageSuffered = in.readUint16();
		c->weaponHit = in.readUint16();
		c->totalMightModifier = in.readUint16();
		c->totalProtectionModifier = in.readUint16();
		c->might = in.readUint16();
		c->protection = in.readUint16();
		c->nextAnimUpdateCountdown = in.readSint16();
		for (int ii = 0; ii < 11; ii++)
			c->items[ii] = in.readUint16();
		for (int ii = 0; ii < 3; ii++)
			c->skillLevels[ii] = in.readByte();
		for (int ii = 0; ii < 3; ii++)
			c->skillModifiers[ii] = in.readSByte();
		for (int ii = 0; ii < 3; ii++)
			c->experiencePts[ii] = in.readUint32();
		for (int ii = 0; ii < 5; ii++)
			c->characterUpdateEvents[ii] = in.readByte();
		for (int ii = 0; ii < 5; ii++)
			c->characterUpdateDelay[ii] = in.readByte();

		if (c->flags & 1) {
			loadCharFaceShapes(i, c->id);
			c->defaultModifiers = cdf[c->raceClassSex];
		}
	}

	if (header.version < 17)
		in.skip(80);

	_currentBlock = in.readUint16();
	_partyPosX = in.readUint16();
	_partyPosY = in.readUint16();
	_updateFlags = in.readUint16();
	_scriptDirection = in.readByte();
	_selectedSpell = in.readByte();

	if (header.originalSave)
		in.skip(2);

	_sceneDefaultUpdate = in.readByte();
	_compassBroken = in.readByte();
	_drainMagic = in.readByte();
	_currentDirection = in.readUint16();
	_compassDirection = in.readUint16();
	_selectedCharacter = in.readSByte();

	if (header.originalSave)
		in.skip(1);

	_currentLevel = in.readByte();
	for (int i = 0; i < 48; i++)
		_inventory[i] = in.readSint16();
	_inventoryCurItem = in.readSint16();
	_itemInHand = in.readSint16();
	_lastMouseRegion = in.readSint16();

	if (header.originalSave || header.version <= 15) {
		uint16 flags[40];
		memset(flags, 0, sizeof(flags));

		if (header.version == 14) {
			for (int i = 0; i < 16; i++)
				flags[i] = in.readUint16();
			flags[26] = in.readUint16();
			flags[36] = in.readUint16();
		} else if (header.originalSave || header.version == 15) {
			for (int i = 0; i < 40; i++)
				flags[i] = in.readUint16();
		}

		memset(_flagsTable, 0, sizeof(_flagsTable));
		for (uint i = 0; i < ARRAYSIZE(flags); ++i) {
			for (uint k = 0; k < 16; ++k) {
				if (flags[i] & (1 << k))
					setGameFlag(((i << 4) & 0xFFF0) | (k & 0x000F));
			}
		}
	} else {
		uint32 flagsSize = in.readUint32();
		assert(flagsSize <= sizeof(_flagsTable));
		in.read(_flagsTable, flagsSize);
	}

	if (header.originalSave)
		in.skip(120);

	for (int i = 0; i < 24; i++)
		_globalScriptVars[i] = in.readUint16();

	if (header.originalSave)
		in.skip(152);

	_brightness = in.readByte();
	_lampOilStatus = in.readByte();
	_lampEffect = in.readSByte();

	if (header.originalSave)
		in.skip(1);

	_credits = in.readUint16();
	for (int i = 0; i < 8; i++)
		_globalScriptVars2[i] = in.readUint16();
	in.read(_availableSpells, 7);
	_hasTempDataFlags = in.readUint32();

	uint8 *origCmp = 0;
	if (header.originalSave) {
		in.skip(6);
		origCmp = new uint8[2496];
	}

	for (int i = 0; i < 400; i++) {
		LoLItem *t = &_itemsInPlay[i];
		t->nextAssignedObject = in.readUint16();
		t->nextDrawObject = in.readUint16();
		t->flyingHeight = in.readByte();
		t->block = in.readUint16();
		t->x = in.readUint16();
		t->y = in.readUint16();
		t->level = in.readSByte();
		t->itemPropertyIndex = in.readUint16();
		t->shpCurFrame_flg = in.readUint16();
		if (header.version < 17)
			in.skip(4);
	}

	for (int i = 0; i < 1024; i++) {
		LevelBlockProperty *l = &_levelBlockProperties[i];
		l->assignedObjects = l->drawObjects = 0;
		l->direction = 5;
	}

	for (int i = 0; i < 29; i++) {
		if (!(_hasTempDataFlags & (1 << i))) {
			if (header.originalSave) {
				if (in.size() - in.pos() >= 2500)
					in.skip(2500);
			}
			continue;
		}

		if (_lvlTempData[i]) {
			delete[] _lvlTempData[i]->wallsXorData;
			delete[] _lvlTempData[i]->flags;
			releaseMonsterTempData(_lvlTempData[i]);
			releaseFlyingObjectTempData(_lvlTempData[i]);
			releaseWallOfForceTempData(_lvlTempData[i]);
			delete _lvlTempData[i];
		}

		_lvlTempData[i] = new LevelTempData;
		_lvlTempData[i]->wallsXorData = new uint8[4096];
		_lvlTempData[i]->flags = new uint16[1024];
		LoLMonster *lm = new LoLMonster[30];
		_lvlTempData[i]->monsters = lm;
		FlyingObject *lf = new FlyingObject[_numFlyingObjects];
		_lvlTempData[i]->flyingObjects = lf;
		LevelTempData *l = _lvlTempData[i];

		uint32 next = in.pos() + 2500;

		if (header.originalSave) {
			in.skip(4);
			in.read(origCmp, in.readUint16());
			_screen->decodeFrame4(origCmp, _tempBuffer5120, 5120);
			memcpy(l->wallsXorData, _tempBuffer5120, 4096);
			for (int ii = 0; ii < 1024; ii++)
				l->flags[ii] = _tempBuffer5120[4096 + ii];
		} else {
			in.read(l->wallsXorData, 4096);
			for (int ii = 0; ii < 1024; ii++)
				l->flags[ii] = in.readByte();
		}

		if (header.originalSave)
			l->monsterDifficulty =  in.readUint16();

		for (int ii = 0; ii < 30; ii++) {
			LoLMonster *m = &lm[ii];
			m->nextAssignedObject = in.readUint16();
			m->nextDrawObject = in.readUint16();
			m->flyingHeight = in.readByte();
			m->block = in.readUint16();
			m->x = in.readUint16();
			m->y = in.readUint16();
			m->shiftStep = in.readSByte();
			m->destX = in.readUint16();
			m->destY = in.readUint16();
			m->destDirection = in.readByte();
			m->hitOffsX = in.readSByte();
			m->hitOffsY = in.readSByte();
			m->currentSubFrame = in.readByte();
			m->mode = in.readByte();
			m->fightCurTick = in.readSByte();
			m->id = in.readByte();
			m->direction = in.readByte();
			m->facing = in.readByte();
			m->flags = in.readUint16();
			m->damageReceived = in.readUint16();
			m->hitPoints = in.readSint16();
			m->speedTick = in.readByte();
			m->type = in.readByte();

			if (header.originalSave)
				in.skip(4);

			m->numDistAttacks = in.readByte();
			m->curDistWeapon = in.readByte();
			m->distAttackTick = in.readSByte();
			m->assignedItems = in.readUint16();
			m->properties = &_monsterProperties[m->type];
			in.read(m->equipmentShapes, 4);
		}

		for (int ii = 0; ii < _numFlyingObjects; ii++) {
			FlyingObject *m = &lf[ii];
			m->enable = in.readByte();
			m->objectType = in.readByte();
			m->attackerId = in.readUint16();
			m->item = in.readSint16();
			m->x = in.readUint16();
			m->y = in.readUint16();
			m->flyingHeight = in.readByte();
			m->direction = in.readByte();
			m->distance = in.readByte();
			m->field_D = in.readSByte();
			m->c = in.readByte();
			m->flags = in.readByte();
			m->wallFlags = in.readByte();
		}

		if (header.originalSave)
			in.seek(next, SEEK_SET);
		else
			l->monsterDifficulty =  in.readByte();
	}

	delete[] origCmp;

	calcCharPortraitXpos();
	memset(_moneyColumnHeight, 0, sizeof(_moneyColumnHeight));
	int t = _credits;
	_credits = 0;
	giveCredits(t, 0);
	setHandItem(_itemInHand);
	loadLevel(_currentLevel);
	gui_drawPlayField();
	restartPlayTimerAt(header.totalPlaySecs);

	timerSpecialCharacterUpdate(0);
	_flagsTable[73] |= 0x08;

	while (!_screen->isMouseVisible())
		_screen->showMouse();

	return Common::kNoError;
}

Common::Error LoLEngine::saveGameStateIntern(int slot, const char *saveName, const Graphics::Surface *thumbnail) {
	const char *fileName = getSavegameFilename(slot);

	Common::OutSaveFile *out = openSaveForWriting(fileName, saveName, thumbnail);
	if (!out)
		return _saveFileMan->getError();

	completeDoorOperations();
	generateTempData();

	for (int i = 0; i < 4; i++) {
		LoLCharacter *c = &_characters[i];
		out->writeUint16BE(c->flags);
		out->write(c->name, 11);
		out->writeByte(c->raceClassSex);
		out->writeSint16BE(c->id);
		out->writeByte(c->curFaceFrame);
		out->writeByte(c->tempFaceFrame);
		out->writeByte(c->screamSfx);
		for (int ii = 0; ii < 8; ii++)
			out->writeUint16BE(c->itemsMight[ii]);
		for (int ii = 0; ii < 8; ii++)
			out->writeUint16BE(c->protectionAgainstItems[ii]);
		out->writeUint16BE(c->itemProtection);
		out->writeSint16BE(c->hitPointsCur);
		out->writeUint16BE(c->hitPointsMax);
		out->writeSint16BE(c->magicPointsCur);
		out->writeUint16BE(c->magicPointsMax);
		out->writeByte(c->field_41);
		out->writeUint16BE(c->damageSuffered);
		out->writeUint16BE(c->weaponHit);
		out->writeUint16BE(c->totalMightModifier);
		out->writeUint16BE(c->totalProtectionModifier);
		out->writeUint16BE(c->might);
		out->writeUint16BE(c->protection);
		out->writeSint16BE(c->nextAnimUpdateCountdown);
		for (int ii = 0; ii < 11; ii++)
			out->writeUint16BE(c->items[ii]);
		for (int ii = 0; ii < 3; ii++)
			out->writeByte(c->skillLevels[ii]);
		for (int ii = 0; ii < 3; ii++)
			out->writeSByte(c->skillModifiers[ii]);
		for (int ii = 0; ii < 3; ii++)
			out->writeUint32BE(c->experiencePts[ii]);
		for (int ii = 0; ii < 5; ii++)
			out->writeByte(c->characterUpdateEvents[ii]);
		for (int ii = 0; ii < 5; ii++)
			out->writeByte(c->characterUpdateDelay[ii]);
	}

	out->writeUint16BE(_currentBlock);
	out->writeUint16BE(_partyPosX);
	out->writeUint16BE(_partyPosY);
	out->writeUint16BE(_updateFlags);
	out->writeByte(_scriptDirection);
	out->writeByte(_selectedSpell);
	out->writeByte(_sceneDefaultUpdate);
	out->writeByte(_compassBroken);
	out->writeByte(_drainMagic);
	out->writeUint16BE(_currentDirection);
	out->writeUint16BE(_compassDirection);
	out->writeSByte(_selectedCharacter);
	out->writeByte(_currentLevel);
	for (int i = 0; i < 48; i++)
		out->writeSint16BE(_inventory[i]);
	out->writeSint16BE(_inventoryCurItem);
	out->writeSint16BE(_itemInHand);
	out->writeSint16BE(_lastMouseRegion);
	out->writeUint32BE(ARRAYSIZE(_flagsTable));
	out->write(_flagsTable, ARRAYSIZE(_flagsTable));
	for (int i = 0; i < 24; i++)
		out->writeUint16BE(_globalScriptVars[i]);
	out->writeByte(_brightness);
	out->writeByte(_lampOilStatus);
	out->writeSByte(_lampEffect);
	out->writeUint16BE(_credits);
	for (int i = 0; i < 8; i++)
		out->writeUint16BE(_globalScriptVars2[i]);
	out->write(_availableSpells, 7);
	out->writeUint32BE(_hasTempDataFlags);

	resetItems(0);

	for (int i = 0; i < 400; i++) {
		LoLItem *t = &_itemsInPlay[i];
		out->writeUint16BE(t->nextAssignedObject);
		out->writeUint16BE(t->nextDrawObject);
		out->writeByte(t->flyingHeight);
		out->writeUint16BE(t->block);
		out->writeUint16BE(t->x);
		out->writeUint16BE(t->y);
		out->writeSByte(t->level);
		out->writeUint16BE(t->itemPropertyIndex);
		out->writeUint16BE(t->shpCurFrame_flg);
	}

	addLevelItems();

	for (int i = 0; i < 29; i++) {
		LevelTempData *l = _lvlTempData[i];
		if (!l || !(_hasTempDataFlags & (1 << i)))
			continue;

		out->write(l->wallsXorData, 4096);
		for (int ii = 0; ii < 1024; ii++)
			out->writeByte(l->flags[ii] & 0xFF);

		LoLMonster *lm = (LoLMonster *)_lvlTempData[i]->monsters;
		FlyingObject *lf = (FlyingObject *)_lvlTempData[i]->flyingObjects;

		for (int ii = 0; ii < 30; ii++) {
			LoLMonster *m = &lm[ii];
			out->writeUint16BE(m->nextAssignedObject);
			out->writeUint16BE(m->nextDrawObject);
			out->writeByte(m->flyingHeight);
			out->writeUint16BE(m->block);
			out->writeUint16BE(m->x);
			out->writeUint16BE(m->y);
			out->writeSByte(m->shiftStep);
			out->writeUint16BE(m->destX);
			out->writeUint16BE(m->destY);
			out->writeByte(m->destDirection);
			out->writeSByte(m->hitOffsX);
			out->writeSByte(m->hitOffsY);
			out->writeByte(m->currentSubFrame);
			out->writeByte(m->mode);
			out->writeSByte(m->fightCurTick);
			out->writeByte(m->id);
			out->writeByte(m->direction);
			out->writeByte(m->facing);
			out->writeUint16BE(m->flags);
			out->writeUint16BE(m->damageReceived);
			out->writeSint16BE(m->hitPoints);
			out->writeByte(m->speedTick);
			out->writeByte(m->type);
			out->writeByte(m->numDistAttacks);
			out->writeByte(m->curDistWeapon);
			out->writeSByte(m->distAttackTick);
			out->writeUint16BE(m->assignedItems);
			out->write(m->equipmentShapes, 4);
		}

		for (int ii = 0; ii < _numFlyingObjects; ii++) {
			FlyingObject *m = &lf[ii];
			out->writeByte(m->enable);
			out->writeByte(m->objectType);
			out->writeUint16BE(m->attackerId);
			out->writeSint16BE(m->item);
			out->writeUint16BE(m->x);
			out->writeUint16BE(m->y);
			out->writeByte(m->flyingHeight);
			out->writeByte(m->direction);
			out->writeByte(m->distance);
			out->writeSByte(m->field_D);
			out->writeByte(m->c);
			out->writeByte(m->flags);
			out->writeByte(m->wallFlags);
		}
		out->writeByte(l->monsterDifficulty);
	}

	out->finalize();

	// check for errors
	if (out->err()) {
		warning("Can't write file '%s'. (Disk full?)", fileName);
		return Common::kUnknownError;
	} else {
		debugC(1, kDebugLevelMain, "Saved game '%s.'", saveName);
	}

	delete out;
	return Common::kNoError;
}

Graphics::Surface *LoLEngine::generateSaveThumbnail() const {
	if (_flags.platform != Common::kPlatformPC98)
		return 0;

	uint8 *screenPal = new uint8[16 * 3];
	assert(screenPal);
	_screen->getRealPalette(0, screenPal);

	uint8 *screenBuf = new uint8[Screen::SCREEN_W * Screen::SCREEN_H];
	assert(screenBuf);

	Graphics::Surface *dst = new Graphics::Surface();
	assert(dst);

	_screen->copyRegionToBuffer(0, 0, 0, 320, 200, screenBuf);
	Screen_LoL::convertPC98Gfx(screenBuf, Screen::SCREEN_W, Screen::SCREEN_H, Screen::SCREEN_W);
	::createThumbnail(dst, screenBuf, Screen::SCREEN_W, Screen::SCREEN_H, screenPal);

	delete[] screenBuf;
	delete[] screenPal;
	return dst;
}

void LoLEngine::restoreBlockTempData(int levelIndex) {
	memset(_tempBuffer5120, 0, 5120);
	KyraRpgEngine::restoreBlockTempData(levelIndex);
	restoreTempDataAdjustMonsterStrength(levelIndex - 1);
}

void *LoLEngine::generateMonsterTempData(LevelTempData *tmp) {
	LoLMonster *m = new LoLMonster[30];
	memcpy(m, _monsters,  sizeof(LoLMonster) * 30);
	tmp->monsterDifficulty = _monsterDifficulty;
	return m;
}

void LoLEngine::restoreTempDataAdjustMonsterStrength(int index) {
	if (_lvlTempData[index]->monsterDifficulty == _monsterDifficulty)
		return;

	uint16 d = (_monsterModifiers1[_lvlTempData[index]->monsterDifficulty] << 8) / _monsterModifiers1[_monsterDifficulty];

	for (int i = 0; i < 30; i++) {
		if (_monsters[i].mode >= 14 || _monsters[i].block == 0 || _monsters[i].hitPoints <= 0)
			continue;

		_monsters[i].hitPoints = (d * _monsters[i].hitPoints) >> 8;
		if (_monsterDifficulty < _lvlTempData[index]->monsterDifficulty)
			_monsters[i].hitPoints++;
		if (_monsters[i].hitPoints == 0)
			_monsters[i].hitPoints = 1;
	}
}

void LoLEngine::restoreMonsterTempData(LevelTempData *tmp) {
	memcpy(_monsters, tmp->monsters, sizeof(LoLMonster) * 30);

	for (int i = 0; i < 30; i++) {
		if (_monsters[i].block) {
			_monsters[i].block = 0;
			_monsters[i].properties = &_monsterProperties[_monsters[i].type];
			placeMonster(&_monsters[i], _monsters[i].x, _monsters[i].y);
		}
	}
}

void LoLEngine::releaseMonsterTempData(LevelTempData *tmp) {
	LoLMonster *p = (LoLMonster *)tmp->monsters;
	delete[] p;
}

} // End of namespace Kyra

#endif // ENABLE_LOL
