/* 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/memstream.h"
#include "common/textconsole.h"

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

#include "backends/audiocd/audiocd.h"

#include "audio/audiostream.h"
#include "audio/mods/protracker.h"

namespace AGOS {

// This data is hardcoded in the executable.
const int AGOSEngine_Simon1::SIMON1_GMF_SIZE[] = {
	8900, 12166,  2848,  3442,  4034,  4508,  7064,  9730,  6014,  4742,
	3138,  6570,  5384,  8909,  6457, 16321,  2742,  8968,  4804,  8442,
	7717,  9444,  5800,  1381,  5660,  6684,  2456,  4744,  2455,  1177,
	1232, 17256,  5103,  8794,  4884,    16
};

// High nibble is the file ID (STINGSx.MUS), low nibble is the SFX number
// in the file (0 based).
const byte AGOSEngine::SIMON1_RHYTHM_SFX[] = {
	0x15, 0x16, 0x2C, 0x31, 0x37, 0x3A, 0x42, 0x43, 0x44,
	0x51, 0x55, 0x61, 0x68, 0x74, 0x78, 0x83, 0x89, 0x90
};

void AGOSEngine_Simon1::playSpeech(uint16 speech_id, uint16 vgaSpriteId) {
	if (speech_id == 9999) {
		if (_subtitles)
			return;
		if (!getBitFlag(14) && !getBitFlag(28)) {
			setBitFlag(14, true);
			_variableArray[100] = 15;
			animate(4, 1, 130, 0, 0, 0);
			waitForSync(130);
		}
		_skipVgaWait = true;
	} else {
		if (_subtitles && _scriptVar2) {
			animate(4, 2, 204, 0, 0, 0);
			waitForSync(204);
			stopAnimate(204);
		}
		if (vgaSpriteId < 100)
			stopAnimate(201 + vgaSpriteId);

		loadVoice(speech_id);

		if (vgaSpriteId < 100)
			animate(4, 2, 201 + vgaSpriteId, 0, 0, 0);
	}
}

void AGOSEngine_Simon2::playSpeech(uint16 speech_id, uint16 vgaSpriteId) {
	if (speech_id == 0xFFFF) {
		if (_subtitles)
			return;
		if (!getBitFlag(14) && !getBitFlag(28)) {
			setBitFlag(14, true);
			_variableArray[100] = 5;
			animate(4, 1, 30, 0, 0, 0);
			waitForSync(130);
		}
		_skipVgaWait = true;
	} else {
		if (getGameType() == GType_SIMON2 && _subtitles && _language != Common::HE_ISR) {
			loadVoice(speech_id);
			return;
		}

		if (_subtitles && _scriptVar2) {
			animate(4, 2, 5, 0, 0, 0);
			waitForSync(205);
			stopAnimateSimon2(2,5);
		}

		stopAnimateSimon2(2, vgaSpriteId + 2);
		loadVoice(speech_id);
		animate(4, 2, vgaSpriteId + 2, 0, 0, 0);
	}
}

void AGOSEngine::skipSpeech() {
	_sound->stopVoice();
	if (!getBitFlag(28)) {
		setBitFlag(14, true);
		if (getGameType() == GType_FF) {
			_variableArray[103] = 5;
			animate(4, 2, 13, 0, 0, 0);
			waitForSync(213);
			stopAnimateSimon2(2, 1);
		} else if (getGameType() == GType_SIMON2) {
			_variableArray[100] = 5;
			animate(4, 1, 30, 0, 0, 0);
			waitForSync(130);
			stopAnimateSimon2(2, 1);
		} else {
			_variableArray[100] = 15;
			animate(4, 1, 130, 0, 0, 0);
			waitForSync(130);
			stopAnimate(1);
		}
	}
}

void AGOSEngine::loadMusic(uint16 music, bool forceSimon2Gm) {
	stopMusic();

	uint16 indexBase = forceSimon2Gm ? MUSIC_INDEX_BASE_SIMON2_GM : _musicIndexBase;

	_gameFile->seek(_gameOffsetsPtr[indexBase + music - 1], SEEK_SET);
	_midi->load(_gameFile);

	// Activate Simon 2 GM to MT-32 remapping if we force GM, otherwise
	// deactivate it (in case it was previously activated).
	_midi->setSimon2Remapping(forceSimon2Gm);

	_lastMusicPlayed = music;
	_nextMusicToPlay = -1;
}

struct ModuleOffs {
	uint8 tune;
	uint8 fileNum;
	uint32 offs;
};

static const ModuleOffs amigaWaxworksOffs[20] = {
	// Pyramid
	{2,   2, 0,   },
	{3,   2, 50980},
	{4,   2, 56160},
	{5,   2, 62364},
	{6,   2, 73688},

	// Zombie
	{8,   8, 0},
	{11,  8, 51156},
	{12,  8, 56336},
	{13,  8, 65612},
	{14,  8, 68744},

	// Mine
	{9,   9, 0},
	{15,  9, 47244},
	{16,  9, 52424},
	{17,  9, 59652},
	{18,  9, 62784},

	// Jack
	{10, 10, 0},
	{19, 10, 42054},
	{20, 10, 47234},
	{21, 10, 49342},
	{22, 10, 51450},
};

void AGOSEngine::playModule(uint16 music) {
	char filename[15];
	Common::File f;
	uint32 offs = 0;

	if (getPlatform() == Common::kPlatformAmiga && getGameType() == GType_WW) {
		// Multiple tunes are stored in music files for main locations
		for (uint i = 0; i < 20; i++) {
			if (amigaWaxworksOffs[i].tune == music) {
				music = amigaWaxworksOffs[i].fileNum;
				offs = amigaWaxworksOffs[i].offs;
			}
		}
	}

	if (getGameType() == GType_ELVIRA1 && getFeatures() & GF_DEMO)
		sprintf(filename, "elvira2");
	else if (getPlatform() == Common::kPlatformAcorn)
		sprintf(filename, "%dtune.DAT", music);
	else
		sprintf(filename, "%dtune", music);

	f.open(filename);
	if (f.isOpen() == false) {
		error("playModule: Can't load module from '%s'", filename);
	}

	Audio::AudioStream *audioStream;
	if (!(getGameType() == GType_ELVIRA1 && getFeatures() & GF_DEMO) &&
		getFeatures() & GF_CRUNCHED) {

		uint32 srcSize = f.size();
		byte *srcBuf = (byte *)malloc(srcSize);
		if (f.read(srcBuf, srcSize) != srcSize)
			error("playModule: Read failed");

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

		Common::MemoryReadStream stream(dstBuf, dstSize);
		audioStream = Audio::makeProtrackerStream(&stream, offs);
		free(dstBuf);
	} else {
		audioStream = Audio::makeProtrackerStream(&f);
	}

	_mixer->playStream(Audio::Mixer::kMusicSoundType, &_modHandle, audioStream);
}

void AGOSEngine_Simon2::playMusic(uint16 music, uint16 track) {
	if (_lastMusicPlayed == 10 && getPlatform() == Common::kPlatformDOS && _midi->usesMT32Data()) {
		// WORKAROUND Simon 2 track 10 (played during the first intro scene)
		// consist of 3 subtracks. Subtracks 2 and 3 are missing from the MT-32
		// MIDI data. The original interpreter just stops playing after track 1
		// and does not restart until the next scene.
		// We fix this by loading the GM version of track 10 and remapping the
		// instruments to MT-32.

		// Reload track 10 and force GM for all subtracks but the first (this
		// also activates the instrument remapping).
		loadMusic(10, track > 0);
	}

	_midi->play(track);
}

void AGOSEngine_Simon1::playMusic(uint16 music, uint16 track) {
	stopMusic();

	if (getPlatform() != Common::kPlatformAmiga && (getFeatures() & GF_TALKIE) && music == 35) {
		// WORKAROUND: For a script bug in the CD versions
		// We skip this music resource, as it was replaced by
		// a sound effect, and the script was never updated.
		return;
	}

	// Support for compressed music from the ScummVM Music Enhancement Project
	_system->getAudioCDManager()->stop();
	_system->getAudioCDManager()->play(music + 1, -1, 0, 0, true);
	if (_system->getAudioCDManager()->isPlaying())
		return;

	if (getPlatform() == Common::kPlatformAmiga) {
		playModule(music);
	} else if ((getPlatform() == Common::kPlatformDOS || getPlatform() == Common::kPlatformAcorn) &&
			getFeatures() & GF_TALKIE) {
		// DOS CD and Acorn CD use the same music data.

		// Data is stored in one large data file and the GMF format does not
		// have an indication of size or end of data, so the data size has to
		// be supplied from a hardcoded list.
		int size = SIMON1_GMF_SIZE[music];

		_gameFile->seek(_gameOffsetsPtr[_musicIndexBase + music], SEEK_SET);
		_midi->load(_gameFile, size);
		_midi->play();
	} else if (getPlatform() == Common::kPlatformDOS) {
		// DOS floppy version.

		// GMF music data is in separate MODxx.MUS files.
		char filename[15];
		Common::File f;
		sprintf(filename, "MOD%d.MUS", music);
		f.open(filename);
		if (f.isOpen() == false)
			error("playMusic: Can't load music from '%s'", filename);

		_midi->load(&f, f.size());
		if (getFeatures() & GF_DEMO) {
			// Full version music data has a loop flag in the file header, but
			// the demo needs to have this set manually.
			_midi->setLoop(true);
		}

		_midi->play();
	} else if (getPlatform() == Common::kPlatformWindows) {
		// Windows version uses SMF data in one large data file.
		_gameFile->seek(_gameOffsetsPtr[_musicIndexBase + music], SEEK_SET);

		_midi->load(_gameFile);
		_midi->setLoop(true);

		_midi->play();
	} else if (getPlatform() == Common::kPlatformAcorn) {
		// Acorn floppy version.
		
		// TODO: Add support for Desktop Tracker format in Acorn disk version
	}
}

void AGOSEngine_Simon1::playMidiSfx(uint16 sound) {
	// The sound effects in floppy disk version of
	// Simon the Sorcerer 1 are only meant for AdLib
	if (!_midi->hasMidiSfx())
		return;

	// AdLib SFX use GMF data bundled in 9 STINGSx.MUS files.
	char filename[16];
	Common::File mus_file;

	sprintf(filename, "STINGS%i.MUS", _soundFileId);
	mus_file.open(filename);
	if (!mus_file.isOpen())
		error("playSting: Can't load sound effect from '%s'", filename);

	// WORKAROUND Some Simon 1 DOS floppy SFX use the OPL rhythm instruments.
	// This can conflict with the music using the rhythm instruments, so the
	// original interpreter disables the music rhythm notes while a sound
	// effect is playing. However, only some sound effects use rhythm notes, so
	// in many cases this is not needed and leads to the music drums needlessly
	// being disabled.
	// To improve this, the sound effect number is checked against a list of
	// SFX using rhythm notes, and only if it is in the list the music drums
	// will be disabled while it plays.
	bool rhythmSfx = false;
	// Search for the file ID / SFX ID combination in the list of SFX that use
	// rhythm notes.
	byte sfxId = (_soundFileId << 4) | sound;
	for (int i = 0; i < ARRAYSIZE(SIMON1_RHYTHM_SFX); i++) {
		if (SIMON1_RHYTHM_SFX[i] == sfxId) {
			rhythmSfx = true;
			break;
		}
	}

	_midi->stop(true);

	_midi->load(&mus_file, mus_file.size(), true);
	_midi->play(sound, true, rhythmSfx);
}

void AGOSEngine::playMusic(uint16 music, uint16 track) {
	stopMusic();

	if (getPlatform() == Common::kPlatformAmiga) {
		playModule(music);
	} else if (getPlatform() == Common::kPlatformAtariST) {
		// TODO: Add support for music formats used
	} else {
		_midi->setLoop(true); // Must do this BEFORE loading music.

		Common::SeekableReadStream *str = nullptr;
		if (getPlatform() == Common::kPlatformPC98) {
			str = createPak98FileStream(Common::String::format("MOD%d.PAK", music).c_str());
			if (!str)
				error("playMusic: Can't load music from 'MOD%d.PAK'", music);
		} else {
			Common::File *file = new Common::File();
			if (!file->open(Common::String::format("MOD%d.MUS", music)))
				error("playMusic: Can't load music from 'MOD%d.MUS'", music);
			str = file;
		}

		//warning("Playing track %d", music);
		_midi->load(str);
		_midi->play();
		delete str;
	}
}

void AGOSEngine::stopMusic() {
	if (_midiEnabled) {
		_midi->stop();
	}
	_mixer->stopHandle(_modHandle);
}

static const byte elvira1_soundTable[100] = {
	0, 2, 0, 1, 0, 0, 0, 0, 0, 3,
	0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
	0, 0, 0, 0, 6, 4, 0, 0, 9, 0,
	0, 2, 0, 0, 0, 0, 0, 0, 0, 0,
	0, 8, 0, 0, 0, 0, 0, 0, 0, 0,
	1, 1, 0, 0, 5, 0, 6, 6, 0, 0,
	0, 5, 0, 0, 6, 0, 0, 0, 0, 8,
	0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
	0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};

bool AGOSEngine::loadVGASoundFile(uint16 id, uint8 type) {
	Common::File in;
	char filename[15];
	byte *dst;
	uint32 srcSize, dstSize;

	if (getPlatform() == Common::kPlatformAmiga || getPlatform() == Common::kPlatformAtariST) {
		if (getGameType() == GType_ELVIRA1 && (getFeatures() & GF_DEMO) &&
			getPlatform() == Common::kPlatformAmiga) {
			sprintf(filename, "%c%d.out", 48 + id, type);
		} else if (getGameType() == GType_ELVIRA1 || getGameType() == GType_ELVIRA2) {
			sprintf(filename, "%.2d%d.out", id, type);
		} else if (getGameType() == GType_PN) {
			sprintf(filename, "%c%d.in", id + 48, type);
		} else {
			sprintf(filename, "%.3d%d.out", id, type);
		}
	} else {
		if (getGameType() == GType_ELVIRA1) {
			if (elvira1_soundTable[id] == 0)
				return false;

			sprintf(filename, "%.2d.SND", elvira1_soundTable[id]);
		} else if (getGameType() == GType_ELVIRA2 || getGameType() == GType_WW) {
			sprintf(filename, "%.2d%d.VGA", id, type);
		} else if (getGameType() == GType_PN) {
			sprintf(filename, "%c%d.out", id + 48, type);
		} else {
			sprintf(filename, "%.3d%d.VGA", id, type);
		}
	}

	in.open(filename);
	if (in.isOpen() == false || in.size() == 0) {
		return false;
	}

	dstSize = srcSize = in.size();
	if (getGameType() == GType_PN && (getFeatures() & GF_CRUNCHED)) {
		Common::Stack<uint32> data;
		byte *dataOut = nullptr;
		int dataOutSize = 0;

		for (uint i = 0; i < srcSize / 4; ++i)
			data.push(in.readUint32BE());

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

		dstSize = READ_BE_UINT32(srcBuffer + srcSize - 4);
		dst = allocBlock (dstSize);
		decrunchFile(srcBuffer, dst, srcSize);
		free(srcBuffer);
	} else {
		dst = allocBlock(dstSize);
		if (in.read(dst, dstSize) != dstSize)
			error("loadVGASoundFile: Read failed");
	}
	in.close();

	return true;
}

static const char *const dimpSoundList[32] = {
	"Beep",
	"Birth",
	"Boiling",
	"Burp",
	"Cough",
	"Die1",
	"Die2",
	"Fart",
	"Inject",
	"Killchik",
	"Puke",
	"Lights",
	"Shock",
	"Snore",
	"Snotty",
	"Whip",
	"Whistle",
	"Work1",
	"Work2",
	"Yawn",
	"And0w",
	"And0x",
	"And0y",
	"And0z",
	"And10",
	"And11",
	"And12",
	"And13",
	"And14",
	"And15",
	"And16",
	"And17",
};


void AGOSEngine::loadSoundFile(const char* filename) {
	Common::File in;
	if (!in.open(filename))
		error("loadSound: Can't load %s", filename);

	uint32 dstSize = in.size();
	byte *dst = (byte *)malloc(dstSize);
	if (in.read(dst, dstSize) != dstSize)
		error("loadSound: Read failed");

	_sound->playSfxData(dst, 0, 0, 0);
}

void AGOSEngine::loadSound(uint16 sound, int16 pan, int16 vol, uint16 type) {
	byte *dst;

	if (getGameId() == GID_DIMP) {
		Common::File in;
		char filename[15];

		assert(sound >= 1 && sound <= 32);
		sprintf(filename, "%s.wav", dimpSoundList[sound - 1]);

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

		uint32 dstSize = in.size();
		dst = (byte *)malloc(dstSize);
		if (in.read(dst, dstSize) != dstSize)
			error("loadSound: Read failed");
	} else if (getFeatures() & GF_ZLIBCOMP) {
		char filename[15];

		uint32 file, offset, srcSize, dstSize;
		if (getPlatform() == Common::kPlatformAmiga) {
			loadOffsets((const char*)"sfxindex.dat", _zoneNumber * 22 + sound, file, offset, srcSize, dstSize);
		} else {
			loadOffsets((const char*)"effects.wav", _zoneNumber * 22 + sound, file, offset, srcSize, dstSize);
		}

		if (getPlatform() == Common::kPlatformAmiga)
			sprintf(filename, "sfx%u.wav", file);
		else
			sprintf(filename, "effects.wav");

		dst = (byte *)malloc(dstSize);
		decompressData(filename, dst, offset, srcSize, dstSize);
	} else {
		if (_curSfxFile == nullptr)
			return;

		dst = _curSfxFile + READ_LE_UINT32(_curSfxFile + sound * 4);
	}

	if (type == Sound::TYPE_AMBIENT)
		_sound->playAmbientData(dst, sound, pan, vol);
	else if (type == Sound::TYPE_SFX)
		_sound->playSfxData(dst, sound, pan, vol);
	else if (type == Sound::TYPE_SFX5)
		_sound->playSfx5Data(dst, sound, pan, vol);
}

void AGOSEngine::playSfx(uint16 sound, uint16 freq, uint16 flags, bool canUseMidiSfx) {
	if (_useDigitalSfx) {
		loadSound(sound, freq, flags);
	} else if (canUseMidiSfx) {
		playMidiSfx(sound);
	}
}

void AGOSEngine::loadSound(uint16 sound, uint16 freq, uint16 flags) {
	byte *dst;
	uint32 offs, size = 0;
	uint32 rate = 8000;

	if (_curSfxFile == nullptr)
		return;

	dst = _curSfxFile;
	if (getGameType() == GType_WW) {
		uint16 tmp = sound;

		while (tmp--) {
			size += READ_LE_UINT16(dst) + 4;
			dst += READ_LE_UINT16(dst) + 4;

			if (size > _curSfxFileSize)
				error("loadSound: Reading beyond EOF (%d, %d)", size, _curSfxFileSize);
		}

		size = READ_LE_UINT16(dst);
		offs = 4;
	} else if (getGameType() == GType_ELVIRA2) {
		while (READ_BE_UINT32(dst + 4) != sound) {
			size += 12;
			dst += 12;

			if (size > _curSfxFileSize)
				error("loadSound: Reading beyond EOF (%d, %d)", size, _curSfxFileSize);
		}

		size = READ_BE_UINT32(dst);
		offs = READ_BE_UINT32(dst + 8);
	} else {
		while (READ_BE_UINT16(dst + 6) != sound) {
			size += 12;
			dst += 12;

			if (size > _curSfxFileSize)
				error("loadSound: Reading beyond EOF (%d, %d)", size, _curSfxFileSize);
		}

		size = READ_BE_UINT16(dst + 2);
		offs = READ_BE_UINT32(dst + 8);
	}

	if (getGameType() == GType_PN) {
		if (freq == 0) {
			rate = 4600;
		} else if (freq == 1) {
			rate = 7400;
		} else {
			rate = 9400;
		}
	}

	// TODO: Handle other sound flags in Amiga/AtariST versions
	if (flags == 2 && _sound->isSfxActive()) {
		_sound->queueSound(dst + offs, sound, size, rate);
	} else {
		if (flags == 0)
			_sound->stopSfx();
		_sound->playRawData(dst + offs, sound, size, rate);
	}
}

void AGOSEngine::loadMidiSfx() {
	if (!_midi->hasMidiSfx())
		return;

	Common::File fxb_file;

	Common::String filename = getGameType() == GType_ELVIRA2 ? "MYLIB.FXB" : "WAX.FXB";
	fxb_file.open(filename);
	if (!fxb_file.isOpen())
		error("loadMidiSfx: Can't open sound effect bank '%s'", filename.c_str());

	_midi->load(&fxb_file, fxb_file.size(), true);

	fxb_file.close();
}

void AGOSEngine::playMidiSfx(uint16 sound) {
	if (!_midi->hasMidiSfx())
		return;

	_midi->play(sound, true);
}

void AGOSEngine::loadVoice(uint speechId) {
	if (getGameType() == GType_PP && speechId == 99) {
		_sound->stopVoice();
		return;
	}

	if (getFeatures() & GF_ZLIBCOMP) {
		char filename[15];

		uint32 file, offset, srcSize, dstSize;
		if (getPlatform() == Common::kPlatformAmiga) {
			loadOffsets((const char*)"spindex.dat", speechId, file, offset, srcSize, dstSize);
		} else {
			loadOffsets((const char*)"speech.wav", speechId, file, offset, srcSize, dstSize);
		}

		// Voice segment doesn't exist
		if (offset == 0xFFFFFFFF && srcSize == 0xFFFFFFFF && dstSize == 0xFFFFFFFF) {
			debug(0, "loadVoice: speechId %d removed", speechId);
			return;
		}

		if (getPlatform() == Common::kPlatformAmiga)
			sprintf(filename, "sp%u.wav", file);
		else
			sprintf(filename, "speech.wav");

		byte *dst = (byte *)malloc(dstSize);
		decompressData(filename, dst, offset, srcSize, dstSize);
		_sound->playVoiceData(dst, speechId);
	} else {
		_sound->playVoice(speechId);
	}
}

void AGOSEngine::stopAllSfx() {
	_sound->stopAllSfx();
	if (_midi->hasMidiSfx())
		_midi->stop(true);
}

} // End of namespace AGOS
