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

#include "adl/adl_v4.h"
#include "adl/display.h"

namespace Adl {

AdlEngine_v4::AdlEngine_v4(OSystem *syst, const AdlGameDescription *gd) :
		AdlEngine_v3(syst, gd),
		_itemPicIndex(nullptr) {

}

AdlEngine_v4::~AdlEngine_v4() {
	delete _itemPicIndex;
}

void AdlEngine_v4::gameLoop() {
	uint verb = 0, noun = 0;
	_isRestarting = false;

	if (_isRestoring) {
		// Game restored from launcher. As this version of ADL long jumps to
		// the game loop after restoring, no special action is required.
		_isRestoring = false;
	}

	showRoom();

	if (_isRestarting || shouldQuit())
		return;

	_canSaveNow = _canRestoreNow = true;
	getInput(verb, noun);
	_canSaveNow = _canRestoreNow = false;

	if (_isRestoring) {
		// Game restored from GMM. Move cursor to next line and jump to
		// start of game loop.
		_display->printAsciiString("\r");
		_isRestoring = false;
		return;
	}

	if (_isRestarting || shouldQuit())
		return;

	_linesPrinted = 0;

	checkInput(verb, noun);

	if (_isRestarting || shouldQuit())
		return;

	doAllCommands(_globalCommands, verb, noun);

	if (_isRestarting || shouldQuit())
		return;

	_state.moves++;
}

void AdlEngine_v4::loadState(Common::ReadStream &stream) {
	_state.room = stream.readByte();
	_state.region = stream.readByte();
	_state.prevRegion = stream.readByte();

	uint32 size = stream.readUint32BE();
	if (size != _state.regions.size())
		error("Region count mismatch (expected %i; found %i)", _state.regions.size(), size);

	for (auto &region : _state.regions) {
		size = stream.readUint32BE();
		if (size != region.rooms.size())
			error("Room count mismatch (expected %i; found %i)", region.rooms.size(), size);

		for (auto &room : region.rooms) {
			room.picture = stream.readByte();
			room.isFirstTime = stream.readByte();
		}

		size = stream.readUint32BE();
		if (size != region.vars.size())
			error("Variable count mismatch (expected %i; found %i)", region.vars.size(), size);

		for (uint i = 0; i < region.vars.size(); ++i)
			region.vars[i] = stream.readByte();
	}

	size = stream.readUint32BE();
	if (size != _state.items.size())
		error("Item count mismatch (expected %i; found %i)", _state.items.size(), size);

	for (auto &item : _state.items) {
		item.room = stream.readByte();
		item.region = stream.readByte();
		item.state = stream.readByte();
	}

	size = stream.readUint32BE();
	const uint expectedSize = _state.vars.size() - getRegion(1).vars.size();
	if (size != expectedSize)
		error("Variable count mismatch (expected %i; found %i)", expectedSize, size);

	for (uint i = getRegion(1).vars.size(); i < _state.vars.size(); ++i)
		_state.vars[i] = stream.readByte();

	if (stream.err() || stream.eos())
		return;

	loadRegion(_state.region);
	restoreRoomState(_state.room);
	_roomOnScreen = _picOnScreen = 0;
}

void AdlEngine_v4::saveState(Common::WriteStream &stream) {
	getCurRoom().isFirstTime = false;
	backupVars();
	backupRoomState(_state.room);

	stream.writeByte(_state.room);
	stream.writeByte(_state.region);
	stream.writeByte(_state.prevRegion);

	stream.writeUint32BE(_state.regions.size());
	for (const auto &region : _state.regions) {
		stream.writeUint32BE(region.rooms.size());
		for (const auto &room : region.rooms) {
			stream.writeByte(room.picture);
			stream.writeByte(room.isFirstTime);
		}

		stream.writeUint32BE(region.vars.size());
		for (uint i = 0; i < region.vars.size(); ++i)
			stream.writeByte(region.vars[i]);
	}

	stream.writeUint32BE(_state.items.size());
	for (const auto &item : _state.items) {
		stream.writeByte(item.room);
		stream.writeByte(item.region);
		stream.writeByte(item.state);
	}

	stream.writeUint32BE(_state.vars.size() - getRegion(1).vars.size());
	for (uint i = getRegion(1).vars.size(); i < _state.vars.size(); ++i)
		stream.writeByte(_state.vars[i]);
}

Common::String AdlEngine_v4::loadMessage(uint idx) const {
	if (_messages[idx]) {
		Common::StreamPtr strStream(_messages[idx]->createReadStream());
		return readString(*strStream, 0xff, "AVISDURGAN");
	}

	return Common::String();
}

Common::String AdlEngine_v4::getItemDescription(const Item &item) const {
	return _itemDesc[item.id - 1];
}

void AdlEngine_v4::loadRegionLocations(Common::ReadStream &stream, uint regions) {
	for (uint r = 0; r < regions; ++r) {
		RegionLocation loc;
		loc.track = stream.readByte();
		loc.sector = stream.readByte();

		if (stream.eos() || stream.err())
			error("Failed to read region locations");

		_regionLocations.push_back(loc);
	}
}

void AdlEngine_v4::loadRegionInitDataOffsets(Common::ReadStream &stream, uint regions) {
	for (uint r = 0; r < regions; ++r) {
		RegionInitDataOffset initOfs;
		initOfs.track = stream.readByte();
		initOfs.sector = stream.readByte();
		initOfs.offset = stream.readByte();
		initOfs.volume = stream.readByte();

		if (stream.eos() || stream.err())
			error("Failed to read region init data offsets");

		_regionInitDataOffsets.push_back(initOfs);
	}
}

void AdlEngine_v4::initRoomState(RoomState &roomState) const {
	roomState.picture = 1;
	roomState.isFirstTime = 1;
}

void AdlEngine_v4::initRegions(const byte *roomsPerRegion, uint regions) {
	_state.regions.resize(regions);

	for (uint r = 0; r < regions; ++r) {
		Region &regn = _state.regions[r];
		// Each region has 24 variables
		regn.vars.resize(24);

		regn.rooms.resize(roomsPerRegion[r]);
		for (uint rm = 0; rm < roomsPerRegion[r]; ++rm)
			initRoomState(regn.rooms[rm]);
	}
}

void AdlEngine_v4::fixupDiskOffset(byte &track, byte &sector) const {
	if (_state.region == 0)
		return;

	sector += _regionLocations[_state.region - 1].sector;
	if (sector >= 16) {
		sector -= 16;
		++track;
	}

	track += _regionLocations[_state.region - 1].track;
}

void AdlEngine_v4::adjustDataBlockPtr(byte &track, byte &sector, byte &offset, byte &size) const {
	fixupDiskOffset(track, sector);
}

AdlEngine_v4::RegionChunkType AdlEngine_v4::getRegionChunkType(const uint16 addr) const {
	switch (addr) {
		case 0x9000:
			return kRegionChunkMessages;
		case 0x4a80:
			return kRegionChunkGlobalPics;
		case 0x4000:
			return kRegionChunkVerbs;
		case 0x1800:
			return kRegionChunkNouns;
		case 0x0e00:
			return kRegionChunkRooms;
		case 0x7b00:
			return kRegionChunkRoomCmds;
		case 0x9500:
			return kRegionChunkGlobalCmds;
		default:
			return kRegionChunkUnknown;
	}
}

void AdlEngine_v4::loadRegion(byte region) {
	if (_currentVolume != _regionInitDataOffsets[region - 1].volume) {
		insertDisk(_regionInitDataOffsets[region - 1].volume);

		// FIXME: This shouldn't be needed, but currently is, due to
		// implementation choices made earlier on for DataBlockPtr and DiskImage.
		_state.region = 0; // To avoid region offset being applied
		_itemPics.clear();
		_itemPicIndex->seek(0);
		loadItemPictures(*_itemPicIndex, _itemPicIndex->size() / 5);
	}

	_state.region = region;

	byte track = _regionInitDataOffsets[region - 1].track;
	byte sector = _regionInitDataOffsets[region - 1].sector;
	uint offset = _regionInitDataOffsets[region - 1].offset;

	fixupDiskOffset(track, sector);

	for (uint block = 0; block < 7; ++block) {
		Common::StreamPtr stream(_disk->createReadStream(track, sector, offset, 1));

		uint16 addr = stream->readUint16LE();
		uint16 size = stream->readUint16LE();

		stream.reset(_disk->createReadStream(track, sector, offset, size / 256 + 1));
		stream->skip(4);

		switch (getRegionChunkType(addr)) {
		case kRegionChunkMessages: {
			// Messages
			_messages.clear();
			uint count = size / 4;
			loadMessages(*stream, count);
			break;
		}
		case kRegionChunkGlobalPics: {
			// Global pics
			_pictures.clear();
			loadPictures(*stream);
			break;
		}
		case kRegionChunkVerbs:
			// Verbs
			loadWords(*stream, _verbs, _priVerbs);
			break;
		case kRegionChunkNouns:
			// Nouns
			loadWords(*stream, _nouns, _priNouns);
			break;
		case kRegionChunkRooms: {
			// Rooms
			uint count = size / 14 - 1;
			stream->skip(14); // Skip invalid room 0

			_state.rooms.clear();
			loadRooms(*stream, count);
			break;
		}
		case kRegionChunkRoomCmds:
			// Room commands
			readCommands(*stream, _roomCommands);
			break;
		case kRegionChunkGlobalCmds:
			// Global commands
			readCommands(*stream, _globalCommands);
			break;
		default:
			error("Unknown data block found (addr %04x; size %04x)", addr, size);
		}

		offset += 4 + size;
		while (offset >= 256) {
			offset -= 256;
			++sector;
			if (sector >= 16) {
				sector = 0;
				++track;
			}
		}
	}

	applyRegionWorkarounds();
	restoreVars();
}

void AdlEngine_v4::loadItemPicIndex(Common::ReadStream &stream, uint items) {
	_itemPicIndex = stream.readStream(items * 5);

	if (stream.eos() || stream.err())
		error("Error reading item index");
}

void AdlEngine_v4::backupRoomState(byte room) {
	RoomState &backup = getCurRegion().rooms[room - 1];

	backup.isFirstTime = getRoom(room).isFirstTime;
	backup.picture = getRoom(room).picture;
}

byte AdlEngine_v4::restoreRoomState(byte room) {
	const RoomState &backup = getCurRegion().rooms[room - 1];

	if (backup.isFirstTime != 1) {
		getRoom(room).curPicture = getRoom(room).picture = backup.picture;
		getRoom(room).isFirstTime = false;
		return 0;
	}

	// WORKAROUND: Fix for bug #15379: "ADL: HIRES5: Game unsolvable after
	// loading savegame. Save state not properly restored?".
	// There's a problem in the original engine when a picture is set for a
	// room that hasn't been visited yet. If the user then saves before
	// visiting that room, the picture change will be ignored when play is
	// resumed from that savegame.
	if (backup.picture != 1) {
		warning("Detected picture change for unvisited room %d in region %d", room, _state.region);
		getRoom(room).curPicture = getRoom(room).picture = backup.picture;
	}

	return 1;
}

void AdlEngine_v4::backupVars() {
	Region &region = getCurRegion();

	for (uint i = 0; i < region.vars.size(); ++i)
		region.vars[i] = getVar(i);
}

void AdlEngine_v4::restoreVars() {
	const Region &region = getCurRegion();

	for (uint i = 0; i < region.vars.size(); ++i)
		setVar(i, region.vars[i]);
}

void AdlEngine_v4::switchRegion(byte region) {
	backupVars();
	backupRoomState(_state.room);
	_state.prevRegion = _state.region;
	_state.region = region;
	loadRegion(region);
	_state.room = 1;
	_picOnScreen = _roomOnScreen = 0;
}

void AdlEngine_v4::switchRoom(byte roomNr) {
	getCurRoom().curPicture = getCurRoom().picture;
	getCurRoom().isFirstTime = false;
	backupRoomState(_state.room);
	_state.room = roomNr;
	restoreRoomState(_state.room);
}

void AdlEngine_v4::setupOpcodeTables() {
	AdlEngine_v3::setupOpcodeTables();

	_condOpcodes[0x08] = opcode(&AdlEngine_v4::o_isVarGT);
	_condOpcodes[0x0a].reset();

	_actOpcodes[0x0a] = opcode(&AdlEngine_v4::o_setRegionToPrev);
	_actOpcodes[0x0b].reset();
	_actOpcodes[0x0e] = opcode(&AdlEngine_v4::o_setRegion);
	_actOpcodes[0x12] = opcode(&AdlEngine_v4::o_setRegionRoom);
	_actOpcodes[0x13].reset();
	_actOpcodes[0x1e].reset();
	_actOpcodes[0x1f].reset();
}

int AdlEngine_v4::o_isItemInRoom(ScriptEnv &e) {
	OP_DEBUG_2("\t&& GET_ITEM_ROOM(%s) == %s", itemStr(e.arg(1)).c_str(), itemRoomStr(e.arg(2)).c_str());

	const Item &item = getItem(e.arg(1));

	if (e.arg(2) != IDI_ANY && item.region != _state.region)
		return -1;

	if (item.room == roomArg(e.arg(2)))
		return 2;

	return -1;
}

int AdlEngine_v4::o_isVarGT(ScriptEnv &e) {
	OP_DEBUG_2("\t&& VARS[%d] > %d", e.arg(1), e.arg(2));

	if (getVar(e.arg(1)) > e.arg(2))
		return 2;

	return -1;
}

int AdlEngine_v4::o_moveItem(ScriptEnv &e) {
	AdlEngine_v3::o_moveItem(e);
	getItem(e.arg(1)).region = _state.region;
	return 2;
}

int AdlEngine_v4::o_setRegionToPrev(ScriptEnv &e) {
	OP_DEBUG_0("\tREGION = PREV_REGION");

	switchRegion(_state.prevRegion);
	// Long jump
	_isRestarting = true;
	return -1;
}

int AdlEngine_v4::o_moveAllItems(ScriptEnv &e) {
	OP_DEBUG_2("\tMOVE_ALL_ITEMS(%s, %s)", itemRoomStr(e.arg(1)).c_str(), itemRoomStr(e.arg(2)).c_str());

	byte room1 = roomArg(e.arg(1));

	if (room1 == _state.room)
		_picOnScreen = 0;

	byte room2 = roomArg(e.arg(2));

	for (auto &item : _state.items) {
		if (room1 != item.room)
			continue;

		if (room1 != IDI_ANY) {
			if (_state.region != item.region)
				continue;
			if (room2 == IDI_ANY) {
				if (isInventoryFull())
					break;
				if (item.state == IDI_ITEM_DOESNT_MOVE)
					continue;
			}
		}

		item.room = room2;
		item.region = _state.region;

		if (room1 == IDI_ANY)
			item.state = IDI_ITEM_DROPPED;
	}

	return 2;
}

int AdlEngine_v4::o_setRegion(ScriptEnv &e) {
	OP_DEBUG_1("\tREGION = %d", e.arg(1));

	switchRegion(e.arg(1));
	// Long jump
	_isRestarting = true;
	return -1;
}

int AdlEngine_v4::o_save(ScriptEnv &e) {
	OP_DEBUG_0("\tSAVE_GAME()");

	_display->printString(_strings_v2.saveReplace);
	const char key = inputKey();

	if (shouldQuit())
		return -1;

	if (key != _display->asciiToNative('Y'))
		return 0;

	const int slot = askForSlot(_strings_v2.saveInsert);

	if (slot < 0)
		return -1;

	saveGameState(slot, "");
	return 0;
}

int AdlEngine_v4::o_restore(ScriptEnv &e) {
	OP_DEBUG_0("\tRESTORE_GAME()");

	const int slot = askForSlot(_strings_v2.restoreInsert);

	if (slot < 0)
		return -1;

	loadGameState(slot);
	_isRestoring = false;

	_picOnScreen = 0;
	_roomOnScreen = 0;

	// Long jump
	_isRestarting = true;
	return -1;
}

int AdlEngine_v4::o_restart(ScriptEnv &e) {
	OP_DEBUG_0("\tRESTART_GAME()");

	while (true) {
		_display->printString(_strings.playAgain);
		const Common::String input(inputString());

		if (shouldQuit())
			return -1;

		if (input.firstChar() == _display->asciiToNative('N')) {
			return o_quit(e);
		} else if (input.firstChar() == _display->asciiToNative('Y')) {
			// The original game loads a special save game from volume 3
			initState();
			// Long jump
			_isRestarting = true;
			return -1;
		}
	}
}

int AdlEngine_v4::o_setRegionRoom(ScriptEnv &e) {
	OP_DEBUG_2("\tSET_REGION_ROOM(%d, %d)", e.arg(1), e.arg(2));

	switchRegion(e.arg(1));
	_state.room = e.arg(2);
	// Long jump
	_isRestarting = true;
	return -1;
}

int AdlEngine_v4::o_setRoomPic(ScriptEnv &e) {
	AdlEngine_v3::o_setRoomPic(e);
	backupRoomState(e.arg(1));
	return 2;
}

} // End of namespace Adl
