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

// Video script opcodes for Simon1/Simon2


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

#include "common/debug-channels.h"
#include "common/endian.h"
#include "common/system.h"
#include "common/textconsole.h"

#include "graphics/surface.h"

namespace AGOS {

// Opcode tables
void AGOSEngine::setupVideoOpcodes(VgaOpcodeProc *op) {
	op[1] = &AGOSEngine::vc1_fadeOut;
	op[2] = &AGOSEngine::vc2_call;
	op[3] = &AGOSEngine::vc3_loadSprite;
	op[4] = &AGOSEngine::vc4_fadeIn;
	op[5] = &AGOSEngine::vc5_ifEqual;
	op[6] = &AGOSEngine::vc6_ifObjectHere;
	op[7] = &AGOSEngine::vc7_ifObjectNotHere;
	op[8] = &AGOSEngine::vc8_ifObjectIsAt;
	op[9] = &AGOSEngine::vc9_ifObjectStateIs;
	op[10] = &AGOSEngine::vc10_draw;
	op[12] = &AGOSEngine::vc12_delay;
	op[13] = &AGOSEngine::vc13_addToSpriteX;
	op[14] = &AGOSEngine::vc14_addToSpriteY;
	op[15] = &AGOSEngine::vc15_sync;
	op[16] = &AGOSEngine::vc16_waitSync;
	op[18] = &AGOSEngine::vc18_jump;
	op[20] = &AGOSEngine::vc20_setRepeat;
	op[21] = &AGOSEngine::vc21_endRepeat;
	op[23] = &AGOSEngine::vc23_setPriority;
	op[24] = &AGOSEngine::vc24_setSpriteXY;
	op[25] = &AGOSEngine::vc25_halt_sprite;
	op[26] = &AGOSEngine::vc26_setSubWindow;
	op[27] = &AGOSEngine::vc27_resetSprite;
	op[29] = &AGOSEngine::vc29_stopAllSounds;
	op[30] = &AGOSEngine::vc30_setFrameRate;
	op[31] = &AGOSEngine::vc31_setWindow;
	op[33] = &AGOSEngine::vc33_setMouseOn;
	op[34] = &AGOSEngine::vc34_setMouseOff;
	op[35] = &AGOSEngine::vc35_clearWindow;
	op[36] = &AGOSEngine::vc36_setWindowImage;
	op[38] = &AGOSEngine::vc38_ifVarNotZero;
	op[39] = &AGOSEngine::vc39_setVar;
	op[40] = &AGOSEngine::vc40_scrollRight;
	op[41] = &AGOSEngine::vc41_scrollLeft;
	op[42] = &AGOSEngine::vc42_delayIfNotEQ;
	op[43] = &AGOSEngine::vc43_ifBitSet;
	op[44] = &AGOSEngine::vc44_ifBitClear;
	op[45] = &AGOSEngine::vc45_setSpriteX;
	op[46] = &AGOSEngine::vc46_setSpriteY;
	op[47] = &AGOSEngine::vc47_addToVar;
	op[49] = &AGOSEngine::vc49_setBit;
	op[50] = &AGOSEngine::vc50_clearBit;
	op[51] = &AGOSEngine::vc51_enableBox;
	op[52] = &AGOSEngine::vc52_playSound;
	op[55] = &AGOSEngine::vc55_moveBox;
}

void AGOSEngine_Elvira1::setupVideoOpcodes(VgaOpcodeProc *op) {
	op[1] = &AGOSEngine::vc1_fadeOut;
	op[2] = &AGOSEngine::vc2_call;
	op[3] = &AGOSEngine::vc3_loadSprite;
	op[4] = &AGOSEngine::vc4_fadeIn;
	op[5] = &AGOSEngine::vc5_ifEqual;
	op[6] = &AGOSEngine::vc6_ifObjectHere;
	op[7] = &AGOSEngine::vc7_ifObjectNotHere;
	op[8] = &AGOSEngine::vc8_ifObjectIsAt;
	op[9] = &AGOSEngine::vc9_ifObjectStateIs;
	op[10] = &AGOSEngine::vc10_draw;
	op[13] = &AGOSEngine::vc12_delay;
	op[14] = &AGOSEngine::vc13_addToSpriteX;
	op[15] = &AGOSEngine::vc14_addToSpriteY;
	op[16] = &AGOSEngine::vc15_sync;
	op[17] = &AGOSEngine::vc16_waitSync;
	op[18] = &AGOSEngine::vc17_waitEnd;
	op[19] = &AGOSEngine::vc18_jump;
	op[20] = &AGOSEngine::vc19_loop;
	op[21] = &AGOSEngine::vc20_setRepeat;
	op[22] = &AGOSEngine::vc21_endRepeat;
	op[23] = &AGOSEngine::vc22_setPalette;
	op[24] = &AGOSEngine::vc23_setPriority;
	op[25] = &AGOSEngine::vc24_setSpriteXY;
	op[26] = &AGOSEngine::vc25_halt_sprite;
	op[27] = &AGOSEngine::vc26_setSubWindow;
	op[28] = &AGOSEngine::vc27_resetSprite;
	op[29] = &AGOSEngine::vc28_playSFX;
	op[30] = &AGOSEngine::vc29_stopAllSounds;
	op[31] = &AGOSEngine::vc30_setFrameRate;
	op[32] = &AGOSEngine::vc31_setWindow;
	op[33] = &AGOSEngine::vc32_saveScreen;
	op[34] = &AGOSEngine::vc33_setMouseOn;
	op[35] = &AGOSEngine::vc34_setMouseOff;
	op[38] = &AGOSEngine::vc35_clearWindow;
	op[40] = &AGOSEngine::vc36_setWindowImage;
	op[41] = &AGOSEngine::vc37_pokePalette;
	op[51] = &AGOSEngine::vc38_ifVarNotZero;
	op[52] = &AGOSEngine::vc39_setVar;
	op[53] = &AGOSEngine::vc40_scrollRight;
	op[54] = &AGOSEngine::vc41_scrollLeft;
	op[56] = &AGOSEngine::vc42_delayIfNotEQ;
}

void AGOSEngine::setupVgaOpcodes() {
	memset(_vga_opcode_table, 0, sizeof(_vga_opcode_table));

	switch (getGameType()) {
	case GType_PN:
	case GType_ELVIRA1:
	case GType_ELVIRA2:
	case GType_WW:
	case GType_SIMON1:
	case GType_SIMON2:
	case GType_FF:
	case GType_PP:
		setupVideoOpcodes(_vga_opcode_table);
		break;
	default:
		error("setupVgaOpcodes: Unknown game");
	}
}

// VGA Script parser
void AGOSEngine::runVgaScript() {
	for (;;) {
		uint opcode;

		if (DebugMan.isDebugChannelEnabled(kDebugVGAOpcode)) {
			if (_vcPtr != (const byte *)&_vcGetOutOfCode) {
				debugN("%.5d %.5X: %5d %4d ", _vgaTickCounter, (unsigned int)(_vcPtr - _curVgaFile1), _vgaCurSpriteId, _vgaCurZoneNum);
				dumpVideoScript(_vcPtr, true);
			}
		}

		if (getGameType() == GType_SIMON2 || getGameType() == GType_FF || getGameType() == GType_PP) {
			opcode = *_vcPtr++;
		} else {
			opcode = READ_BE_UINT16(_vcPtr);
			_vcPtr += 2;
		}

		if (opcode == 0)
			return;

		if (opcode >= _numVideoOpcodes || !_vga_opcode_table[opcode])
			error("runVgaScript: Invalid VGA opcode '%d' encountered", opcode);

		(this->*_vga_opcode_table[opcode]) ();
	}
}

bool AGOSEngine::ifObjectHere(uint16 a) {
	Item *item;

	CHECK_BOUNDS(a, _objectArray);

	item = _objectArray[a];
	if (item == NULL)
		return true;

	return me()->parent == item->parent;
}

bool AGOSEngine::ifObjectAt(uint16 a, uint16 b) {
	Item *item_a, *item_b;

	CHECK_BOUNDS(a, _objectArray);
	CHECK_BOUNDS(b, _objectArray);

	item_a = _objectArray[a];
	item_b = _objectArray[b];

	if (item_a == NULL || item_b == NULL)
		return true;

	return derefItem(item_a->parent) == item_b;
}

bool AGOSEngine::ifObjectState(uint16 a, int16 b) {
	Item *item;

	CHECK_BOUNDS(a, _objectArray);

	item = _objectArray[a];
	if (item == NULL)
		return true;
	return item->state == b;
}

void AGOSEngine::dirtyBackGround() {
	AnimTable *animTable = _screenAnim1;
	while (animTable->srcPtr) {
		if (animTable->id == _vgaCurSpriteId && animTable->zoneNum == _vgaCurZoneNum) {
			animTable->windowNum |= 0x8000;
			break;
		}
		animTable++;
	}
}

VgaSprite *AGOSEngine::findCurSprite() {
	VgaSprite *vsp = _vgaSprites;
	while (vsp->id) {
		if (vsp->id == _vgaCurSpriteId && vsp->zoneNum == _vgaCurZoneNum)
			break;
		vsp++;
	}
	return vsp;
}

bool AGOSEngine::isSpriteLoaded(uint16 id, uint16 zoneNum) {
	VgaSprite *vsp = _vgaSprites;
	while (vsp->id) {
		if (vsp->id == id && vsp->zoneNum == zoneNum)
			return true;
		vsp++;
	}
	return false;
}

bool AGOSEngine::getBitFlag(uint bit) {
	uint16 *bits = &_bitArray[bit / 16];
	return (*bits & (1 << (bit & 15))) != 0;
}

void AGOSEngine::setBitFlag(uint bit, bool value) {
	uint16 *bits = &_bitArray[bit / 16];
	*bits = (*bits & ~(1 << (bit & 15))) | (value << (bit & 15));
}

int AGOSEngine::vcReadVarOrWord() {
	if (getGameType() == GType_PN || getGameType() == GType_ELVIRA1) {
		return vcReadNextWord();
	} else {
		int16 var = vcReadNextWord();
		if (var < 0)
			var = vcReadVar(-var);
		return var;
	}
}

uint AGOSEngine::vcReadNextWord() {
	uint a;
	a = readUint16Wrapper(_vcPtr);
	_vcPtr += 2;
	return a;
}

uint AGOSEngine::vcReadNextByte() {
	return *_vcPtr++;
}

uint AGOSEngine::vcReadVar(uint var) {
	assert(var < _numVars);
	return (uint16)_variableArrayPtr[var];
}

void AGOSEngine::vcWriteVar(uint var, int16 value) {
	assert(var < _numVars);
	_variableArrayPtr[var] = value;
}

void AGOSEngine::vcSkipNextInstruction() {

	static const byte opcodeParamLenPN[] = {
		0, 6,  2, 10, 6, 4, 2, 2,
		4, 4,  8,  2, 0, 2, 2, 2,
		0, 2,  2,  2, 0, 4, 2, 2,
		2, 8,  0, 10, 0, 8, 0, 2,
		2, 0,  0,  0, 0, 2, 4, 2,
		4, 4,  0,  0, 2, 2, 2, 4,
		4, 0, 18,  2, 4, 4, 4, 0,
		4
	};

	static const byte opcodeParamLenElvira1[] = {
		0, 6,  2, 10, 6, 4, 2, 2,
		4, 4,  8,  2, 0, 2, 2, 2,
		2, 2,  2,  2, 0, 4, 2, 2,
		2, 8,  0, 10, 0, 8, 0, 2,
		2, 0,  0,  0, 0, 2, 4, 2,
		4, 4,  0,  0, 2, 2, 2, 4,
		4, 0, 18,  2, 4, 4, 4, 0,
		4
	};

	static const byte opcodeParamLenWW[] = {
		0, 6,  2, 10, 6, 4, 2, 2,
		4, 4,  8,  2, 2, 2, 2, 2,
		2, 2,  2,  0, 4, 2, 2, 2,
		8, 0, 10,  0, 8, 0, 2, 2,
		0, 0,  0,  4, 4, 4, 2, 4,
		4, 4,  4,  2, 2, 4, 2, 2,
		2, 2,  2,  2, 2, 4, 6, 6,
		0, 0,  0,  0, 2, 2, 0, 0,
	};

	static const byte opcodeParamLenSimon1[] = {
		0, 6,  2, 10, 6, 4, 2, 2,
		4, 4, 10,  0, 2, 2, 2, 2,
		2, 0,  2,  0, 4, 2, 4, 2,
		8, 0, 10,  0, 8, 0, 2, 2,
		4, 0,  0,  4, 4, 2, 2, 4,
		4, 4,  4,  2, 2, 2, 2, 4,
		0, 2,  2,  2, 2, 4, 6, 6,
		0, 0,  0,  0, 2, 6, 0, 0,
	};

	static const byte opcodeParamLenSimon2[] = {
		0, 6,  2, 12, 6, 4, 2, 2,
		4, 4,  9,  0, 1, 2, 2, 2,
		2, 0,  2,  0, 4, 2, 4, 2,
		7, 0, 10,  0, 8, 0, 2, 2,
		4, 0,  0,  4, 4, 2, 2, 4,
		4, 4,  4,  2, 2, 2, 2, 4,
		0, 2,  2,  2, 2, 4, 6, 6,
		2, 0,  6,  6, 4, 6, 0, 0,
		0, 0,  4,  4, 4, 4, 4, 0,
		4, 2,  2
	};

	static const byte opcodeParamLenFeebleFiles[] = {
		0, 6, 2, 12, 6, 4, 2, 2,
		4, 4, 9, 0, 1, 2, 2, 2,
		2, 0, 2, 0, 4, 2, 4, 2,
		7, 0, 10, 0, 8, 0, 2, 2,
		4, 0, 0, 4, 4, 2, 2, 4,
		4, 4, 4, 2, 2, 2, 2, 4,
		0, 2, 2, 2, 6, 6, 6, 6,
		2, 0, 6, 6, 4, 6, 0, 0,
		0, 0, 4, 4, 4, 4, 4, 0,
		4, 2, 2, 4, 6, 6, 0, 0,
		6, 4, 2, 6, 0
	};

	uint16 opcode;
	if (getGameType() == GType_FF || getGameType() == GType_PP) {
		opcode = vcReadNextByte();
		_vcPtr += opcodeParamLenFeebleFiles[opcode];
	} else if (getGameType() == GType_SIMON2) {
		opcode = vcReadNextByte();
		_vcPtr += opcodeParamLenSimon2[opcode];
	} else if (getGameType() == GType_SIMON1) {
		opcode = vcReadNextWord();
		_vcPtr += opcodeParamLenSimon1[opcode];
	} else if (getGameType() == GType_ELVIRA2 || getGameType() == GType_WW) {
		opcode = vcReadNextWord();
		_vcPtr += opcodeParamLenWW[opcode];
	} else if (getGameType() == GType_ELVIRA1) {
		opcode = vcReadNextWord();
		_vcPtr += opcodeParamLenElvira1[opcode];
	} else {
		opcode = vcReadNextWord();
		_vcPtr += opcodeParamLenPN[opcode];
	}

	debugCN(kDebugVGAOpcode, "; skipped\n");
}

// VGA Script commands
void AGOSEngine::vc1_fadeOut() {
	/* dummy opcode */
	_vcPtr += 6;
}

void AGOSEngine::vc2_call() {
	uint16 num;
	byte *old_file_1, *old_file_2;

	if (getGameType() == GType_ELVIRA2) {
		num = vcReadNextWord();
	} else {
		num = vcReadVarOrWord();
	}

	old_file_1 = _curVgaFile1;
	old_file_2 = _curVgaFile2;

	setImage(num, true);

	_curVgaFile1 = old_file_1;
	_curVgaFile2 = old_file_2;
}

void AGOSEngine::vc3_loadSprite() {
	uint16 windowNum, zoneNum, palette, vgaSpriteId;
	int16 x, y;
	byte *old_file_1;

	windowNum = vcReadNextWord();
	if (getGameType() == GType_SIMON1 && windowNum == 3) {
		_window3Flag = 1;
	}

	if (getGameType() == GType_SIMON2 || getGameType() == GType_FF || getGameType() == GType_PP) {
		zoneNum = vcReadNextWord();
		vgaSpriteId = vcReadNextWord();
	} else {
		vgaSpriteId = vcReadNextWord();
		zoneNum = (getGameType() == GType_PN) ? 0 : vgaSpriteId / 100;
	}

	x = vcReadNextWord();
	y = vcReadNextWord();
	palette = vcReadNextWord();

	old_file_1 = _curVgaFile1;

	animate(windowNum, zoneNum, vgaSpriteId, x, y, palette, true);

	_curVgaFile1 = old_file_1;
}

void AGOSEngine::vc4_fadeIn() {
	/* dummy opcode */
	_vcPtr += 6;
}

void AGOSEngine::vc5_ifEqual() {
	uint16 var;

	if (getGameType() == GType_PP)
		var = vcReadVarOrWord();
	else
		var = vcReadNextWord();

	uint16 value = vcReadNextWord();
	if (vcReadVar(var) != value)
		vcSkipNextInstruction();
}

void AGOSEngine::vc6_ifObjectHere() {
	if (!ifObjectHere(vcReadNextWord())) {
		vcSkipNextInstruction();
	}
}

void AGOSEngine::vc7_ifObjectNotHere() {
	if (ifObjectHere(vcReadNextWord()))
		vcSkipNextInstruction();
}

void AGOSEngine::vc8_ifObjectIsAt() {
	uint16 a = vcReadNextWord();
	uint16 b = vcReadNextWord();
	if (!ifObjectAt(a, b))
		vcSkipNextInstruction();
}

void AGOSEngine::vc9_ifObjectStateIs() {
	uint16 a = vcReadNextWord();
	uint16 b = vcReadNextWord();
	if (!ifObjectState(a, b))
		vcSkipNextInstruction();
}

byte *AGOSEngine::vc10_uncompressFlip(const byte *src, uint16 w, uint16 h) {
	w *= 8;

	byte *dst, *dstPtr, *srcPtr;
	byte color;
	int8 cur = -0x80;
	uint i, w_cur = w;

	dstPtr = _videoBuf1 + w;

	do {
		dst = dstPtr;
		uint h_cur = h;

		if (cur == -0x80)
			cur = *src++;

		for (;;) {
			if (cur >= 0) {
				/* rle_same */
				color = *src++;
				do {
					*dst = color;
					dst += w;
					if (!--h_cur) {
						if (--cur < 0)
							cur = -0x80;
						else
							src--;
						goto next_line;
					}
				} while (--cur >= 0);
			} else {
				/* rle_diff */
				do {
					*dst = *src++;
					dst += w;
					if (!--h_cur) {
						if (++cur == 0)
							cur = -0x80;
						goto next_line;
					}
				} while (++cur != 0);
			}
			cur = *src++;
		}
	next_line:
		dstPtr++;
	} while (--w_cur);

	srcPtr = dstPtr = _videoBuf1 + w;

	do {
		dst = dstPtr;
		for (i = 0; i != w; ++i) {
			byte b = srcPtr[i];
			b = (b >> 4) | (b << 4);
			*--dst = b;
		}

		srcPtr += w;
		dstPtr += w;
	} while (--h);

	return _videoBuf1;
}

byte *AGOSEngine::vc10_flip(const byte *src, uint16 w, uint16 h) {
	byte *dstPtr;
	uint i;

	if (getFeatures() & GF_32COLOR) {
		w *= 16;
		dstPtr = _videoBuf1 + w;

		do {
			byte *dst = dstPtr;
			for (i = 0; i != w; ++i) {
				*--dst = src[i];
			}

			src += w;
			dstPtr += w;
		} while (--h);
	} else {
		w *= 8;
		dstPtr = _videoBuf1 + w;

		do {
			byte *dst = dstPtr;
			for (i = 0; i != w; ++i) {
				byte b = src[i];
				b = (b >> 4) | (b << 4);
				*--dst = b;
			}

			src += w;
			dstPtr += w;
		} while (--h);
	}

	return _videoBuf1;
}

void AGOSEngine::vc10_draw() {
	uint16 palette, x, y, flags;
	int16 image;

	image = (int16)vcReadNextWord();

	palette = 0;
	if (getGameType() == GType_FF || getGameType() == GType_PP) {
		palette = _vcPtr[0];
		_vcPtr += 2;
	} else if (getGameType() == GType_SIMON1 || getGameType() == GType_SIMON2) {
		palette = _vcPtr[1];
		_vcPtr += 2;
	}

	x = (int16)vcReadNextWord();
	y = (int16)vcReadNextWord();

	if (getGameType() == GType_SIMON2 || getGameType() == GType_FF || getGameType() == GType_PP) {
		flags = vcReadNextByte();
	} else {
		flags = vcReadNextWord();
	}

	drawImage_init(image, palette, x, y, flags);
}

void AGOSEngine::drawImage_init(int16 image, uint16 palette, int16 x, int16 y, uint16 flags) {
	if (image == 0)
		return;

	byte *src;
	uint width, height;
	VC10_state state;

	state.image = image;
	if (state.image < 0)
		state.image = vcReadVar(-state.image);

	state.palette = (getGameType() == GType_PN) ? 0 : palette * 16;
	state.paletteMod = 0;

	state.x = x - _scrollX;
	state.y = y - _scrollY;

	state.flags = flags;

	src = _curVgaFile2 + state.image * 8;
	state.srcPtr = _curVgaFile2 + readUint32Wrapper(src);
	if (getGameType() == GType_FF || getGameType() == GType_PP) {
		width = READ_LE_UINT16(src + 6);
		height = READ_LE_UINT16(src + 4) & 0x7FFF;
		flags = src[5];
	} else {
		width = READ_BE_UINT16(src + 6) / 16;
		height = src[5];
		flags = src[4];
	}

	if (height == 0 || width == 0)
		return;

	if (DebugMan.isDebugChannelEnabled(kDebugImageDump))
		dumpSingleBitmap(_vgaCurZoneNum, state.image, state.srcPtr, width, height,
											 state.palette);
	state.width = state.draw_width = width;		/* cl */
	state.height = state.draw_height = height;	/* ch */

	state.depack_cont = -0x80;

	state.x_skip = 0;				/* colums to skip = bh */
	state.y_skip = 0;				/* rows to skip   = bl */

	if (getFeatures() & GF_PLANAR) {
		if (getGameType() == GType_PN) {
			state.srcPtr = convertImage(&state, ((state.flags & (kDFCompressed | kDFCompressedFlip)) != 0));
		}
		else
			state.srcPtr = convertImage(&state, ((flags & 0x80) != 0));

		// converted planar clip is already uncompressed
		if (state.flags & kDFCompressedFlip) {
			state.flags &= ~kDFCompressedFlip;
			state.flags |= kDFFlip;
		}
		if (state.flags & kDFCompressed) {
			state.flags &= ~kDFCompressed;
		}
	} else if (getGameType() == GType_FF || getGameType() == GType_PP) {
		if (flags & 0x80) {
			state.flags |= kDFCompressed;
		}
	} else {
		if (flags & 0x80 && !(state.flags & kDFCompressedFlip)) {
			if (state.flags & kDFFlip) {
				state.flags &= ~kDFFlip;
				state.flags |= kDFCompressedFlip;
			} else {
				state.flags |= kDFCompressed;
			}
		}
	}

	uint maxWidth = (getGameType() == GType_FF || getGameType() == GType_PP) ? 640 : 20;
	if ((getGameType() == GType_SIMON2 || getGameType() == GType_FF) && width > maxWidth) {
		horizontalScroll(&state);
		return;
	}
	if (getGameType() == GType_FF && height > 480) {
		verticalScroll(&state);
		return;
	}

	if (getGameType() != GType_FF && getGameType() != GType_PP) {
		if (state.flags & kDFCompressedFlip) {
			state.srcPtr = vc10_uncompressFlip(state.srcPtr, width, height);
		} else if (state.flags & kDFFlip) {
			state.srcPtr = vc10_flip(state.srcPtr, width, height);
		}
	}

	drawImage(&state);
}

void AGOSEngine::checkOnStopTable() {
	VgaSleepStruct *vfs = _onStopTable, *vfs_tmp;
	while (vfs->ident != 0) {
		if (vfs->ident == _vgaCurSpriteId) {
			VgaSprite *vsp = findCurSprite();
			animate(vsp->windowNum, vsp->zoneNum, vfs->id, vsp->x, vsp->y, vsp->palette, true);
			vfs_tmp = vfs;
			do {
				memcpy(vfs_tmp, vfs_tmp + 1, sizeof(VgaSleepStruct));
				vfs_tmp++;
			} while (vfs_tmp->ident != 0);
		} else {
			vfs++;
		}
	}
}

void AGOSEngine::vc11_onStop() {
	uint16 id = vcReadNextWord();

	VgaSleepStruct *vfs = _onStopTable;
	while (vfs->ident)
		vfs++;

	vfs->ident = _vgaCurSpriteId;
	vfs->codePtr = _vcPtr;
	vfs->id = id;
	vfs->zoneNum = _vgaCurZoneNum;
}

void AGOSEngine::vc12_delay() {
	uint16 num;

	if (getGameType() == GType_FF || getGameType() == GType_PP) {
		num = vcReadNextByte();
	} else if (getGameType() == GType_SIMON2) {
		num = vcReadNextByte() * _frameCount;
	} else {
		num = vcReadVarOrWord() * _frameCount;
	}

	num += _vgaBaseDelay;

	addVgaEvent(num, ANIMATE_EVENT, _vcPtr, _vgaCurSpriteId, _vgaCurZoneNum);
	_vcPtr = (byte *)&_vcGetOutOfCode;
}

void AGOSEngine::vc13_addToSpriteX() {
	VgaSprite *vsp = findCurSprite();
	vsp->x += (int16)vcReadNextWord();

	vsp->windowNum |= 0x8000;
	dirtyBackGround();
	_vgaSpriteChanged++;
}

void AGOSEngine::vc14_addToSpriteY() {
	VgaSprite *vsp = findCurSprite();
	vsp->y += (int16)vcReadNextWord();

	vsp->windowNum |= 0x8000;
	dirtyBackGround();
	_vgaSpriteChanged++;
}

void AGOSEngine::vc15_sync() {
	VgaSleepStruct *vfs = _waitSyncTable, *vfs_tmp;
	uint16 id;

	if (getGameType() == GType_PN)
		id = _vgaCurSpriteId;
	else
		id = vcReadNextWord();

	while (vfs->ident != 0) {
		if (vfs->ident == id) {
			addVgaEvent(_vgaBaseDelay, ANIMATE_EVENT, vfs->codePtr, vfs->id, vfs->zoneNum);
			vfs_tmp = vfs;
			do {
				memcpy(vfs_tmp, vfs_tmp + 1, sizeof(VgaSleepStruct));
				vfs_tmp++;
			} while (vfs_tmp->ident != 0);
		} else {
			vfs++;
		}
	}

	_lastVgaWaitFor = id;
	/* clear a wait event */
	if (id == _vgaWaitFor)
		_vgaWaitFor = 0;
}

void AGOSEngine::vc16_waitSync() {
	VgaSleepStruct *vfs = _waitSyncTable;
	while (vfs->ident)
		vfs++;

	vfs->ident = vcReadNextWord();
	vfs->codePtr = _vcPtr;
	vfs->id = _vgaCurSpriteId;
	vfs->zoneNum = _vgaCurZoneNum;

	_vcPtr = (byte *)&_vcGetOutOfCode;
}

void AGOSEngine::checkWaitEndTable() {
	VgaSleepStruct *vfs = _waitEndTable, *vfs_tmp;
	while (vfs->ident != 0) {
		if (vfs->ident == _vgaCurSpriteId) {
			addVgaEvent(_vgaBaseDelay, ANIMATE_EVENT, vfs->codePtr, vfs->id, vfs->zoneNum);
			vfs_tmp = vfs;
			do {
				memcpy(vfs_tmp, vfs_tmp + 1, sizeof(VgaSleepStruct));
				vfs_tmp++;
			} while (vfs_tmp->ident != 0);
		} else {
			vfs++;
		}
	}
}

void AGOSEngine::vc17_waitEnd() {
	uint16 id = vcReadNextWord();
	uint16 zoneNum = (getGameType() == GType_PN) ? 0 : id / 100;

	VgaSleepStruct *vfs = _waitEndTable;
	while (vfs->ident)
		vfs++;

	if (isSpriteLoaded(id, zoneNum)) {
		vfs->ident = id;
		vfs->codePtr = _vcPtr;
		vfs->id = _vgaCurSpriteId;
		vfs->zoneNum = _vgaCurZoneNum;
		_vcPtr = (byte *)&_vcGetOutOfCode;
	}
}

void AGOSEngine::vc18_jump() {
	int16 offs = vcReadNextWord();
	_vcPtr += offs;
}

void AGOSEngine::vc19_loop() {
	uint16 count;
	byte *b, *bb;

	bb = _curVgaFile1;
	b = _curVgaFile1 + READ_BE_UINT16(bb + 10);
	b += 20;

	count = READ_BE_UINT16(&((VgaFile1Header_Common *) b)->animationCount);
	b = bb + READ_BE_UINT16(&((VgaFile1Header_Common *) b)->animationTable);

	while (count--) {
		if (READ_BE_UINT16(&((AnimationHeader_WW *) b)->id) == _vgaCurSpriteId)
			break;
		b += sizeof(AnimationHeader_WW);
	}
	assert(READ_BE_UINT16(&((AnimationHeader_WW *) b)->id) == _vgaCurSpriteId);

	_vcPtr = _curVgaFile1 + READ_BE_UINT16(&((AnimationHeader_WW *) b)->scriptOffs);
}

void AGOSEngine::vc20_setRepeat() {
	// Sets counter used by the endRepeat opcode below.
	uint16 a = vcReadNextWord();
	WRITE_LE_UINT16(const_cast<byte *>(_vcPtr), a);
	_vcPtr += 2;
}

void AGOSEngine::vc21_endRepeat() {
	int16 a = vcReadNextWord();
	const byte *tmp = _vcPtr + a;
	if (getGameType() == GType_SIMON2 || getGameType() == GType_FF || getGameType() == GType_PP)
		tmp += 3;
	else
		tmp += 4;

	uint16 val = READ_LE_UINT16(tmp);
	if (val != 0) {
		// Decrement counter
		WRITE_LE_UINT16(const_cast<byte *>(tmp), val - 1);
		_vcPtr = tmp + 2;
	}
}

static const uint8 iconPalette[64] = {
	0x00, 0x00, 0x00,
	0x77, 0x77, 0x55,
	0x55, 0x00, 0x00,
	0x77, 0x00, 0x00,
	0x22, 0x00, 0x00,
	0x00, 0x11, 0x00,
	0x11, 0x22, 0x11,
	0x22, 0x33, 0x22,
	0x44, 0x55, 0x44,
	0x33, 0x44, 0x00,
	0x11, 0x33, 0x00,
	0x00, 0x11, 0x44,
	0x77, 0x44, 0x00,
	0x66, 0x22, 0x00,
	0x00, 0x22, 0x66,
	0x77, 0x55, 0x00,
};

void AGOSEngine::vc22_setPalette() {
	byte *offs, *palptr, *src;
	uint16 b, num;

	b = vcReadNextWord();

	// PC EGA version of Personal Nightmare uses standard EGA palette
	if (getGameType() == GType_PN && (getFeatures() & GF_EGA))
		return;

	num = 16;

	palptr = _displayPalette;
	_bottomPalette = 1;

	if (getGameType() == GType_PN) {
		if (b > 128) {
			b -= 128;
			palptr = _displayPalette + 3 * 16;
		}
	} else if (getGameType() == GType_ELVIRA1) {
		if (b >= 1000) {
			b -= 1000;
			_bottomPalette = 0;
		} else {
			const byte extraColors[19 * 3] = {
				40,  0,  0,   24, 24, 16,   48, 48, 40,
				 0,  0,  0,   16,  0,  0,    8,  8,  0,
				48, 24,  0,   56, 40,  0,    0,  0, 24,
				 8, 16, 24,   24, 32, 40,   16, 24,  0,
				24,  8,  0,   16, 16,  0,   40, 40, 32,
				32, 32, 24,   40,  0,  0,   24, 24, 16,
				48, 48, 40
			};

			num = 13;

			for (int i = 0; i < 19; i++) {
				palptr[(13 + i) * 3 + 0] = extraColors[i * 3 + 0] * 4;
				palptr[(13 + i) * 3 + 1] = extraColors[i * 3 + 1] * 4;
				palptr[(13 + i) * 3 + 2] = extraColors[i * 3 + 2] * 4;
			}
		}
	}

	if (getGameType() == GType_ELVIRA2 && getPlatform() == Common::kPlatformAtariST) {
		// Custom palette used for icon area
		palptr = &_displayPalette[13 * 3 * 16];
		for (uint8 c = 0; c < 16; c++) {
			palptr[0] = iconPalette[c * 3 + 0] * 2;
			palptr[1] = iconPalette[c * 3 + 1] * 2;
			palptr[2] = iconPalette[c * 3 + 2] * 2;

			palptr += 3;
		};
		palptr = _displayPalette;
	}

	offs = _curVgaFile1 + READ_BE_UINT16(_curVgaFile1 + 6);
	src = offs + b * 32;

	do {
		uint16 color = READ_BE_UINT16(src);
		palptr[0] = ((color & 0xf00) >> 8) * 32;
		palptr[1] = ((color & 0x0f0) >> 4) * 32;
		palptr[2] = ((color & 0x00f) >> 0) * 32;

		palptr += 3;
		src += 2;
	} while (--num);

	_paletteFlag = 2;
	_vgaSpriteChanged++;
}

void AGOSEngine::vc23_setPriority() {
	VgaSprite *vsp = findCurSprite(), *vus2;
	uint16 pri = vcReadNextWord();
	VgaSprite bak;

	if (vsp->id == 0)
		return;

	memcpy(&bak, vsp, sizeof(bak));
	bak.priority = pri;
	bak.windowNum |= 0x8000;

	vus2 = vsp;

	if (vsp != _vgaSprites && pri < vsp[-1].priority) {
		do {
			vsp--;
		} while (vsp != _vgaSprites && pri < vsp[-1].priority);
		do {
			memcpy(vus2, vus2 - 1, sizeof(VgaSprite));
		} while (--vus2 != vsp);
		memcpy(vus2, &bak, sizeof(VgaSprite));
	} else if (vsp[1].id != 0 && pri >= vsp[1].priority) {
		do {
			vsp++;
		} while (vsp[1].id != 0 && pri >= vsp[1].priority);
		do {
			memcpy(vus2, vus2 + 1, sizeof(VgaSprite));
		} while (++vus2 != vsp);
		memcpy(vus2, &bak, sizeof(VgaSprite));
	} else {
		vsp->priority = pri;
	}
	_vgaSpriteChanged++;
}

void AGOSEngine::vc24_setSpriteXY() {
	VgaSprite *vsp = findCurSprite();

	if (getGameType() == GType_ELVIRA2) {
		vsp->image = vcReadNextWord();
	} else {
		vsp->image = vcReadVarOrWord();
	}

	vsp->x += (int16)vcReadNextWord();
	vsp->y += (int16)vcReadNextWord();
	if (getGameType() == GType_SIMON2 || getGameType() == GType_FF || getGameType() == GType_PP) {
		vsp->flags = vcReadNextByte();
	} else {
		vsp->flags = vcReadNextWord();
	}

	vsp->windowNum |= 0x8000;
	dirtyBackGround();
	_vgaSpriteChanged++;
}

void AGOSEngine::vc25_halt_sprite() {
	checkWaitEndTable();
	checkOnStopTable();

	VgaSprite *vsp = findCurSprite();
	while (vsp->id != 0) {
		memcpy(vsp, vsp + 1, sizeof(VgaSprite));
		vsp++;
	}
	_vcPtr = (byte *)&_vcGetOutOfCode;

	dirtyBackGround();
	_vgaSpriteChanged++;
}

void AGOSEngine::vc26_setSubWindow() {
	uint16 *as = &_videoWindows[vcReadNextWord() * 4]; // number
	as[0] = vcReadNextWord(); // x
	as[1] = vcReadNextWord(); // y
	as[2] = vcReadNextWord(); // width
	as[3] = vcReadNextWord(); // height
}

void AGOSEngine::vc27_resetSprite() {
	VgaSprite bak, *vsp;
	VgaSleepStruct *vfs;
	VgaTimerEntry *vte, *vte2;

	_videoLockOut |= 8;

	_lastVgaWaitFor = 0;

	memset(&bak, 0, sizeof(bak));

	vsp = _vgaSprites;
	while (vsp->id) {
		// For animated heart in Elvira 2
		if (getGameType() == GType_ELVIRA2 && vsp->id == 100) {
			memcpy(&bak, vsp, sizeof(VgaSprite));
		}
		vsp->id = 0;
		vsp++;
	}

	if (bak.id != 0)
		memcpy(_vgaSprites, &bak, sizeof(VgaSprite));

	vfs = _waitEndTable;
	while (vfs->ident) {
		vfs->ident = 0;
		vfs++;
	}

	vfs = _waitSyncTable;
	while (vfs->ident) {
		vfs->ident = 0;
		vfs++;
	}

	vfs = _onStopTable;
	while (vfs->ident) {
		vfs->ident = 0;
		vfs++;
	}

	vte = _vgaTimerList;
	while (vte->delay) {
		// Skip the animateSprites event in earlier games
		if (vte->type == ANIMATE_INT) {
			vte++;
		// For animated heart in Elvira 2
		} else if (getGameType() == GType_ELVIRA2 && vte->id == 100) {
			vte++;
		} else {
			vte2 = vte;
			while (vte2->delay) {
				memcpy(vte2, vte2 + 1, sizeof(VgaTimerEntry));
				vte2++;
			}
		}
	}

	if (_videoLockOut & 0x20) {
		AnimTable *animTable = _screenAnim1;
		while (animTable->srcPtr) {
			animTable->srcPtr = 0;
			animTable++;
		}
	}

	if (getGameType() == GType_SIMON2 || getGameType() == GType_FF || getGameType() == GType_PP)
		vcWriteVar(254, 0);

	// Stop any OmniTV video that is currently been played
	if (getGameType() == GType_FF || getGameType() == GType_PP)
		setBitFlag(42, true);

	_videoLockOut &= ~8;
}

void AGOSEngine::vc28_playSFX() {
	uint16 sound = vcReadNextWord();
	uint16 chans = vcReadNextWord();
	uint16 freq = vcReadNextWord();
	uint16 flags = vcReadNextWord();
	debug(0, "vc28_playSFX: (sound %d, channels %d, frequency %d, flags %d)", sound, chans, freq, flags);

	loadSound(sound, freq, flags);
}

void AGOSEngine::vc29_stopAllSounds() {
	if (getGameType() != GType_PP)
		_sound->stopVoice();

	_sound->stopAllSfx();
}

void AGOSEngine::vc30_setFrameRate() {
	_frameCount = vcReadNextWord();
}

void AGOSEngine::vc31_setWindow() {
	_windowNum = vcReadNextWord();
}

void AGOSEngine::vc32_saveScreen() {
	if (getGameType() == GType_PN) {
		Graphics::Surface *screen = _system->lockScreen();
		byte *dst = getBackGround();
		byte *src = (byte *)screen->getPixels();
		for (int i = 0; i < _screenHeight; i++) {
			memcpy(dst, src, _screenWidth);
			dst += _backGroundBuf->pitch;
			src += screen->pitch;
		}
		_system->unlockScreen();
	} else {
		uint16 xoffs = _videoWindows[4 * 4 + 0] * 16;
		uint16 yoffs = _videoWindows[4 * 4 + 1];
		uint16 width = _videoWindows[4 * 4 + 2] * 16;
		uint16 height = _videoWindows[4 * 4 + 3];

		byte *dst = (byte *)_backGroundBuf->getBasePtr(xoffs, yoffs);
		byte *src = (byte *)_window4BackScn->getPixels();
		uint16 srcWidth = _videoWindows[4 * 4 + 2] * 16;
		for (; height > 0; height--) {
			memcpy(dst, src, width);
			dst += _backGroundBuf->pitch;
			src += srcWidth;
		}
	}
}

void AGOSEngine::vc33_setMouseOn() {
	if (_mouseHideCount != 0) {
		_mouseHideCount = 1;
		if (getGameType() == GType_ELVIRA2 || getGameType() == GType_WW) {
			// Set mouse palette
			_displayPalette[65 * 3 + 0] = 48 * 4;
			_displayPalette[65 * 3 + 1] = 48 * 4;
			_displayPalette[65 * 3 + 2] = 40 * 4;
			_paletteFlag = 1;
		}
		mouseOn();
	}
}

void AGOSEngine::vc34_setMouseOff() {
	mouseOff();
	_mouseHideCount = 200;
	_leftButtonDown = false;
}

void AGOSEngine::clearVideoBackGround(uint16 num, uint16 color) {
	const uint16 *vlut = &_videoWindows[num * 4];
	byte *dst = (byte *)_backGroundBuf->getBasePtr(vlut[0] * 16, vlut[1]);

	for (uint h = 0; h < vlut[3]; h++) {
		memset(dst, color, vlut[2] * 16);
		dst += _backGroundBuf->pitch;
	}
}

void AGOSEngine::clearVideoWindow(uint16 num, uint16 color) {
	if (getGameType() == GType_ELVIRA1) {
		if (num == 2 || num == 6)
			return;
	} else if (getGameType() == GType_ELVIRA2 || getGameType() == GType_WW) {
		if (num != 4 && num < 10)
			return;
	} else if (getGameType() == GType_SIMON1) {
		if (num != 4)
			return;
	}

	if (getGameType() == GType_ELVIRA1 && num == 3) {
		Graphics::Surface *screen = _system->lockScreen();
		byte *dst = (byte *)screen->getPixels();
		for (int i = 0; i < _screenHeight; i++) {
			memset(dst, color, _screenWidth);
			dst += screen->pitch;
		}
		 _system->unlockScreen();
	} else {
		const uint16 *vlut = &_videoWindows[num * 4];
		uint16 xoffs = (vlut[0] - _videoWindows[16]) * 16;
		uint16 yoffs = (vlut[1] - _videoWindows[17]);
		uint16 dstWidth = _videoWindows[18] * 16;
		// TODO: Is there any known connection between dstWidth and the pitch
		// of the _window4BackScn Surface? If so, we might be able to pass
		// yoffs as proper y parameter to getBasePtr.
		byte *dst = (byte *)_window4BackScn->getBasePtr(xoffs, 0) + yoffs * dstWidth;

		setMoveRect(0, 0, vlut[2] * 16, vlut[3]);

		for (uint h = 0; h < vlut[3]; h++) {
			memset(dst, color, vlut[2] * 16);
			dst += dstWidth;
		}

		_window4Flag = 1;
	}
}

void AGOSEngine::vc35_clearWindow() {
	uint16 num = vcReadNextWord();
	uint16 color = vcReadNextWord();

	// Clear video background
	if (getGameType() == GType_ELVIRA1) {
		if (num == 2 || num == 6)
			return;
	} else if (getGameType() == GType_ELVIRA2 || getGameType() == GType_WW) {
		if (num != 4 && num < 10)
			return;
	} else if (getGameType() == GType_SIMON1) {
		if (num != 4)
			return;
	}

	// Clear video window
	clearVideoWindow(num, color);
	clearVideoBackGround(num, color);
	_vgaSpriteChanged++;
}

void AGOSEngine::vc36_setWindowImage() {
	_displayFlag = 0;
	uint16 vga_res = vcReadNextWord();
	uint16 windowNum = vcReadNextWord();
	setWindowImage(windowNum, vga_res);
}

void AGOSEngine::vc37_pokePalette() {
	uint16 offs = vcReadNextWord();
	uint16 color = vcReadNextWord();

	// PC EGA version of Personal Nightmare uses standard EGA palette
	if (getGameType() == GType_PN && (getFeatures() & GF_EGA))
		return;

	byte *palptr = _displayPalette + offs * 3;
	palptr[0] = ((color & 0xf00) >> 8) * 32;
	palptr[1] = ((color & 0x0f0) >> 4) * 32;
	palptr[2] = ((color & 0x00f) >> 0) * 32;

	if (!(_videoLockOut & 0x20)) {
		_paletteFlag = 1;
		_displayFlag++;
	}
}

void AGOSEngine::vc38_ifVarNotZero() {
	uint16 var;
	if (getGameType() == GType_PP)
		var = vcReadVarOrWord();
	else
		var = vcReadNextWord();

	if (vcReadVar(var) == 0)
		vcSkipNextInstruction();
}

void AGOSEngine::vc39_setVar() {
	uint16 var;
	if (getGameType() == GType_PP)
		var = vcReadVarOrWord();
	else
		var = vcReadNextWord();

	int16 value = vcReadNextWord();
	vcWriteVar(var, value);
}

void AGOSEngine::vc40_scrollRight() {
	uint16 var = vcReadNextWord();
	int16 value = vcReadVar(var) + vcReadNextWord();

	if (getGameType() == GType_SIMON2 && var == 15 && !getBitFlag(80)) {
		if ((_scrollCount < 0) || (_scrollCount == 0 && _scrollFlag == 0)) {
			_scrollCount = 0;
			if (value - _scrollX >= 30) {
				_scrollCount = MIN(20, _scrollXMax - _scrollX);
				addVgaEvent(6, SCROLL_EVENT, NULL, 0, 0);
			}
		}
	}

	vcWriteVar(var, value);
}

void AGOSEngine::vc41_scrollLeft() {
	uint16 var = vcReadNextWord();
	int16 value = vcReadVar(var) - vcReadNextWord();

	if (getGameType() == GType_SIMON2 && var == 15 && !getBitFlag(80)) {
		if ((_scrollCount > 0) || (_scrollCount == 0 && _scrollFlag == 0)) {
			_scrollCount = 0;
			if ((uint16)(value - _scrollX) < 11) {
				_scrollCount = -MIN(20, (int)_scrollX);
				addVgaEvent(6, SCROLL_EVENT, NULL, 0, 0);
			}
		}
	}

	vcWriteVar(var, value);
}

void AGOSEngine::vc42_delayIfNotEQ() {
	uint16 val = vcReadVar(vcReadNextWord());
	if (val != vcReadNextWord()) {
		addVgaEvent(_frameCount + 1, ANIMATE_EVENT, _vcPtr - 4, _vgaCurSpriteId, _vgaCurZoneNum);
		_vcPtr = (byte *)&_vcGetOutOfCode;
	}
}

} // End of namespace AGOS
