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

// Resource file routines for Simon1/Simon2


#include "common/archive.h"
#include "common/file.h"
#include "common/memstream.h"
#include "common/textconsole.h"
#include "common/util.h"

#include "agos/agos.h"
#include "agos/intern.h"
#include "agos/sound.h"

#include "common/zlib.h"

namespace AGOS {

#ifdef ENABLE_AGOS2
uint16 AGOSEngine_Feeble::to16Wrapper(uint value) {
	return TO_LE_16(value);
}

uint16 AGOSEngine_Feeble::readUint16Wrapper(const void *src) {
	return READ_LE_UINT16(src);
}

uint32 AGOSEngine_Feeble::readUint32Wrapper(const void *src) {
	return READ_LE_UINT32(src);
}
#endif

uint16 AGOSEngine::to16Wrapper(uint value) {
	return TO_BE_16(value);
}

uint16 AGOSEngine::readUint16Wrapper(const void *src) {
	return READ_BE_UINT16(src);
}

uint32 AGOSEngine::readUint32Wrapper(const void *src) {
	return READ_BE_UINT32(src);
}

void AGOSEngine::decompressData(const char *srcName, byte *dst, uint32 offset, uint32 srcSize, uint32 dstSize) {
#ifdef USE_ZLIB
		Common::File in;
		in.open(srcName);
		if (in.isOpen() == false)
			error("decompressData: Can't load %s", srcName);

		in.seek(offset, SEEK_SET);
		if (srcSize != dstSize) {
			byte *srcBuffer = (byte *)malloc(srcSize);

			if (in.read(srcBuffer, srcSize) != srcSize)
				error("decompressData: Read failed");

			unsigned long decompressedSize = dstSize;
			if (!Common::uncompress(dst, &decompressedSize, srcBuffer, srcSize))
				error("decompressData: Zlib uncompress error");
			free(srcBuffer);
		} else {
			if (in.read(dst, dstSize) != dstSize)
				error("decompressData: Read failed");
		}
		in.close();
#else
	error("Zlib support is required for Amiga and Macintosh versions");
#endif
}

void AGOSEngine::loadOffsets(const char *filename, int number, uint32 &file, uint32 &offset, uint32 &srcSize, uint32 &dstSize) {
	Common::File in;

	int offsSize = (getPlatform() == Common::kPlatformAmiga) ? 16 : 12;

	/* read offsets from index */
	in.open(filename);
	if (in.isOpen() == false) {
		error("loadOffsets: Can't load index file '%s'", filename);
	}

	in.seek(number * offsSize, SEEK_SET);
	offset = in.readUint32LE();
	dstSize = in.readUint32LE();
	srcSize = in.readUint32LE();
	file = in.readUint32LE();
	in.close();
}

int AGOSEngine::allocGamePcVars(Common::SeekableReadStream *in) {
	uint32 itemArraySize, itemArrayInited, stringTableNum;
	uint32 version;
	uint32 i;

	itemArraySize = in->readUint32BE();
	version = in->readUint32BE();
	itemArrayInited = in->readUint32BE();
	stringTableNum = in->readUint32BE();

	// First two items are predefined
	if (getGameType() == GType_ELVIRA1 || getGameType() == GType_ELVIRA2) {
		itemArraySize += 2;
		itemArrayInited = itemArraySize;
	} else {
		itemArrayInited += 2;
		itemArraySize += 2;
	}

	if (version != 0x80)
		error("allocGamePcVars: Not a runtime database");

	_itemArrayPtr = (Item **)calloc(itemArraySize, sizeof(Item *));
	if (_itemArrayPtr == NULL)
		error("allocGamePcVars: Out of memory for Item array");

	_itemArraySize = itemArraySize;
	_itemArrayInited = itemArrayInited;

	for (i = 1; i < itemArrayInited; i++) {
		_itemArrayPtr[i] = (Item *)allocateItem(sizeof(Item));
	}

	// The rest is cleared automatically by calloc
	allocateStringTable(stringTableNum + 10);
	_stringTabNum = stringTableNum;

	return itemArrayInited;
}

void AGOSEngine_PN::loadGamePcFile() {
	if (getFileName(GAME_BASEFILE) != NULL) {
		Common::File in;
		// Read dataBase
		if (!in.open(getFileName(GAME_BASEFILE))) {
			error("loadGamePcFile: Can't load database file '%s'", getFileName(GAME_BASEFILE));
		}

		_dataBaseSize = in.size();
		_dataBase = (byte *)malloc(_dataBaseSize);
		if (_dataBase == NULL)
			error("loadGamePcFile: Out of memory for dataBase");
		in.read(_dataBase, _dataBaseSize);

		if (_dataBase[31] != 0)
			error("Later version of system requested");
	}

	if (getFileName(GAME_TEXTFILE) != NULL) {
		Common::File in;
		// Read textBase
		if (!in.open(getFileName(GAME_TEXTFILE))) {
			error("loadGamePcFile: Can't load textbase file '%s'", getFileName(GAME_TEXTFILE));
		}

		_textBaseSize = in.size();
		_textBase = (byte *)malloc(_textBaseSize);
		if (_textBase == NULL)
			error("loadGamePcFile: Out of memory for textBase");
		in.read(_textBase, _textBaseSize);

		if (_textBase[getlong(30L)] != 128)
			error("Unknown compression format");
	}
}

void AGOSEngine::loadGamePcFile() {
	int fileSize;

	if (getFileName(GAME_BASEFILE) != NULL) {
		/* Read main gamexx file */
		Common::File in;
		if (!in.open(getFileName(GAME_BASEFILE))) {
			error("loadGamePcFile: Can't load gamexx file '%s'", getFileName(GAME_BASEFILE));
		}

		if (getFeatures() & GF_CRUNCHED_GAMEPC) {
			uint srcSize = in.size();
			byte *srcBuf = (byte *)malloc(srcSize);
			in.read(srcBuf, srcSize);

			uint dstSize = READ_BE_UINT32(srcBuf + srcSize - 4);
			byte *dstBuf = (byte *)malloc(dstSize);
			decrunchFile(srcBuf, dstBuf, srcSize);
			free(srcBuf);

			Common::MemoryReadStream stream(dstBuf, dstSize);
			readGamePcFile(&stream);
			free(dstBuf);
		} else {
			readGamePcFile(&in);
		}
	}

	if (getFileName(GAME_TBLFILE) != NULL) {
		/* Read list of TABLE resources */
		Common::File in;
		if (!in.open(getFileName(GAME_TBLFILE))) {
			error("loadGamePcFile: Can't load table resources file '%s'", getFileName(GAME_TBLFILE));
		}

		fileSize = in.size();

		_tblList = (byte *)malloc(fileSize);
		if (_tblList == NULL)
			error("loadGamePcFile: Out of memory for strip table list");
		in.read(_tblList, fileSize);

		/* Remember the current state */
		_subroutineListOrg = _subroutineList;
		_tablesHeapPtrOrg = _tablesHeapPtr;
		_tablesHeapCurPosOrg = _tablesHeapCurPos;
	}

	if (getFileName(GAME_STRFILE) != NULL) {
		/* Read list of TEXT resources */
		Common::File in;
		if (!in.open(getFileName(GAME_STRFILE)))
			error("loadGamePcFile: Can't load text resources file '%s'", getFileName(GAME_STRFILE));

		fileSize = in.size();
		_strippedTxtMem = (byte *)malloc(fileSize);
		if (_strippedTxtMem == NULL)
			error("loadGamePcFile: Out of memory for strip text list");
		in.read(_strippedTxtMem, fileSize);
	}

	if (getFileName(GAME_STATFILE) != NULL) {
		/* Read list of ROOM STATE resources */
		Common::File in;
		if (!in.open(getFileName(GAME_STATFILE))) {
			error("loadGamePcFile: Can't load state resources file '%s'", getFileName(GAME_STATFILE));
		}

		_numRoomStates = in.size() / 8;

		_roomStates = (RoomState *)calloc(_numRoomStates, sizeof(RoomState));
		if (_roomStates == NULL)
			error("loadGamePcFile: Out of memory for room state list");

		for (uint s = 0; s < _numRoomStates; s++) {
			uint16 num = in.readUint16BE() - (_itemArrayInited - 2);

			_roomStates[num].state = in.readUint16BE();
			_roomStates[num].classFlags = in.readUint16BE();
			_roomStates[num].roomExitStates = in.readUint16BE();
		}
	}

	if (getFileName(GAME_RMSLFILE) != NULL) {
		/* Read list of ROOM ITEMS resources */
		Common::File in;
		if (!in.open(getFileName(GAME_RMSLFILE))) {
			error("loadGamePcFile: Can't load room resources file '%s'", getFileName(GAME_RMSLFILE));
		}

		fileSize = in.size();

		_roomsList = (byte *)malloc(fileSize);
		if (_roomsList == NULL)
			error("loadGamePcFile: Out of memory for room items list");
		in.read(_roomsList, fileSize);
	}

	if (getFileName(GAME_XTBLFILE) != NULL) {
		/* Read list of XTABLE resources */
		Common::File in;
		if (!in.open(getFileName(GAME_XTBLFILE))) {
			error("loadGamePcFile: Can't load xtable resources file '%s'", getFileName(GAME_XTBLFILE));
		}

		fileSize = in.size();

		_xtblList = (byte *)malloc(fileSize);
		if (_xtblList == NULL)
			error("loadGamePcFile: Out of memory for strip xtable list");
		in.read(_xtblList, fileSize);

		/* Remember the current state */
		_xsubroutineListOrg = _subroutineList;
		_xtablesHeapPtrOrg = _tablesHeapPtr;
		_xtablesHeapCurPosOrg = _tablesHeapCurPos;
	}
}

void AGOSEngine::readGamePcFile(Common::SeekableReadStream *in) {
	int num_inited_objects;
	int i;

	num_inited_objects = allocGamePcVars(in);

	createPlayer();
	readGamePcText(in);

	for (i = 2; i < num_inited_objects; i++) {
		readItemFromGamePc(in, _itemArrayPtr[i]);
	}

	readSubroutineBlock(in);
}

void AGOSEngine::readGamePcText(Common::SeekableReadStream *in) {
	_textSize = in->readUint32BE();
	_textMem = (byte *)malloc(_textSize);
	if (_textMem == NULL)
		error("readGamePcText: Out of text memory");

	in->read(_textMem, _textSize);

	setupStringTable(_textMem, _stringTabNum);
}

void AGOSEngine::readItemFromGamePc(Common::SeekableReadStream *in, Item *item) {
	uint32 type;

	if (getGameType() == GType_ELVIRA1) {
		item->itemName = (uint16)in->readUint32BE();
		item->adjective = in->readUint16BE();
		item->noun = in->readUint16BE();
		item->state = in->readUint16BE();
		in->readUint16BE();
		item->next = (uint16)fileReadItemID(in);
		item->child = (uint16)fileReadItemID(in);
		item->parent = (uint16)fileReadItemID(in);
		in->readUint16BE();
		in->readUint16BE();
		in->readUint16BE();
		item->classFlags = in->readUint16BE();
		item->children = NULL;
	} else if (getGameType() == GType_ELVIRA2) {
		item->itemName = (uint16)in->readUint32BE();
		item->adjective = in->readUint16BE();
		item->noun = in->readUint16BE();
		item->state = in->readUint16BE();
		item->next = (uint16)fileReadItemID(in);
		item->child = (uint16)fileReadItemID(in);
		item->parent = (uint16)fileReadItemID(in);
		in->readUint16BE();
		item->classFlags = in->readUint16BE();
		item->children = NULL;
	} else {
		item->adjective = in->readUint16BE();
		item->noun = in->readUint16BE();
		item->state = in->readUint16BE();
		item->next = (uint16)fileReadItemID(in);
		item->child = (uint16)fileReadItemID(in);
		item->parent = (uint16)fileReadItemID(in);
		in->readUint16BE();
		item->classFlags = in->readUint16BE();
		item->children = NULL;
	}


	type = in->readUint32BE();
	while (type) {
		type = in->readUint16BE();
		if (type != 0)
			readItemChildren(in, item, type);
	}
}

void AGOSEngine::readItemChildren(Common::SeekableReadStream *in, Item *item, uint type) {
	if (type == kRoomType) {
		SubRoom *subRoom = (SubRoom *)allocateChildBlock(item, kRoomType, sizeof(SubRoom));
		subRoom->roomShort = in->readUint32BE();
		subRoom->roomLong = in->readUint32BE();
		subRoom->flags = in->readUint16BE();
	} else if (type == kObjectType) {
		SubObject *subObject = (SubObject *)allocateChildBlock(item, kObjectType, sizeof(SubObject));
		in->readUint32BE();
		in->readUint32BE();
		in->readUint32BE();
		subObject->objectName = in->readUint32BE();
		subObject->objectSize = in->readUint16BE();
		subObject->objectWeight = in->readUint16BE();
		subObject->objectFlags = in->readUint16BE();
	} else if (type == kGenExitType) {
		SubGenExit *genExit = (SubGenExit *)allocateChildBlock(item, kGenExitType, sizeof(SubGenExit));
		genExit->dest[0] = (uint16)fileReadItemID(in);
		genExit->dest[1] = (uint16)fileReadItemID(in);
		genExit->dest[2] = (uint16)fileReadItemID(in);
		genExit->dest[3] = (uint16)fileReadItemID(in);
		genExit->dest[4] = (uint16)fileReadItemID(in);
		genExit->dest[5] = (uint16)fileReadItemID(in);
		fileReadItemID(in);
		fileReadItemID(in);
		fileReadItemID(in);
		fileReadItemID(in);
		fileReadItemID(in);
		fileReadItemID(in);
	} else if (type == kContainerType) {
		SubContainer *container = (SubContainer *)allocateChildBlock(item, kContainerType, sizeof(SubContainer));
		container->volume = in->readUint16BE();
		container->flags = in->readUint16BE();
	} else if (type == kChainType) {
		SubChain *chain = (SubChain *)allocateChildBlock(item, kChainType, sizeof(SubChain));
		chain->chChained = (uint16)fileReadItemID(in);
	} else if (type == kUserFlagType) {
		setUserFlag(item, 0, in->readUint16BE());
		setUserFlag(item, 1, in->readUint16BE());
		setUserFlag(item, 2, in->readUint16BE());
		setUserFlag(item, 3, in->readUint16BE());
		setUserFlag(item, 4, in->readUint16BE());
		setUserFlag(item, 5, in->readUint16BE());
		setUserFlag(item, 6, in->readUint16BE());
		setUserFlag(item, 7, in->readUint16BE());
		SubUserFlag *subUserFlag = (SubUserFlag *)findChildOfType(item, kUserFlagType);
		subUserFlag->userItems[0] = (uint16)fileReadItemID(in);
		fileReadItemID(in);
		fileReadItemID(in);
		fileReadItemID(in);
	} else if (type == kInheritType) {
		SubInherit *inherit = (SubInherit *)allocateChildBlock(item, kInheritType, sizeof(SubInherit));
		inherit->inMaster = (uint16)fileReadItemID(in);
	} else {
		error("readItemChildren: invalid type %d", type);
	}
}

void AGOSEngine_Elvira2::readItemChildren(Common::SeekableReadStream *in, Item *item, uint type) {
	if (type == kRoomType) {
		uint fr1 = in->readUint16BE();
		uint fr2 = in->readUint16BE();
		uint i, size;
		uint j, k;
		SubRoom *subRoom;

		size = SubRoom_SIZE;
		for (i = 0, j = fr2; i != 6; i++, j >>= 2)
			if (j & 3)
				size += sizeof(subRoom->roomExit[0]);

		subRoom = (SubRoom *)allocateChildBlock(item, kRoomType, size);
		subRoom->subroutine_id = fr1;
		subRoom->roomExitStates = fr2;

		for (i = k = 0, j = fr2; i != 6; i++, j >>= 2)
			if (j & 3)
				subRoom->roomExit[k++] = (uint16)fileReadItemID(in);
	} else if (type == kObjectType) {
		uint32 fr = in->readUint32BE();
		uint i, k, size;
		SubObject *subObject;

		size = SubObject_SIZE;
		for (i = 0; i != 16; i++)
			if (fr & (1 << i))
				size += sizeof(subObject->objectFlagValue[0]);

		subObject = (SubObject *)allocateChildBlock(item, kObjectType, size);
		subObject->objectFlags = fr;

		k = 0;
		if (fr & 1) {
			subObject->objectFlagValue[k++] = (uint16)in->readUint32BE();
		}
		for (i = 1; i != 16; i++)
			if (fr & (1 << i))
				subObject->objectFlagValue[k++] = in->readUint16BE();

		if (getGameType() != GType_ELVIRA2)
			subObject->objectName = (uint16)in->readUint32BE();
	} else if (type == kSuperRoomType) {
		assert(getGameType() == GType_ELVIRA2);

		uint i, j, k, size;
		uint id, x, y, z;
		SubSuperRoom *subSuperRoom;

		id = in->readUint16BE();
		x = in->readUint16BE();
		y = in->readUint16BE();
		z = in->readUint16BE();

		j = x * y * z;
		size = SubSuperRoom_SIZE;
		for (i = 0; i != j; i++)
			size += sizeof(subSuperRoom->roomExitStates[0]);

		subSuperRoom = (SubSuperRoom *)allocateChildBlock(item, kSuperRoomType, size);
		subSuperRoom->subroutine_id = id;
		subSuperRoom->roomX = x;
		subSuperRoom->roomY = y;
		subSuperRoom->roomZ = z;

		for (i = k = 0; i != j; i++)
			subSuperRoom->roomExitStates[k++] = in->readUint16BE();
	} else if (type == kContainerType) {
		SubContainer *container = (SubContainer *)allocateChildBlock(item, kContainerType, sizeof(SubContainer));
		container->volume = in->readUint16BE();
		container->flags = in->readUint16BE();
	} else if (type == kChainType) {
		SubChain *chain = (SubChain *)allocateChildBlock(item, kChainType, sizeof(SubChain));
		chain->chChained = (uint16)fileReadItemID(in);
	} else if (type == kUserFlagType) {
		setUserFlag(item, 0, in->readUint16BE());
		setUserFlag(item, 1, in->readUint16BE());
		setUserFlag(item, 2, in->readUint16BE());
		setUserFlag(item, 3, in->readUint16BE());
	} else if (type == kInheritType) {
		SubInherit *inherit = (SubInherit *)allocateChildBlock(item, kInheritType, sizeof(SubInherit));
		inherit->inMaster = (uint16)fileReadItemID(in);
	} else {
		error("readItemChildren: invalid type %d", type);
	}
}

uint fileReadItemID(Common::SeekableReadStream *in) {
	uint32 val = in->readUint32BE();
	if (val == 0xFFFFFFFF)
		return 0;
	return val + 2;
}

void AGOSEngine::openGameFile() {
	_gameFile = new Common::File();
	_gameFile->open(getFileName(GAME_GMEFILE));

	if (!_gameFile->isOpen())
		error("openGameFile: Can't load game file '%s'", getFileName(GAME_GMEFILE));

	uint32 size = _gameFile->readUint32LE();

	_gameOffsetsPtr = (uint32 *)malloc(size);
	if (_gameOffsetsPtr == NULL)
		error("openGameFile: Out of memory, game offsets");

	_gameFile->seek(0, SEEK_SET);

	for (uint r = 0; r < size / 4; r++)
		_gameOffsetsPtr[r] = _gameFile->readUint32LE();
}

void AGOSEngine::readGameFile(void *dst, uint32 offs, uint32 size) {
	_gameFile->seek(offs, SEEK_SET);
	if (_gameFile->read(dst, size) != size)
		error("readGameFile: Read failed (%d,%d)", offs, size);
}

// Thanks to Stuart Caie for providing the original
// C conversion upon which this decruncher is based.

#define SD_GETBIT(var) do {     \
	if (!bits--) {              \
		s -= 4;                 \
		if (s < src)            \
			return false;       \
		bb = READ_BE_UINT32(s); \
		bits = 31;              \
	}                           \
	(var) = bb & 1;             \
	bb >>= 1;                   \
}while (0)

#define SD_GETBITS(var, nbits) do { \
	bc = (nbits);                   \
	(var) = 0;                      \
	while (bc--) {                   \
		(var) <<= 1;                \
		SD_GETBIT(bit);             \
		(var) |= bit;               \
	}                               \
}while (0)

#define SD_TYPE_LITERAL (0)
#define SD_TYPE_MATCH   (1)

bool AGOSEngine::decrunchFile(byte *src, byte *dst, uint32 size) {
	byte *s = src + size - 4;
	uint32 destlen = READ_BE_UINT32 (s);
	uint32 bb, x, y;
	byte *d = dst + destlen;
	byte bc, bit, bits, type;

	// Initialize bit buffer.
	s -= 4;
	bb = x = READ_BE_UINT32 (s);
	bits = 0;
	do {
		x >>= 1;
		bits++;
	} while (x);
	bits--;

	while (d > dst) {
		SD_GETBIT(x);
		if (x) {
			SD_GETBITS(x, 2);
			switch (x) {
			case 0:
				type = SD_TYPE_MATCH;
				x = 9;
				y = 2;
				break;

			case 1:
				type = SD_TYPE_MATCH;
				x = 10;
				y = 3;
				break;

			case 2:
				type = SD_TYPE_MATCH;
				x = 12;
				SD_GETBITS(y, 8);
				break;

			default:
				type = SD_TYPE_LITERAL;
				x = 8;
				y = 8;
			}
		} else {
			SD_GETBIT(x);
			if (x) {
				type = SD_TYPE_MATCH;
				x = 8;
				y = 1;
			} else {
				type = SD_TYPE_LITERAL;
				x = 3;
				y = 0;
			}
		}

		if (type == SD_TYPE_LITERAL) {
			SD_GETBITS(x, x);
			y += x;
			if ((int)(y + 1) > (d - dst))
				return false; // Overflow?
			do {
				SD_GETBITS(x, 8);
				*--d = x;
			} while (y-- > 0);
		} else {
			if ((int)(y + 1) > (d - dst))
				return false; // Overflow?
			SD_GETBITS(x, x);
			if ((d + x) > (dst + destlen))
				return false; // Offset overflow?
			do {
				d--;
				*d = d[x];
			} while (y-- > 0);
		}
	}

	// Successful decrunch.
	return true;
}

#undef SD_GETBIT
#undef SD_GETBITS
#undef SD_TYPE_LITERAL
#undef SD_TYPE_MATCH

static bool getBit(Common::Stack<uint32> &dataList, uint32 &srcVal) {
	bool result = srcVal & 1;
	srcVal >>= 1;
	if (srcVal == 0) {
		srcVal = dataList.pop();

		result = srcVal & 1;
		srcVal = (srcVal >> 1) | 0x80000000L;
	}

	return result;
}

static uint32 copyBits(Common::Stack<uint32> &dataList, uint32 &srcVal, int numBits) {
	uint32 destVal = 0;

	for (int i = 0; i < numBits; ++i) {
		bool f = getBit(dataList, srcVal);
		destVal = (destVal << 1) | (f ? 1 : 0);
	}

	return destVal;
}

static void transferLoop(uint8 *dataOut, int &outIndex, uint32 destVal, int max) {
	assert(outIndex > max - 1);
	byte *pDest = dataOut + outIndex;

	 for (int i = 0; (i <= max) && (outIndex > 0); ++i) {
		pDest = dataOut + --outIndex;
		*pDest = pDest[destVal];
	 }
}

void AGOSEngine::decompressPN(Common::Stack<uint32> &dataList, uint8 *&dataOut, int &dataOutSize) {
	// Set up the output data area
	dataOutSize = dataList.pop();
	dataOut = new uint8[dataOutSize];
	int outIndex = dataOutSize;

	// Decompression routine
	uint32 srcVal = dataList.pop();
	uint32 destVal;

	while (outIndex > 0) {
		uint32 numBits = 0;
		int count = 0;

		if (getBit(dataList, srcVal)) {
			destVal = copyBits(dataList, srcVal, 2);

			if (destVal < 2) {
				count = destVal + 2;
				destVal = copyBits(dataList, srcVal, destVal + 9);
				transferLoop(dataOut, outIndex, destVal, count);
				continue;
			} else if (destVal != 3) {
				count = copyBits(dataList, srcVal, 8);
				destVal = copyBits(dataList, srcVal, 8);
				transferLoop(dataOut, outIndex, destVal, count);
				continue;
			} else {
				numBits = 8;
				count = 8;
			}
		} else if (getBit(dataList, srcVal)) {
			destVal = copyBits(dataList, srcVal, 8);
			transferLoop(dataOut, outIndex, destVal, 1);
			continue;
		} else {
			numBits = 3;
			count = 0;
		}

		destVal = copyBits(dataList, srcVal, numBits);
		count += destVal;

		// Loop through extracting specified number of bytes
		for (int i = 0; i <= count; ++i) {
			// Shift 8 bits from the source to the destination
			for (int bitCtr = 0; bitCtr < 8; ++bitCtr) {
				bool flag = getBit(dataList, srcVal);
				destVal = (destVal << 1) | (flag ? 1 : 0);
			}

			dataOut[--outIndex] = destVal & 0xff;
		}
	}
}

void AGOSEngine::loadVGABeardFile(uint16 id) {
	uint32 offs, size;

	if (getFeatures() & GF_OLD_BUNDLE) {
		Common::File in;
		char filename[15];
		if (id == 23)
			id = 112;
		else if (id == 328)
			id = 119;

		if (getPlatform() == Common::kPlatformAmiga) {
			if (getFeatures() & GF_TALKIE)
				sprintf(filename, "0%d.out", id);
			else
				sprintf(filename, "0%d.pkd", id);
		} else {
			sprintf(filename, "0%d.VGA", id);
		}

		if (!in.open(filename))
			error("loadSimonVGAFile: Can't load %s", filename);

		size = in.size();
		if (getFeatures() & GF_CRUNCHED) {
			byte *srcBuffer = (byte *)malloc(size);
			if (in.read(srcBuffer, size) != size)
				error("loadSimonVGAFile: Read failed");
			decrunchFile(srcBuffer, _vgaBufferPointers[11].vgaFile2, size);
			free(srcBuffer);
		} else {
			if (in.read(_vgaBufferPointers[11].vgaFile2, size) != size)
				error("loadSimonVGAFile: Read failed");
		}
	} else {
		offs = _gameOffsetsPtr[id];
		size = _gameOffsetsPtr[id + 1] - offs;
		readGameFile(_vgaBufferPointers[11].vgaFile2, offs, size);
	}
}

void AGOSEngine::loadVGAVideoFile(uint16 id, uint8 type, bool useError) {
	Common::File in;
	char filename[15];
	byte *dst;
	uint32 file, offs, srcSize, dstSize;
	uint extraBuffer = 0;

	if ((getGameType() == GType_SIMON1 || getGameType() == GType_SIMON2) &&
		id == 2 && type == 2) {
		// WORKAROUND: For the extra long strings in foreign languages
		// Allocate more space for text to cope with foreign languages that use
		// up more space than English. I hope 6400 bytes are enough. This number
		// is base on: 2 (lines) * 320 (screen width) * 10 (textheight) -- olki
		extraBuffer += 6400;
	}

	if (getFeatures() & GF_ZLIBCOMP) {
		loadOffsets(getFileName(GAME_GFXIDXFILE), id * 3 + type, file, offs, srcSize, dstSize);

		if (getPlatform() == Common::kPlatformAmiga)
			sprintf(filename, "GFX%d.VGA", file);
		else
			sprintf(filename, "graphics.vga");

		dst = allocBlock(dstSize + extraBuffer);
		decompressData(filename, dst, offs, srcSize, dstSize);
	} else if (getFeatures() & GF_OLD_BUNDLE) {
		if (getPlatform() == Common::kPlatformAcorn) {
			sprintf(filename, "%.3d%d.DAT", id, type);
		} else if (getPlatform() == Common::kPlatformAmiga || getPlatform() == Common::kPlatformAtariST) {
			if (getFeatures() & GF_TALKIE) {
				sprintf(filename, "%.3d%d.out", id, type);
			} else if (getGameType() == GType_ELVIRA1 && getFeatures() & GF_DEMO) {
				if (getPlatform() == Common::kPlatformAtariST)
					sprintf(filename, "%.2d%d.out", id, type);
				else
					sprintf(filename, "%c%d.out", 48 + id, type);
			} else if (getGameType() == GType_ELVIRA1 || getGameType() == GType_ELVIRA2) {
				sprintf(filename, "%.2d%d.pkd", id, type);
			} else if (getGameType() == GType_PN) {
				sprintf(filename, "%c%d.in", id + 48, type);
			} else {
				sprintf(filename, "%.3d%d.pkd", id, type);
			}
		} else {
			if (getGameType() == GType_ELVIRA1 || getGameType() == GType_ELVIRA2 || getGameType() == GType_WW) {
				sprintf(filename, "%.2d%d.VGA", id, type);
			} else if (getGameType() == GType_PN) {
				sprintf(filename, "%c%d.out", id + 48, type);
			} else {
				sprintf(filename, "%.3d%d.VGA", id, type);
			}
		}

		if (!in.open(filename)) {
			if (useError)
				error("loadVGAVideoFile: Can't load %s", filename);

			_block = _blockEnd = NULL;
			return;
		}

		dstSize = srcSize = in.size();
		if (getGameType() == GType_PN && getPlatform() == Common::kPlatformDOS && id == 17 && type == 2) {
			// The A2.out file isn't compressed in PC version of Personal Nightmare
			dst = allocBlock(dstSize + extraBuffer);
			if (in.read(dst, dstSize) != dstSize)
				error("loadVGAVideoFile: Read failed");
		} else if (getGameType() == GType_PN && (getFeatures() & GF_CRUNCHED)) {
			Common::Stack<uint32> data;
			byte *dataOut = 0;
			int dataOutSize = 0;

			for (uint i = 0; i < srcSize / 4; ++i) {
				uint32 dataVal = in.readUint32BE();
				// Correct incorrect byte, in corrupt 72.out file, included in some PC versions.
				if (dataVal == 168042714)
					data.push(168050906);
				else
					data.push(dataVal);
			}

			decompressPN(data, dataOut, dataOutSize);
			dst = allocBlock (dataOutSize + extraBuffer);
			memcpy(dst, dataOut, dataOutSize);
			delete[] dataOut;
		} else if (getFeatures() & GF_CRUNCHED) {
			byte *srcBuffer = (byte *)malloc(srcSize);
			if (in.read(srcBuffer, srcSize) != srcSize)
				error("loadVGAVideoFile: Read failed");

			dstSize = READ_BE_UINT32(srcBuffer + srcSize - 4);
			dst = allocBlock (dstSize + extraBuffer);
			decrunchFile(srcBuffer, dst, srcSize);
			free(srcBuffer);
		} else {
			dst = allocBlock(dstSize + extraBuffer);
			if (in.read(dst, dstSize) != dstSize)
				error("loadVGAVideoFile: Read failed");
		}
	} else {
		id = id * 2 + (type - 1);
		offs = _gameOffsetsPtr[id];
		dstSize = _gameOffsetsPtr[id + 1] - offs;

		if (!dstSize) {
			if (useError)
				error("loadVGAVideoFile: Can't load id %d type %d", id, type);

			_block = _blockEnd = NULL;
			return;
		}

		dst = allocBlock(dstSize + extraBuffer);
		readGameFile(dst, offs, dstSize);
	}
}

} // End of namespace AGOS
