/* 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 "dgds/ads.h"
#include "dgds/globals.h"

namespace Dgds {


Common::Error ADSData::syncState(Common::Serializer &s) {
	uint16 arrSize = ARRAYSIZE(_state);
	s.syncAsUint16LE(arrSize);
	if (arrSize != ARRAYSIZE(_state))
		error("Expected fixed size state array");
	for (uint i = 0; i < arrSize; i++)
		s.syncAsSint16LE(_state[i]);

	uint16 nenvs = _scriptEnvs.size();
	s.syncAsUint16LE(nenvs);
	// This should be the same on load as the data comes from the ADS/TTM files.
	if (nenvs != _scriptEnvs.size())
		error("Unexpected number of script envs (%d in save vs %d in ADS)", nenvs, _scriptEnvs.size());

	for (auto &env : _scriptEnvs)
		env.syncState(s);

	uint16 nseqs = _ttmSeqs.size();
	s.syncAsUint16LE(nseqs);
	if (nseqs != _ttmSeqs.size())
		error("Unexpected number of ttm seqeunces (%d in save vs %d in ADS)", nseqs, _ttmSeqs.size());

	for (auto &seq : _ttmSeqs)
		seq->syncState(s);

	return Common::kNoError;
}


ADSInterpreter::ADSInterpreter(DgdsEngine *vm) : _vm(vm), _currentTTMSeq(nullptr), _adsData(nullptr) {
	_ttmInterpreter = new TTMInterpreter(_vm);
}

ADSInterpreter::~ADSInterpreter() {
	delete _ttmInterpreter;
	_ttmInterpreter = nullptr;
	for (auto &data : _adsTexts)
		delete data._value.scr;
}

bool ADSInterpreter::load(const Common::String &filename) {
	// Don't clear current _adsData, we reset that below.
	_currentTTMSeq = nullptr;

	// For high detail, replace extension ADS with ADH.  Low detail is ADL.
	Common::String detailfile = filename.substr(0, filename.size() - 1);
	if (_vm->getDetailLevel() == kDgdsDetailLow)
		detailfile += "L";
	else
		detailfile += "H";

	if (!_vm->getResourceManager()->hasResource(detailfile))
		detailfile = filename;

	debug(1, "ADSInterpreter: load %s", detailfile.c_str());

	// Reset the state
	_vm->setFlipMode(false);
	_adsTexts.setVal(detailfile, ADSData());
	_adsData = &(_adsTexts.getVal(detailfile));

	ADSParser dgds(_vm->getResourceManager(), _vm->getDecompressor());
	dgds.parse(_adsData, detailfile);

	for (const auto &file : _adsData->_scriptNames) {
		// Environments are numbered based on this list, so extend the list even if there
		// is no file to load.  Eg, Willy Beamish TVO.ADS uses env 5 but only loads 4
		// files (and has 1 empty name)
		_adsData->_scriptEnvs.resize(_adsData->_scriptEnvs.size() + 1);
		if (file.empty())
			continue;

		debug(1, "   load TTM %s to env %d", file.c_str(), _adsData->_scriptEnvs.size());
		TTMEnviro &data = _adsData->_scriptEnvs.back();
		data._enviro = _adsData->_scriptEnvs.size();
		_ttmInterpreter->load(file, data);
		_ttmInterpreter->findAndAddSequences(data, _adsData->_ttmSeqs);
	}

	_adsData->scr->seek(0);

	uint16 opcode = 0;
	int segcount = 0;
	findUsedSequencesForSegment(0);
	_adsData->_segments[0] = 0;
	opcode = _adsData->scr->readUint16LE();
	while (_adsData->scr->pos() < _adsData->scr->size()) {
		if (opcode == 0xffff) {
			segcount++;
			_adsData->_segments[segcount] = _adsData->scr->pos();
			findUsedSequencesForSegment(segcount);
		} else {
			_adsData->scr->skip(numArgs(opcode) * 2);
		}
		opcode = _adsData->scr->readUint16LE();
	}

	for (uint i = segcount + 1; i < ARRAYSIZE(_adsData->_segments); i++)
		_adsData->_segments[i] = -1;

	_adsData->_maxSegments = segcount + 1;
	_adsData->_filename = filename;

	for (uint i = 0; i < ARRAYSIZE(_adsData->_state); i++)
		_adsData->_state[i] = 8;
	for (auto &seq : _adsData->_ttmSeqs)
		seq->reset();

	return true;
}

bool ADSInterpreter::updateSeqTimeAndFrame(const TTMEnviro *env, Common::SharedPtr<TTMSeq> seq) {
	if (seq->_timeInterval != 0) {
		uint32 now = DgdsEngine::getInstance()->getThisFrameMs();
		if (now < seq->_timeNext) {
			debug(10, "env %d seq %d (%s) not advancing from frame %d (now %d timeNext %d interval %d)", seq->_enviro,
					seq->_seqNum, env->_tags.getValOrDefault(seq->_seqNum).c_str(), seq->_currentFrame, now, seq->_timeNext, seq->_timeInterval);
			return false;
		}
		seq->_timeNext = now + seq->_timeInterval;
	}

	seq->_executed = false;
	if (seq->_gotoFrame == -1) {
		debug(10, "env %d seq %d (%s) advance to frame %d->%d (start %d last %d)", seq->_enviro, seq->_seqNum,
				env->_tags.getValOrDefault(seq->_seqNum).c_str(), seq->_currentFrame, seq->_currentFrame + 1, seq->_startFrame, seq->_lastFrame);
		seq->_currentFrame++;
	} else {
		debug(10, "env %d seq %d (%s) goto to frame %d->%d (start %d last %d)", seq->_enviro, seq->_seqNum,
				env->_tags.getValOrDefault(seq->_seqNum).c_str(), seq->_currentFrame, seq->_gotoFrame, seq->_startFrame, seq->_lastFrame);
		seq->_currentFrame = seq->_gotoFrame;
		seq->_gotoFrame = -1;
	}

	return true;
}

static const uint16 ADS_USED_SEQ_OPCODES[] = {
	0x2000, 0x2005, 0x2010, 0x2015, 0x4000, 0x4010, 0x1330,
	0x1340, 0x1360, 0x1370, 0x1320, 0x1310, 0x1350
};

void ADSInterpreter::findUsedSequencesForSegment(int idx) {
	_adsData->_usedSeqs[idx].clear();
	int64 startoff = _adsData->scr->pos();
	uint16 opcode = 0;
	// Skip the segment number.
	int16 segno = _adsData->scr->readUint16LE();

	DgdsGameId gameId = DgdsEngine::getInstance()->getGameId();
	//
	// HoC and Dragon call a sequence "used" if there is any dependency on
	// the sequence (reordering, if conditions), but to simplify the use of
	// getStateForSceneOp, later games only call it "used" if it is directly
	// started (0x2000 or 0x2005).
	//
	int n_ops_to_check = (gameId == GID_DRAGON || gameId == GID_HOC) ? ARRAYSIZE(ADS_USED_SEQ_OPCODES) : 2;
	while (opcode != 0xffff && _adsData->scr->pos() < _adsData->scr->size()) {
		opcode = _adsData->scr->readUint16LE();
		for (int i = 0; i < n_ops_to_check; i++) {
			uint16 op = ADS_USED_SEQ_OPCODES[i];
			if (opcode == op) {
				int16 envno = _adsData->scr->readSint16LE();
				int16 seqno = _adsData->scr->readSint16LE();
				Common::SharedPtr<TTMSeq> seq = findTTMSeq(envno, seqno);
				if (!seq) {
					warning("ADS opcode %04x at offset %d references unknown seq %d %d",
							opcode, (int)_adsData->scr->pos(), envno, seqno);
				} else {
					bool already_added = false;
					for (const Common::SharedPtr<TTMSeq> &s : _adsData->_usedSeqs[idx]) {
						if (s == seq) {
							already_added = true;
							break;
						}
					}
					if (!already_added) {
						debug(10, "ADS seg no %d (idx %d) uses env %d seq %d", segno, idx, envno, seqno);
						_adsData->_usedSeqs[idx].push_back(seq);
					}
				}
				// Rewind as we will go forward again outside this loop.
				_adsData->scr->seek(-4, SEEK_CUR);
				break;
			}
		}
		_adsData->scr->skip(numArgs(opcode) * 2);
	}
	_adsData->scr->seek(startoff);
}


void ADSInterpreter::unload() {
	_adsData = nullptr;
	_currentTTMSeq = nullptr;
	_adsTexts.clear();
}

bool ADSInterpreter::playScene() {
	if (!_currentTTMSeq)
		return false;

	TTMEnviro *env = findTTMEnviro(_currentTTMSeq->_enviro);
	if (!env)
		error("Couldn't find environment num %d", _currentTTMSeq->_enviro);

	_adsData->_gotoTarget = -1;
	return _ttmInterpreter->run(*env, *_currentTTMSeq);
}

bool ADSInterpreter::skipSceneLogicBranch() {
	Common::SeekableReadStream *scr = _adsData->scr;
	bool result = true;
	while (scr->pos() < scr->size()) {
		uint16 op = scr->readUint16LE();
		if (op == 0x1510 || op == 0x1500) { // else or endif
			scr->seek(-2, SEEK_CUR);
			return true;
		} else if (op == 0 || op == 0xffff) {
			// end of segment
			return false;
		} else if ((op & 0xff00) == 0x1300) {
			// A nested IF (0x13xx) block. Skip to endif ignoring else.
			scr->skip(numArgs(op) * 2);
			result = skipToEndIf();
		} else {
			scr->skip(numArgs(op) * 2);
		}
	}
	return result && scr->pos() < scr->size();
}

bool ADSInterpreter::skipToElseOrEndif() {
	Common::SeekableReadStream *scr = _adsData->scr;
	bool result = skipSceneLogicBranch();
	if (result) {
		uint16 op = scr->readUint16LE();
		if (op == 0x1500)
			// Hit else block - we want to run that
			result = runUntilBranchOpOrEnd();
		// don't rewind - the calls to this should always return ptr+2
	}
	return result;
}

bool ADSInterpreter::skipToEndIf() {
	//
	// This is similar to skipSceneLogicBranch, but it does not stop for ELSE,
	// only ENDIF.  It's used to skip nested IF blocks inside another skipped
	// block.
	// It should be called with the pointer at the op after the IF, which can
	// also be AND or OR ops.
	//
	Common::SeekableReadStream *scr = _adsData->scr;
	while (scr->pos() < scr->size()) {
		uint16 op = scr->readUint16LE();
		// don't rewind - the calls to this should always return after the last op.
		if (op == 0x1510) { // ENDIF
			return true;
		} else if (op == 0 || op == 0xffff) {
			return false;
		} else if (op == 0x1420 || op == 0x1430) {
			// AND or OR. Skip the IF that follows like a nested block
			// (this should work even for multiple AND/OR conditions)
			uint16 op2 = scr->readUint16LE();
			if ((op2 & 0xff00) != 0x1300)
				error("AND/OR ADS op not followed by another IF (got 0x%04x)", op2);
			scr->skip(numArgs(op) * 2);
			return skipToEndIf();
		} else if ((op & 0xff00) == 0x1300) {
			// A nested IF (0x13xx) block. Skip to endif ignoring else.
			scr->skip(numArgs(op) * 2);
			if (!skipToEndIf())
				return false;
		} else {
			scr->skip(numArgs(op) * 2);
		}
	}
	return false;
}

bool ADSInterpreter::skipToEndWhile() {
	Common::SeekableReadStream *scr = _adsData->scr;
	while (scr->pos() < scr->size()) {
		uint16 op = scr->readUint16LE();
		// don't rewind - the calls to this should always return after the last op.
		if (op == 0x1520)
			return true;
		else if (op == 0 || op == 0xffff)
			return false;

		scr->skip(numArgs(op) * 2);
	}
	return false;
}

TTMEnviro *ADSInterpreter::findTTMEnviro(int16 enviro) {
	for (auto & env : _adsData->_scriptEnvs) {
		if (env._enviro == enviro)
			return &env;
	}
	return nullptr;
}

Common::SharedPtr<TTMSeq> ADSInterpreter::findTTMSeq(int16 enviro, int16 seqno) {
	for (auto &seq : _adsData->_ttmSeqs) {
		if (seq->_enviro == enviro && seq->_seqNum == seqno)
			return seq;
	}
	return nullptr;
}

void ADSInterpreter::segmentOrState(int16 seg, uint16 val) {
	int idx = getArrIndexOfSegNum(seg);
	if (idx >= 0) {
		_adsData->_charWhile[idx] = 0;
		_adsData->_state[idx] = (_adsData->_state[idx] & 8) | val;
	}
}


void ADSInterpreter::segmentSetState(int16 seg, uint16 val) {
	int idx = getArrIndexOfSegNum(seg);
	if (idx >= 0) {
		_adsData->_charWhile[idx] = 0;
		if (_adsData->_state[idx] != 8)
			_adsData->_state[idx] = val;
	}
}

void ADSInterpreter::findEndOrInitOp() {
	Common::SeekableReadStream *scr = _adsData->scr;
	int32 startoff = scr->pos();
	while (scr->pos() < scr->size()) {
		uint16 opcode = scr->readUint16LE();
		// on FFFF return the original offset
		if (opcode == 0xffff) {
			scr->seek(startoff);
			return;
		}
		// on 5 (init) return the next offset (don't rewind)
		if (opcode == 0x0005)
			return;
		// everything else just go forward.
		scr->skip(numArgs(opcode) * 2);
	}
}

bool ADSInterpreter::logicOpResult(uint16 code, const TTMEnviro *env, const TTMSeq *seq, uint16 arg) {
	const char *tag = (seq && env) ? env->_tags.getValOrDefault(seq->_seqNum).c_str() : "";
	int16 envNum = env ? env->_enviro : 0;
	int16 seqNum = seq ? seq->_seqNum : 0;
	const char *optype = (code < 0x1300 ? "while" : "if");

	Globals *globals = DgdsEngine::getInstance()->getGameGlobals();

	switch (code) {
	case 0x1010: // WHILE paused
	case 0x1310: // IF paused, 2 params
		debugN(10, "ADS 0x%04x: %s paused env %d seq %d (%s)", code, optype, envNum, seqNum, tag);
		return seq && seq->_runFlag == kRunTypePaused;
	case 0x1020: // WHILE not paused
	case 0x1320: // IF not paused, 2 params
		debugN(10, "ADS 0x%04x: %s not paused env %d seq %d (%s)", code, optype, envNum, seqNum, tag);
		return !seq || seq->_runFlag != kRunTypePaused;
	case 0x1030: // WHILE NOT PLAYED
	case 0x1330: // IF_NOT_PLAYED, 2 params
		debugN(10, "ADS 0x%04x: %s not played env %d seq %d (%s)", code, optype, envNum, seqNum, tag);
		return !seq || !seq->_runPlayed;
	case 0x1040: // WHILE PLAYED
	case 0x1340: // IF_PLAYED, 2 params
		debugN(10, "ADS 0x%04x: %s played env %d seq %d (%s)", code, optype, envNum, seqNum, tag);
		return seq && seq->_runPlayed;
	case 0x1050: // WHILE FINISHED
	case 0x1350: // IF_FINISHED, 2 params
		debugN(10, "ADS 0x%04x: %s finished env %d seq %d (%s)", code, optype, envNum, seqNum, tag);
		return seq && seq->_runFlag == kRunTypeFinished;
	case 0x1060: // WHILE NOT RUNNING
	case 0x1360: { // IF_NOT_RUNNING, 2 params
		debugN(10, "ADS 0x%04x: %s not running env %d seq %d (%s)", code, optype, envNum, seqNum, tag);
		// Dragon only checks kRunTypeStopped, HoC onward also check for kRunTypeFinished
		bool isDragon = _vm->getGameId() == GID_DRAGON;
		return !seq || seq->_runFlag == kRunTypeStopped || (!isDragon && seq->_runFlag == kRunTypeFinished);
	}
	case 0x1070: // WHILE RUNNING
	case 0x1370: // IF_RUNNING, 2 params
		debugN(10, "ADS 0x%04x: %s running env %d seq %d (%s)", code, optype, envNum, seqNum, tag);
		return seq && (seq->_runFlag == kRunTypeKeepGoing || seq->_runFlag == kRunTypeMulti || seq->_runFlag == kRunTypeTimeLimited);
	case 0x1080:
	case 0x1090:
		warning("Unimplemented IF/WHILE operation 0x%x", code);
		return true;
	case 0x1380: // IF_DETAIL_LTE, 1 param
		debugN(10, "ADS 0x%04x: if detail <= %d", code, arg);
		// TODO: This check is right, but we only have detail 0/1 and HOC onward use
		// different numbers.  HOC intro checks for >= 4, and default in HOC is 4.
		return false;
		//return ((int)DgdsEngine::getInstance()->getDetailLevel() <= arg);
	case 0x1390: // IF_DETAIL_GTE, 1 param
		debugN(10, "ADS 0x%04x: if detail >= %d", code, arg);
		return true;
		//return ((int)DgdsEngine::getInstance()->getDetailLevel() >= arg);
	//
	// NOTE: The globals for the following ops use the numbers from Willy
	// Beamish (0x4F, 0x50).  If these ops are used in any of the other newer
	// games (Quarky or Johnny Castaway) they may need updating.
	//
	case 0x13A0: // IF some_ads_variable[0] <=
		debugN(10, "ADS 0x%04x: if adsVariable[0] <= %d", code, arg);
		return globals->getGlobal(0x50) <= arg;
	case 0x13A1: // IF some_ads_variable[1] <=
		debugN(10, "ADS 0x%04x: if adsVariable[1] <= %d", code, arg);
		return globals->getGlobal(0x4F) <= arg;
	case 0x13B0: // IF some_ads_variable[0] >
		debugN(10, "ADS 0x%04x: if adsVariable[0] > %d", code, arg);
		return globals->getGlobal(0x50) > arg;
	case 0x13B1: // IF some_ads_variable[1] >
		debugN(10, "ADS 0x%04x: if adsVariable[1] > %d", code, arg);
		return globals->getGlobal(0x4F) > arg;
	case 0x13C0: // IF some_ads_variable[0] ==
		debugN(10, "ADS 0x%04x: if adsVariable[0] == %d", code, arg);
		return globals->getGlobal(0x50) == arg;
	case 0x13C1: // IF some_ads_variable[1] ==
		debugN(10, "ADS 0x%04x: if adsVariable[1] == %d", code, arg);
		return globals->getGlobal(0x4F) == arg;
	default:
		error("Not an ADS logic op: %04x, how did we get here?", code);
	}
}

bool ADSInterpreter::handleLogicOp(uint16 code, Common::SeekableReadStream *scr) {
	bool testval = true;
	uint16 andor = 0x1420; // start with "true" AND..
	int32 startPos = scr->pos() - 2;
	while (scr->pos() < scr->size()) {
		uint16 enviro;
		uint16 seqnum;
		Common::SharedPtr<TTMSeq> seq;
		TTMEnviro *env = nullptr;

		if ((code & 0xFF) < 0x80) {
			enviro = scr->readUint16LE();
			seqnum = scr->readUint16LE();
			seq = findTTMSeq(enviro, seqnum);
			env = findTTMEnviro(enviro);
			if (!seq) {
				warning("ADS if op 0x%04x referenced non-existent env %d seq %d", code, enviro, seqnum);
				// Dragon always returns false for this.  Others return result
				// based on opcode - eg, "if stopped" is true for non-existent sequence.
				if (DgdsEngine::getInstance()->getGameId() == GID_DRAGON)
					return false;
			}
		} else {
			// We load this into "enviro" but it's just the parameter of the op.
			enviro = scr->readUint16LE();
		}

		bool logicResult = logicOpResult(code, env, seq.get(), enviro);

		if (andor == 0x1420) // AND
			testval &= logicResult;
		else // OR
			testval |= logicResult;

		debug(10, "  -> %s (overall %s)", logicResult ? "true" : "false", testval ? "true" : "false");
		bool isWhile = code < 0x1300;

		code = scr->readUint16LE();

		if (code == 0x1420 || code == 0x1430) {
			andor = code;
			debug(10, "  ADS 0x%04x: %s", code, code == 0x1420 ? "AND" : "OR");
			code = scr->readUint16LE();
			// The next op should be another logic op
		} else {
			// No AND or OR, next op is just what to do.
			scr->seek(-2, SEEK_CUR);
			if (testval) {
				if (isWhile) {
					_adsData->_countdown[_adsData->_runningSegmentIdx]++;
					_adsData->_charWhile[_adsData->_runningSegmentIdx] = startPos;
				}
				bool runResult = runUntilBranchOpOrEnd();
				// WHILE (10x0) series always return false
				return (!isWhile) && runResult;
			} else {
				if (isWhile) {
					_adsData->_countdown[_adsData->_runningSegmentIdx] = 0;
					_adsData->_charWhile[_adsData->_runningSegmentIdx] = 0;
					return skipToEndWhile();
				} else {
					return skipToElseOrEndif();
				}
			}
		}
	}
	error("didn't return from ADS logic test");
}

int16 ADSInterpreter::randomOpGetProportion(uint16 code, Common::SeekableReadStream *scr) {
	// Leaves the pointer at the same place it started
	int argsize = numArgs(code) * 2;
	if (argsize == 0)
		error("Unexpected 0-arg ADS opcode 0x%04x inside random block", code);
	// skip args before the random proportion
	if (argsize > 2)
		scr->seek(argsize - 2, SEEK_CUR);
	int16 result = scr->readSint16LE();
	scr->seek(-argsize, SEEK_CUR);
	return result;
}

void ADSInterpreter::handleRandomOp(Common::SeekableReadStream *scr) {
	int16 max = 0;
	int64 startpos = scr->pos();

	// Collect the random proportions
	uint16 code = scr->readUint16LE();
	while (code != 0 && code != 0x30FF && scr->pos() < scr->size()) {
		int16 val = randomOpGetProportion(code, scr);
		// leaves pointer at beginning of next op
		max += val;
		scr->skip(numArgs(code) * 2);
		if (scr->pos() >= scr->size())
			break;
		code = scr->readUint16LE();
	}
	if (!max)
		return;

	int64 endpos = scr->pos();

	int16 randval = _vm->getRandom().getRandomNumber(max - 1) + 1; // Random from 1-max.
	scr->seek(startpos, SEEK_SET);

	// Now find the random bit to jump to
	code = scr->readUint16LE();
	do {
		int16 val = randomOpGetProportion(code, scr);
		randval -= val;
		if (randval < 1) {
			// This is the opcode we want to execute
			break;
		}
		scr->skip(numArgs(code) * 2);
		if (scr->pos() >= scr->size())
			break;
		code = scr->readUint16LE();
	} while (code != 0 && scr->pos() < scr->size());
	if (code && code != 0x3020)
		handleOperation(code, scr);

	scr->seek(endpos, SEEK_SET);
}

bool ADSInterpreter::handleOperation(uint16 code, Common::SeekableReadStream *scr) {
	uint16 enviro, seqnum;
	const char *segname; // for debug messages

	switch (code) {
	case 0x0001:
	case 0x0005:
		debug(10, "ADS 0x%04x: init", code);
		// "init".  0x0005 can be used for searching for next thing.
		break;
	case 0x1010: // WHILE runtype, 2 params
	case 0x1020: // WHILE not runtype, 2 params
	case 0x1030: // WHILE not played, 2 params
	case 0x1040: // WHILE played, 2 params
	case 0x1050: // WHILE finished, 2 params
	case 0x1060: // WHILE not running, 2 params
	case 0x1070: // WHILE running, 2 params
	case 0x1080: // WHILE countdown <= , 1 param (HOC+ only)
	case 0x1090: // WHILE ??, 1 param (HOC+ only)
	case 0x1310: // IF paused, 2 params
	case 0x1320: // IF not paused, 2 params
	case 0x1330: // IF NOT_PLAYED, 2 params
	case 0x1340: // IF PLAYED, 2 params
	case 0x1350: // IF FINISHED, 2 params
	case 0x1360: // IF NOT_RUNNING, 2 params
	case 0x1370: // IF RUNNING, 2 params
	case 0x1380: // IF DETAIL LEVEL <= x, 1 param (HOC+ only)
	case 0x1390: // IF DETAIL LEVEL >= x, 1 param (HOC+ only)
	// The next 6 are in HoC code but maybe never used?
	case 0x13A0: // IF _adsVariable[0] <=
	case 0x13A1: // IF _adsVariable[1] <=
	case 0x13B0: // IF _adsVariable[0] >
	case 0x13B1: // IF _adsVariable[1] >
	case 0x13C0: // IF _adsVariable[0] ==
	case 0x13C1: // IF _adsVariable[1] ==
		return handleLogicOp(code, scr);
	case 0x1500: // ELSE / Skip to end-if, 0 params
		debug(10, "ADS 0x%04x: else (skip to end if)", code);
		skipToEndIf();
		_adsData->_hitBranchOp = true;
		return true;
	case 0x1510: // END IF 0 params
		debug(10, "ADS 0x%04x: hit branch op endif", code);
		_adsData->_hitBranchOp = true;
		return true;
	case 0x1520: // END WHILE 0 params
		debug(10, "ADS 0x%04x: hit branch op endwhile", code);
		_adsData->_hitBranchOp = true;
		return false;
	case 0x2000:
	case 0x2005: { // ADD sequence, set run count
		enviro = scr->readUint16LE();
		seqnum = scr->readUint16LE();
		int16 runCount = scr->readSint16LE();
		uint16 unk = scr->readUint16LE(); // proportion

		Common::SharedPtr<TTMSeq> seq = findTTMSeq(enviro, seqnum);
		TTMEnviro *env = findTTMEnviro(enviro);

		// Set this even if null, check for null below.
		_currentTTMSeq = seq;

		if (!seq || !env) {
			warning("ADS op %04x invalid env + seq requested %d %d", code, enviro, seqnum);
			break;
		}

		debug(10, "ADS 0x%04x: add scene - env %d seq %d (%s) runCount %d prop %d", code,
					enviro, seqnum, env->_tags.getValOrDefault(seqnum).c_str(), runCount, unk);

		if (code == 0x2000) {
			//
			// HACKY WORKAROUND? If we change the frame here we should also reset
			// the `executed` flag surely?  Without this, the cola vendor disappears
			// after you buy cola from him in Willy Beamish scene 31 (park).
			//
			if (seq->_currentFrame != seq->_startFrame && _vm->getGameId() == GID_WILLY)
				seq->_executed = false;
			seq->_currentFrame = seq->_startFrame;
		}

		if (runCount == 0) {
			seq->_runFlag = kRunTypeKeepGoing;
		} else if (runCount < 0) {
			// Negative run count sets the cut time
			seq->_timeCut = DgdsEngine::getInstance()->getThisFrameMs() + (-runCount * MS_PER_FRAME);
			seq->_runFlag = kRunTypeTimeLimited;
		} else {
			seq->_runFlag = kRunTypeMulti;
			seq->_runCount = runCount - 1;
		}
		seq->_runPlayed++;
		break;
	}
	case 0x2010: { // STOP SCENE, 3 params (ttmenv, ttmseq, proportion)
		enviro = scr->readUint16LE();
		seqnum = scr->readUint16LE();
		uint16 unk = scr->readUint16LE();
		_currentTTMSeq = findTTMSeq(enviro, seqnum);
		const TTMEnviro *env = findTTMEnviro(enviro);
		debug(10, "ADS 0x2010: stop seq env %d seq %d (%s) prop %d", enviro, seqnum,
				env->_tags.getValOrDefault(seqnum).c_str(), unk);
		if (_currentTTMSeq)
			_currentTTMSeq->_runFlag = kRunTypeStopped;
		break;
	}
	case 0x2015: { // PAUSE SEQ, 3 params (ttmenv, ttmseq, proportion)
		enviro = scr->readUint16LE();
		seqnum = scr->readUint16LE();
		uint16 unk = scr->readUint16LE();
		_currentTTMSeq = findTTMSeq(enviro, seqnum);
		const TTMEnviro *env = findTTMEnviro(enviro);
		debug(10, "ADS 0x2015: set paused env %d seq %d (%s) prop %d", enviro, seqnum,
				env->_tags.getValOrDefault(seqnum).c_str(), unk);
		if (_currentTTMSeq)
			_currentTTMSeq->_runFlag = kRunTypePaused;
		break;
	}
	case 0x2020: { // RESET SEQ, 2 params (env, seq, proportion)
		enviro = scr->readUint16LE();
		seqnum = scr->readUint16LE();
		uint16 unk = scr->readUint16LE();
		_currentTTMSeq = findTTMSeq(enviro, seqnum);
		const TTMEnviro *env = findTTMEnviro(enviro);
		debug(10, "ADS 0x2020: reset scene env %d seq %d (%s) prop %d", enviro, seqnum,
				env->_tags.getValOrDefault(seqnum).c_str(), unk);
		if (_currentTTMSeq)
			_currentTTMSeq->reset();
		break;
	}

	case 0x3020: {// RANDOM_NOOP, 1 param (proportion)
		uint16 unk = scr->readUint16LE();
		debug(10, "ADS 0x3020: random noop? prop %d", unk);
		return true;
	}
	case 0x3010: // RANDOM_START, 0 params
		debug(10, "ADS 0x3010: random start");
		handleRandomOp(scr);
		break;

	case 0x30FF: // RANDOM_END, 0 params
		debug(10, "ADS 0x30FF: random end");
		error("Unexpected RANDOM END mid-stream (no RANDOM START?).");

	case 0x4000: { // MOVE SEQ TO BACK
		enviro = scr->readUint16LE();
		seqnum = scr->readUint16LE();
		debug(10, "ADS 0x%04x: mov seq to back env %d seq %d", code, enviro, seqnum);
		/*uint16 unk = */scr->readUint16LE();
		// This is O(N) but the N is small and it's not called often.
		Common::SharedPtr<TTMSeq> seq;
		for (uint i = 0; i < _adsData->_ttmSeqs.size(); i++) {
			if (_adsData->_ttmSeqs[i]->_enviro == enviro && _adsData->_ttmSeqs[i]->_seqNum == seqnum) {
				seq = _adsData->_ttmSeqs[i];
				_adsData->_ttmSeqs.remove_at(i);
				break;
			}
		}

		if (seq)
			_adsData->_ttmSeqs.push_back(seq);
		else
			warning("ADS: 0x4000 Request to move env %d seq %d which doesn't exist", enviro, seqnum);

		break;
	}

	case 0x4010: { // MOVE SEQ TO FRONT
		enviro = scr->readUint16LE();
		seqnum = scr->readUint16LE();
		debug(10, "ADS 0x%04x: mov seq to front env %d seq %d", code, enviro, seqnum);
		/*uint16 unk = */scr->readUint16LE();
		// This is O(N) but the N is small and it's not called often.
		Common::SharedPtr<TTMSeq> seq;
		for (uint i = 0; i < _adsData->_ttmSeqs.size(); i++) {
			if (_adsData->_ttmSeqs[i]->_enviro == enviro && _adsData->_ttmSeqs[i]->_seqNum == seqnum) {
				seq = _adsData->_ttmSeqs[i];
				_adsData->_ttmSeqs.remove_at(i);
				break;
			}
		}

		if (seq)
			_adsData->_ttmSeqs.insert_at(0, seq);
		else
			warning("ADS: 0x4010 Request to move env %d seq %d which doesn't exist", enviro, seqnum);

		break;
	}

	case 0x1420: // AND, 0 params - should not hit this here.
	case 0x1430: // OR, 0 params - should not hit this here.
		warning("ADS: Unexpected logic opcode 0x%04x - should be after IF/WHILE", code);
		break;

	case 0xF000:
		debug(10, "ADS 0x%04x: set state 2, current segment idx (%d)", code, _adsData->_runningSegmentIdx);
		if (_adsData->_runningSegmentIdx != -1)
			_adsData->_state[_adsData->_runningSegmentIdx] = 2;
		return false;

	case 0xF010: { // FADE_OUT, 1 param
		int16 segment = scr->readSint16LE();
		int16 idx = _adsData->_runningSegmentIdx;
		if (segment >= 0)
			idx = getArrIndexOfSegNum(segment);
		segname = _adsData->_tags.contains(segment) ? _adsData->_tags.getVal(segment).c_str() : "";
		debug(10, "ADS 0x%04x: set state 2, seg %d idx %d %s", code, segment, idx, segname);
		if (idx >= 0)
			_adsData->_state[idx] = 2;
		if (idx == _adsData->_runningSegmentIdx)
			return false;
		else
			return true;
	}

	case 0xF200: { // RUN_SCRIPT, 1 param
		int16 segment = scr->readSint16LE();
		int16 idx = getArrIndexOfSegNum(segment);
		segname = _adsData->_tags.contains(segment) ? _adsData->_tags.getVal(segment).c_str() : "";
		debug(10, "ADS 0x%04x: run seg %d idx %d %s", code, segment, idx, segname);
		if (segment >= 0 && idx >= 0) {
			int state = (_adsData->_state[idx] & 8) | 4;
			_adsData->_state[idx] = state;
		}
		return true;
	}

	case 0xF210: { // RESTART_SCRIPT, 1 param
		int16 segment = scr->readSint16LE();
		int16 idx = getArrIndexOfSegNum(segment);
		segname = _adsData->_tags.contains(segment) ? _adsData->_tags.getVal(segment).c_str() : "";
		debug(10, "ADS 0x%04x: restart seg %d idx %d %s", code, segment, idx, segname);
		if (segment >= 0 && idx >= 0) {
			int state = (_adsData->_state[idx] & 8) | 3;
			_adsData->_state[idx] = state;
		}
		return true;
	}

	case 0xFFFF:	// END
		debug(10, "ADS 0xFFFF: end");
		return false;

	//// unknown / to-be-implemented
	case 0xFF10:
	case 0xFFF0: // END_IF, 0 params
	default: {
		int nops = numArgs(code);
		warning("ADS 0x%04x: Unimplemented opcode: (skip %d args)", code, nops);
		for (int i = 0; i < nops; i++)
			scr->readUint16LE();
		break;
	}
	}

	return true;
}

int16 ADSInterpreter::getStateForSceneOp(uint16 segnum) {
	int idx = getArrIndexOfSegNum(segnum);
	if (idx < 0)
		return 0;

	// Slightly different implementation after Dragon.
	// Finished is also a "stopped" state in HoC+
	if (DgdsEngine::getInstance()->getGameId() == GID_DRAGON) {
		if (!(_adsData->_state[idx] & 4)) {
			for (const Common::SharedPtr<TTMSeq> &seq: _adsData->_usedSeqs[idx]) {
				if (!seq)
					error("getStateForSceneOp: used seq for seg %d should not be null", segnum);
				if (seq->_runFlag != kRunTypeStopped && !seq->_selfLoop)
					return 1;
			}
			return 0;
		}
	} else {
		int state = (_adsData->_state[idx] & 0xfff7);
		if (state != 4 && state != 1) {
			for (const Common::SharedPtr<TTMSeq> &seq: _adsData->_usedSeqs[idx]) {
				if (!seq)
					error("getStateForSceneOp: used seq for seg %d should not be null", segnum);
				if (seq->_runFlag != kRunTypeStopped && seq->_runFlag != kRunTypeFinished && !seq->_selfLoop)
					return 1;
			}
			return 0;
		}
	}

	return 1;
}


int ADSInterpreter::getArrIndexOfSegNum(uint16 segnum) {
	const int32 startoff = _adsData->scr->pos();
	int result = -1;
	for (int i = 0; i < _adsData->_maxSegments; i++) {
		_adsData->scr->seek(_adsData->_segments[i]);
		int16 seg = _adsData->scr->readSint16LE();
		if (seg == segnum) {
			result = i;
			break;
		}
	}
	_adsData->scr->seek(startoff);
	return result;
}


bool ADSInterpreter::run() {
	if (!_adsData || _adsData->_ttmSeqs.empty())
		return false;

	for (int idx = 0; idx < _adsData->_maxSegments; idx++) {
		int16 flag = _adsData->_state[idx] & 0xfff7;
		for (auto seq : _adsData->_usedSeqs[idx]) {
			if (flag == 3) {
				debug(10, "ADS: Segment idx %d, Reset env %d seq %d", idx, seq->_enviro, seq->_seqNum);
				seq->reset();
			} else {
				if (flag != seq->_scriptFlag) {
					//debug(10, "ADS: Segment idx %d, update seq %d scriptflag %d -> %d",
					//	idx, seq->_seqNum, seq->_scriptFlag, flag);
					seq->_scriptFlag = flag;
				}
			}
		}
	}

	assert(_adsData->scr || !_adsData->_maxSegments);
	for (int idx = 0; idx < _adsData->_maxSegments; idx++) {
		int16 state = _adsData->_state[idx];
		int32 offset = _adsData->_segments[idx];
		_adsData->scr->seek(offset);
		// skip over the segment num
		offset += 2;
		int16 segnum = _adsData->scr->readSint16LE();
		if (state & 8) {
			state &= 0xfff7;
			_adsData->_state[idx] = state;
		} else {
			findEndOrInitOp();
			offset = _adsData->scr->pos();
		}

		if (_adsData->_charWhile[idx])
			offset = _adsData->_charWhile[idx];

		if (state == 3 || state == 4) {
			_adsData->_state[idx] = 1;
			state = 1;
		}

		_adsData->_runningSegmentIdx = idx;
		if (state == 1) {
			_adsData->scr->seek(offset);
			const char *tag = "";
			if (_adsData->_tags.contains(segnum))
				tag = _adsData->_tags[segnum].c_str();
			debug(10, "ADS: Run segment %d idx %d/%d %s", segnum, idx, _adsData->_maxSegments, tag);
			runUntilBranchOpOrEnd();
		}
	}

	bool result = false;
	for (Common::SharedPtr<TTMSeq> seq : _adsData->_ttmSeqs) {
		_currentTTMSeq = seq;
		seq->_lastFrame = -1;
		int sflag = seq->_scriptFlag;
		TTMRunType rflag = seq->_runFlag;
		if (sflag == 6 || (rflag != kRunTypeKeepGoing && rflag != kRunTypeTimeLimited && rflag != kRunTypeMulti && rflag != kRunTypePaused)) {
			if (sflag != 6 && sflag != 5 && rflag == kRunTypeFinished) {
				seq->_runFlag = kRunTypeStopped;
			}
		} else {
			int16 curframe = seq->_currentFrame;
			TTMEnviro *env = findTTMEnviro(seq->_enviro);
			_adsData->_hitTTMOp0110 = false;
			_adsData->_scriptDelay = -1;
			bool scriptresult = false;
			// Next few lines of code in a separate function in the original..
			if (curframe < env->_totalFrames && curframe > -1 && env->_frameOffsets[curframe] > -1) {
				env->scr->seek(env->_frameOffsets[curframe]);
				scriptresult = playScene();
			}

			if (scriptresult && sflag != 5) {
				seq->_executed = true;
				seq->_lastFrame = seq->_currentFrame;
				result = true;
				if (_adsData->_scriptDelay != -1 && seq->_timeInterval != _adsData->_scriptDelay) {
					uint32 now = DgdsEngine::getInstance()->getThisFrameMs();
					seq->_timeNext = now + _adsData->_scriptDelay;
					seq->_timeInterval = _adsData->_scriptDelay;
				}

				if (!_adsData->_hitTTMOp0110) {
					if (_adsData->_gotoTarget != -1) {
						seq->_gotoFrame = _adsData->_gotoTarget;
						if (seq->_currentFrame == _adsData->_gotoTarget)
							seq->_selfLoop = true;
					}
					if (seq->_runFlag != kRunTypePaused)
						updateSeqTimeAndFrame(env, seq);
				} else {
					seq->_gotoFrame = seq->_startFrame;
					if (seq->_runFlag == kRunTypeMulti && seq->_runCount != 0) {
						bool updated = updateSeqTimeAndFrame(env, seq);
						if (updated) {
							seq->_runCount--;
						}
					} else if (seq->_runFlag == kRunTypeTimeLimited && seq->_timeCut != 0) {
						updateSeqTimeAndFrame(env, seq);
					} else {
						bool updated = updateSeqTimeAndFrame(env, seq);
						if (updated) {
							seq->_runFlag = kRunTypeFinished;
							seq->_timeInterval = 0;
						}
					}
				}
			} else if (sflag != 5) {
				seq->_gotoFrame = seq->_startFrame;
				seq->_runFlag = kRunTypeFinished;
			}
		}

		if (rflag == kRunTypeTimeLimited && seq->_timeCut <= DgdsEngine::getInstance()->getThisFrameMs()) {
			seq->_runFlag = kRunTypeFinished;
		}
	}
	return result;
}

bool ADSInterpreter::runUntilBranchOpOrEnd() {
	Common::SeekableReadStream *scr = _adsData->scr;
	if (!scr || scr->pos() >= scr->size())
		return false;

	bool more = true;
	do {
		uint16 code = scr->readUint16LE();
		if (code == 0xffff)
			return false;
		more = handleOperation(code, scr);
	} while (!_adsData->_hitBranchOp && more && scr->pos() < scr->size());

	_adsData->_hitBranchOp = false;

	return more;
}

void ADSInterpreter::setHitTTMOp0110(bool val /* = true */) {
	_adsData->_hitTTMOp0110 = val;
}

void ADSInterpreter::setGotoTarget(int32 target) {
	_adsData->_gotoTarget = target;
}

int ADSInterpreter::numArgs(uint16 opcode) const {
	switch (opcode) {
	case 0x1080:
	case 0x1090:
	case 0x1380:
	case 0x1390:
	case 0x13A0:
	case 0x13A1:
	case 0x13B0:
	case 0x13B1:
	case 0x13C0:
	case 0x13C1:
	case 0x3020:
	case 0xF010:
	case 0xF200:
	case 0xF210:
		return 1;

	case 0x1010:
	case 0x1020:
	case 0x1030:
	case 0x1040:
	case 0x1050:
	case 0x1060:
	case 0x1070:
	case 0x1310:
	case 0x1320:
	case 0x1330:
	case 0x1340:
	case 0x1350:
	case 0x1360:
	case 0x1370:
		return 2;

	case 0x2010:
	case 0x2015:
	case 0x2020:
	case 0x4000:
	case 0x4010:
		return 3;

	case 0x2000:
	case 0x2005:
		return 4;

	default:
		return 0;
	}
}

Common::Error ADSInterpreter::syncState(Common::Serializer &s) {
	//TODO: Currently sync all states then set the active one,
	// do we need to load/save all?
	uint32 numTexts = _adsTexts.size();
	s.syncAsUint32LE(numTexts);

	Common::Array<Common::String> scriptNames;
	Common::String activeScript;

	if (s.isLoading()) {
		for (uint32 i = 0; i < numTexts; i++) {
			Common::String txtName;
			s.syncString(txtName);
			// We save all the names, but we only actually need to reload one script -
			// the most recent one.  Do that at the end of this function.
			if (s.getVersion() < 3) {
				load(txtName);
				scriptNames.push_back(txtName);
			}
		}
	} else {
		for (const auto &node : _adsTexts) {
			Common::String txtName = node._key;
			s.syncString(txtName);
			scriptNames.push_back(txtName);
			if (&node._value == _adsData)
				activeScript = txtName;
		}
	}

	// Text order should be the same
	if (s.getVersion() < 3) {
		for (const Common::String &name : scriptNames) {
			_adsTexts[name].syncState(s);
		}
	}

	for (const Common::String &name : scriptNames) {
		load(name);
	}

	s.syncString(activeScript);

	if (s.getVersion() >= 3 && !activeScript.empty()) {
		load(activeScript);
		scriptNames.push_back(activeScript);
	}

	assert(activeScript.empty() || _adsTexts.contains(activeScript));
	_adsData = activeScript.empty() ? nullptr : &_adsTexts[activeScript];

	return Common::kNoError;
}

} // end namespace Dgds
