/* 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 "agi/preagi/preagi.h"
#include "agi/preagi/winnie.h"
#include "agi/graphics.h"

#include "graphics/cursorman.h"

#include "common/events.h"
#include "common/memstream.h"
#include "common/savefile.h"
#include "common/textconsole.h"

#include "audio/mididrv.h"

namespace Agi {

void WinnieEngine::parseRoomHeader(WTP_ROOM_HDR *roomHdr, byte *buffer, int len) {
	int i;

	Common::MemoryReadStreamEndian readS(buffer, len, _isBigEndian);

	roomHdr->roomNumber = readS.readByte();
	roomHdr->objId = readS.readByte();
	roomHdr->ofsPic = readS.readUint16();
	roomHdr->fileLen = readS.readUint16();
	roomHdr->reserved0 = readS.readUint16();

	for (i = 0; i < IDI_WTP_MAX_DIR; i++)
		roomHdr->roomNew[i] = readS.readByte();

	roomHdr->objX = readS.readByte();
	roomHdr->objY = readS.readByte();

	roomHdr->reserved1 = readS.readUint16();

	for (i = 0; i < IDI_WTP_MAX_BLOCK; i++)
		roomHdr->ofsDesc[i] = readS.readUint16();

	for (i = 0; i < IDI_WTP_MAX_BLOCK; i++)
		roomHdr->ofsBlock[i] = readS.readUint16();

	for (i = 0; i < IDI_WTP_MAX_STR; i++)
		roomHdr->ofsStr[i] = readS.readUint16();

	roomHdr->reserved2 = readS.readUint32();

	for (i = 0; i < IDI_WTP_MAX_BLOCK; i++)
		for (byte j = 0; j < IDI_WTP_MAX_BLOCK; j++)
			roomHdr->opt[i].ofsOpt[j] = readS.readUint16();
}

void WinnieEngine::parseObjHeader(WTP_OBJ_HDR *objHdr, byte *buffer, int len) {
	int i;

	Common::MemoryReadStreamEndian readS(buffer, len, _isBigEndian);

	objHdr->fileLen = readS.readUint16();
	objHdr->objId = readS.readUint16();

	for (i = 0; i < IDI_WTP_MAX_OBJ_STR_END; i++)
		objHdr->ofsEndStr[i] = readS.readUint16();

	for (i = 0; i < IDI_WTP_MAX_OBJ_STR; i++)
		objHdr->ofsStr[i] = readS.readUint16();

	objHdr->ofsPic = readS.readUint16();
}

uint32 WinnieEngine::readRoom(int iRoom, uint8 *buffer, WTP_ROOM_HDR &roomHdr) {
	Common::String fileName;

	if (getPlatform() == Common::kPlatformDOS)
		fileName = Common::String::format(IDS_WTP_ROOM_DOS, iRoom);
	else if (getPlatform() == Common::kPlatformAmiga)
		fileName = Common::String::format(IDS_WTP_ROOM_AMIGA, iRoom);
	else if (getPlatform() == Common::kPlatformC64)
		fileName = Common::String::format(IDS_WTP_ROOM_C64, iRoom);
	else if (getPlatform() == Common::kPlatformApple2)
		fileName = Common::String::format(IDS_WTP_ROOM_APPLE, iRoom);

	Common::File file;
	if (!file.open(fileName)) {
		warning("Could not open file \'%s\'", fileName.c_str());
		return 0;
	}

	uint32 filelen = file.size();
	if (getPlatform() == Common::kPlatformC64) { // Skip the loading address
		filelen -= 2;
		file.seek(2, SEEK_CUR);
	}

	memset(buffer, 0, 4096);
	file.read(buffer, filelen);
	file.close();

	parseRoomHeader(&roomHdr, buffer, filelen);

	return filelen;
}

uint32 WinnieEngine::readObj(int iObj, uint8 *buffer) {
	Common::String fileName;

	if (getPlatform() == Common::kPlatformDOS)
		fileName = Common::String::format(IDS_WTP_OBJ_DOS, iObj);
	else if (getPlatform() == Common::kPlatformAmiga)
		fileName = Common::String::format(IDS_WTP_OBJ_AMIGA, iObj);
	else if (getPlatform() == Common::kPlatformC64)
		fileName = Common::String::format(IDS_WTP_OBJ_C64, iObj);
	else if (getPlatform() == Common::kPlatformApple2)
		fileName = Common::String::format(IDS_WTP_OBJ_APPLE, iObj);

	Common::File file;
	if (!file.open(fileName)) {
		warning("Could not open file \'%s\'", fileName.c_str());
		return 0;
	}

	uint32 filelen = file.size();
	if (getPlatform() == Common::kPlatformC64) { // Skip the loading address
		filelen -= 2;
		file.seek(2, SEEK_CUR);
	}

	memset(buffer, 0, 2048);
	file.read(buffer, filelen);
	file.close();
	return filelen;
}

void WinnieEngine::randomize() {
	int iObj = 0;
	int iRoom = 0;
	bool done;

	for (int i = 0; i < IDI_WTP_MAX_OBJ_MISSING; i++) {
		done = false;

		while (!done) {
			iObj = rnd(IDI_WTP_MAX_OBJ - 1);
			done = true;

			for (int j = 0; j < IDI_WTP_MAX_OBJ_MISSING; j++) {
				if (_gameStateWinnie.iUsedObj[j] == iObj) {
					done = false;
					break;
				}
			}
		}

		_gameStateWinnie.iUsedObj[i] = iObj;

		done = false;
		while (!done) {
			iRoom = rnd(IDI_WTP_MAX_ROOM_NORMAL);
			done = true;

			for (int j = 0; j < IDI_WTP_MAX_ROOM_OBJ; j++) {
				if (_gameStateWinnie.iObjRoom[j] == iRoom) {
					done = false;
					break;
				}
			}
		}

		_gameStateWinnie.iObjRoom[iObj] = iRoom;
	}
}

void WinnieEngine::intro() {
	drawPic(IDS_WTP_FILE_LOGO);
	printStr(IDS_WTP_INTRO_0);
	g_system->updateScreen();
	_system->delayMillis(0x640);

	if (getPlatform() == Common::kPlatformAmiga)
		_gfx->clearDisplay(0);

	drawPic(IDS_WTP_FILE_TITLE);

	printStr(IDS_WTP_INTRO_1);
	g_system->updateScreen();
	_system->delayMillis(0x640);

	if (!playSound(IDI_WTP_SND_POOH_0))
		return;

	if (!playSound(IDI_WTP_SND_POOH_1))
		return;

	if (!playSound(IDI_WTP_SND_POOH_2))
		return;
}

int WinnieEngine::getObjInRoom(int iRoom) {
	for (int iObj = 1; iObj < IDI_WTP_MAX_ROOM_OBJ; iObj++)
		if (_gameStateWinnie.iObjRoom[iObj] == iRoom)
			return iObj;
	return 0;
}

void WinnieEngine::setTakeDrop(int fCanSel[]) {
	fCanSel[IDI_WTP_SEL_TAKE] = getObjInRoom(_room);
	fCanSel[IDI_WTP_SEL_DROP] = _gameStateWinnie.iObjHave;
}

void WinnieEngine::setWinnieFlag(int iFlag) {
	_gameStateWinnie.fGame[iFlag] = 1;
}

void WinnieEngine::clearWinnieFlag(int iFlag) {
	_gameStateWinnie.fGame[iFlag] = 0;
}

int WinnieEngine::parser(int pc, int index, uint8 *buffer) {
	WTP_ROOM_HDR hdr;
	int startpc = pc;
	int8 opcode;
	int iNewRoom = 0;

	int iSel, iDir, iBlock;
	int fCanSel[IDI_WTP_SEL_LAST + 1];
	char szMenu[121] = {0};
	bool done;
	int fBlock;

	// extract header from buffer
	parseRoomHeader(&hdr, buffer, sizeof(WTP_ROOM_HDR));

	while (!shouldQuit()) {
		pc = startpc;

		// check if block is to be run

		iBlock = *(buffer + pc++);
		if (iBlock == 0)
			return IDI_WTP_PAR_OK;

		fBlock = *(buffer + pc++);
		if (_gameStateWinnie.fGame[iBlock] != fBlock)
			return IDI_WTP_PAR_OK;

		// extract text from block

		opcode = *(buffer + pc);
		switch (opcode) {
		case 0:
		case IDO_WTP_OPTION_0:
		case IDO_WTP_OPTION_1:
		case IDO_WTP_OPTION_2:
			// clear fCanSel block
			memset(fCanSel, 0, sizeof(fCanSel));

			// check if NSEW directions should be displayed
			if (hdr.roomNew[0]) {
				fCanSel[IDI_WTP_SEL_NORTH] = fCanSel[IDI_WTP_SEL_SOUTH] =
				fCanSel[IDI_WTP_SEL_EAST] = fCanSel[IDI_WTP_SEL_WEST] = true;
			}

			// check if object in room or player carrying one
			setTakeDrop(fCanSel);

			// check which rows have a menu option
			for (iSel = 0; iSel < IDI_WTP_MAX_OPTION; iSel++) {
				opcode = *(buffer + pc++);
				if (opcode) {
					fCanSel[opcode - IDO_WTP_OPTION_0] = true;
					fCanSel[iSel + IDI_WTP_SEL_REAL_OPT_1] = opcode - 0x14;
				}
			}

			// extract menu string
			Common::strlcpy(szMenu, (char *)(buffer + pc), 121);
			XOR80(szMenu);
			break;
		default:
			// print description
			printStrWinnie((char *)(buffer + pc));
			if (getSelection(kSelBackspace) == 1)
				return IDI_WTP_PAR_OK;
			else
				return IDI_WTP_PAR_BACK;
		}

		// input handler

		done = false;
		while (!done) {
			// run wind if it's time
			if (_doWind)
				wind();

			// get menu selection
			getMenuSel(szMenu, &iSel, fCanSel);

			if (++_gameStateWinnie.nMoves == IDI_WTP_MAX_MOVES_UNTIL_WIND)
				_doWind = true;

			if (_winnieEvent && (_room <= IDI_WTP_MAX_ROOM_TELEPORT)) {
				if (!_tiggerMist) {
					_tiggerMist = 1;
					tigger();
				} else {
					_tiggerMist = 0;
					mist();
				}
				_winnieEvent = false;
				return IDI_WTP_PAR_GOTO;
			}

			// process selection
			switch (iSel) {
			case IDI_WTP_SEL_HOME:
				switch (_room) {
				case IDI_WTP_ROOM_HOME:
				case IDI_WTP_ROOM_MIST:
				case IDI_WTP_ROOM_TIGGER:
					break;
				default:
					_room = IDI_WTP_ROOM_HOME;
					return IDI_WTP_PAR_GOTO;
				}
				break;
			case IDI_WTP_SEL_BACK:
				return IDI_WTP_PAR_BACK;
			case IDI_WTP_SEL_OPT_1:
			case IDI_WTP_SEL_OPT_2:
			case IDI_WTP_SEL_OPT_3:
				done = true;
				break;
			case IDI_WTP_SEL_NORTH:
			case IDI_WTP_SEL_SOUTH:
			case IDI_WTP_SEL_EAST:
			case IDI_WTP_SEL_WEST:
				iDir = iSel - IDI_WTP_SEL_NORTH;

				if (hdr.roomNew[iDir] == IDI_WTP_ROOM_NONE) {
					printStr(IDS_WTP_CANT_GO);
					getSelection(kSelAnyKey);
				} else {
					_room = hdr.roomNew[iDir];
					return IDI_WTP_PAR_GOTO;
				}
				break;
			case IDI_WTP_SEL_TAKE:
				takeObj(_room);
				setTakeDrop(fCanSel);
				break;
			case IDI_WTP_SEL_DROP:
				dropObj(_room);
				setTakeDrop(fCanSel);
				break;
			default:
				break;
			}
		}

		// jump to the script block of the selected option
		pc = hdr.opt[index].ofsOpt[iSel] - _roomOffset;

		opcode = *(buffer + pc);
		if (!opcode) pc++;

		// process script
		do {
			opcode = *(buffer + pc++);
			switch (opcode) {
			case IDO_WTP_GOTO_ROOM:
				opcode = *(buffer + pc++);
				iNewRoom = opcode;
				break;
			case IDO_WTP_PRINT_MSG:
				opcode = *(buffer + pc++);
				printRoomStr(_room, opcode);
				getSelection(kSelAnyKey);
				break;
			case IDO_WTP_PRINT_STR:
				opcode = *(buffer + pc++);
				printRoomStr(_room, opcode);
				break;
			case IDO_WTP_DROP_OBJ:
				opcode = *(buffer + pc++);
				opcode = -1;
				dropObjRnd();
				break;
			case IDO_WTP_FLAG_CLEAR:
				opcode = *(buffer + pc++);
				clearWinnieFlag(opcode);
				break;
			case IDO_WTP_FLAG_SET:
				opcode = *(buffer + pc++);
				setWinnieFlag(opcode);
				break;
			case IDO_WTP_GAME_OVER:
				gameOver();
				break;
			case IDO_WTP_WALK_MIST:
				_mist--;
				if (!_mist) {
					_room = rnd(IDI_WTP_MAX_ROOM_TELEPORT) + 1;
					return IDI_WTP_PAR_GOTO;
				}
				break;
			case IDO_WTP_PLAY_SOUND:
				opcode = *(buffer + pc++);
				playSound((ENUM_WTP_SOUND)opcode);
				break;
			case IDO_WTP_SAVE_GAME:
				saveGame();
				_room = IDI_WTP_ROOM_HOME;
				return IDI_WTP_PAR_GOTO;
			case IDO_WTP_LOAD_GAME:
				loadGame();
				_room = IDI_WTP_ROOM_HOME;
				return IDI_WTP_PAR_GOTO;
			case IDO_WTP_OWL_HELP:
				opcode = *(buffer + pc++);
				showOwlHelp();
				break;
			case IDO_WTP_GOTO_RND:
				_room = rnd(IDI_WTP_MAX_ROOM_TELEPORT) + 1;
				return IDI_WTP_PAR_GOTO;
			default:
				opcode = 0;
				break;
			}
		} while (opcode && !shouldQuit());

		if (iNewRoom) {
			_room = iNewRoom;
			return IDI_WTP_PAR_GOTO;
		}

		if (iBlock == 1)
			return IDI_WTP_PAR_OK;
		g_system->updateScreen();
	}

	return IDI_WTP_PAR_OK;
}

void WinnieEngine::keyHelp() {
	playSound(IDI_WTP_SND_KEYHELP);
	printStr(IDS_WTP_HELP_0);
	getSelection(kSelAnyKey);
	printStr(IDS_WTP_HELP_1);
	getSelection(kSelAnyKey);
}

void WinnieEngine::inventory() {
	if (_gameStateWinnie.iObjHave)
		printObjStr(_gameStateWinnie.iObjHave, IDI_WTP_OBJ_TAKE);
	else {
		clearTextArea();
		drawStr(IDI_WTP_ROW_MENU, IDI_WTP_COL_MENU, IDA_DEFAULT, IDS_WTP_INVENTORY_0);
	}

	Common::String missing = Common::String::format(IDS_WTP_INVENTORY_1, _gameStateWinnie.nObjMiss);

	drawStr(IDI_WTP_ROW_OPTION_4, IDI_WTP_COL_MENU, IDA_DEFAULT, missing.c_str());
	g_system->updateScreen();
	getSelection(kSelAnyKey);
}

void WinnieEngine::printObjStr(int iObj, int iStr) {
	WTP_OBJ_HDR hdr;
	uint8 *buffer = (uint8 *)malloc(2048);

	readObj(iObj, buffer);
	parseObjHeader(&hdr, buffer, sizeof(hdr));

	printStrWinnie((char *)(buffer + hdr.ofsStr[iStr] - _objOffset));
	free(buffer);
}

bool WinnieEngine::isRightObj(int iRoom, int iObj, int *iCode) {
	WTP_ROOM_HDR roomhdr;
	WTP_OBJ_HDR objhdr;
	uint8 *roomdata = (uint8 *)malloc(4096);
	uint8 *objdata = (uint8 *)malloc(2048);

	readRoom(iRoom, roomdata, roomhdr);
	readObj(iObj, objdata);
	parseObjHeader(&objhdr, objdata, sizeof(WTP_OBJ_HDR));

	free(roomdata);
	free(objdata);

	*iCode = objhdr.objId;

	if (objhdr.objId == 11) objhdr.objId = 34;

	if (roomhdr.objId == objhdr.objId)
		return true;
	else
		return false;
}

void WinnieEngine::takeObj(int iRoom) {
	if (_gameStateWinnie.iObjHave) {
		// player is already carrying an object, can't take
		printStr(IDS_WTP_CANT_TAKE);
		getSelection(kSelAnyKey);
	} else {
		// take object
		int iObj = getObjInRoom(iRoom);

		_gameStateWinnie.iObjHave = iObj;
		_gameStateWinnie.iObjRoom[iObj] = 0;

		printStr(IDS_WTP_OK);
		playSound(IDI_WTP_SND_TAKE);

		drawRoomPic();

		// print object "take" string
		printObjStr(_gameStateWinnie.iObjHave, IDI_WTP_OBJ_TAKE);
		getSelection(kSelAnyKey);

		// HACK WARNING
		if (iObj == 18) {
			_gameStateWinnie.fGame[0x0d] = 1;
		}
	}
}

void WinnieEngine::dropObj(int iRoom) {
	int iCode;

	if (getObjInRoom(iRoom)) {
		// there already is an object in the room, can't drop
		printStr(IDS_WTP_CANT_DROP);
		getSelection(kSelAnyKey);
	} else {
		// HACK WARNING
		if (_gameStateWinnie.iObjHave == 18) {
			_gameStateWinnie.fGame[0x0d] = 0;
		}

		if (isRightObj(iRoom, _gameStateWinnie.iObjHave, &iCode)) {
			// object has been dropped in the right place
			printStr(IDS_WTP_OK);
			getSelection(kSelAnyKey);
			playSound(IDI_WTP_SND_DROP_OK);
			printObjStr(_gameStateWinnie.iObjHave, IDI_WTP_OBJ_DROP);
			getSelection(kSelAnyKey);

			// increase amount of objects returned, decrease amount of objects missing
			_gameStateWinnie.nObjMiss--;
			_gameStateWinnie.nObjRet++;

			// xor the dropped object with 0x80 to signify it has been dropped in the right place
			for (int i = 0; i < IDI_WTP_MAX_OBJ_MISSING; i++) {
				if (_gameStateWinnie.iUsedObj[i] == _gameStateWinnie.iObjHave) {
					_gameStateWinnie.iUsedObj[i] ^= 0x80;
					break;
				}
			}

			// set flag according to dropped object's id
			_gameStateWinnie.fGame[iCode] = 1;

			// player is carrying nothing
			_gameStateWinnie.iObjHave = 0;

			if (!_gameStateWinnie.nObjMiss) {
				// all objects returned, tell player to find party
				playSound(IDI_WTP_SND_FANFARE);
				printStr(IDS_WTP_GAME_OVER_0);
				getSelection(kSelAnyKey);
				printStr(IDS_WTP_GAME_OVER_1);
				getSelection(kSelAnyKey);
			}
		} else {
			// drop object in the given room
			_gameStateWinnie.iObjRoom[_gameStateWinnie.iObjHave] = iRoom;

			// object has been dropped in the wrong place
			printStr(IDS_WTP_WRONG_PLACE);
			getSelection(kSelAnyKey);

			playSound(IDI_WTP_SND_DROP);
			drawRoomPic();

			printStr(IDS_WTP_WRONG_PLACE);
			getSelection(kSelAnyKey);

			// print object description
			printObjStr(_gameStateWinnie.iObjHave, IDI_WTP_OBJ_DESC);
			getSelection(kSelAnyKey);

			_gameStateWinnie.iObjHave = 0;
		}
	}
}

void WinnieEngine::dropObjRnd() {
	if (!_gameStateWinnie.iObjHave)
		return;

	int iRoom = 0;
	bool done = false;

	while (!done) {
		iRoom = rnd(IDI_WTP_MAX_ROOM_NORMAL);
		done = true;
		if (iRoom == _room)
			done = false;
		for (int j = 0; j < IDI_WTP_MAX_ROOM_OBJ; j++) {
			if (_gameStateWinnie.iObjRoom[j] == iRoom) {
				done = false;
			}
		}
	}

	_gameStateWinnie.iObjRoom[_gameStateWinnie.iObjHave] = iRoom;
	_gameStateWinnie.iObjHave = 0;
}

void WinnieEngine::wind() {
	int iRoom = 0;
	bool done;

	_doWind = 0;
	_gameStateWinnie.nMoves = 0;
	if (!_gameStateWinnie.nObjMiss)
		return;

	printStr(IDS_WTP_WIND_0);
	playSound(IDI_WTP_SND_WIND_0);
	getSelection(kSelAnyKey);

	printStr(IDS_WTP_WIND_1);
	playSound(IDI_WTP_SND_WIND_0);
	getSelection(kSelAnyKey);

	dropObjRnd();

	// randomize positions of objects at large
	for (int i = 0; i < IDI_WTP_MAX_OBJ_MISSING; i++) {
		if (!(_gameStateWinnie.iUsedObj[i] & IDI_XOR_KEY)) {
			done = false;
			while (!done) {
				iRoom = rnd(IDI_WTP_MAX_ROOM_NORMAL);
				done = true;

				for (int j = 0; j < IDI_WTP_MAX_ROOM_OBJ; j++) {
					if (_gameStateWinnie.iObjRoom[j] == iRoom) {
						done = false;
					}
				}
			}
			_gameStateWinnie.iObjRoom[_gameStateWinnie.iUsedObj[i]] = iRoom;
		}
	}
}

void WinnieEngine::mist() {
	// mist length in turns is (2-5)
	_mist = rnd(4) + 2;

	_room = IDI_WTP_ROOM_MIST;
	drawRoomPic();

	printStr(IDS_WTP_MIST);
}

void WinnieEngine::tigger() {
	_room = IDI_WTP_ROOM_TIGGER;

	drawRoomPic();
	printStr(IDS_WTP_TIGGER);

	dropObjRnd();
}

void WinnieEngine::showOwlHelp() {
	if (_gameStateWinnie.iObjHave) {
		printStr(IDS_WTP_OWL_0);
		getSelection(kSelAnyKey);
		printObjStr(_gameStateWinnie.iObjHave, IDI_WTP_OBJ_HELP);
		getSelection(kSelAnyKey);
	}
	if (getObjInRoom(_room)) {
		printStr(IDS_WTP_OWL_0);
		getSelection(kSelAnyKey);
		printObjStr(getObjInRoom(_room), IDI_WTP_OBJ_HELP);
		getSelection(kSelAnyKey);
	}
}


void WinnieEngine::drawMenu(char *szMenu, int iSel, int fCanSel[]) {
	int iRow = 0, iCol = 0;

	clearTextArea();
	drawStr(IDI_WTP_ROW_MENU, IDI_WTP_COL_MENU, IDA_DEFAULT, szMenu);

	if (fCanSel[IDI_WTP_SEL_NORTH])
		drawStr(IDI_WTP_ROW_OPTION_4, IDI_WTP_COL_NSEW, IDA_DEFAULT, IDS_WTP_NSEW);
	if (fCanSel[IDI_WTP_SEL_TAKE])
		drawStr(IDI_WTP_ROW_OPTION_4, IDI_WTP_COL_TAKE, IDA_DEFAULT, IDS_WTP_TAKE);
	if (fCanSel[IDI_WTP_SEL_DROP])
		drawStr(IDI_WTP_ROW_OPTION_4, IDI_WTP_COL_DROP, IDA_DEFAULT, IDS_WTP_DROP);

	switch (iSel) {
	case IDI_WTP_SEL_OPT_1:
	case IDI_WTP_SEL_OPT_2:
	case IDI_WTP_SEL_OPT_3:
		iRow = IDI_WTP_ROW_OPTION_1 + iSel;
		iCol = IDI_WTP_COL_OPTION;
		break;
	case IDI_WTP_SEL_NORTH:
		iRow = IDI_WTP_ROW_OPTION_4;
		iCol = IDI_WTP_COL_NORTH;
		break;
	case IDI_WTP_SEL_SOUTH:
		iRow = IDI_WTP_ROW_OPTION_4;
		iCol = IDI_WTP_COL_SOUTH;
		break;
	case IDI_WTP_SEL_EAST:
		iRow = IDI_WTP_ROW_OPTION_4;
		iCol = IDI_WTP_COL_EAST;
		break;
	case IDI_WTP_SEL_WEST:
		iRow = IDI_WTP_ROW_OPTION_4;
		iCol = IDI_WTP_COL_WEST;
		break;
	case IDI_WTP_SEL_TAKE:
		iRow = IDI_WTP_ROW_OPTION_4;
		iCol = IDI_WTP_COL_TAKE;
		break;
	case IDI_WTP_SEL_DROP:
		iRow = IDI_WTP_ROW_OPTION_4;
		iCol = IDI_WTP_COL_DROP;
		break;
	default:
		break;
	}
	drawStr(iRow, iCol - 1, IDA_DEFAULT, ">");
	g_system->updateScreen();
}

void WinnieEngine::incMenuSel(int *iSel, int fCanSel[]) {
	do {
		*iSel += 1;
		if (*iSel > IDI_WTP_SEL_DROP) *iSel = IDI_WTP_SEL_OPT_1;
	} while (!fCanSel[*iSel]);
}

void WinnieEngine::decMenuSel(int *iSel, int fCanSel[]) {
	do {
		*iSel -= 1;
		if (*iSel < IDI_WTP_SEL_OPT_1) *iSel = IDI_WTP_SEL_DROP;
	} while (!fCanSel[*iSel]);
}

void WinnieEngine::getMenuMouseSel(int *iSel, int fCanSel[], int x, int y) {
	switch (y) {
	case IDI_WTP_ROW_OPTION_1:
	case IDI_WTP_ROW_OPTION_2:
	case IDI_WTP_ROW_OPTION_3:
		if (fCanSel[y - IDI_WTP_ROW_OPTION_1])  *iSel = y - IDI_WTP_ROW_OPTION_1;
		break;
	case IDI_WTP_ROW_OPTION_4:
		if (fCanSel[IDI_WTP_SEL_NORTH] && (x > IDI_WTP_COL_NORTH - 1) && (x < 6)) *iSel = IDI_WTP_SEL_NORTH;
		if (fCanSel[IDI_WTP_SEL_SOUTH] && (x > IDI_WTP_COL_SOUTH - 1) && (x < 13)) *iSel = IDI_WTP_SEL_SOUTH;
		if (fCanSel[IDI_WTP_SEL_EAST] && (x > IDI_WTP_COL_EAST - 1) && (x < 19)) *iSel = IDI_WTP_SEL_EAST;
		if (fCanSel[IDI_WTP_SEL_WEST] && (x > IDI_WTP_COL_WEST - 1) && (x < 25)) *iSel = IDI_WTP_SEL_WEST;
		if (fCanSel[IDI_WTP_SEL_TAKE] && (x > IDI_WTP_COL_TAKE - 1) && (x < 33)) *iSel = IDI_WTP_SEL_TAKE;
		if (fCanSel[IDI_WTP_SEL_DROP] && (x > IDI_WTP_COL_DROP - 1) && (x < 39)) *iSel = IDI_WTP_SEL_DROP;
		break;
	default:
		break;
	}
}

void WinnieEngine::makeSel(int *iSel, int fCanSel[]) {
	if (fCanSel[*iSel])
		return;

	keyHelp();
	clrMenuSel(iSel, fCanSel);
}

void WinnieEngine::getMenuSel(char *szMenu, int *iSel, int fCanSel[]) {
	Common::Event event;
	int x, y;

	clrMenuSel(iSel, fCanSel);
	drawMenu(szMenu, *iSel, fCanSel);

	// Show the mouse cursor for the menu
	CursorMan.showMouse(true);

	while (!shouldQuit()) {
		while (_system->getEventManager()->pollEvent(event)) {
			switch (event.type) {
			case Common::EVENT_RETURN_TO_LAUNCHER:
			case Common::EVENT_QUIT:
				return;
			case Common::EVENT_MOUSEMOVE:
				x = event.mouse.x / 8;
				y = event.mouse.y / 8;
				getMenuMouseSel(iSel, fCanSel, x, y);

				// Change cursor
				if (fCanSel[IDI_WTP_SEL_NORTH] && hotspotNorth.contains(event.mouse.x, event.mouse.y)) {
					//_gfx->setCursorPalette(true);
					// ????
				} else if (fCanSel[IDI_WTP_SEL_SOUTH] && hotspotSouth.contains(event.mouse.x, event.mouse.y)) {
					//_gfx->setCursorPalette(true);
				} else if (fCanSel[IDI_WTP_SEL_WEST] && hotspotWest.contains(event.mouse.x, event.mouse.y)) {
					//_gfx->setCursorPalette(true);
				} else if (fCanSel[IDI_WTP_SEL_EAST] && hotspotEast.contains(event.mouse.x, event.mouse.y)) {
					//_gfx->setCursorPalette(true);
				} else {
					//_gfx->setCursorPalette(false);
				}

				break;
			case Common::EVENT_LBUTTONUP:
				// Click to move
				if (fCanSel[IDI_WTP_SEL_NORTH] && hotspotNorth.contains(event.mouse.x, event.mouse.y)) {
					*iSel = IDI_WTP_SEL_NORTH;
					makeSel(iSel, fCanSel);
					//_gfx->setCursorPalette(false);
					// TODO???
					return;
				} else if (fCanSel[IDI_WTP_SEL_SOUTH] && hotspotSouth.contains(event.mouse.x, event.mouse.y)) {
					*iSel = IDI_WTP_SEL_SOUTH;
					makeSel(iSel, fCanSel);
					//_gfx->setCursorPalette(false);
					// TODO???
					return;
				} else if (fCanSel[IDI_WTP_SEL_WEST] && hotspotWest.contains(event.mouse.x, event.mouse.y)) {
					*iSel = IDI_WTP_SEL_WEST;
					makeSel(iSel, fCanSel);
					//_gfx->setCursorPalette(false);
					// TODO???
					return;
				} else if (fCanSel[IDI_WTP_SEL_EAST] && hotspotEast.contains(event.mouse.x, event.mouse.y)) {
					*iSel = IDI_WTP_SEL_EAST;
					makeSel(iSel, fCanSel);
					//_gfx->setCursorPalette(false);
					// TODO???
					return;
				} else {
					//_gfx->setCursorPalette(false);
					// TODO???
				}

				switch (*iSel) {
				case IDI_WTP_SEL_OPT_1:
				case IDI_WTP_SEL_OPT_2:
				case IDI_WTP_SEL_OPT_3:
					for (int iSel2 = 0; iSel2 < IDI_WTP_MAX_OPTION; iSel2++) {
						if (*iSel == (fCanSel[iSel2 + IDI_WTP_SEL_REAL_OPT_1] - 1)) {
							*iSel = iSel2;
							// Menu selection made, hide the mouse cursor
							CursorMan.showMouse(false);
							return;
						}
					}
					break;
				default:
					if (fCanSel[*iSel]) {
						// Menu selection made, hide the mouse cursor
						CursorMan.showMouse(false);
						return;
					}
					break;
				}
				break;
			case Common::EVENT_RBUTTONUP:
				*iSel = IDI_WTP_SEL_BACK;
				// Menu selection made, hide the mouse cursor
				CursorMan.showMouse(false);
				return;
			case Common::EVENT_WHEELUP:
				decMenuSel(iSel, fCanSel);
				break;
			case Common::EVENT_WHEELDOWN:
				incMenuSel(iSel, fCanSel);
				break;
			case Common::EVENT_KEYDOWN:
				switch (event.kbd.keycode) {
				case Common::KEYCODE_ESCAPE:
					*iSel = IDI_WTP_SEL_HOME;
					// Menu selection made, hide the mouse cursor
					CursorMan.showMouse(false);
					return;
				case Common::KEYCODE_BACKSPACE:
					*iSel = IDI_WTP_SEL_BACK;
					// Menu selection made, hide the mouse cursor
					CursorMan.showMouse(false);
					return;
				case Common::KEYCODE_c:
					inventory();
					break;
				case Common::KEYCODE_SPACE:
				case Common::KEYCODE_RIGHT:
				case Common::KEYCODE_DOWN:
					incMenuSel(iSel, fCanSel);
					break;
				case Common::KEYCODE_LEFT:
				case Common::KEYCODE_UP:
					decMenuSel(iSel, fCanSel);
					break;
				case Common::KEYCODE_1:
				case Common::KEYCODE_2:
				case Common::KEYCODE_3:
					*iSel = event.kbd.keycode - Common::KEYCODE_1;
					if (fCanSel[*iSel + IDI_WTP_SEL_REAL_OPT_1]) {
						// Menu selection made, hide the mouse cursor
						CursorMan.showMouse(false);
						return;
					} else {
						keyHelp();
						clrMenuSel(iSel, fCanSel);
					}
					break;
				case Common::KEYCODE_n:
					*iSel = IDI_WTP_SEL_NORTH;
					makeSel(iSel, fCanSel);
					break;
				case Common::KEYCODE_s:
					if (event.kbd.flags & Common::KBD_CTRL) {
						flipFlag(VM_FLAG_SOUND_ON);
					} else {
						*iSel = IDI_WTP_SEL_SOUTH;
						makeSel(iSel, fCanSel);
					}
					break;
				case Common::KEYCODE_e:
					*iSel = IDI_WTP_SEL_EAST;
					makeSel(iSel, fCanSel);
					break;
				case Common::KEYCODE_w:
					*iSel = IDI_WTP_SEL_WEST;
					makeSel(iSel, fCanSel);
					break;
				case Common::KEYCODE_t:
					*iSel = IDI_WTP_SEL_TAKE;
					makeSel(iSel, fCanSel);
					break;
				case Common::KEYCODE_d:
					*iSel = IDI_WTP_SEL_DROP;
					makeSel(iSel, fCanSel);
					break;
				case Common::KEYCODE_RETURN:
					switch (*iSel) {
					case IDI_WTP_SEL_OPT_1:
					case IDI_WTP_SEL_OPT_2:
					case IDI_WTP_SEL_OPT_3:
						for (int iSel2 = 0; iSel2 < IDI_WTP_MAX_OPTION; iSel2++) {
							if (*iSel == (fCanSel[iSel2 + IDI_WTP_SEL_REAL_OPT_1] - 1)) {
								*iSel = iSel2;
								// Menu selection made, hide the mouse cursor
								CursorMan.showMouse(false);
								return;
							}
						}
						break;
					default:
						if (fCanSel[*iSel]) {
							// Menu selection made, hide the mouse cursor
							CursorMan.showMouse(false);
							return;
						}
						break;
					}
					break;
				default:
					if (!event.kbd.flags) { // if the control/alt/shift keys are not pressed
						keyHelp();
						clrMenuSel(iSel, fCanSel);
					}
					break;
				}
				break;
			default:
				break;
			}

			drawMenu(szMenu, *iSel, fCanSel);
		}
	}
}

void WinnieEngine::gameLoop() {
	WTP_ROOM_HDR hdr;
	uint8 *roomdata = (uint8 *)malloc(4096);
	int iBlock;
	uint8 decodePhase = 0;

	while (!shouldQuit()) {
		if (decodePhase == 0) {
			if (!_gameStateWinnie.nObjMiss && (_room == IDI_WTP_ROOM_PICNIC))
				_room = IDI_WTP_ROOM_PARTY;

			readRoom(_room, roomdata, hdr);
			drawRoomPic();
			g_system->updateScreen();
			decodePhase = 1;
		}

		if (decodePhase == 1) {
			if (getObjInRoom(_room)) {
				printObjStr(getObjInRoom(_room), IDI_WTP_OBJ_DESC);
				getSelection(kSelAnyKey);
			}
			decodePhase = 2;
		}

		if (decodePhase == 2) {
			for (iBlock = 0; iBlock < IDI_WTP_MAX_BLOCK; iBlock++) {
				if (parser(hdr.ofsDesc[iBlock] - _roomOffset, iBlock, roomdata) == IDI_WTP_PAR_BACK) {
					decodePhase = 1;
					break;
				}
			}
			if (decodePhase == 2)
				decodePhase = 3;
		}

		if (decodePhase == 3) {
			for (iBlock = 0; iBlock < IDI_WTP_MAX_BLOCK; iBlock++) {
				if (parser(hdr.ofsBlock[iBlock] - _roomOffset, iBlock, roomdata) == IDI_WTP_PAR_GOTO) {
					decodePhase = 0;
					break;
				} else if (parser(hdr.ofsBlock[iBlock] - _roomOffset, iBlock, roomdata) == IDI_WTP_PAR_BACK) {
					decodePhase = 2;
					break;
				}
			}
		}
	}

	free(roomdata);
}

void WinnieEngine::drawPic(const char *szName) {
	Common::String fileName = szName;

	if (getPlatform() != Common::kPlatformAmiga)
		fileName += ".pic";
	else
		fileName = "misc/" + fileName;

	Common::File file;

	if (!file.open(fileName)) {
		warning("Could not open file \'%s\'", fileName.c_str());
		return;
	}

	uint8 *buffer = (uint8 *)malloc(4096);
	uint32 size = file.size();
	file.read(buffer, size);
	file.close();

	_picture->decodePicture(buffer, size, 1, IDI_WTP_PIC_WIDTH, IDI_WTP_PIC_HEIGHT);
	_picture->showPic(IDI_WTP_PIC_X0, IDI_WTP_PIC_Y0, IDI_WTP_PIC_WIDTH, IDI_WTP_PIC_HEIGHT);

	free(buffer);
}

void WinnieEngine::drawObjPic(int iObj, int x0, int y0) {
	if (!iObj)
		return;

	WTP_OBJ_HDR objhdr;
	uint8 *buffer = (uint8 *)malloc(2048);
	uint32 objSize = readObj(iObj, buffer);
	parseObjHeader(&objhdr, buffer, sizeof(WTP_OBJ_HDR));

	_picture->setOffset(x0, y0);
	_picture->decodePicture(buffer + objhdr.ofsPic - _objOffset, objSize, 0, IDI_WTP_PIC_WIDTH, IDI_WTP_PIC_HEIGHT);
	_picture->setOffset(0, 0);
	_picture->showPic(10, 0, IDI_WTP_PIC_WIDTH, IDI_WTP_PIC_HEIGHT);

	free(buffer);
}

void WinnieEngine::drawRoomPic() {
	WTP_ROOM_HDR roomhdr;
	uint8 *buffer = (uint8 *)malloc(4096);
	int iObj = getObjInRoom(_room);

	// clear gfx screen
	_gfx->clearDisplay(0);

	// read room picture
	readRoom(_room, buffer, roomhdr);

	// draw room picture
	_picture->decodePicture(buffer + roomhdr.ofsPic - _roomOffset, 4096, 1, IDI_WTP_PIC_WIDTH, IDI_WTP_PIC_HEIGHT);
	_picture->showPic(IDI_WTP_PIC_X0, IDI_WTP_PIC_Y0, IDI_WTP_PIC_WIDTH, IDI_WTP_PIC_HEIGHT);

	// draw object picture
	drawObjPic(iObj, IDI_WTP_PIC_X0 + roomhdr.objX, IDI_WTP_PIC_Y0 + roomhdr.objY);

	free(buffer);
}

bool WinnieEngine::playSound(ENUM_WTP_SOUND iSound) {
	// TODO: Only DOS sound is supported, currently
	if (getPlatform() != Common::kPlatformDOS) {
		warning("STUB: playSound(%d)", iSound);
		return false;
	}

	Common::String fileName = Common::String::format(IDS_WTP_SND_DOS, iSound);

	Common::File file;
	if (!file.open(fileName))
		return false;

	uint32 size = file.size();
	byte *data = new byte[size];
	file.read(data, size);
	file.close();

	_game.sounds[0] = AgiSound::createFromRawResource(data, size, 0, _soundemu);
	_sound->startSound(0, 0);

	bool cursorShowing = CursorMan.showMouse(false);
	_system->updateScreen();

	// Loop until the sound is done
	bool skippedSound = false;
	while (!shouldQuit() && _game.sounds[0]->isPlaying()) {
		Common::Event event;
		while (_system->getEventManager()->pollEvent(event)) {
			switch (event.type) {
			case Common::EVENT_KEYDOWN:
				_sound->stopSound();
				skippedSound = true;
				break;
			default:
				break;
			}
		}

		_system->delayMillis(10);
	}

	if (cursorShowing) {
		CursorMan.showMouse(true);
		_system->updateScreen();
	}

	delete _game.sounds[0];
	_game.sounds[0] = nullptr;

	return !shouldQuit() && !skippedSound;
}

void WinnieEngine::clrMenuSel(int *iSel, int fCanSel[]) {
	*iSel = IDI_WTP_SEL_OPT_1;
	while (!fCanSel[*iSel]) {
		*iSel += 1;
	}
	//_gfx->setCursorPalette(false);
	// TODO???
}

void WinnieEngine::printRoomStr(int iRoom, int iStr) {
	WTP_ROOM_HDR hdr;
	uint8 *buffer = (uint8 *)malloc(4096);

	readRoom(iRoom, buffer, hdr);
	printStrWinnie((char *)(buffer + hdr.ofsStr[iStr - 1] - _roomOffset));

	free(buffer);
}

void WinnieEngine::gameOver() {
	// sing the Pooh song forever
	while (!shouldQuit()) {
		printStr(IDS_WTP_SONG_0);
		playSound(IDI_WTP_SND_POOH_0);
		printStr(IDS_WTP_SONG_1);
		playSound(IDI_WTP_SND_POOH_1);
		printStr(IDS_WTP_SONG_2);
		playSound(IDI_WTP_SND_POOH_2);
		getSelection(kSelAnyKey);
	}
}

void WinnieEngine::saveGame() {
	int i = 0;

	Common::OutSaveFile *outfile = getSaveFileMan()->openForSaving(IDS_WTP_FILE_SAVEGAME);

	if (!outfile)
		return;

	outfile->writeUint32BE(MKTAG('W', 'I', 'N', 'N')); // header
	outfile->writeByte(WTP_SAVEGAME_VERSION);

	outfile->writeByte(_gameStateWinnie.fSound);
	outfile->writeByte(_gameStateWinnie.nMoves);
	outfile->writeByte(_gameStateWinnie.nObjMiss);
	outfile->writeByte(_gameStateWinnie.nObjRet);
	outfile->writeByte(_gameStateWinnie.iObjHave);

	for (i = 0; i < IDI_WTP_MAX_FLAG; i++)
		outfile->writeByte(_gameStateWinnie.fGame[i]);

	for (i = 0; i < IDI_WTP_MAX_OBJ_MISSING; i++)
		outfile->writeByte(_gameStateWinnie.iUsedObj[i]);

	for (i = 0; i < IDI_WTP_MAX_ROOM_OBJ; i++)
		outfile->writeByte(_gameStateWinnie.iObjRoom[i]);

	outfile->finalize();

	if (outfile->err())
		warning("Can't write file '%s'. (Disk full?)", IDS_WTP_FILE_SAVEGAME);

	delete outfile;
}

void WinnieEngine::loadGame() {
	int saveVersion = 0;
	int i = 0;

	Common::InSaveFile *infile = getSaveFileMan()->openForLoading(IDS_WTP_FILE_SAVEGAME);

	if (!infile)
		return;

	if (infile->readUint32BE() == MKTAG('W', 'I', 'N', 'N')) {
		saveVersion = infile->readByte();
		if (saveVersion != WTP_SAVEGAME_VERSION)
			warning("Old save game version (%d, current version is %d). Will try and read anyway, but don't be surprised if bad things happen", saveVersion, WTP_SAVEGAME_VERSION);

		_gameStateWinnie.fSound = infile->readByte();
		_gameStateWinnie.nMoves = infile->readByte();
		_gameStateWinnie.nObjMiss = infile->readByte();
		_gameStateWinnie.nObjRet = infile->readByte();
		_gameStateWinnie.iObjHave = infile->readByte();
	} else {
		// This is probably a save from the original interpreter, throw a warning and attempt
		// to read it as LE
		warning("No header found in save game, assuming it came from the original interpreter");
		// Note that the original saves variables as 16-bit integers, but only 8 bits are used.
		// Since we read the save file data as little-endian, we skip the first byte of each
		// variable

		infile->seek(0);                    // Jump back to the beginning of the file

		infile->readUint16LE();             // skip unused field
		infile->readByte();                 // first 8 bits of fSound
		_gameStateWinnie.fSound = infile->readByte();
		infile->readByte();                 // first 8 bits of nMoves
		_gameStateWinnie.nMoves = infile->readByte();
		infile->readByte();                 // first 8 bits of nObjMiss
		_gameStateWinnie.nObjMiss = infile->readByte();
		infile->readByte();                 // first 8 bits of nObjRet
		_gameStateWinnie.nObjRet = infile->readByte();
		infile->readUint16LE();             // skip unused field
		infile->readUint16LE();             // skip unused field
		infile->readUint16LE();             // skip unused field
		infile->readByte();                 // first 8 bits of iObjHave
		_gameStateWinnie.iObjHave = infile->readByte();
		infile->readUint16LE();             // skip unused field
		infile->readUint16LE();             // skip unused field
		infile->readUint16LE();             // skip unused field
	}

	for (i = 0; i < IDI_WTP_MAX_FLAG; i++)
		_gameStateWinnie.fGame[i] = infile->readByte();

	for (i = 0; i < IDI_WTP_MAX_OBJ_MISSING; i++)
		_gameStateWinnie.iUsedObj[i] = infile->readByte();

	for (i = 0; i < IDI_WTP_MAX_ROOM_OBJ; i++)
		_gameStateWinnie.iObjRoom[i] = infile->readByte();

	// Note that saved games from the original interpreter have 2 more 16-bit fields here
	// which are ignored

	delete infile;
}

void WinnieEngine::printStrWinnie(char *szMsg) {
	if (getPlatform() != Common::kPlatformAmiga)
		printStrXOR(szMsg);
	else
		printStr(szMsg);
}

// Console-related functions

void WinnieEngine::debugCurRoom() {
	getDebugger()->debugPrintf("Current Room = %d\n", _room);
}

WinnieEngine::WinnieEngine(OSystem *syst, const AGIGameDescription *gameDesc) : PreAgiEngine(syst, gameDesc) {
	setDebugger(new WinnieConsole(this));
}

WinnieEngine::~WinnieEngine() {
}

void WinnieEngine::init() {
	// Initialize sound

	switch (MidiDriver::getMusicType(MidiDriver::detectDevice(MDT_PCSPK | MDT_PCJR))) {
	case MT_PCSPK:
		_soundemu = SOUND_EMU_PC;
		break;
	case MT_PCJR:
		_soundemu = SOUND_EMU_PCJR;
		break;
	default:
		_soundemu = SOUND_EMU_NONE;
		break;
	}

	_sound = new SoundMgr(this, _mixer);
	setFlag(VM_FLAG_SOUND_ON, true); // enable sound

	memset(&_gameStateWinnie, 0, sizeof(_gameStateWinnie));
	_gameStateWinnie.fSound = 1;
	_gameStateWinnie.nObjMiss = IDI_WTP_MAX_OBJ_MISSING;
	_gameStateWinnie.nObjRet = 0;
	_gameStateWinnie.fGame[0] = 1;
	_gameStateWinnie.fGame[1] = 1;
	_room = IDI_WTP_ROOM_HOME;

	_mist = -1;
	_doWind = false;
	_winnieEvent = false;

	if (getPlatform() != Common::kPlatformAmiga) {
		_isBigEndian = false;
		_roomOffset = IDI_WTP_OFS_ROOM;
		_objOffset = IDI_WTP_OFS_OBJ;
	} else {
		_isBigEndian = true;
		_roomOffset = 0;
		_objOffset = 0;
	}

	if (getPlatform() == Common::kPlatformC64 || getPlatform() == Common::kPlatformApple2)
		_picture->setPictureVersion(AGIPIC_C64);

	hotspotNorth = Common::Rect(20, 0, (IDI_WTP_PIC_WIDTH + 10) * 2, 10);
	hotspotSouth = Common::Rect(20, IDI_WTP_PIC_HEIGHT - 10, (IDI_WTP_PIC_WIDTH + 10) * 2, IDI_WTP_PIC_HEIGHT);
	hotspotEast  = Common::Rect(IDI_WTP_PIC_WIDTH * 2, 0, (IDI_WTP_PIC_WIDTH + 10) * 2, IDI_WTP_PIC_HEIGHT);
	hotspotWest  = Common::Rect(20, 0, 30, IDI_WTP_PIC_HEIGHT);
}

Common::Error WinnieEngine::go() {
	init();
	randomize();

	// The intro is not supported on these platforms yet
	if (getPlatform() != Common::kPlatformC64 && getPlatform() != Common::kPlatformApple2)
		intro();

	gameLoop();

	return Common::kNoError;
}

} // End of namespace AGI
