/* 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/system.h"
#include "common/debug.h"
#include "common/error.h"
#include "common/file.h"
#include "common/stream.h"
#include "common/memstream.h"

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

namespace Adl {

#define IDI_HR6_NUM_ROOMS 35
#define IDI_HR6_NUM_MESSAGES 256
#define IDI_HR6_NUM_VARS 40
#define IDI_HR6_NUM_ITEM_DESCS 15
#define IDI_HR6_NUM_ITEM_PICS 15
#define IDI_HR6_NUM_ITEM_OFFSETS 16

// Messages used outside of scripts
#define IDI_HR6_MSG_CANT_GO_THERE      249
#define IDI_HR6_MSG_DONT_UNDERSTAND    247
#define IDI_HR6_MSG_ITEM_DOESNT_MOVE   253
#define IDI_HR6_MSG_ITEM_NOT_HERE      254
#define IDI_HR6_MSG_THANKS_FOR_PLAYING 252

struct DiskDataDesc {
	byte track;
	byte sector;
	byte offset;
	byte volume;
};

class HiRes6Engine : public AdlEngine_v4 {
public:
	HiRes6Engine(OSystem *syst, const AdlGameDescription *gd) :
			AdlEngine_v4(syst, gd),
			_boot(nullptr),
			_currVerb(0),
			_currNoun(0) {
	}

	~HiRes6Engine() { delete _boot; }

private:
	// AdlEngine
	void runIntro() const;
	void init();
	void initGameState();
	void printRoomDescription();
	void showRoom();
	Common::String formatVerbError(const Common::String &verb) const;
	Common::String formatNounError(const Common::String &verb, const Common::String &noun) const;

	// AdlEngine_v2
	void printString(const Common::String &str);

	void loadDisk(byte disk);

	DiskImage *_boot;
	byte _currVerb, _currNoun;
	Common::Array<DiskDataDesc> _diskDataDesc;
};

static const char *disks[] = { "DARK1A.DSK", "DARK1B.NIB", "DARK2A.NIB", "DARK2B.NIB" };

#define SECTORS_PER_TRACK 16
#define BYTES_PER_SECTOR 256

static Common::MemoryReadStream *loadSectors(DiskImage *disk, byte track, byte sector = SECTORS_PER_TRACK - 1, byte count = SECTORS_PER_TRACK) {
	const int bufSize = count * BYTES_PER_SECTOR;
	byte *const buf = (byte *)malloc(bufSize);
	byte *p = buf;

	while (count-- > 0) {
		StreamPtr stream(disk->createReadStream(track, sector, 0, 0));
		stream->read(p, BYTES_PER_SECTOR);

		if (stream->err() || stream->eos())
			error("Error loading from disk image");

		p += BYTES_PER_SECTOR;
		if (sector > 0)
			--sector;
		else {
			++track;

			// Skip VTOC track
			if (track == 17)
				++track;

			sector = SECTORS_PER_TRACK - 1;
		}
	}

	return new Common::MemoryReadStream(buf, bufSize, DisposeAfterUse::YES);
}

void HiRes6Engine::runIntro() const {
	DiskImage *boot(new DiskImage());

	if (!boot->open(disks[0]))
		error("Failed to open disk image '%s'", disks[0]);

	StreamPtr stream(loadSectors(boot, 11, 1, 96));

	_display->setMode(DISPLAY_MODE_HIRES);
	_display->loadFrameBuffer(*stream);
	_display->updateHiResScreen();
	delay(256 * 8609 / 1000);

	_display->loadFrameBuffer(*stream);
	_display->updateHiResScreen();
	delay(256 * 8609 / 1000);

	_display->loadFrameBuffer(*stream);

	delete boot;

	// Load copyright string from boot file
	Files_DOS33 *files(new Files_DOS33());

	if (!files->open(disks[0]))
		error("Failed to open disk image '%s'", disks[0]);

	stream.reset(files->createReadStream("\010\010\010\010\010\010"));
	Common::String copyright(readStringAt(*stream, 0x103, APPLECHAR('\r')));

	delete files;

	_display->updateHiResScreen();
	_display->home();
	_display->setMode(DISPLAY_MODE_MIXED);
	_display->moveCursorTo(Common::Point(0, 21));
	_display->printString(copyright);
	delay(256 * 8609 / 1000);
}

void HiRes6Engine::init() {
	_boot = new DiskImage();
	_graphics = new Graphics_v2(*_display);

	if (!_boot->open(disks[0]))
		error("Failed to open disk image '%s'", disks[0]);

	StreamPtr stream(loadSectors(_boot, 0x7));

	// Read parser messages
	_strings.verbError = readStringAt(*stream, 0x666);
	_strings.nounError = readStringAt(*stream, 0x6bd);
	_strings.enterCommand = readStringAt(*stream, 0x6e9);

	// Read line feeds
	_strings.lineFeeds = readStringAt(*stream, 0x408);

	// Read opcode strings (TODO)
	_strings_v2.saveInsert = readStringAt(*stream, 0xad8);
	readStringAt(*stream, 0xb95); // Confirm save
	// _strings_v2.saveReplace
	_strings_v2.restoreInsert = readStringAt(*stream, 0xc07);
	// _strings_v2.restoreReplace
	_strings.playAgain = readStringAt(*stream, 0xcdf, 0xff);

	_messageIds.cantGoThere = IDI_HR6_MSG_CANT_GO_THERE;
	_messageIds.dontUnderstand = IDI_HR6_MSG_DONT_UNDERSTAND;
	_messageIds.itemDoesntMove = IDI_HR6_MSG_ITEM_DOESNT_MOVE;
	_messageIds.itemNotHere = IDI_HR6_MSG_ITEM_NOT_HERE;
	_messageIds.thanksForPlaying = IDI_HR6_MSG_THANKS_FOR_PLAYING;

	// Item descriptions
	stream.reset(loadSectors(_boot, 0x6, 0xb, 2));
	stream->seek(0x16);
	loadItemDescriptions(*stream, IDI_HR6_NUM_ITEM_DESCS);

	// Load dropped item offsets
	stream.reset(_boot->createReadStream(0x8, 0x9, 0x16));
	loadDroppedItemOffsets(*stream, IDI_HR6_NUM_ITEM_OFFSETS);

	// Location of game data for each disc
	stream.reset(_boot->createReadStream(0x5, 0xa, 0x03));
	for (uint i = 0; i < sizeof(disks); ++i) {
		DiskDataDesc desc;
		desc.track = stream->readByte();
		desc.sector = stream->readByte();
		desc.offset = stream->readByte();
		desc.volume = stream->readByte();
		_diskDataDesc.push_back(desc);
	}

	// DataBlockPtr offsets for each disk
	stream.reset(_boot->createReadStream(0x3, 0xf, 0x03));
	for (uint i = 0; i < sizeof(disks); ++i) {
		DiskOffset offset;
		offset.track = stream->readByte();
		offset.sector = stream->readByte();
		_diskOffsets.push_back(offset);
	}
}

void HiRes6Engine::loadDisk(byte disk) {
	delete _disk;
	_disk = new DiskImage();

	if (!_disk->open(disks[disk]))
		error("Failed to open disk image '%s'", disks[disk]);

	_curDisk = 0;

	// Load item picture data (indexed on boot disk)
	StreamPtr stream(_boot->createReadStream(0xb, 0xd, 0x08));
	_itemPics.clear();
	loadItemPictures(*stream, IDI_HR6_NUM_ITEM_PICS);

	_curDisk = disk;

	byte track = _diskDataDesc[disk].track;
	byte sector = _diskDataDesc[disk].sector;
	uint offset = _diskDataDesc[disk].offset;

	applyDiskOffset(track, sector);

	for (uint block = 0; block < 7; ++block) {
		stream.reset(_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 (addr) {
		case 0x9000: {
			// Messages
			_messages.clear();
			uint count = size / 4;
			loadMessages(*stream, count);
			break;
		}
		case 0x4a80: {
			// Global pics
			_pictures.clear();
			loadPictures(*stream);
			break;
		}
		case 0x4000:
			// Verbs
			loadWords(*stream, _verbs, _priVerbs);
			break;
		case 0x1800:
			// Nouns
			loadWords(*stream, _nouns, _priNouns);
			break;
		case 0x0e00: {
			// Rooms
			uint count = size / 14 - 1;
			stream->skip(14); // Skip invalid room 0

			_state.rooms.clear();
			loadRooms(*stream, count);
			break;
		}
		case 0x7b00:
			// Global commands
			readCommands(*stream, _globalCommands);
			break;
		case 0x9500:
			// Room commands
			readCommands(*stream, _roomCommands);
			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;
			}
		}
	}
}

void HiRes6Engine::initGameState() {
	_state.vars.resize(IDI_HR6_NUM_VARS);

	loadDisk(1);

	StreamPtr stream(_boot->createReadStream(0x3, 0xe, 0x03));

	loadItems(*stream);

	_currVerb = _currNoun = 0;
}

void HiRes6Engine::showRoom() {
	_state.curPicture = getCurRoom().curPicture;

	bool redrawPic = false;

	if (getVar(26) == 0xfe)
		setVar(26, 0);
	else if (getVar(26) != 0xff)
		setVar(26, _state.room);

	if (_state.room != _roomOnScreen) {
		loadRoom(_state.room);

		if (getVar(26) < 0x80 && getCurRoom().isFirstTime)
			setVar(26, 0);

		clearScreen();

		if (!_state.isDark)
			redrawPic = true;
	} else {
		if (getCurRoom().curPicture != _picOnScreen || _itemRemoved)
			redrawPic = true;
	}

	if (redrawPic) {
		_roomOnScreen = _state.room;
		_picOnScreen = getCurRoom().curPicture;

		drawPic(getCurRoom().curPicture);
		_itemRemoved = false;
		_itemsOnScreen = 0;

		Common::List<Item>::iterator item;
		for (item = _state.items.begin(); item != _state.items.end(); ++item)
			item->isOnScreen = false;
	}

	if (!_state.isDark)
		drawItems();

	_display->updateHiResScreen();
	setVar(2, 0xff);
	printString(_roomData.description);

	// FIXME: move to main loop?
	_linesPrinted = 0;
}

Common::String HiRes6Engine::formatVerbError(const Common::String &verb) const {
	Common::String err = _strings.verbError;

	for (uint i = 0; i < verb.size(); ++i)
		err.setChar(verb[i], i + 24);

	err.setChar(APPLECHAR(' '), 32);

	uint i = 24;
	while (err[i] != APPLECHAR(' '))
		++i;

	err.setChar(APPLECHAR('.'), i);

	return err;
}

Common::String HiRes6Engine::formatNounError(const Common::String &verb, const Common::String &noun) const {
	Common::String err = _strings.nounError;

	for (uint i = 0; i < noun.size(); ++i)
		err.setChar(noun[i], i + 24);

	for (uint  i = 35; i > 31; --i)
		err.setChar(APPLECHAR(' '), i);

	uint i = 24;
	while (err[i] != APPLECHAR(' '))
		++i;

	err.setChar(APPLECHAR('I'), i + 1);
	err.setChar(APPLECHAR('S'), i + 2);
	err.setChar(APPLECHAR('.'), i + 3);

	return err;
}

void HiRes6Engine::printString(const Common::String &str) {
	Common::String s;
	uint found = 0;

	// This does not emulate the corner cases of the original, hence this check
	if (getVar(27) > 1)
		error("Invalid value %i encountered for variable 27", getVar(27));

	for (uint i = 0; i < str.size(); ++i) {
		if (str[i] == '%') {
			++found;
			if (found == 3)
				found = 0;
		} else {
			if (found == 0 || found - 1 == getVar(27))
				s += str[i];
		}
	}

	if (getVar(2) != 0xff) {
		AdlEngine_v2::printString(s);
	} else {
		if (getVar(26) == 0) {
			if (str.size() != 1 || APPLECHAR(str[0]) != APPLECHAR(' '))
				return AdlEngine_v2::printString(s);
			setVar(2, APPLECHAR(' '));
		} else if (getVar(26) != 0xff) {
			setVar(2, 'P');
		} else {
			setVar(26, _state.room);
			setVar(2, 1);
		}

		doAllCommands(_globalCommands, _currVerb, _currNoun);
	}
}

Engine *HiRes6Engine_create(OSystem *syst, const AdlGameDescription *gd) {
	return new HiRes6Engine(syst, gd);
}

} // End of namespace Adl
