/* 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 "common/config-manager.h"
#include "common/file.h"
#include "common/system.h"
#include "common/util.h"
#include "common/rect.h"

#include "audio/mixer.h"

#include "graphics/cursorman.h"
#include "graphics/palette.h"

#include "scumm/file.h"
#include "scumm/imuse_digi/dimuse_engine.h"
#include "scumm/scumm.h"
#include "scumm/scumm_v7.h"
#include "scumm/sound.h"
#include "scumm/smush/codec37.h"
#include "scumm/smush/codec47.h"
#include "scumm/smush/smush_font.h"
#include "scumm/smush/smush_player.h"

#include "scumm/insane/insane.h"

#include "audio/audiostream.h"
#include "audio/mixer.h"
#include "audio/decoders/mp3.h"
#include "audio/decoders/raw.h"
#include "audio/decoders/vorbis.h"

#include "common/zlib.h"

namespace Scumm {

static const int MAX_STRINGS = 200;
static const int ETRS_HEADER_LENGTH = 16;

class StringResource {
private:

	struct {
		int id;
		char *string;
	} _strings[MAX_STRINGS];

	int _nbStrings;
	int _lastId;
	const char *_lastString;

public:

	StringResource() :
		_nbStrings(0),
		_lastId(-1),
		_lastString(nullptr) {
		for (int i = 0; i < MAX_STRINGS; i++) {
			_strings[i].id = 0;
			_strings[i].string = nullptr;
		}
	}
	~StringResource() {
		for (int32 i = 0; i < _nbStrings; i++) {
			delete[] _strings[i].string;
		}
	}

	bool init(char *buffer, int32 length) {
		char *def_start = strchr(buffer, '#');
		while (def_start != nullptr) {
			char *def_end = strchr(def_start, '\n');
			assert(def_end != nullptr);

			char *id_end = def_end;
			while (id_end >= def_start && !Common::isDigit(*(id_end-1))) {
				id_end--;
			}

			assert(id_end > def_start);
			char *id_start = id_end;
			while (Common::isDigit(*(id_start - 1))) {
				id_start--;
			}

			char idstring[32];
			memcpy(idstring, id_start, id_end - id_start);
			idstring[id_end - id_start] = 0;
			int32 id = atoi(idstring);
			char *data_start = def_end;

			while (*data_start == '\n' || *data_start == '\r') {
				data_start++;
			}
			char *data_end = data_start;

			while (1) {
				if (data_end[-2] == '\r' && data_end[-1] == '\n' && data_end[0] == '\r' && data_end[1] == '\n') {
					break;
				}
				// In the Steam Mac version of The Dig, LF-LF is used
				// instead of CR-LF
				if (data_end[-2] == '\n' && data_end[-1] == '\n') {
					break;
				}
				// In Russian Full Throttle strings are finished with
				// just one pair of CR-LF
				if (data_end[-2] == '\r' && data_end[-1] == '\n' && data_end[0] == '#') {
					break;
				}
				data_end++;
				if (data_end >= buffer + length) {
					data_end = buffer + length;
					break;
				}
			}

			data_end -= 2;
			assert(data_end > data_start);
			char *value = new char[data_end - data_start + 1];
			assert(value);
			memcpy(value, data_start, data_end - data_start);
			value[data_end - data_start] = 0;
			char *line_start = value;
			char *line_end;

			while ((line_end = strchr(line_start, '\n'))) {
				line_start = line_end+1;
				if (line_start[0] == '/' && line_start[1] == '/') {
					line_start += 2;
					if	(line_end[-1] == '\r')
						line_end[-1] = ' ';
					else
						*line_end++ = ' ';
					memmove(line_end, line_start, strlen(line_start)+1);
				}
			}
			_strings[_nbStrings].id = id;
			_strings[_nbStrings].string = value;
			_nbStrings ++;
			def_start = strchr(data_end + 2, '#');
		}
		return true;
	}

	const char *get(int id) {
		if (id == _lastId) {
			return _lastString;
		}
		debugC(DEBUG_SMUSH, "StringResource::get(%d)", id);
		for (int i = 0; i < _nbStrings; i++) {
			if (_strings[i].id == id) {
				_lastId = id;
				_lastString = _strings[i].string;
				return _strings[i].string;
			}
		}
		warning("invalid string id : %d", id);
		_lastId = -1;
		_lastString = "unknown string";
		return _lastString;
	}
};

static StringResource *getStrings(ScummEngine *vm, const char *file, bool is_encoded) {
	debugC(DEBUG_SMUSH, "trying to read text resources from %s", file);
	ScummFile theFile;

	vm->openFile(theFile, file);
	if (!theFile.isOpen()) {
		return 0;
	}
	int32 length = theFile.size();
	char *filebuffer = new char [length + 1];
	assert(filebuffer);
	theFile.read(filebuffer, length);
	filebuffer[length] = 0;

	if (is_encoded && READ_BE_UINT32(filebuffer) == MKTAG('E','T','R','S')) {
		assert(length > ETRS_HEADER_LENGTH);
		length -= ETRS_HEADER_LENGTH;
		for (int i = 0; i < length; ++i) {
			filebuffer[i] = filebuffer[i + ETRS_HEADER_LENGTH] ^ 0xCC;
		}
		filebuffer[length] = '\0';
	}
	StringResource *sr = new StringResource;
	assert(sr);
	sr->init(filebuffer, length);
	delete[] filebuffer;
	return sr;
}

void SmushPlayer::timerCallback() {
	parseNextFrame();
}

SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane) {
	_vm = scumm;
	_imuseDigital = imuseDigital;
	_insane = insane;
	_nbframes = 0;
	_codec37 = 0;
	_codec47 = 0;
	_strings = nullptr;
	_sf[0] = nullptr;
	_sf[1] = nullptr;
	_sf[2] = nullptr;
	_sf[3] = nullptr;
	_sf[4] = nullptr;
	_base = nullptr;
	_frameBuffer = nullptr;
	_specialBuffer = nullptr;

	_seekPos = -1;

	_skipNext = false;
	_dst = nullptr;
	_storeFrame = false;
	_compressedFileMode = false;
	_width = 0;
	_height = 0;
	_IACTpos = 0;
	_speed = -1;
	_insanity = false;
	_middleAudio = false;
	_skipPalette = false;
	_IACTstream = nullptr;
	_paused = false;
	_pauseStartTime = 0;
	_pauseTime = 0;

	for (int i = 0; i < 4; i++)
		_iactTable[i] = 0;

	_IACTchannel = new Audio::SoundHandle();
	_compressedFileSoundHandle = new Audio::SoundHandle();

	_smushNumTracks = 0;
	_gainReductionLowerBound = 64;
	_gainReductionFactor = 256;
	_gainReductionMultiplier = 256;
	_smushTracksNeedInit = true;
	_smushAudioInitialized = false;
	_smushAudioCallbackEnabled = false;

	initAudio(DIMUSE_SAMPLERATE, 200000);
}

SmushPlayer::~SmushPlayer() {
	delete _IACTchannel;
	delete _compressedFileSoundHandle;
	terminateAudio();
}

void SmushPlayer::init(int32 speed) {
	VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];

	_frame = 0;
	_speed = speed;
	_endOfFile = false;

	_vm->_smushVideoShouldFinish = false;
	_vm->_smushActive = true;

	_vm->setDirtyColors(0, 255);
	_dst = vs->getPixels(0, 0);

	// HACK HACK HACK: This is an *evil* trick, beware!
	// We do this to fix bug #1792. A proper solution would change all the
	// drawing code to use the pitch value specified by the virtual screen.
	// However, since a lot of the SMUSH code currently assumes the screen
	// width and pitch to be equal, this will require lots of changes. So
	// we resort to this hackish solution for now.
	_origPitch = vs->pitch;
	_origNumStrips = _vm->_gdi->_numStrips;
	vs->pitch = vs->w;
	_vm->_gdi->_numStrips = vs->w / 8;

	_vm->_mixer->stopHandle(*_compressedFileSoundHandle);
	_vm->_mixer->stopHandle(*_IACTchannel);
	_IACTpos = 0;
}

void SmushPlayer::release() {
	_vm->_smushVideoShouldFinish = true;

	for (int i = 0; i < 5; i++) {
		delete _sf[i];
		_sf[i] = nullptr;
	}

	delete _strings;
	_strings = nullptr;

	delete _base;
	_base = nullptr;

	free(_specialBuffer);
	_specialBuffer = nullptr;

	free(_frameBuffer);
	_frameBuffer = nullptr;

	_IACTstream = nullptr;

	_vm->_smushActive = false;
	_vm->_fullRedraw = true;

	// HACK HACK HACK: This is an *evil* trick, beware! See above for
	// some explanation.
	_vm->_virtscr[kMainVirtScreen].pitch = _origPitch;
	_vm->_gdi->_numStrips = _origNumStrips;

	delete _codec37;
	_codec37 = 0;
	delete _codec47;
	_codec47 = 0;
}

void SmushPlayer::handleStore(int32 subSize, Common::SeekableReadStream &b) {
	debugC(DEBUG_SMUSH, "SmushPlayer::handleStore()");
	assert(subSize >= 4);
	_storeFrame = true;
}

void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
	debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
	assert(subSize >= 6);

	if (_frameBuffer != nullptr) {
		memcpy(_dst, _frameBuffer, _width * _height);
	}
}

void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
	debugC(DEBUG_SMUSH, "SmushPlayer::IACT()");
	assert(subSize >= 8);

	int code = b.readUint16LE();
	int flags = b.readUint16LE();
	int unknown = b.readSint16LE();
	int userId = b.readUint16LE();

	if ((code != 8) && (flags != 46)) {
		_vm->_insane->procIACT(_dst, 0, 0, 0, b, 0, 0, code, flags, unknown, userId);
		return;
	}

	if (_compressedFileMode) {
		return;
	}

	assert(flags == 46 && unknown == 0);
	/*int trkId =*/ b.readUint16LE();
	int index = b.readUint16LE();
	int nbframes = b.readUint16LE();
	/*int32 size =*/ b.readUint32LE();
	int32 bsize = subSize - 18;

	if (_vm->_game.id == GID_CMI) {
		// TODO: Move this code into another SmushChannel subclass?
		byte *src = (byte *)malloc(bsize);
		b.read(src, bsize);
		byte *d_src = src;
		byte value;

		while (bsize > 0) {
			if (_IACTpos >= 2) {
				int32 len = READ_BE_UINT16(_IACToutput) + 2;
				len -= _IACTpos;
				if (len > bsize) {
					memcpy(_IACToutput + _IACTpos, d_src, bsize);
					_IACTpos += bsize;
					bsize = 0;
				} else {
					byte *output_data = (byte *)malloc(4096);

					memcpy(_IACToutput + _IACTpos, d_src, len);
					byte *dst = output_data;
					byte *d_src2 = _IACToutput;
					d_src2 += 2;
					int32 count = 1024;
					byte variable1 = *d_src2++;
					byte variable2 = variable1 / 16;
					variable1 &= 0x0f;
					do {
						value = *(d_src2++);
						if (value == 0x80) {
							*dst++ = *d_src2++;
							*dst++ = *d_src2++;
						} else {
							int16 val = (int8)value << variable2;
							*dst++ = val >> 8;
							*dst++ = (byte)(val);
						}
						value = *(d_src2++);
						if (value == 0x80) {
							*dst++ = *d_src2++;
							*dst++ = *d_src2++;
						} else {
							int16 val = (int8)value << variable1;
							*dst++ = val >> 8;
							*dst++ = (byte)(val);
						}
					} while (--count);

					if (!_IACTstream) {
						_IACTstream = Audio::makeQueuingAudioStream(22050, true);
						_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, _IACTchannel, _IACTstream);
					}
					_IACTstream->queueBuffer(output_data, 0x1000, DisposeAfterUse::YES, Audio::FLAG_STEREO | Audio::FLAG_16BITS);

					bsize -= len;
					d_src += len;
					_IACTpos = 0;
				}
			} else {
				if (bsize > 1 && _IACTpos == 0) {
					*(_IACToutput + 0) = *d_src++;
					_IACTpos = 1;
					bsize--;
				}
				*(_IACToutput + _IACTpos) = *d_src++;
				_IACTpos++;
				bsize--;
			}
		}

		free(src);
	} else if ((_vm->_game.id == GID_DIG) && !(_vm->_game.features & GF_DEMO)) {
		int bufId, volume, paused, curSoundId;

		byte *dataBuffer = (byte *)malloc(bsize);
		b.read(dataBuffer, bsize);

		switch (userId) {
		case 1:
			bufId = 1;
			volume = 127;
			break;
		case 2:
			bufId = 2;
			volume = 127;
			break;
		case 3:
			bufId = 3;
			volume = 127;
			break;
		default:
			if (userId >= 100 && userId <= 163) {
				bufId = DIMUSE_BUFFER_SPEECH;
				volume = 2 * userId - 200;
			} else if (userId >= 200 && userId <= 263) {
				bufId = DIMUSE_BUFFER_MUSIC;
				volume = 2 * userId - 400;
			} else if (userId >= 300 && userId <= 363) {
				bufId = DIMUSE_BUFFER_SMUSH;
				volume = 2 * userId - 600;
			} else {
				free(dataBuffer);
				error("SmushPlayer::handleIACT(): ERROR: got invalid userID (%d)", userId);
			}
			break;
		}

		paused = nbframes - index == 1;

		// Apparently this is expected to happen (e.g.: Brink's death video)
		if (index && _iactTable[bufId] - index != -1) {
			free(dataBuffer);
			debugC(DEBUG_SMUSH, "SmushPlayer::handleIACT(): WARNING: got out of order block");
			return;
		}

		_iactTable[bufId] = index;

		if (index) {
			if (_imuseDigital->diMUSEGetParam(bufId + DIMUSE_SMUSH_SOUNDID, DIMUSE_P_SND_TRACK_NUM)) {
				_imuseDigital->diMUSEFeedStream(bufId + DIMUSE_SMUSH_SOUNDID, dataBuffer, subSize - 18, paused);
				free(dataBuffer);
				return;
			}
			free(dataBuffer);
			error("SmushPlayer::handleIACT(): ERROR: got unexpected non-zero IACT block, bufID %d", bufId);
		} else {
			if (READ_BE_UINT32(dataBuffer) != MKTAG('i', 'M', 'U', 'S')) {
				free(dataBuffer);
				error("SmushPlayer::handleIACT(): ERROR: got non-IMUS IACT block");
			}

			curSoundId = 0;
			do {
				curSoundId = _imuseDigital->diMUSEGetNextSound(curSoundId);
				if (!curSoundId)
					break;
			} while (_imuseDigital->diMUSEGetParam(curSoundId, DIMUSE_P_SND_HAS_STREAM) != 1 || _imuseDigital->diMUSEGetParam(curSoundId, DIMUSE_P_STREAM_BUFID) != bufId);

			if (!curSoundId) {
				// There isn't any previous sound running: start a new stream
				if (_imuseDigital->diMUSEStartStream(bufId + DIMUSE_SMUSH_SOUNDID, 126, bufId)) {
					free(dataBuffer);
					error("SmushPlayer::handleIACT(): ERROR: couldn't start stream");
				}
			} else {
				// There's an old sound running: switch the stream from the old one to the new one
				_imuseDigital->diMUSESwitchStream(curSoundId, bufId + DIMUSE_SMUSH_SOUNDID, bufId == 2 ? 1000 : 150, 0, 0);
			}

			_imuseDigital->diMUSESetParam(bufId + DIMUSE_SMUSH_SOUNDID, DIMUSE_P_VOLUME, volume);

			if (bufId == DIMUSE_BUFFER_SPEECH) {
				_imuseDigital->diMUSESetParam(bufId + DIMUSE_SMUSH_SOUNDID, DIMUSE_P_GROUP, DIMUSE_GROUP_SPEECH);
			} else if (bufId == DIMUSE_BUFFER_MUSIC) {
				_imuseDigital->diMUSESetParam(bufId + DIMUSE_SMUSH_SOUNDID, DIMUSE_P_GROUP, DIMUSE_GROUP_MUSIC);
			} else {
				_imuseDigital->diMUSESetParam(bufId + DIMUSE_SMUSH_SOUNDID, DIMUSE_P_GROUP, DIMUSE_GROUP_SFX);
			}

			_imuseDigital->diMUSEFeedStream(bufId + DIMUSE_SMUSH_SOUNDID, dataBuffer, subSize - 18, paused);
			free(dataBuffer);
			return;
		}
	}
}

void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) {
	int pos_x = b.readSint16LE();
	int pos_y = b.readSint16LE();
	int flags = b.readSint16LE();
	int left = b.readSint16LE();
	int top = b.readSint16LE();
	int width = b.readSint16LE();
	int height = b.readSint16LE();
	/*int32 unk2 =*/ b.readUint16LE();

	const char *str;
	char *string = nullptr, *string2 = nullptr;
	if (subType == MKTAG('T','E','X','T')) {
		string = (char *)malloc(subSize - 16);
		str = string;
		b.read(string, subSize - 16);
	} else {
		int string_id = b.readUint16LE();
		if (!_strings)
			return;
		str = _strings->get(string_id);
	}

	// if subtitles disabled and bit 3 is set, then do not draw
	//
	// Query ConfMan here. However it may be slower, but
	// player may want to switch the subtitles on or off during the
	// playback. This fixes bug #2812
	if ((!ConfMan.getBool("subtitles")) && ((flags & 8) == 8))
		return;

	bool isCJKComi = (_vm->_game.id == GID_CMI && _vm->_useCJKMode);
	int color = 15;
	int fontId = isCJKComi ? 1 : 0;

	while (*str == '/') {
		str++; // For Full Throttle text resources
	}

	byte transBuf[512];
	if (_vm->_game.id == GID_CMI) {
		_vm->translateText((const byte *)str - 1, transBuf);
		while (*str++ != '/')
			;
		string2 = (char *)transBuf;

		// If string2 contains formatting information there probably
		// wasn't any translation for it in the language.tab file. In
		// that case, pretend there is no string2.
		if (string2[0] == '^')
			string2[0] = 0;
	}

	while (str[0] == '^') {
		switch (str[1]) {
		case 'f':
			fontId = str[3] - '0';
			str += 4;
			break;
		case 'c':
			color = str[4] - '0' + 10 *(str[3] - '0');
			str += 5;
		break;
		default:
			error("invalid escape code in text string");
		}
	}

	if (_vm->_game.id == GID_CMI && string2[0] != 0)
		str = string2;

	// This is a hack from the original COMI CJK interpreter. Its purpose is to avoid
	// ugly combinations of two byte characters (rendered with the respective special
	// font) and standard one byte (NUT font) characters (see bug #11947).
	if (isCJKComi && !(fontId == 0 && color == 1)) {
		fontId = 1;
		color = 255;
	}

	SmushFont *sf = getFont(fontId);
	assert(sf != nullptr);

	// The hack that used to be here to prevent bug #2220 is no longer necessary and
	// has been removed. The font renderer can handle all ^codes it encounters (font
	// changes on the fly will be ignored for Smush texts, since our code design does
	// not permit it and the feature isn't used anyway).

	if (_vm->_language == Common::HE_ISR && !(flags & kStyleAlignCenter)) {
		flags |= kStyleAlignRight;
		pos_x = _width - 1 - pos_x;
	}

	TextStyleFlags flg = (TextStyleFlags)(flags & 7);
	// flags:
	// bit 0 - center                  0x01
	// bit 1 - not used (align right)  0x02
	// bit 2 - word wrap               0x04
	// bit 3 - switchable              0x08
	// bit 4 - fill background         0x10
	// bit 5 - outline/shadow          0x20        (apparently only set by the text renderer itself, not from the smush data)
	// bit 6 - vertical fix (COMI)     0x40        (COMI handles this in the printing method, but I haven't seen a case where it is used)
	// bit 7 - skip ^ codes (COMI)     0x80        (should be irrelevant for Smush, we strip these commands anyway)
	// bit 8 - no vertical fix (COMI)  0x100       (COMI handles this in the printing method, but I haven't seen a case where it is used)

	if (flg & kStyleWordWrap) {
		// COMI has to do it all a bit different, of course. SCUMM7 games immediately render the text from here and actually use the clipping data
		// provided by the text resource. COMI does not render directly, but enqueues a blast string (which is then drawn through the usual main
		// loop routines). During that process the rect data will get dumped and replaced with the following default values. It's hard to tell
		// whether this is on purpose or not (the text looks not necessarily better or worse, just different), so we follow the original...
		if (_vm->_game.id == GID_CMI) {
			left = top = 10;
			width = _width - 20;
			height = _height - 20;
		}
		Common::Rect clipRect(MAX<int>(0, left), MAX<int>(0, top), MIN<int>(left + width, _width), MIN<int>(top + height, _height));
		sf->drawStringWrap(str, _dst, clipRect, pos_x, pos_y, color, flg);
	} else {
		// Similiar to the wrapped text, COMI will pass on rect coords here, which will later be lost. Unlike with the wrapped text, it will
		// finally use the full screen dimenstions. SCUMM7 renders directly from here (see comment above), but also with the full screen.
		Common::Rect clipRect(0, 0, _width, _height);
		sf->drawString(str, _dst, clipRect, pos_x, pos_y, color, flg);
	}

	free(string);
}

const char *SmushPlayer::getString(int id) {
	return _strings->get(id);
}

bool SmushPlayer::readString(const char *file) {
	const char *i = strrchr(file, '.');
	if (i == nullptr) {
		error("invalid filename : %s", file);
	}
	char fname[260];
	memcpy(fname, file, i - file);
	strcpy(fname + (i - file), ".trs");
	if ((_strings = getStrings(_vm, fname, false)) != 0) {
		return true;
	}

	if (_vm->_game.id == GID_DIG && (_strings = getStrings(_vm, "digtxt.trs", true)) != 0) {
		return true;
	}
	return false;
}

void SmushPlayer::readPalette(byte *out, Common::SeekableReadStream &in) {
	in.read(out, 0x300);
}

static byte delta_color(byte org_color, int16 delta_color) {
	int t = (org_color * 129 + delta_color) / 128;
	return CLIP(t, 0, 255);
}

void SmushPlayer::handleDeltaPalette(int32 subSize, Common::SeekableReadStream &b) {
	debugC(DEBUG_SMUSH, "SmushPlayer::handleDeltaPalette()");

	if (subSize == 0x300 * 3 + 4) {

		b.readUint16LE();
		b.readUint16LE();

		for (int i = 0; i < 0x300; i++) {
			_deltaPal[i] = b.readUint16LE();
		}
		readPalette(_pal, b);
		setDirtyColors(0, 255);
	} else if (subSize == 6) {

		b.readUint16LE();
		b.readUint16LE();
		b.readUint16LE();

		for (int i = 0; i < 0x300; i++) {
			_pal[i] = delta_color(_pal[i], _deltaPal[i]);
		}
		setDirtyColors(0, 255);
	} else {
		error("SmushPlayer::handleDeltaPalette() Wrong size for DeltaPalette");
	}
}

void SmushPlayer::handleNewPalette(int32 subSize, Common::SeekableReadStream &b) {
	debugC(DEBUG_SMUSH, "SmushPlayer::handleNewPalette()");
	assert(subSize >= 0x300);

	if (_skipPalette)
		return;

	readPalette(_pal, b);
	setDirtyColors(0, 255);
}

void smush_decode_codec1(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
void smush_decode_codec20(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);

void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height) {
	if ((height == 242) && (width == 384)) {
		if (_specialBuffer == 0)
			_specialBuffer = (byte *)malloc(242 * 384);
		_dst = _specialBuffer;
	} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
		return;
	// FT Insane uses smaller frames to draw overlays with moving objects
	// Other .san files do have them as well but their purpose in unknown
	// and often it causes memory overdraw. So just skip those frames
	else if (!_insanity && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth)))
		return;

	if ((height == 242) && (width == 384)) {
		_width = width;
		_height = height;
	} else {
		_width = _vm->_screenWidth;
		_height = _vm->_screenHeight;
	}

	switch (codec) {
	case 1:
	case 3:
		smush_decode_codec1(_dst, src, left, top, width, height, _vm->_screenWidth);
		break;
	case 37:
		if (!_codec37)
			_codec37 = new Codec37Decoder(width, height);
		if (_codec37)
			_codec37->decode(_dst, src);
		break;
	case 47:
		if (!_codec47)
			_codec47 = new Codec47Decoder(width, height);
		if (_codec47)
			_codec47->decode(_dst, src);
		break;
	case 20:
		// Used by Full Throttle Classic (from Remastered)
		smush_decode_codec20(_dst, src, left, top, width, height, _vm->_screenWidth);
		break;
	default:
		error("Invalid codec for frame object : %d", codec);
	}

	if (_storeFrame) {
		if (_frameBuffer == nullptr) {
			_frameBuffer = (byte *)malloc(_width * _height);
		}
		memcpy(_frameBuffer, _dst, _width * _height);
		_storeFrame = false;
	}
}

#ifdef USE_ZLIB
void SmushPlayer::handleZlibFrameObject(int32 subSize, Common::SeekableReadStream &b) {
	if (_skipNext) {
		_skipNext = false;
		return;
	}

	int32 chunkSize = subSize;
	byte *chunkBuffer = (byte *)malloc(chunkSize);
	assert(chunkBuffer);
	b.read(chunkBuffer, chunkSize);

	unsigned long decompressedSize = READ_BE_UINT32(chunkBuffer);
	byte *fobjBuffer = (byte *)malloc(decompressedSize);
	if (!Common::uncompress(fobjBuffer, &decompressedSize, chunkBuffer + 4, chunkSize - 4))
		error("SmushPlayer::handleZlibFrameObject() Zlib uncompress error");
	free(chunkBuffer);

	byte *ptr = fobjBuffer;
	int codec = READ_LE_UINT16(ptr); ptr += 2;
	int left = READ_LE_UINT16(ptr); ptr += 2;
	int top = READ_LE_UINT16(ptr); ptr += 2;
	int width = READ_LE_UINT16(ptr); ptr += 2;
	int height = READ_LE_UINT16(ptr); ptr += 2;

	decodeFrameObject(codec, fobjBuffer + 14, left, top, width, height);

	free(fobjBuffer);
}
#endif

void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b) {
	assert(subSize >= 14);
	if (_skipNext) {
		_skipNext = false;
		return;
	}

	int codec = b.readUint16LE();
	int left = b.readUint16LE();
	int top = b.readUint16LE();
	int width = b.readUint16LE();
	int height = b.readUint16LE();

	b.readUint16LE();
	b.readUint16LE();

	int32 chunk_size = subSize - 14;
	byte *chunk_buffer = (byte *)malloc(chunk_size);
	assert(chunk_buffer);
	b.read(chunk_buffer, chunk_size);

	decodeFrameObject(codec, chunk_buffer, left, top, width, height);

	free(chunk_buffer);
}

void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
	debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame(%d)", _frame);
	uint8 *audioChunk = nullptr;
	_skipNext = false;

	if (_insanity) {
		_vm->_insane->procPreRendering();
	}

	while (frameSize > 0) {
		const uint32 subType = b.readUint32BE();
		const int32 subSize = b.readUint32BE();
		const int32 subOffset = b.pos();
		switch (subType) {
		case MKTAG('N','P','A','L'):
			handleNewPalette(subSize, b);
			break;
		case MKTAG('F','O','B','J'):
			handleFrameObject(subSize, b);
			break;
#ifdef USE_ZLIB
		case MKTAG('Z','F','O','B'):
			handleZlibFrameObject(subSize, b);
			break;
#endif
		case MKTAG('P','S','A','D'):
			if (!_compressedFileMode) {
				audioChunk = (uint8 *)malloc(subSize + 8);
				b.seek(-8, SEEK_CUR);
				b.read(audioChunk, subSize + 8);
				feedAudio(audioChunk, 0, 127, 0, 0);
				free(audioChunk);
				audioChunk = nullptr;
			}

			break;
		case MKTAG('T','R','E','S'):
			handleTextResource(subType, subSize, b);
			break;
		case MKTAG('X','P','A','L'):
			handleDeltaPalette(subSize, b);
			break;
		case MKTAG('I','A','C','T'):
			handleIACT(subSize, b);
			break;
		case MKTAG('S','T','O','R'):
			handleStore(subSize, b);
			break;
		case MKTAG('F','T','C','H'):
			handleFetch(subSize, b);
			break;
		case MKTAG('S','K','I','P'):
			_vm->_insane->procSKIP(subSize, b);
			break;
		case MKTAG('T','E','X','T'):
			handleTextResource(subType, subSize, b);
			break;
		default:
			error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
		}

		frameSize -= subSize + 8;
		b.seek(subOffset + subSize, SEEK_SET);
		if (subSize & 1) {
			b.skip(1);
			frameSize--;
		}
	}

	if (_insanity) {
		_vm->_insane->procPostRendering(_dst, 0, 0, 0, _frame, _nbframes-1);
	}

	if (_width != 0 && _height != 0) {
		updateScreen();
	}

	_frame++;
}

void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b) {
	debugC(DEBUG_SMUSH, "SmushPlayer::handleAnimHeader()");
	assert(subSize >= 0x300 + 6);

	/* _version = */ b.readUint16LE();
	_nbframes = b.readUint16LE();
	b.readUint16LE();

	if (_skipPalette)
		return;

	readPalette(_pal, b);
	setDirtyColors(0, 255);
}

void SmushPlayer::setupAnim(const char *file) {
	if (_insanity) {
		if (!((_vm->_game.features & GF_DEMO) && (_vm->_game.platform == Common::kPlatformDOS)))
			readString("mineroad.trs");
	} else
		readString(file);
}

SmushFont *SmushPlayer::getFont(int font) {
	char file_font[11];

	if (_sf[font])
		return _sf[font];

	if (_vm->_game.id == GID_FT) {
		if (!((_vm->_game.features & GF_DEMO) && (_vm->_game.platform == Common::kPlatformDOS))) {
			const char *ft_fonts[] = {
				"scummfnt.nut",
				"techfnt.nut",
				"titlfnt.nut",
				"specfnt.nut"
			};

			assert(font >= 0 && font < ARRAYSIZE(ft_fonts));

			_sf[font] = new SmushFont(_vm, ft_fonts[font], true);
		}
	} else {
		int numFonts = (_vm->_game.id == GID_CMI && !(_vm->_game.features & GF_DEMO)) ? 5 : 4;
		assert(font >= 0 && font < numFonts);
		sprintf(file_font, "font%d.nut", font);
		_sf[font] = new SmushFont(_vm, file_font, _vm->_game.id == GID_DIG && font != 0);
	}

	assert(_sf[font]);
	return _sf[font];
}

void SmushPlayer::parseNextFrame() {

	if (_seekPos >= 0) {
		if (_seekFile.size() > 0) {
			delete _base;

			ScummFile *tmp = new ScummFile();
			if (!g_scumm->openFile(*tmp, _seekFile))
				error("SmushPlayer: Unable to open file %s", _seekFile.c_str());
			_base = tmp;
			_base->readUint32BE();
			_baseSize = _base->readUint32BE();

			if (_seekPos > 0) {
				assert(_seekPos > 8);
				// In this case we need to get palette and number of frames
				const uint32 subType = _base->readUint32BE();
				const int32 subSize = _base->readUint32BE();
				const int32 subOffset = _base->pos();
				assert(subType == MKTAG('A','H','D','R'));
				handleAnimHeader(subSize, *_base);
				_base->seek(subOffset + subSize, SEEK_SET);

				_middleAudio = true;
				_seekPos -= 8;
			} else {
				// We need this in Full Throttle when entering/leaving
				// the old mine road.
				tryCmpFile(_seekFile.c_str());
			}
			_skipPalette = false;
		} else {
			_skipPalette = true;
		}

		_base->seek(_seekPos + 8, SEEK_SET);
		_frame = _seekFrame;
		_startFrame = _frame;
		_startTime = _vm->_system->getMillis();

		_seekPos = -1;
	}

	assert(_base);

	const uint32 subType = _base->readUint32BE();
	const int32 subSize = _base->readUint32BE();
	const int32 subOffset = _base->pos();

	if (_base->pos() >= (int32)_baseSize) {
		_vm->_smushVideoShouldFinish = true;
		_endOfFile = true;
		return;
	}

	debug(3, "Chunk: %s at %x", tag2str(subType), subOffset);

	switch (subType) {
	case MKTAG('A','H','D','R'): // FT INSANE may seek file to the beginning
		handleAnimHeader(subSize, *_base);
		break;
	case MKTAG('F','R','M','E'):
		handleFrame(subSize, *_base);
		break;
	default:
		error("Unknown Chunk found at %x: %s, %d", subOffset, tag2str(subType), subSize);
	}

	_base->seek(subOffset + subSize, SEEK_SET);

	if (_insanity)
		_vm->_sound->processSound();

	_vm->_imuseDigital->flushTracks();
}

void SmushPlayer::setPalette(const byte *palette) {
	memcpy(_pal, palette, 0x300);
	setDirtyColors(0, 255);
}

void SmushPlayer::setPaletteValue(int n, byte r, byte g, byte b) {
	_pal[n * 3 + 0] = r;
	_pal[n * 3 + 1] = g;
	_pal[n * 3 + 2] = b;
	setDirtyColors(n, n);
}

void SmushPlayer::setDirtyColors(int min, int max) {
	if (_palDirtyMin > min)
		_palDirtyMin = min;
	if (_palDirtyMax < max)
		_palDirtyMax = max;
}

void SmushPlayer::warpMouse(int x, int y, int buttons) {
	_warpNeeded = true;
	_warpX = x;
	_warpY = y;
	_warpButtons = buttons;
}

void SmushPlayer::updateScreen() {
	uint32 end_time, start_time = _vm->_system->getMillis();
	_updateNeeded = true;
	end_time = _vm->_system->getMillis();
	debugC(DEBUG_SMUSH, "Smush stats: updateScreen( %03d )", end_time - start_time);
}

void SmushPlayer::insanity(bool flag) {
	_insanity = flag;
}

void SmushPlayer::seekSan(const char *file, int32 pos, int32 contFrame) {
	_seekFile = file ? file : "";
	_seekPos = pos;
	_seekFrame = contFrame;
	_pauseTime = 0;
}

void SmushPlayer::tryCmpFile(const char *filename) {
	_vm->_mixer->stopHandle(*_compressedFileSoundHandle);

	_compressedFileMode = false;
	const char *i = strrchr(filename, '.');
	if (i == NULL) {
		error("invalid filename : %s", filename);
	}
#if defined(USE_MAD) || defined(USE_VORBIS)
	char fname[260];
#endif
	Common::File *file = new Common::File();

	// FIXME: How about using AudioStream::openStreamFile instead of the code below?

#ifdef USE_VORBIS
	memcpy(fname, filename, i - filename);
	strcpy(fname + (i - filename), ".ogg");
	if (file->open(fname)) {
		_compressedFileMode = true;
		_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, _compressedFileSoundHandle, Audio::makeVorbisStream(file, DisposeAfterUse::YES));
		return;
	}
#endif
#ifdef USE_MAD
	memcpy(fname, filename, i - filename);
	strcpy(fname + (i - filename), ".mp3");
	if (file->open(fname)) {
		_compressedFileMode = true;
		_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, _compressedFileSoundHandle, Audio::makeMP3Stream(file, DisposeAfterUse::YES));
		return;
	}
#endif
	delete file;
}

void SmushPlayer::pause() {
	if (!_paused) {
		_paused = true;
		_pauseStartTime = _vm->_system->getMillis();
	}
}

void SmushPlayer::unpause() {
	if (_paused) {
		_paused = false;
		_pauseTime += (_vm->_system->getMillis() - _pauseStartTime);
		_pauseStartTime = 0;
	}
}

void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 startFrame) {
	// Verify the specified file exists
	ScummFile f;
	_vm->openFile(f, filename);
	if (!f.isOpen()) {
		warning("SmushPlayer::play() File not found %s", filename);
		return;
	}
	f.close();

	_updateNeeded = false;
	_warpNeeded = false;
	_palDirtyMin = 256;
	_palDirtyMax = -1;

	// Hide mouse
	bool oldMouseState = CursorMan.showMouse(false);

	// Load the video
	_seekFile = filename;
	_seekPos = offset;
	_seekFrame = startFrame;
	_base = 0;

	setupAnim(filename);
	init(speed);

	_startTime = _vm->_system->getMillis();
	_startFrame = startFrame;
	_frame = startFrame;

	_pauseTime = 0;

	int skipped = 0;

	for (;;) {
		uint32 now, elapsed;
		bool skipFrame = false;

		if (_insanity) {
			// Seeking makes a mess of trying to sync the audio to
			// the sound. Synt to time instead.
			now = _vm->_system->getMillis() - _pauseTime;
			elapsed = now - _startTime;
		} else if (_vm->_mixer->isSoundHandleActive(*_compressedFileSoundHandle)) {
			// Compressed SMUSH files.
			elapsed = _vm->_mixer->getSoundElapsedTime(*_compressedFileSoundHandle);
		} else if (_vm->_mixer->isSoundHandleActive(*_IACTchannel)) {
			// Curse of Monkey Island SMUSH files.
			elapsed = _vm->_mixer->getSoundElapsedTime(*_IACTchannel);
		} else {
			// For other SMUSH files, we don't necessarily have any
			// one channel to sync against, so we have to use
			// elapsed real time.
			now = _vm->_system->getMillis() - _pauseTime;
			elapsed = now - _startTime;
		}

		if (elapsed >= ((_frame - _startFrame) * 1000) / _speed) {
			if (elapsed >= ((_frame + 1) * 1000) / _speed)
				skipFrame = true;
			else
				skipFrame = false;
			timerCallback();
		}

		_vm->scummLoop_handleSound();

		if (_warpNeeded) {
			_vm->_system->warpMouse(_warpX, _warpY);
			_warpNeeded = false;
		}
		_vm->parseEvents();
		_vm->processInput();
		if (_palDirtyMax >= _palDirtyMin) {
			_vm->_system->getPaletteManager()->setPalette(_pal + _palDirtyMin * 3, _palDirtyMin, _palDirtyMax - _palDirtyMin + 1);

			_palDirtyMax = -1;
			_palDirtyMin = 256;
			skipFrame = false;
		}
		if (skipFrame) {
			if (++skipped > 10) {
				skipFrame = false;
				skipped = 0;
			}
		} else
			skipped = 0;
		if (_updateNeeded) {
			if (!skipFrame) {
				// Workaround for bug #2415: "FT DEMO: assertion triggered
				// when playing movie". Some frames there are 384 x 224
				int w = MIN(_width, _vm->_screenWidth);
				int h = MIN(_height, _vm->_screenHeight);

				_vm->_system->copyRectToScreen(_dst, _width, 0, 0, w, h);
				_vm->_system->updateScreen();
				_updateNeeded = false;
			}
		}
		if (_endOfFile)
			break;
		if (_vm->shouldQuit() || _vm->_saveLoadFlag || _vm->_smushVideoShouldFinish) {
			_vm->_mixer->stopHandle(*_compressedFileSoundHandle);
			_vm->_mixer->stopHandle(*_IACTchannel);
			_IACTpos = 0;

			resetAudioTracks(); // For DIG demo
			_imuseDigital->stopSMUSHAudio(); // For DIG & COMI
			break;
		}
		_vm->_system->delayMillis(10);
	}

	release();

	// Reset mouse state
	CursorMan.showMouse(oldMouseState);
}

void SmushPlayer::initAudio(int samplerate, int32 maxChunkSize) {
	int32 maxSizes[SMUSH_MAX_TRACKS] = {100000, 100000, 100000, 400000};

	_imuseDigital->setSmushPlayer(this);

	// DIG demo uses this audio system but doesn't use INSANE
	if (_insane)
		_insane->setSmushPlayer(this);

	setGainReductionParams(114, 2048);

	memset(_smushAudioTable, 0, sizeof(_smushAudioTable));

	for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
		_smushTrackVols[i] = 127;
		_smushTrackFlags[i] = 1;
		addAudioTrack(maxSizes[i], maxChunkSize);
	}

	_smushAudioSampleRate = samplerate;
	_smushAudioInitialized = true;
	_smushAudioCallbackEnabled = true;

	resetAudioTracks();
}

void SmushPlayer::terminateAudio() {
	if (_smushAudioInitialized) {
		_smushAudioInitialized = false;
		_smushAudioCallbackEnabled = false;
	}

	for (int i = 0; i < _smushNumTracks; i++) {
		free(_smushTracks[i].blockPtr);
		free(_smushTracks[i].fadeBuf);
	}
	_smushNumTracks = 0;
}

int SmushPlayer::isChanActive(int flagId) {
	return _smushTrackFlags[flagId];
}

int SmushPlayer::setChanFlag(int id, int flagVal) {
	if (id >= 0 && id <= _smushNumTracks)
		_smushTrackFlags[id] = flagVal;

	return id;
}

int SmushPlayer::addAudioTrack(int32 trackBlockSize, int32 maxBlockSize) {
	int id = _smushNumTracks;
	_smushTracks[id].state = TRK_STATE_INACTIVE;
	_smushTracks[id].audioRemaining = 0;
	_smushTracks[id].flags = 0;
	_smushTracks[id].groupId = GRP_MASTER;
	_smushTracks[id].blockSize = trackBlockSize;
	_smushTracks[id].parsedChunks = 0;

	_smushTracks[id].fadeBuf = (uint8 *)malloc(SMUSH_FADE_SIZE);
	if (!_smushTracks[id].fadeBuf)
		return -1;

	_smushTracks[id].blockPtr = (uint8 *)malloc(_smushTracks[id].blockSize);
	if (!_smushTracks[id].blockPtr)
		return -1;

	memset(_smushTracks[id].blockPtr, 127, _smushTracks[id].blockSize);
	// Track the effective number of tracks, so that if only one fails,
	// the others can carry on as they should
	_smushNumTracks++;

	return 0;
}

void SmushPlayer::resetAudioTracks() {
	for (int i = 0; i < _smushNumTracks; i++) {
		_smushTracks[i].state = TRK_STATE_INACTIVE;
		_smushTracks[i].groupId = GRP_MASTER;
	}
}

void SmushPlayer::setGainReductionParams(int16 gainReductionLowerBound, int16 gainReductionMultiplier) {
	if (gainReductionLowerBound)
		_gainReductionLowerBound = gainReductionLowerBound;
	if (gainReductionMultiplier)
		_gainReductionFactor = gainReductionMultiplier;
}

void SmushPlayer::setGroupVolume(int groupId, int volValue) {
	switch (groupId) {
	case GRP_MASTER:
		_smushTrackVols[0] = volValue; // Master
		break;
	case GRP_SFX:
		_smushTrackVols[1] = volValue; // Sfx
		break;
	case GRP_BKGMUS:
		_smushTrackVols[3] = volValue; // Music
		break;
	case GRP_SPEECH:
		_smushTrackVols[2] = volValue; // Voice
		break;
	default: // Internal groups
		for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
			if (_smushTracks[i].groupId == groupId)
				_smushTracks[i].volume = volValue;
		}
	}
}

void SmushPlayer::handleSAUDChunk(uint8 *srcBuf, uint32 size, int groupId, int vol, int pan, int16 flags, int trkId, int index, int maxFrames) {
	int targetTrk, diff1, diff2;
	uint32 dataSize, mod;
	int32 maxBlockSize, saudSize;
	uint16 flagsAccumulator[SMUSH_MAX_TRACKS];

	if (!index) {
		saudSize = READ_BE_UINT32(&srcBuf[4]);

		// Find the maximum block sizes between the current ones
		maxBlockSize = 0;
		for (int i = 0; i < _smushNumTracks; i++) {
			if (_smushTracks[i].blockSize > maxBlockSize) {
				maxBlockSize = _smushTracks[i].blockSize;
			}
		}

		diff2 = 10000000;
		if ((flags & TRK_TYPE_MASK) != IS_BKG_MUSIC) {
			for (int i = 0; i < _smushNumTracks; i++) {
				if (_smushTracks[i].blockSize - saudSize >= 0) {
					diff1 = _smushTracks[i].blockSize - saudSize;
				} else {
					diff1 = saudSize - _smushTracks[i].blockSize;
				}

				if (diff1 < diff2 && saudSize < 3 * _smushTracks[i].blockSize / 2) {
					if (_smushTracks[i].blockSize - saudSize >= 0) {
						diff2 = _smushTracks[i].blockSize - saudSize;
					} else {
						diff2 = saudSize - _smushTracks[i].blockSize;
					}

					maxBlockSize = _smushTracks[i].blockSize;
				}
			}
		}

		for (int i = 0; i < _smushNumTracks; i++) {
			flagsAccumulator[i] = 0;
			if (_smushTracks[i].blockSize == maxBlockSize) {
				if (_smushTracks[i].state == TRK_STATE_INACTIVE ||
					_smushTracks[i].state == TRK_STATE_ENDING   ||
					_smushTracks[i].flags <= flags) {
					if (_smushTracks[i].state == TRK_STATE_INACTIVE)
						flagsAccumulator[i] = 0x1000;

					if (_smushTracks[i].state == TRK_STATE_ENDING)
						flagsAccumulator[i] += 0x200;

					if (_smushTracks[i].flags == flags)
						flagsAccumulator[i] += 0x400;

					if (_smushTracks[i].flags < flags)
						flagsAccumulator[i] += 0x800;

					flagsAccumulator[i] += _smushTracks[i].parsedChunks + 1;
				}
			}
		}
		fillAudioTrackInfo(srcBuf, flagsAccumulator, size, groupId, vol, pan, flags, trkId, index, maxFrames);
		return;
	}

	targetTrk = -1;
	for (int i = 0; i < _smushNumTracks; i++) {
		if (_smushTracks[i].state != TRK_STATE_INACTIVE &&
			trkId == _smushTrackIds[i] &&
			index == (_smushTrackIdxs[i] + 1) &&
			maxFrames == _smushMaxFrames[i]) {
			targetTrk = i;
			break;
		}
	}

	if (targetTrk != -1) {
		_smushTrackIdxs[targetTrk]++;
		dataSize = _smushTracks[targetTrk].dataSize;
		mod = _smushTracks[targetTrk].availableSize % dataSize;

		if (mod + size <= dataSize) {
			memcpy(&_smushTracks[targetTrk].subChunkPtr[mod], srcBuf, size);
		} else {
			memcpy(&_smushTracks[targetTrk].subChunkPtr[mod], srcBuf, dataSize - mod);
			memcpy(
				_smushTracks[targetTrk].subChunkPtr,
				&srcBuf[_smushTracks[targetTrk].dataSize - mod],
				size + mod - _smushTracks[targetTrk].dataSize);
		}

		if (vol >= 0 && vol < 128)
			_smushTracks[targetTrk].volume = vol;

		if (pan > -128 && pan < 128)
			_smushTracks[targetTrk].pan = pan;

		_smushTracks[targetTrk].availableSize += size;
	}
}

void SmushPlayer::fillAudioTrackInfo(uint8 *srcBuf, uint16 *flagsAccumulator, uint32 size, int groupId, int vol, int pan, int16 flags, int trkId, int index, int maxFrames) {
	uint32 sdatSize, subChunkOffset, chunkSize;

	int maxFlagAcc = -1;
	int maxAccId = -1;

	for (int i = 0; i < _smushNumTracks; i++) {
		if (flagsAccumulator[i] && flagsAccumulator[i] > maxFlagAcc) {
			maxFlagAcc = flagsAccumulator[i];
			maxAccId = i;
		}
	}

	if (maxAccId != -1) {
		for (int i = 0; i < _smushNumTracks; i++) {
			if (_smushTracks[i].parsedChunks < 255) {
				_smushTracks[i].parsedChunks++;
			}
		}

		_smushTracks[maxAccId].parsedChunks = 0;
		_smushTracks[maxAccId].state = TRK_STATE_INACTIVE;
		_smushTrackIds[maxAccId] = trkId;
		_smushTrackIdxs[maxAccId] = 0;
		_smushMaxFrames[maxAccId] = maxFrames;
		subChunkOffset = READ_BE_UINT32(&srcBuf[12]);
		sdatSize = READ_BE_UINT32(&srcBuf[subChunkOffset + 20]);

		chunkSize = _smushTracks[maxAccId].blockSize;

		if (size < chunkSize)
			chunkSize = size;

		memset(_smushTracks[maxAccId].blockPtr, 127, _smushTracks[maxAccId].blockSize);
		memcpy(_smushTracks[maxAccId].blockPtr, srcBuf, chunkSize);
		_smushTracks[maxAccId].dataBuf = _smushTracks[maxAccId].blockPtr + 16;
		_smushTracks[maxAccId].dataSize = _smushTracks[maxAccId].blockSize - subChunkOffset - 24;
		_smushTracks[maxAccId].subChunkPtr = &_smushTracks[maxAccId].dataBuf[subChunkOffset + 8];
		_smushTracks[maxAccId].availableSize = size - subChunkOffset - 24;
		_smushTracks[maxAccId].sdatSize = sdatSize;
		_smushTracks[maxAccId].groupId = groupId;
		_smushTracks[maxAccId].volume = 127;
		_smushTracks[maxAccId].pan = 0;

		if (vol >= 0 && vol < 128)
			_smushTracks[maxAccId].volume = vol;

		if (pan > -128 && pan < 128)
			_smushTracks[maxAccId].pan = pan;

		_smushTracks[maxAccId].flags = flags;
		_smushTracks[maxAccId].audioRemaining = 0;
		_smushTracks[maxAccId].state = TRK_STATE_FADING;
	}

	return;
}

void SmushPlayer::processDispatches(int16 feedSize) {
	int32 fadeStartOffset, cpySize, fadeRemaining, fadeFeedSize, mixFeedSize, mixInFrameCount;
	int32 offset, tmpFeedSize, maxFadeChunkSize;
	int16 fadeInFrameCount, flags, fadeMixStartingPoint;

	int fadePan, fadeVolume, mixPan, mixVolume, baseVolume, mixStartingPoint;

	bool isPlayableTrack;
	bool speechIsPlaying = false;

	if (!_paused) {
		if (_smushTracksNeedInit) {
			_smushTracksNeedInit = false;
			for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
				_smushDispatch[i].fadeRemaining = 0;
				_smushDispatch[i].audioLength = 0;
			}
		}

		for (int i = 0; i < _smushNumTracks; i++) {
			isPlayableTrack = ((_smushTracks[i].flags & TRK_TYPE_MASK) == IS_SPEECH    && isChanActive(CHN_SPEECH)) ||
							  ((_smushTracks[i].flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && isChanActive(CHN_BKGMUS)) ||
							  ((_smushTracks[i].flags & TRK_TYPE_MASK) == IS_SFX       && isChanActive(CHN_OTHER));
			flags = _smushTracks[i].flags;

			switch (flags & TRK_TYPE_MASK) {
			case IS_SFX:
				baseVolume = (_smushTrackVols[1] * _smushTracks[i].volume) >> 7;
				break;
			case IS_BKG_MUSIC:
				baseVolume = (_smushTrackVols[3] * _smushTracks[i].volume) >> 7;
				break;
			case IS_SPEECH:
				baseVolume = (_smushTrackVols[2] * _smushTracks[i].volume) >> 7;
				break;
			default:
				error("SmushPlayer::processDispatches(): unrecognized flag %d", _smushTracks[i].flags & TRK_TYPE_MASK);
			};

			mixVolume = baseVolume * _smushTrackVols[0] / 127;
			if ((flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && isChanActive(CHN_SPEECH))
				mixVolume = ((baseVolume * _smushTrackVols[0] / 127) * _gainReductionMultiplier) >> 8;

			// Check if there's the need to allocate a crossfade
			if (_smushTracks[i].state == TRK_STATE_FADING && _smushDispatch[i].state == TRK_STATE_PLAYING) {
				fadeStartOffset = _smushDispatch[i].audioRemaining % _smushDispatch[i].dataSize;
				_smushDispatch[i].fadeRemaining = SMUSH_FADE_SIZE;
				_smushDispatch[i].fadeVolume = _smushTracks[i].volume;
				_smushDispatch[i].fadeSampleRate = _smushDispatch[i].sampleRate;

				memset(_smushTracks[i].fadeBuf, 127, _smushDispatch[i].fadeRemaining);
				cpySize = _smushDispatch[i].dataSize - fadeStartOffset;

				if (cpySize > _smushDispatch[i].fadeRemaining)
					cpySize = _smushDispatch[i].fadeRemaining;
				memcpy(_smushTracks[i].fadeBuf, &_smushDispatch[i].dataBuf[fadeStartOffset], cpySize);

				_smushDispatch[i].volumeStep = 0;
			} else if (_smushTracks[i].state == TRK_STATE_PLAYING) {
				if (_smushDispatch[i].audioRemaining < _smushTracks[i].availableSize - _smushTracks[i].dataSize + 15000) {
					if (_smushTracks[i].availableSize < _smushTracks[i].sdatSize) {
						_smushDispatch[i].volumeStep = 0;
						mixInFrameCount = _smushTracks[i].availableSize - _smushDispatch[i].audioRemaining - 15000;

						if (mixInFrameCount > _smushDispatch[i].currentOffset)
							mixInFrameCount = _smushDispatch[i].currentOffset;

						if (_smushDispatch[i].audioRemaining + mixInFrameCount > _smushTracks[i].sdatSize - _smushDispatch[i].dataSize)
							mixInFrameCount = _smushTracks[i].sdatSize - _smushDispatch[i].dataSize - _smushDispatch[i].audioRemaining;

						if (mixInFrameCount > 0) {
							_smushDispatch[i].fadeRemaining = SMUSH_FADE_SIZE;
							_smushDispatch[i].fadeSampleRate = _smushDispatch[i].sampleRate;

							memcpy(
								_smushTracks[i].fadeBuf,
								&_smushDispatch[i].dataBuf[_smushDispatch[i].audioRemaining % _smushDispatch[i].dataSize],
								_smushDispatch[i].fadeRemaining);

							_smushDispatch[i].audioRemaining += mixInFrameCount;
							_smushDispatch[i].currentOffset -= mixInFrameCount;
						}
					}
				}
			}

			// If the fade has been allocated, flush it in the mixer
			if (_smushDispatch[i].fadeRemaining) {
				maxFadeChunkSize = _smushDispatch[i].fadeSampleRate * feedSize / _smushAudioSampleRate;

				if (_smushDispatch[i].fadeRemaining > maxFadeChunkSize) {
					fadeRemaining = maxFadeChunkSize;
				} else {
					fadeRemaining = _smushDispatch[i].fadeRemaining;
				}

				fadeMixStartingPoint = 0;
				while (fadeRemaining) {
					fadeInFrameCount = (fadeRemaining < DIMUSE_FEEDSIZE / 2) ? fadeRemaining : DIMUSE_FEEDSIZE / 2;

					if (fadeInFrameCount == maxFadeChunkSize) {
						fadeFeedSize = feedSize;
					} else {
						fadeFeedSize = _smushAudioSampleRate * fadeInFrameCount / _smushDispatch[i].fadeSampleRate;
					}

					if (isPlayableTrack) {
						fadeVolume = _smushDispatch[i].fadeRemaining * _smushDispatch[i].fadeVolume * _smushTrackVols[0] / (SMUSH_FADE_SIZE * 127);
						fadePan = _smushTracks[i].pan;

						sendAudioToDiMUSE(
							&_smushTracks[i].fadeBuf[SMUSH_FADE_SIZE - _smushDispatch[i].fadeRemaining],
							fadeMixStartingPoint,
							fadeFeedSize,
							fadeInFrameCount,
							fadeVolume,
							fadePan);
					}

					fadeMixStartingPoint += fadeFeedSize;
					fadeRemaining -= fadeInFrameCount;
					_smushDispatch[i].fadeRemaining -= fadeInFrameCount;
				}
			}

			if (_smushTracks[i].state == TRK_STATE_FADING) {
				if (_smushDispatch[i].state != TRK_STATE_PLAYING)
					_smushDispatch[i].volumeStep = 16;

				_smushDispatch[i].headerPtr = _smushTracks[i].dataBuf;
				_smushDispatch[i].dataBuf = _smushTracks[i].subChunkPtr;
				_smushDispatch[i].dataSize = _smushTracks[i].dataSize;
				_smushDispatch[i].currentOffset = 0;
				_smushDispatch[i].audioLength = 0;
				_smushTracks[i].state = TRK_STATE_PLAYING;
			}

			if (_smushTracks[i].state != TRK_STATE_INACTIVE) {
				tmpFeedSize = feedSize;
				mixStartingPoint = 0;
				if (feedSize > 0) {
					while (1) {
						mixInFrameCount = _smushDispatch[i].currentOffset;
						if (mixInFrameCount > 0) {
							offset = _smushDispatch[i].audioRemaining % _smushDispatch[i].dataSize;

							if (mixInFrameCount > _smushDispatch[i].sampleRate * tmpFeedSize / _smushAudioSampleRate)
								mixInFrameCount = _smushDispatch[i].sampleRate * tmpFeedSize / _smushAudioSampleRate;

							if (offset + mixInFrameCount > _smushDispatch[i].dataSize)
								mixInFrameCount = _smushDispatch[i].dataSize - offset;

							if (mixInFrameCount + _smushDispatch[i].audioRemaining <= _smushTracks[i].availableSize) {
								// Fade-in until full volume is reached
								if (_smushDispatch[i].volumeStep < 16)
									_smushDispatch[i].volumeStep++;

								if (mixInFrameCount > DIMUSE_FEEDSIZE / 2)
									mixInFrameCount = DIMUSE_FEEDSIZE / 2;

								_smushTracks[i].state = TRK_STATE_PLAYING;

								// This flag is toggled one time per chunk: if it's yields "true" for
								// even one track, it stays like that for the whole chunk
								speechIsPlaying = !speechIsPlaying ? (_smushTracks[i].flags & TRK_TYPE_MASK) == IS_SPEECH : true;
							} else {
								// Fade-out until silent
								if (_smushDispatch[i].volumeStep)
									_smushDispatch[i].volumeStep--;

								_smushTracks[i].state = TRK_STATE_ENDING;

								if (mixInFrameCount > DIMUSE_FEEDSIZE / 2)
									mixInFrameCount = DIMUSE_FEEDSIZE / 2;

								_smushDispatch[i].audioRemaining -= mixInFrameCount;
								_smushDispatch[i].currentOffset += mixInFrameCount;
								offset = _smushDispatch[i].audioRemaining % _smushDispatch[i].dataSize;
							}

							if (mixInFrameCount == _smushDispatch[i].sampleRate * tmpFeedSize / _smushAudioSampleRate) {
								mixFeedSize = tmpFeedSize;
							} else {
								mixFeedSize = mixInFrameCount * _smushAudioSampleRate / _smushDispatch[i].sampleRate;
							}

							if (isPlayableTrack) {
								mixPan = _smushTracks[i].pan;
								sendAudioToDiMUSE(
									&_smushDispatch[i].dataBuf[offset],
									mixStartingPoint,
									mixFeedSize,
									mixInFrameCount,
									(mixVolume * _smushDispatch[i].volumeStep) >> 4,
									mixPan);
							}

							_smushDispatch[i].currentOffset -= mixInFrameCount;
							_smushDispatch[i].audioRemaining += mixInFrameCount;
							tmpFeedSize -= mixFeedSize;
							mixStartingPoint += mixFeedSize;
						}

						if (_smushDispatch[i].currentOffset <= 0) {
							if (processAudioCodes(i, tmpFeedSize, mixVolume) && tmpFeedSize <= 0) {
								break;
							}
						} else if (tmpFeedSize <= 0) {
							break;
						}
					}
				}
			}

			_smushTracks[i].audioRemaining = _smushDispatch[i].audioRemaining;
			_smushDispatch[i].state = _smushTracks[i].state;
		};

		if (speechIsPlaying) {
			if (_gainReductionMultiplier > _gainReductionLowerBound) {
				_gainReductionMultiplier -= (feedSize * 2 * _gainReductionFactor) >> 13;
				if (_gainReductionMultiplier < _gainReductionLowerBound)
					_gainReductionMultiplier = _gainReductionLowerBound;
			}
		} else {
			if (_gainReductionMultiplier < 256) {
				_gainReductionMultiplier += (feedSize * 2 * _gainReductionFactor) >> 15;
				if (_gainReductionMultiplier > 256)
					_gainReductionMultiplier = 256;
			}
		}
	}
}

bool SmushPlayer::processAudioCodes(int idx, int32 &tmpFeedSize, int &mixVolume) {
	uint8 *code, *buf, subcode, value;
	int chunk;

	while (tmpFeedSize) {
		code = _smushDispatch[idx].headerPtr;

		switch (code[0]) {
		case 1: // Init
			_smushDispatch[idx].audioLength = 0;
			buf = _smushDispatch[idx].headerPtr;
			_smushDispatch[idx].audioRemaining = READ_BE_UINT32(buf + 2);
			_smushDispatch[idx].currentOffset = READ_BE_UINT32(buf + 6);
			_smushDispatch[idx].sampleRate = _smushAudioSampleRate;
			_smushDispatch[idx].headerPtr += _smushDispatch[idx].headerPtr[1] + 2;
			if (_smushDispatch[idx].audioRemaining < _smushTracks[idx].availableSize + (_smushTracks[idx].availableSize >= _smushTracks[idx].sdatSize ? 0 : 15000) - _smushTracks[idx].dataSize) {
				chunk = _smushTracks[idx].availableSize - _smushTracks[idx].dataSize - _smushDispatch[idx].audioRemaining + 15000;
				if (chunk > _smushDispatch[idx].currentOffset) {
					_smushTracks[idx].state = TRK_STATE_INACTIVE;
					_smushTracks[idx].groupId = GRP_MASTER;
					tmpFeedSize = 0;
					break;
				}

				_smushDispatch[idx].audioRemaining += chunk;
				_smushDispatch[idx].currentOffset -= chunk;
			}
			break;
		case 2:   //
		case 8:	  //
		case 9:	  // Compare params
		case 0xA: //
		case 0xB: //
			subcode = code[4];
			switch (subcode) {
			case 0xFF:
				value = _smushTrackVols[0];
				break;
			case 0xFE:
				value = _smushTracks[idx].volume;
				break;
			case 0xFD:
				value = _smushTracks[idx].pan;
				break;
			default:
				value = _smushAudioTable[subcode];
				break;
			}

			switch (code[0]) {
			case 2:
				if (value || (subcode == 0)) {
					_smushDispatch[idx].headerPtr = &code[READ_BE_UINT16(&code[2])];
				}
				break;
			case 8:
				value = value > code[5];
				break;
			case 9:
				value = value < code[5];
				break;
			case 0xA:
				value = value == code[5];
				break;
			case 0xB:
				value = value != code[5];
				break;
			default:
				break;
			}

			if (!value) {
				_smushDispatch[idx].headerPtr = &code[code[1] + 2];
			} else {
				_smushDispatch[idx].headerPtr = &code[READ_BE_UINT16(&code[2])];
			}

			break;
		case 3: // Set params
			switch (code[2]) {
			case 0xFF:
				_smushTrackVols[0] = code[3];
				break;
			case 0xFE:
				_smushTracks[idx].volume = code[3];
				mixVolume = (_smushTrackVols[0] * _smushTracks[idx].volume) / 127;

				// Set a lower mix volume of the background music if speech is active
				if ((_smushTracks[idx].flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && isChanActive(CHN_SPEECH))
					mixVolume = (mixVolume * _gainReductionMultiplier) >> 8;
				break;
			case 0xFD:
				_smushTracks[idx].pan = code[3];
				break;
			default:
				_smushAudioTable[code[2]] = code[3];
				break;
			}
			_smushDispatch[idx].headerPtr = &code[code[1] + 2];
			break;
		case 4: // Increment params
			switch (code[2]) {
			case 0xFF:
				_smushTrackVols[0] += code[3];
				break;
			case 0xFE:
				_smushTracks[idx].volume += code[3];
				break;
			case 0xFD:
				_smushTracks[idx].pan += code[3];
				break;
			default:
				_smushAudioTable[code[2]] += code[3];
				break;
			}
			_smushDispatch[idx].headerPtr = &code[code[1] + 2];
			break;
		case 6: // Set offset
			_smushDispatch[idx].audioLength = 0;
			buf = _smushDispatch[idx].headerPtr;
			_smushDispatch[idx].audioRemaining = READ_BE_UINT32(buf + 2);
			_smushDispatch[idx].currentOffset = READ_BE_UINT32(buf + 6);
			_smushDispatch[idx].sampleRate = _smushAudioSampleRate;

			_smushDispatch[idx].headerPtr += _smushDispatch[idx].headerPtr[1] + 2;
			if (_smushDispatch[idx].audioRemaining < _smushTracks[idx].availableSize + (_smushTracks[idx].availableSize >= _smushTracks[idx].sdatSize ? 0 : 15000) - _smushTracks[idx].dataSize) {
				chunk = _smushTracks[idx].availableSize - _smushTracks[idx].dataSize - _smushDispatch[idx].audioRemaining + 15000;
				if (chunk > _smushDispatch[idx].currentOffset) {
					_smushTracks[idx].state = TRK_STATE_INACTIVE;
					_smushTracks[idx].groupId = GRP_MASTER;
					tmpFeedSize = 0;
					break;
				}

				_smushDispatch[idx].audioRemaining += chunk;
				_smushDispatch[idx].currentOffset -= chunk;
			}
			break;
		case 7: // Set audio length
			if (!_smushDispatch[idx].audioLength) {
				_smushDispatch[idx].audioLength = READ_BE_UINT32(&code[6]);
				_smushDispatch[idx].elapsedAudio = 0;
			}

			buf = _smushDispatch[idx].headerPtr;
			_smushDispatch[idx].audioRemaining = _smushDispatch[idx].elapsedAudio + READ_BE_UINT32(buf + 2);

			_smushDispatch[idx].currentOffset = READ_BE_UINT32(buf + 14);
			if (_smushDispatch[idx].currentOffset > _smushDispatch[idx].audioLength)
				_smushDispatch[idx].currentOffset = _smushDispatch[idx].audioLength;

			_smushDispatch[idx].sampleRate = _smushAudioSampleRate;

			_smushDispatch[idx].audioLength -= _smushDispatch[idx].currentOffset;
			_smushDispatch[idx].elapsedAudio += _smushDispatch[idx].currentOffset;

			if (_smushDispatch[idx].audioLength) {
				_smushDispatch[idx].headerPtr = &code[code[1] + 2];
			} else {
				_smushDispatch[idx].headerPtr = &code[READ_BE_UINT16(&code[18])];
			}

			if (_smushDispatch[idx].audioRemaining >= _smushTracks[idx].availableSize + (_smushTracks[idx].availableSize >= _smushTracks[idx].sdatSize ? 0 : 15000) - _smushTracks[idx].dataSize) {
				chunk = _smushTracks[idx].availableSize - _smushTracks[idx].dataSize - _smushDispatch[idx].audioRemaining + 15000;
				if (chunk > _smushDispatch[idx].currentOffset) {
					_smushTracks[idx].state = TRK_STATE_INACTIVE;
					_smushTracks[idx].groupId = GRP_MASTER;
					tmpFeedSize = 0;
				} else {
					_smushDispatch[idx].audioRemaining += chunk;
					_smushDispatch[idx].currentOffset -= chunk;
				}
			}

			break;
		default:
			_smushTracks[idx].state = TRK_STATE_INACTIVE;
			_smushTracks[idx].groupId = GRP_MASTER;
			tmpFeedSize = 0;
		}

		if (_smushDispatch[idx].currentOffset > 0) {
			return false;
		}
	}

	return true;
}

void SmushPlayer::sendAudioToDiMUSE(uint8 *mixBuf, int32 mixStartingPoint, int32 mixFeedSize, int32 mixInFrameCount, int volume, int pan) {
	int clampedVol, clampedPan;
	bool is11025Hz = false;

	if (mixFeedSize == 2 * mixInFrameCount) {
		is11025Hz = true;
	} else if (mixFeedSize != mixInFrameCount) {
		return;
	}

	clampedPan = CLIP<int>((pan / 2) + 64, 0, 127);
	clampedVol = CLIP<int>(volume, 0, 127);
	_imuseDigital->receiveAudioFromSMUSH(mixBuf, mixInFrameCount, mixFeedSize, mixStartingPoint, clampedVol, clampedPan, is11025Hz);
}

void SmushPlayer::feedAudio(uint8 *srcBuf, int groupId, int volume, int pan, int16 flags) {
	int panDelta, effPan;
	int32 maxFrames;
	uint16 trkId, index;

	if (_smushAudioInitialized) {
		// Check file encoding
		if (srcBuf[8] == 0 && srcBuf[9] == 0 && srcBuf[12] == 0 && srcBuf[13] == 0 && srcBuf[16] == 0 && srcBuf[17] == 0) {
			trkId = READ_BE_INT16(&srcBuf[10]);
			index = READ_BE_INT16(&srcBuf[14]);
			maxFrames = READ_BE_INT16(&srcBuf[18]);

			handleSAUDChunk(
				srcBuf + 20,
				READ_BE_UINT32(&srcBuf[4]) - 12,
				groupId,
				volume,
				pan,
				flags,
				trkId,
				index,
				maxFrames);
		} else {
			trkId = READ_LE_INT16(&srcBuf[8]);
			index = READ_LE_INT16(&srcBuf[10]);
			maxFrames = READ_LE_INT16(&srcBuf[12]);
			flags |= READ_LE_INT16(&srcBuf[14]);
			volume = (volume * srcBuf[16]) >> 7;
			panDelta = srcBuf[17];

			if (panDelta == 128) {
				effPan = 128;
			} else {
				effPan = pan + panDelta;
			}

			handleSAUDChunk(
				srcBuf + 18,
				READ_BE_UINT32(&srcBuf[4]) - 10,
				groupId,
				(volume * srcBuf[16]) >> 7,
				effPan,
				flags,
				trkId,
				index,
				maxFrames);
		}
	}
}

bool SmushPlayer::isAudioCallbackEnabled() {
	return _smushAudioCallbackEnabled;
}

} // End of namespace Scumm
