/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include "common/md5.h"
#include "common/config-manager.h"
#include "xeen/sound_driver.h"
#include "xeen/xeen.h"
#include "xeen/files.h"

namespace Xeen {

SoundDriver::SoundDriver() : _musicPlaying(false), _fxPlaying(false),
		_musCountdownTimer(0), _fxCountdownTimer(0), _musDataPtr(nullptr),
		_fxDataPtr(nullptr), _fxStartPtr(nullptr), _musStartPtr(nullptr),
		_exclude7(false), _frameCtr(0) {
	_channels.resize(CHANNEL_COUNT);
}

SoundDriver::~SoundDriver() {
	_musicPlaying = _fxPlaying = false;
	_musCountdownTimer = _fxCountdownTimer = 0;
}

void SoundDriver::execute() {
	bool isFX = false;
	const byte *srcP = nullptr;
	const byte *startP = nullptr;

	// Single iteration loop to avoid use of GOTO
	do {
		if (_musicPlaying) {
			startP = _musStartPtr;
			srcP = _musDataPtr;
			isFX = false;
			if (_musCountdownTimer == 0 || --_musCountdownTimer == 0)
				break;
		}

		if (_fxPlaying) {
			startP = _fxStartPtr;
			srcP = _fxDataPtr;
			isFX = true;
			if (_fxCountdownTimer == 0 || --_fxCountdownTimer == 0)
				break;
		}

		pausePostProcess();
		return;
	} while (0);

	++_frameCtr;
	debugC(3, kDebugSound, "\nSoundDriver frame - #%x", _frameCtr);

	// Main loop
	bool breakFlag = false;
	while (!breakFlag) {
		debugCN(3, kDebugSound, "MUSCODE %.4x - %.2x  ", (uint)(srcP - startP), (uint)*srcP);
		byte nextByte = *srcP++;
		int cmd = (nextByte >> 4) & 15;
		int param = (nextByte & 15);

		CommandFn fn = isFX ? FX_COMMANDS[cmd] : MUSIC_COMMANDS[cmd];
		breakFlag = (this->*fn)(srcP, param);
	}
}


bool SoundDriver::musCallSubroutine(const byte *&srcP, byte param) {
	debugC(3, kDebugSound, "musCallSubroutine");
	if (_musSubroutines.size() < 16) {
		const byte *returnP = srcP + 2;
		srcP = _musStartPtr + READ_LE_UINT16(srcP);

		_musSubroutines.push(Subroutine(returnP, srcP));
	}

	return false;
}

bool SoundDriver::musSetCountdown(const byte *&srcP, byte param) {
	// Set the countdown timer
	if (!param)
		param = *srcP++;
	_musCountdownTimer = param;
	_musDataPtr = srcP;
	debugC(3, kDebugSound, "musSetCountdown %d", param);

	// Do paused handling and break out of processing loop
	pausePostProcess();
	return true;
}

bool SoundDriver::cmdNoOperation(const byte *&srcP, byte param) {
	debugC(3, kDebugSound, "cmdNoOperation");
	return false;
}

bool SoundDriver::musSkipWord(const byte *&srcP, byte param) {
	debugC(3, kDebugSound, "musSkipWord");
	srcP += 2;
	return false;
}

bool SoundDriver::cmdFreezeFrequency(const byte *&srcP, byte param) {
	debugC(3, kDebugSound, "cmdFreezeFrequency %d", param);
	_channels[param]._changeFrequency = false;
	return false;
}

bool SoundDriver::cmdChangeFrequency(const byte *&srcP, byte param) {
	debugC(3, kDebugSound, "cmdChangeFrequency %d", param);

	if (param != 7 || !_exclude7) {
		_channels[param]._freqCtrChange = (int8)*srcP++;
		_channels[param]._freqCtr = 0xFF;
		_channels[param]._changeFrequency = true;
		_channels[param]._freqChange = (int16)READ_BE_UINT16(srcP);
		srcP += 2;
	} else {
		srcP += 3;
	}

	return false;
}

bool SoundDriver::musEndSubroutine(const byte *&srcP, byte param) {
	debugC(3, kDebugSound, "musEndSubroutine %d", param);

	if (param != 15) {
		// Music has ended, so flag it stopped
		_musicPlaying = false;
		return true;
	}

	// Returning from subroutine, or looping back to start of music
	srcP = _musSubroutines.empty() ? _musStartPtr : _musSubroutines.pop()._returnP;
	return false;
}

bool SoundDriver::fxCallSubroutine(const byte *&srcP, byte param) {
	debugC(3, kDebugSound, "fxCallSubroutine");

	if (_fxSubroutines.size() < 16) {
		const byte *startP = srcP + 2;
		srcP = _musStartPtr + READ_LE_UINT16(srcP);

		_fxSubroutines.push(Subroutine(startP, srcP));
	}

	return false;
}

bool SoundDriver::fxSetCountdown(const byte *&srcP, byte param) {
	// Set the countdown timer
	if (!param)
		param = *srcP++;
	_fxCountdownTimer = param;
	_fxDataPtr = srcP;
	debugC(3, kDebugSound, "fxSetCountdown %d", param);

	// Do paused handling and break out of processing loop
	pausePostProcess();
	return true;
}

bool SoundDriver::fxEndSubroutine(const byte *&srcP, byte param) {
	debugC(3, kDebugSound, "fxEndSubroutine %d", param);

	if (param != 15) {
		// FX has ended, so flag it stopped
		_fxPlaying = false;
		return true;
	}

	srcP = _fxSubroutines.empty() ? _fxStartPtr : _fxSubroutines.pop()._returnP;
	return false;
}

void SoundDriver::playFX(uint effectId, const byte *data) {
	if (!_fxPlaying || effectId < 7 || effectId >= 11) {
		_fxDataPtr = _fxStartPtr = data;
		_fxCountdownTimer = 0;
		_channels[7]._changeFrequency = _channels[8]._changeFrequency = false;
		resetFX();
		_fxPlaying = true;
	}

	debugC(1, kDebugSound, "Starting FX %d", effectId);
}

void SoundDriver::stopFX() {
	resetFX();
	_fxPlaying = false;
	_fxStartPtr = _fxDataPtr = nullptr;
}

void SoundDriver::playSong(const byte *data) {
	_musDataPtr = _musStartPtr = data;
	_musSubroutines.clear();
	_musCountdownTimer = 0;
	_musicPlaying = true;
	debugC(1, kDebugSound, "Starting song");
}

int SoundDriver::songCommand(uint commandId, byte musicVolume, byte sfxVolume) {
	if (commandId == STOP_SONG) {
		_musicPlaying = false;
	} else if (commandId == RESTART_SONG) {
		_musicPlaying = true;
		_musDataPtr = nullptr;
		_musSubroutines.clear();
	}

	return 0;
}

const CommandFn SoundDriver::MUSIC_COMMANDS[16] = {
	&SoundDriver::musCallSubroutine,   &SoundDriver::musSetCountdown,
	&SoundDriver::musSetInstrument,    &SoundDriver::cmdNoOperation,
	&SoundDriver::musSetPitchWheel,    &SoundDriver::musSkipWord,
	&SoundDriver::musSetPanning,       &SoundDriver::cmdNoOperation,
	&SoundDriver::musFade,             &SoundDriver::musStartNote,
	&SoundDriver::musSetVolume,        &SoundDriver::musInjectMidi,
	&SoundDriver::musPlayInstrument,   &SoundDriver::cmdFreezeFrequency,
	&SoundDriver::cmdChangeFrequency,  &SoundDriver::musEndSubroutine
};

const CommandFn SoundDriver::FX_COMMANDS[16] = {
	&SoundDriver::fxCallSubroutine,    &SoundDriver::fxSetCountdown,
	&SoundDriver::fxSetInstrument,     &SoundDriver::fxSetVolume,
	&SoundDriver::fxMidiReset,         &SoundDriver::fxMidiDword,
	&SoundDriver::fxSetPanning,        &SoundDriver::fxChannelOff,
	&SoundDriver::fxFade,              &SoundDriver::fxStartNote,
	&SoundDriver::cmdNoOperation,      &SoundDriver::fxInjectMidi,
	&SoundDriver::fxPlayInstrument,    &SoundDriver::cmdFreezeFrequency,
	&SoundDriver::cmdChangeFrequency,  &SoundDriver::fxEndSubroutine
};

} // End of namespace Xeen