/* 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 "kyra/sequence/seqplayer_lok.h"
#include "kyra/resource/resource.h"
#include "kyra/sound/sound.h"

#include "common/system.h"

namespace Kyra {

SeqPlayer::SeqPlayer(KyraEngine_LoK *vm, OSystem *system) {
	_vm = vm;
	_system = system;

	_screen = vm->screen();
	_sound = vm->sound();
	_res = vm->resource();

	_copyViewOffs = false;
	_specialBuffer = nullptr;
	_seqCode = 0;
	_seqVocStartTimer = 0;
	_seqSkipCommand = false;
	_seqLoopPos = 0;

	_seqData = nullptr;
	_seqWsaCurDecodePage = 0;
	_seqDisplayedTextTimer = 0;
	_seqDisplayTextFlag = false;
	_seqDisplayedText = 0;
	_seqDisplayedChar = 0;
	_seqDisplayedTextX = 0;
	_seqTalkTextPrinted = false;
	_seqTalkTextRestored = false;
	_seqQuitFlag = false;

	for (int i = 0; i < ARRAYSIZE(_handShapes); ++i)
		_handShapes[i] = nullptr;
	for (int i = 0; i < ARRAYSIZE(_seqMovies); ++i)
		_seqMovies[i].movie = nullptr;
}

SeqPlayer::~SeqPlayer() {
	freeHandShapes();

	for (int i = 0; i < ARRAYSIZE(_seqMovies); ++i) {
		if (!_seqMovies[i].movie)
			continue;
		_seqMovies[i].movie->close();
		delete _seqMovies[i].movie;
		_seqMovies[i].movie = nullptr;
	}
}

uint8 *SeqPlayer::setPanPages(int pageNum, int shape) {
	uint8 *panPage = nullptr;
	const uint8 *data = _screen->getCPagePtr(pageNum);
	uint16 numShapes = READ_LE_UINT16(data);
	if (shape < numShapes) {
		uint32 offs = 0;
		if (_vm->gameFlags().useAltShapeHeader)
			offs = READ_LE_UINT32(data + 2 + shape * 4);
		else
			offs = READ_LE_UINT16(data + 2 + shape * 2);

		if (offs != 0) {
			data += offs;
			uint16 sz = READ_LE_UINT16(data + 6);
			panPage = new uint8[sz];
			assert(panPage);
			memcpy(panPage, data, sz);
		}
	}
	return panPage;
}

void SeqPlayer::makeHandShapes() {
	_screen->loadBitmap("WRITING.CPS", 3, 3, &_screen->getPalette(0));
	if (_vm->gameFlags().platform == Common::kPlatformMacintosh || _vm->gameFlags().platform == Common::kPlatformAmiga) {
		freeHandShapes();

		int pageBackUp = _screen->_curPage;
		_screen->_curPage = 2;
		_handShapes[0] = _screen->encodeShape(0, 0, 88, 122, 0);
		assert(_handShapes[0]);
		_handShapes[1] = _screen->encodeShape(88, 0, 80, 117, 0);
		assert(_handShapes[1]);
		_handShapes[2] = _screen->encodeShape(168, 0, 117, 124, 0);
		assert(_handShapes[2]);
		_screen->_curPage = pageBackUp;
	} else {
		for (int i = 0; i < ARRAYSIZE(_handShapes); ++i) {
			if (_handShapes[i])
				delete[] _handShapes[i];
			_handShapes[i] = setPanPages(3, i);
			assert(_handShapes[i]);
		}
	}
}

void SeqPlayer::freeHandShapes() {
	for (int i = 0; i < ARRAYSIZE(_handShapes); ++i) {
		delete[] _handShapes[i];
		_handShapes[i] = nullptr;
	}
}

void SeqPlayer::s1_wsaOpen() {
	uint8 wsaObj = *_seqData++;
	assert(wsaObj < ARRAYSIZE(_seqMovies));
	uint8 offscreenDecode = *_seqData++;
	_seqWsaCurDecodePage = _seqMovies[wsaObj].page = (offscreenDecode == 0) ? 0 : 3;
	if (!_seqMovies[wsaObj].movie)
		_seqMovies[wsaObj].movie = _vm->createWSAMovie();
	_seqMovies[wsaObj].movie->open(_vm->seqWSATable()[wsaObj], offscreenDecode, nullptr);
	_seqMovies[wsaObj].frame = 0;
	_seqMovies[wsaObj].numFrames = _seqMovies[wsaObj].movie->frames() - 1;
}

void SeqPlayer::s1_wsaClose() {
	uint8 wsaObj = *_seqData++;
	assert(wsaObj < ARRAYSIZE(_seqMovies));
	if (_seqMovies[wsaObj].movie)
		_seqMovies[wsaObj].movie->close();
}

void SeqPlayer::s1_wsaPlayFrame() {
	uint8 wsaObj = *_seqData++;
	assert(wsaObj < ARRAYSIZE(_seqMovies));
	int16 frame = (int8)*_seqData++;
	_seqMovies[wsaObj].pos.x = READ_LE_UINT16(_seqData); _seqData += 2;
	_seqMovies[wsaObj].pos.y = *_seqData++;
	assert(_seqMovies[wsaObj].movie);
	_seqMovies[wsaObj].movie->displayFrame(frame, _seqMovies[wsaObj].page, _seqMovies[wsaObj].pos.x, _seqMovies[wsaObj].pos.y, 0, nullptr, nullptr);
	_seqMovies[wsaObj].frame = frame;
}

void SeqPlayer::s1_wsaPlayNextFrame() {
	uint8 wsaObj = *_seqData++;
	assert(wsaObj < ARRAYSIZE(_seqMovies));
	int16 frame = ++_seqMovies[wsaObj].frame;
	if (frame > _seqMovies[wsaObj].numFrames) {
		frame = 0;
		_seqMovies[wsaObj].frame = 0;
	}
	_seqMovies[wsaObj].movie->displayFrame(frame, _seqMovies[wsaObj].page, _seqMovies[wsaObj].pos.x, _seqMovies[wsaObj].pos.y, 0, nullptr, nullptr);
}

void SeqPlayer::s1_wsaPlayPrevFrame() {
	uint8 wsaObj = *_seqData++;
	assert(wsaObj < ARRAYSIZE(_seqMovies));
	int16 frame = --_seqMovies[wsaObj].frame;
	if (frame < 0) {
		frame = _seqMovies[wsaObj].numFrames;
		_seqMovies[wsaObj].frame = frame;
	} else {
		_seqMovies[wsaObj].movie->displayFrame(frame, _seqMovies[wsaObj].page, _seqMovies[wsaObj].pos.x, _seqMovies[wsaObj].pos.y, 0, nullptr, nullptr);
	}
}

void SeqPlayer::s1_drawShape() {
	uint8 shapeNum = *_seqData++;
	int x = READ_LE_UINT16(_seqData); _seqData += 2;
	int y = *_seqData++;
	_screen->drawShape(2, _handShapes[shapeNum], x, y, 0, 0, 0);
}

void SeqPlayer::s1_waitTicks() {
	uint16 ticks = READ_LE_UINT16(_seqData); _seqData += 2;
	if (_seqCode == 6 && _vm->speechEnabled() && !_vm->textEnabled())
		return;
	_vm->delay(ticks * _vm->tickLength());
}

void SeqPlayer::s1_copyWaitTicks() {
	s1_copyView();
	s1_waitTicks();
}

void SeqPlayer::s1_shuffleScreen() {
	_screen->shuffleScreen(0, 16, 320, 128, 2, 0, 0, false);
	if (_specialBuffer)
		_screen->copyRegionToBuffer(2, 0, 16, 320, 128, _specialBuffer);
	_screen->_curPage = 0;
}

void SeqPlayer::s1_copyView() {
	int h = !_copyViewOffs ? 120 : 128;

	if (_specialBuffer && !_copyViewOffs)
		_screen->copyToPage0(16, h, 3, _specialBuffer);
	else
		_screen->copyRegion(0, 16, 0, 16, 320, h, 2, 0);
}

void SeqPlayer::s1_loopInit() {
	_seqLoopPos = *_seqData++;
	if (_seqLoopPos < ARRAYSIZE(_seqLoopTable))
		_seqLoopTable[_seqLoopPos].ptr = _seqData;
	else
		_seqQuitFlag = true;
}

void SeqPlayer::s1_loopInc() {
	_seqLoopPos = *_seqData++;
	uint16 seqLoopCount = READ_LE_UINT16(_seqData); _seqData += 2;

	if (_seqCode == 13 && _vm->speechEnabled() && !_vm->textEnabled()) {
		if (_vm->snd_voiceIsPlaying()) {
			_seqData = _seqLoopTable[_seqLoopPos].ptr;
		} else {
			_seqLoopTable[_seqLoopPos].count = 0xFFFF;
			_seqLoopTable[_seqLoopPos].ptr = nullptr;
		}
	} else if (_seqLoopTable[_seqLoopPos].count == 0xFFFF) {
		_seqLoopTable[_seqLoopPos].count = seqLoopCount - 1;
		_seqData = _seqLoopTable[_seqLoopPos].ptr;
	} else if (_seqLoopTable[_seqLoopPos].count == 0) {
		_seqLoopTable[_seqLoopPos].count = 0xFFFF;
		_seqLoopTable[_seqLoopPos].ptr = nullptr;
	} else {
		--_seqLoopTable[_seqLoopPos].count;
		_seqData = _seqLoopTable[_seqLoopPos].ptr;
	}
}

void SeqPlayer::s1_skip() {
	uint8 val = *_seqData++;
	if (!_vm->speechEnabled() || _vm->textEnabled() || !val)
		return;

	_seqSkipCommand = true;
	uint32 vocPlayTime = _vm->snd_getVoicePlayTime();
	if (vocPlayTime) {
		if (((_system->getMillis() - _seqVocStartTimer) / _vm->tickLength()) < (val * (vocPlayTime / _vm->tickLength()) / 100))
			_seqSkipCommand = false;		
	}
}

void SeqPlayer::s1_loadPalette() {
	uint8 colNum = *_seqData++;

	if (_vm->gameFlags().platform == Common::kPlatformAmiga) {
		if (!colNum)
			_screen->copyPalette(0, 6);
		else if (colNum == 3)
			_screen->copyPalette(0, 7);
		else if (colNum == 4)
			_screen->copyPalette(0, 3);

		_screen->setScreenPalette(_screen->getPalette(0));
	} else {
		_screen->loadPalette(_vm->seqCOLTable()[colNum], _screen->getPalette(0));
	}
}

void SeqPlayer::s1_loadBitmap() {
	uint8 cpsNum = *_seqData++;
	_screen->loadBitmap(_vm->seqCPSTable()[cpsNum], 3, 3, &_screen->getPalette(0));
}

void SeqPlayer::s1_fadeToBlack() {
	_screen->fadeToBlack();
}

void SeqPlayer::s1_printText() {
	static const uint8 colorMap[] = { 0, 0, 0, 0, 12, 12, 12, 0, 0, 0, 0, 0 };
	uint8 txt = *_seqData++;

	if (!_vm->textEnabled())
		return;

	if (_vm->gameFlags().platform == Common::kPlatformAmiga)
		_screen->fillRect(0, 180, 319, 195, 0);
	else
		_screen->fillRect(0, 180, 319, 195, 12);
	_screen->setTextColorMap(colorMap);
	if (!_seqDisplayTextFlag) {
		const char *str = _vm->seqTextsTable()[txt];
		int x = (Screen::SCREEN_W - _screen->getTextWidth(str)) / 2;
		_screen->printText(str, x, 180, 0xF, 0xC);
	} else {
		_seqDisplayedTextTimer = _system->getMillis() + 1000 / ((_vm->gameFlags().lang == Common::FR_FRA) ? 120 : 60);
		_seqDisplayedText = txt;
		_seqDisplayedChar = 0;
		const char *str = _vm->seqTextsTable()[_seqDisplayedText];
		_seqDisplayedTextX = (Screen::SCREEN_W - _screen->getTextWidth(str)) / 2;
	}
}

void SeqPlayer::s1_printTalkText() {
	uint8 txt = *_seqData++;
	int x = READ_LE_UINT16(_seqData); _seqData += 2;
	int y = *_seqData++;
	uint8 fillColor = *_seqData++;

	int b;
	if (_seqTalkTextPrinted && !_seqTalkTextRestored) {
		if (_seqWsaCurDecodePage != 0 && !_specialBuffer)
			b = 2;
		else
			b = 0;
		_vm->text()->restoreTalkTextMessageBkgd(2, b);
	}

	if (!_vm->textEnabled())
		return;

	_seqTalkTextPrinted = true;
	_seqTalkTextRestored = false;
	if (_seqWsaCurDecodePage != 0 && !_specialBuffer)
		b = 2;
	else
		b = 0;
	_vm->text()->printTalkTextMessage(_vm->seqTextsTable()[txt], x, y, fillColor, b, 2);
}

void SeqPlayer::s1_restoreTalkText() {
	if (_seqTalkTextPrinted && !_seqTalkTextRestored && _vm->textEnabled()) {
		int b;
		if (_seqWsaCurDecodePage != 0 && !_specialBuffer)
			b = 2;
		else
			b = 0;
		_vm->text()->restoreTalkTextMessageBkgd(2, b);
		_seqTalkTextRestored = true;
	}
}

void SeqPlayer::s1_clearCurrentScreen() {
	if (_vm->gameFlags().platform == Common::kPlatformAmiga)
		_screen->fillRect(10, 180, 319, 195, 0);
	else
		_screen->fillRect(10, 180, 319, 196, 0xC);
}

void SeqPlayer::s1_break() {
	// Do nothing
}

void SeqPlayer::s1_fadeFromBlack() {
	_screen->fadeFromBlack();
}

void SeqPlayer::s1_copyRegion() {
	uint8 srcPage = *_seqData++;
	uint8 dstPage = *_seqData++;
	_screen->copyRegion(0, 0, 0, 0, 320, 200, srcPage, dstPage);
}

void SeqPlayer::s1_copyRegionSpecial() {
	static const uint8 colorMap[] = { 0, 0, 0, 0, 0, 12, 12, 0, 0, 0, 0, 0 };
	const char *copyStr = nullptr;
	if (!_vm->gameFlags().isTalkie)
		copyStr = "Copyright (c) 1992 Westwood Studios";
	else
		copyStr = "Copyright (c) 1992,1993 Westwood Studios";

	uint8 so = *_seqData++;
	switch (so) {
	case 0:
		if (_vm->gameFlags().platform == Common::kPlatformAmiga)
			_screen->copyRegion(0, 0, 0, 47, 312, 76, 2, 0);
		else
			_screen->copyRegion(0, 0, 0, 47, 320, 77, 2, 0);
		break;
	case 1:
		if (_vm->gameFlags().platform == Common::kPlatformAmiga)
			_screen->copyRegion(0, 0, 8, 47, 312, 55, 2, 0);
		else
			_screen->copyRegion(0, 0, 0, 47, 320, 56, 2, 0);
		break;
	case 2:
		if (_vm->gameFlags().platform == Common::kPlatformAmiga) {
			_screen->copyRegion(104, 72, 104, 72, 40, 87, 2, 0);
			_screen->copyRegion(128, 159, 128, 159, 32, 17, 2, 0);
			_screen->copyRegion(160, 105, 160, 105, 32, 9, 2, 0);
			_screen->copyRegion(200, 83, 200, 83, 88, 93, 2, 0);
		} else {
			_screen->copyRegion(107, 72, 107, 72, 43, 87, 2, 0);
			_screen->copyRegion(130, 159, 130, 159, 35, 17, 2, 0);
			_screen->copyRegion(165, 105, 165, 105, 32, 9, 2, 0);
			_screen->copyRegion(206, 83, 206, 83, 94, 93, 2, 0);
		}
		break;
	case 3:
		_screen->copyRegion(152, 56, 152, 56, 48, 48, 2, 0);
		break;
	case 4: {
		_screen->_charSpacing = -2;
		const int x = (Screen::SCREEN_W - _screen->getTextWidth(copyStr)) / 2;
		const int y = 179;
		_screen->setTextColorMap(colorMap);
		if (_vm->gameFlags().platform != Common::kPlatformAmiga)
			_screen->printText(copyStr, x + 1, y + 1, 0xB, 0xC);
		_screen->printText(copyStr, x, y, 0xF, 0xC);
		} break;
	case 5:
		_screen->_curPage = 2;
		break;
	default:
		error("Invalid subopcode %d for s1_copyRegionSpecial", so);
	}
}

void SeqPlayer::s1_fillRect() {
	int x1 = READ_LE_UINT16(_seqData); _seqData += 2;
	int y1 = *_seqData++;
	int x2 = READ_LE_UINT16(_seqData); _seqData += 2;
	int y2 = *_seqData++;
	uint8 color = *_seqData++;
	uint8 page = *_seqData++;
	_screen->fillRect(x1, y1, x2, y2, color, page);
}

void SeqPlayer::s1_playEffect() {
	uint8 track = *_seqData++;
	_vm->delay(3 * _vm->tickLength());

	if (_vm->gameFlags().platform == Common::kPlatformPC98) {
		if (track > 21 && track < 38)
			track -= 22;
		else
			return;
	}

	_sound->playSoundEffect(track);
}

void SeqPlayer::s1_playTrack() {
	uint8 msg = *_seqData++;

	if (_vm->gameFlags().platform != Common::kPlatformMacintosh && _vm->gameFlags().platform != Common::kPlatformAmiga)
		_vm->delay(3 * _vm->tickLength());

	if (msg == 0 && (_vm->gameFlags().platform != Common::kPlatformDOS && _vm->gameFlags().platform != Common::kPlatformAmiga)) {
		_sound->haltTrack();
	} else if (msg == 1) {
		_sound->beginFadeOut();
	} else {
		_sound->haltTrack();
		if (_vm->gameFlags().platform != Common::kPlatformMacintosh && _vm->gameFlags().platform != Common::kPlatformAmiga)
			_vm->delay(3 * _vm->tickLength());
		_sound->playTrack(_vm->gameFlags().platform == Common::kPlatformFMTowns ? msg + 2 : msg);
	}

	if (msg < 2 && (_vm->gameFlags().platform != Common::kPlatformMacintosh && _vm->gameFlags().platform != Common::kPlatformAmiga))
		_vm->delay(3 * _vm->tickLength());
}

void SeqPlayer::s1_allocTempBuffer() {
	if (_vm->gameFlags().isDemo && !_vm->gameFlags().isTalkie) {
		_seqQuitFlag = true;
	} else {
		if (!_specialBuffer && !_copyViewOffs) {
			_specialBuffer = new uint8[40960];
			assert(_specialBuffer);
			_screen->copyRegionToBuffer(2, 0, 16, 320, 128, _specialBuffer);
		}
	}
}

void SeqPlayer::s1_textDisplayEnable() {
	_seqDisplayTextFlag = true;
}

void SeqPlayer::s1_textDisplayDisable() {
	_seqDisplayTextFlag = false;
}

void SeqPlayer::s1_endOfScript() {
	_seqQuitFlag = true;
}

void SeqPlayer::s1_loadIntroVRM() {
	_res->loadPakFile("INTRO.VRM");
}

void SeqPlayer::s1_playVocFile() {
	_vm->snd_voiceWaitForFinish(false);
	uint8 a = *_seqData++;
	if (_vm->speechEnabled()) {
		_seqVocStartTimer = _system->getMillis();
		_vm->snd_playVoiceFile(a);
	}
}

void SeqPlayer::s1_miscUnk3() {
	// This is just a file cash flushing function in the original. We don't need that.
}

void SeqPlayer::s1_prefetchVocFile() {
	_seqData++;
	// we do not have to prefetch the vocfiles on modern systems
}

void SeqPlayer::s1_textDisplayWait() {
	// This is used in the Mac Talkie version for the Kallak writing sequence.
	// But in my tests the condition was never reached...
	if (_seqDisplayedTextTimer != 0xFFFFFFFF)
		_seqData--;
}

#define SEQOP(n, x) { n, &SeqPlayer::x, #x }

bool SeqPlayer::playSequence(const uint8 *seqData, bool skipSeq) {
	assert(seqData);

	static const SeqEntry floppySeqProcs[] = {
		// 0x00
		SEQOP(3, s1_wsaOpen),
		SEQOP(2, s1_wsaClose),
		SEQOP(6, s1_wsaPlayFrame),
		SEQOP(2, s1_wsaPlayNextFrame),
		// 0x04
		SEQOP(2, s1_wsaPlayPrevFrame),
		SEQOP(5, s1_drawShape),
		SEQOP(3, s1_waitTicks),
		SEQOP(3, s1_copyWaitTicks),
		// 0x08
		SEQOP(1, s1_shuffleScreen),
		SEQOP(1, s1_copyView),
		SEQOP(2, s1_loopInit),
		SEQOP(4, s1_loopInc),
		// 0x0C
		SEQOP(2, s1_loadPalette),
		SEQOP(2, s1_loadBitmap),
		SEQOP(1, s1_fadeToBlack),
		SEQOP(2, s1_printText),
		// 0x10
		SEQOP(6, s1_printTalkText),
		SEQOP(1, s1_restoreTalkText),
		SEQOP(1, s1_clearCurrentScreen),
		SEQOP(1, s1_break),
		// 0x14
		SEQOP(1, s1_fadeFromBlack),
		SEQOP(3, s1_copyRegion),
		SEQOP(2, s1_copyRegionSpecial),
		SEQOP(9, s1_fillRect),
		// 0x18
		SEQOP(2, s1_playEffect),
		SEQOP(2, s1_playTrack),
		SEQOP(1, s1_allocTempBuffer),
		SEQOP(1, s1_textDisplayEnable),
		// 0x1C
		SEQOP(1, s1_textDisplayDisable),
		SEQOP(1, s1_endOfScript)
	};

	static const SeqEntry cdromSeqProcs[] = {
		// 0x00
		SEQOP(3, s1_wsaOpen),
		SEQOP(2, s1_wsaClose),
		SEQOP(6, s1_wsaPlayFrame),
		SEQOP(2, s1_wsaPlayNextFrame),
		// 0x04
		SEQOP(2, s1_wsaPlayPrevFrame),
		SEQOP(5, s1_drawShape),
		SEQOP(3, s1_waitTicks),
		SEQOP(3, s1_waitTicks),
		// 0x08
		SEQOP(3, s1_copyWaitTicks),
		SEQOP(1, s1_shuffleScreen),
		SEQOP(1, s1_copyView),
		SEQOP(2, s1_loopInit),
		// 0x0C
		SEQOP(4, s1_loopInc),
		SEQOP(4, s1_loopInc),
		SEQOP(2, s1_skip),
		SEQOP(2, s1_loadPalette),
		// 0x10
		SEQOP(2, s1_loadBitmap),
		SEQOP(1, s1_fadeToBlack),
		SEQOP(2, s1_printText),
		SEQOP(6, s1_printTalkText),
		// 0x14
		SEQOP(1, s1_restoreTalkText),
		SEQOP(1, s1_clearCurrentScreen),
		SEQOP(1, s1_break),
		SEQOP(1, s1_fadeFromBlack),
		// 0x18
		SEQOP(3, s1_copyRegion),
		SEQOP(2, s1_copyRegionSpecial),
		SEQOP(9, s1_fillRect),
		SEQOP(2, s1_playEffect),
		// 0x1C
		SEQOP(2, s1_playTrack),
		SEQOP(1, s1_allocTempBuffer),
		SEQOP(1, s1_textDisplayEnable),
		SEQOP(1, s1_textDisplayDisable),
		// 0x20
		SEQOP(1, s1_endOfScript),
		SEQOP(1, s1_loadIntroVRM),
		SEQOP(2, s1_playVocFile),
		SEQOP(1, s1_miscUnk3),
		// 0x24
		SEQOP(2, s1_prefetchVocFile),
		SEQOP(1, s1_textDisplayWait)
	};

	const SeqEntry *commands;
	int numCommands;

	if (_vm->gameFlags().isTalkie) {
		commands = cdromSeqProcs;
		numCommands = ARRAYSIZE(cdromSeqProcs);
	} else {
		commands = floppySeqProcs;
		numCommands = ARRAYSIZE(floppySeqProcs);
	}

	bool seqSkippedFlag = false;

	_seqData = seqData;

	_seqDisplayedTextTimer = 0xFFFFFFFF;
	_seqDisplayTextFlag = false;
	_seqDisplayedTextX = 0;
	_seqDisplayedText = 0;
	_seqDisplayedChar = 0;
	_seqTalkTextRestored = false;
	_seqTalkTextPrinted = false;
	_seqSkipCommand = false;

	_seqQuitFlag = false;
	_seqWsaCurDecodePage = 0;
	_seqLoopPos = 0;

	for (int i = 0; i < 20; ++i) {
		_seqLoopTable[i].ptr = nullptr;
		_seqLoopTable[i].count = 0xFFFF;
	}

	for (uint i = 0; i < ARRAYSIZE(_seqMovies); i++) {
		_seqMovies[i].clear();
	}

	_screen->_curPage = 0;
	char revBuffer[384];
	memset(revBuffer, 0, sizeof(revBuffer));
	int charIdx = 0;
	while (!_seqQuitFlag && !_vm->shouldQuit()) {
		uint32 startFrameCt = _vm->_system->getMillis();
		if (_seqSkipCommand || (skipSeq && _vm->seq_skipSequence())) {
			while (1) {
				uint8 code = *_seqData;
				if (commands[code].proc == &SeqPlayer::s1_endOfScript)
					break;
				if (_seqSkipCommand && commands[code].proc == &SeqPlayer::s1_skip && _seqData[1] == 0)
					break;
				if (commands[code].proc == &SeqPlayer::s1_break)
					break;

				_seqData += commands[code].len;
			}
			skipSeq = false;
			seqSkippedFlag = true;

			if (_seqSkipCommand) {
				_seqSkipCommand = false;
				_seqData += commands[14].len;
				_seqLoopTable[_seqLoopPos].count = 0xFFFF;
				_seqLoopTable[_seqLoopPos].ptr = nullptr;
			}
		}
		// used in Kallak writing intro
		if (_seqDisplayTextFlag && _seqDisplayedTextTimer != 0xFFFFFFFF && _vm->textEnabled()) {
			if (_seqDisplayedTextTimer < _system->getMillis()) {
				char charStr[3];
				charStr[0] = _vm->seqTextsTable()[_seqDisplayedText][_seqDisplayedChar];
				if (_vm->gameFlags().lang == Common::HE_ISR) {
					for (int k = charIdx; k > 0; k--) {
						revBuffer[k] = revBuffer[k - 1];
					}
					revBuffer[0] = charStr[0];
					if (!charIdx) {
						int w = _screen->getTextWidth(_vm->seqTextsTable()[_seqDisplayedText] + _seqDisplayedChar);
						_seqDisplayedTextX += w;
					}
					charIdx++;
				}
				charStr[1] = charStr[2] = '\0';
				if (_vm->gameFlags().lang == Common::JA_JPN || _vm->gameFlags().lang == Common::ZH_TWN || (_vm->gameFlags().lang == Common::KO_KOR && (charStr[0] & 0x80))) {
					charStr[1] = _vm->seqTextsTable()[_seqDisplayedText][++_seqDisplayedChar];
					_screen->printText(charStr, _seqDisplayedTextX, 180, 0xF, 0xC);
					_seqDisplayedTextX += _screen->getTextWidth(charStr);
				} else if (_vm->gameFlags().lang == Common::HE_ISR) {
					_seqDisplayedTextX -= _screen->getCharWidth((uint8)charStr[0]);
					_screen->printText(revBuffer, _seqDisplayedTextX, 180, 0xF, 0xC);
				} else {
					_screen->printText(charStr, _seqDisplayedTextX, 180, 0xF, 0xC);
					_seqDisplayedTextX += _screen->getCharWidth((uint8)charStr[0]);
				}
				++_seqDisplayedChar;

				if (_vm->seqTextsTable()[_seqDisplayedText][_seqDisplayedChar] == '\0') {
					_seqDisplayedTextTimer = 0xFFFFFFFF;
					memset(revBuffer, 0, sizeof(revBuffer));
					charIdx = 0;
				} else {
					_seqDisplayedTextTimer = _system->getMillis() + 1000 / ((_vm->gameFlags().lang == Common::FR_FRA) ? 120 : 60);
				}
			}
		}

		_seqCode = *_seqData++;
		if (_seqCode < numCommands) {
			SeqProc currentProc = commands[_seqCode].proc;
			debugC(5, kDebugLevelSequence, "0x%.4X seqCode = %d (%s)", (uint16)(_seqData - 1 - seqData), _seqCode, commands[_seqCode].desc);
			(this->*currentProc)();
		} else {
			error("Invalid sequence opcode %d called from 0x%.04X", _seqCode, (uint16)(_seqData - 1 - seqData));
		}

		int extraDelay = _screen->updateScreen();
		uint32 ct = _system->getMillis();
		_vm->delayUntil(startFrameCt + extraDelay > ct ? startFrameCt + extraDelay : ct + 8);
	}
	delete[] _specialBuffer;
	_specialBuffer = nullptr;

	for (uint i = 0; i < ARRAYSIZE(_seqMovies); ++i) {
		delete _seqMovies[i].movie;
		_seqMovies[i].movie = nullptr;
	}
	return seqSkippedFlag;
}

#undef SEQOP
#undef KYRA_SEQ_SCREENFRAMEDELAY_MIN

} // End of namespace Kyra
