/* 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 "engines/engine.h"
#include "scumm/players/player_v3a.h"
#include "scumm/scumm.h"

namespace Scumm {

Player_V3A::Player_V3A(ScummEngine *scumm, Audio::Mixer *mixer)
	: Paula(true, mixer->getOutputRate(), mixer->getOutputRate() / 60),
	  _vm(scumm),
	  _mixer(mixer),
	  _soundHandle(),
	  _songData(nullptr),
	  _wavetableData(nullptr),
	  _wavetablePtrs(nullptr),
	  _musicTimer(0),
	  _initState(kInitStateNotReady) {

	assert(scumm);
	assert(mixer); // this one's a bit pointless, since we had to dereference it to initialize Paula
	assert((_vm->_game.id == GID_INDY3) || (_vm->_game.id == GID_LOOM));

	stopAllSounds();

	// As in the original game, the same Paula is shared between both SFX and music and plays continuously.
	// Doing them separately would require subclassing Paula and creating two instances
	// (since all of the important methods are protected)
	startPaula();

	_mixer->playStream(Audio::Mixer::kPlainSoundType, &_soundHandle, this, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO, true);
}

bool Player_V3A::init() {
	byte *ptr;
	int numInstruments = 0;

	// Determine which sound resource contains the wavetable data and how large it is
	// This is hardcoded into each game's executable
	if (_vm->_game.id == GID_INDY3) {
		ptr = _vm->getResourceAddress(rtSound, 83);
		numInstruments = 12;
	} else if (_vm->_game.id == GID_LOOM) {
		ptr = _vm->getResourceAddress(rtSound, 79);
		numInstruments = 9;
	} else {
		error("player_v3a - unknown game");
		return false;
	}
	if (!ptr) {
		error("player_v3a - unable to load music samples resource");
		return false;
	}

	// Keep a copy of the resource data, since the original pointer may eventually go bad
	int length = READ_LE_UINT16(ptr);
	_wavetableData = new int8[length];
	if (!_wavetableData) {
		error("player_v3a - failed to allocate copy of wavetable data");
		return false;
	}
	memcpy(_wavetableData, ptr, length);

	int offset = 4;

	// Parse the header tables into a more convenient structure
	_wavetablePtrs = new InstData[numInstruments];
	for (int i = 0; i < numInstruments; i++) {

		// Each instrument defines 6 octaves
		for (int j = 0; j < 6; j++) {
			// Offset/length for intro/main component
			int dataOff = READ_BE_UINT16(_wavetableData + offset + 0);
			int dataLen = READ_BE_UINT16(_wavetableData + offset + 2);

			if (dataLen) {
				_wavetablePtrs[i].mainLen[j] = dataLen;
				_wavetablePtrs[i].mainData[j] = &_wavetableData[dataOff];
			} else {
				_wavetablePtrs[i].mainLen[j] = 0;
				_wavetablePtrs[i].mainData[j] = nullptr;
			}

			// Offset/length for looped component, if any
			dataOff = READ_BE_UINT16(ptr + offset + 4);
			dataLen = READ_BE_UINT16(ptr + offset + 6);

			if (dataLen) {
				_wavetablePtrs[i].loopLen[j] = dataLen;
				_wavetablePtrs[i].loopData[j] = &_wavetableData[dataOff];
			} else {
				_wavetablePtrs[i].loopLen[j] = 0;
				_wavetablePtrs[i].loopData[j] = nullptr;
			}

			// Octave shift for this octave
			_wavetablePtrs[i].octave[j] = READ_BE_INT16(ptr + offset + 8);
			offset += 10;
		}

		// Fadeout rate, in 1/256ths of a volume level
		_wavetablePtrs[i].volumeFade = READ_BE_INT16(ptr + offset);
		offset += 2;

		if (_vm->_game.id == GID_LOOM) {
			// Loom's sound samples aren't all in tune with each other,
			// so it stores an extra adjustment here
			_wavetablePtrs[i].pitchAdjust = READ_BE_INT16(ptr + offset);
			offset += 2;
		} else {
			_wavetablePtrs[i].pitchAdjust = 0;
		}
	}
	return true;
}

Player_V3A::~Player_V3A() {
	_mixer->stopHandle(_soundHandle);
	if (_initState == kInitStateReady) {
		delete[] _wavetableData;
		delete[] _wavetablePtrs;
	}
}

void Player_V3A::setMusicVolume (int vol) {
	_mixer->setChannelVolume(_soundHandle, vol);
}

void Player_V3A::stopAllSounds() {
	for (int i = 0; i < 4; i++) {
		clearVoice(i);
		_channels[i].resourceId = -1;
	}
	_curSong = -1;
	_songPtr = 0;
	_songDelay = 0;
	_songData = nullptr;
}

void Player_V3A::stopSound(int nr) {
	if (nr <= 0)
		return;

	for (int i = 0; i < 4; i++) {
		if (_channels[i].resourceId == nr) {
			clearVoice(i);
			_channels[i].resourceId = -1;
		}
	}
	if (nr == _curSong) {
		_curSong = -1;
		_songDelay = 0;
		_songPtr = 0;
		_songData = nullptr;
	}
}

void Player_V3A::startSound(int nr) {
	assert(_vm);
	int8 *data = (int8 *)_vm->getResourceAddress(rtSound, nr);
	if (!data)
		return;

	if ((_vm->_game.id != GID_INDY3) && (_vm->_game.id != GID_LOOM))
		error("player_v3a - unknown game");

	if (_initState == kInitStateNotReady)
		_initState = init() ? kInitStateReady : kInitStateFailed;

	// is this a Music resource?
	if (data[26]) {
		if (_initState == kInitStateReady) {
			stopAllSounds();
			for (int i = 0; i < 4; i++) {
				_channels[i].haltTimer = 0;
				_channels[i].resourceId = nr;
				_channels[i].priority = READ_BE_UINT16(data + 4);
			}

			// Keep a local copy of the song data
			_songData = data;
			_curSong = nr;
			_songPtr = 0;
			_songDelay = 1;

			// Start timer at 0 and increment every 30 frames (see below)
			_musicTimer = 0;
		} else {
			// debug("player_v3a - wavetable unavailable, cannot play music");
		}
	} else {
		int priority = READ_BE_UINT16(data + 4);
		int channel = READ_BE_UINT16(data + 6);
		if (_channels[channel].resourceId != -1 && _channels[channel].priority > priority)
			return;

		int chan1 = SFX_CHANNEL_MAP[channel][0];
		int chan2 = SFX_CHANNEL_MAP[channel][1];

		int offsetL = READ_BE_UINT16(data + 8);
		int offsetR = READ_BE_UINT16(data + 10);
		int lengthL = READ_BE_UINT16(data + 12);
		int lengthR = READ_BE_UINT16(data + 14);

		// Period and Volume are both stored in fixed-point
		_channels[chan1].period = READ_BE_UINT16(data + 20) << 16;
		_channels[chan2].period = READ_BE_UINT16(data + 22) << 16;
		_channels[chan1].volume = data[24] << 8;
		_channels[chan2].volume = data[25] << 8;
		_channels[chan1].loopCount = data[27];
		_channels[chan2].loopCount = data[27];

		int sweepOffset = READ_BE_UINT16(data + 16);
		if (sweepOffset) {
			// This data contains a list of offset/value pairs, processed in sequence
			// The offset points into a data structure in the original sound engine
			// Offset 0x18 sets the channel's Sweep Rate (fractional)
			// Offset 0x2C with nonzero value delays until reading the next packet
			// Offset 0x2C with zero value stops playback immediately
			// The other offsets are unknown, but they are never used

			// Indy3 always uses 0x18, 0x2C-nonzero, then 0x2C-zero
			// Loom doesn't use these at all

			for (int i = 0; i < 3; i++)
			{
				int offset = READ_BE_UINT32(data + sweepOffset + i*8 + 0);
				int value = READ_BE_INT32(data + sweepOffset + i*8 + 4);
				if (offset == 0x18)
				{
					_channels[chan1].sweepRate = value;
					_channels[chan2].sweepRate = value;
				}
				if (offset == 0x2c && value != 0)
				{
					_channels[chan1].haltTimer = value;
					_channels[chan2].haltTimer = value;
				}
			}
		} else {
			_channels[chan1].sweepRate = 0;
			_channels[chan1].haltTimer = 0;
		}

		_channels[chan1].priority = priority;
		_channels[chan2].priority = priority;
		_channels[chan1].resourceId = nr;
		_channels[chan2].resourceId = nr;

		// Start the Paula playing it
		setChannelInterrupt(chan1, true);
		setChannelInterrupt(chan2, true);
		setChannelPeriod(chan1, MAX((_channels[chan1].period >> 16) & 0xFFFF, 124));
		setChannelPeriod(chan2, MAX((_channels[chan2].period >> 16) & 0xFFFF, 124));
		setChannelVolume(chan1, MIN((_channels[chan1].volume >> 8) & 0x3F, 0x3F));
		setChannelVolume(chan2, MIN((_channels[chan2].volume >> 8) & 0x3F, 0x3F));

		// Start as looped, then generate interrupts to handle looping properly
		setChannelData(chan1, (int8 *)data + offsetL, (int8 *)data + offsetL, lengthL, lengthL);
		setChannelData(chan2, (int8 *)data + offsetR, (int8 *)data + offsetR, lengthR, lengthR);
		interruptChannel(chan1);
		interruptChannel(chan2);
	}
}

void Player_V3A::interrupt() {
	if (_vm->_game.id == GID_INDY3) {
		updateMusicIndy();
	} else if (_vm->_game.id == GID_LOOM) {
		updateMusicLoom();
	}
	updateSounds();
}

void Player_V3A::interruptChannel(byte channel) {
	// check looping
	if (_channels[channel].loopCount == -1)
		return;

	if (_channels[channel].loopCount) {
		_channels[channel].loopCount--;
		if (_channels[channel].loopCount <= 0) {
			// On the last loop, set it to no longer repeat
			setChannelInterrupt(channel, false);
			setChannelSampleStart(channel, nullptr);
			setChannelSampleLen(channel, 0);

			// If there was no music playing, mark the channel as Unused
			if (_curSong == -1)
				_channels[channel].resourceId = -1;
		}
	}
}

void Player_V3A::updateSounds() {
	for (int i = 0; i < 4; i++) {
		if (!_channels[i].loopCount)
			continue;

		setChannelVolume(i, MIN((_channels[i].volume >> 8) & 0x3F, 0x3F));
		setChannelPeriod(i, MAX((_channels[i].period >> 16) & 0xFFFF, 124));

		// Only process ones that are sweeping, since others are handled by interruptChannel above
		if (!_channels[i].sweepRate)
			continue;

		if (_channels[i].haltTimer) {
			_channels[i].haltTimer--;
			if (!_channels[i].haltTimer) {
				// Once the timer reaches zero, immediately it stop looping
				_channels[i].loopCount = 1;
				interruptChannel(i);
			}
		}
		_channels[i].period += _channels[i].sweepRate;
	}
}

void Player_V3A::updateMusicIndy() {
	// technically, musicTimer should only be incremented during playback, but that seems to cause problems
	_musicTimer++;

	if (!_songDelay || !_songData)
		return;

	for (int i = 0; i < 4; i++) {
		if (_channels[i].haltTimer)
			_channels[i].haltTimer--;

		// When a looped sample runs out, fade the volume to zero
		// Non-looped samples will be allowed to continue playing
		if (!_channels[i].haltTimer && _channels[i].loopCount) {
			_channels[i].volume -= _channels[i].fadeRate;

			// Once the volume hits zero, immediately silence it
			if (_channels[i].volume < 1) {
				_channels[i].volume = 0;
				_channels[i].loopCount = 0;
				clearVoice(i);
				setChannelInterrupt(i, false);
			} else
				setChannelVolume(i, MIN((_channels[i].volume >> 8) & 0x3F, 0x3F));
		}
	}
	if (--_songDelay)
		return;

	int8 *songData = &_songData[0x1C + _songPtr];
	while (1) {
		int code = songData[0];
		if ((code & 0xF0) == 0x80) {
			// play a note
			int instrument = songData[0] & 0xF;
			int pitch = songData[1] & 0xFF;
			int volume = (songData[2] / 2) & 0xFF;
			int duration = songData[3] & 0xFF;

			_songPtr += 4;
			songData += 4;

			// pitch 0 == global rest
			if (pitch == 0) {
				_songDelay = duration;
				return;
			}

			// Find an available sound channel
			// Indy3 starts at channel (inst & 3) and tries them in sequence
			int channel = instrument & 0x3;
			for (int i = 0; i < 4; i++) {
				if (!_channels[channel].haltTimer)
					break;
				channel = (channel + 1) & 3;
			}

			startNote(channel, instrument, pitch, volume, duration);
		} else {
			// Reached the end
			for (int i = 0; i < 4; i++) {
				// Subtle bug in the original engine - it only checks the LAST playing channel
				// (rather than checking all of them)
				if (_channels[i].loopCount)
					_songDelay = _channels[i].haltTimer;
			}
			if (_songDelay == 0) {
				if ((code & 0xFF) == 0xFB) {
					// repeat
					_songPtr = 0;
					_songDelay = 1;
				} else {
					// stop
					stopSound(_curSong);
				}
			}
		}
		if ((_songDelay) || (_curSong == -1))
			break;
	}
}

void Player_V3A::updateMusicLoom() {
	// technically, musicTimer should only be incremented during playback, but that seems to cause problems
	_musicTimer++;

	if (!_songDelay || !_songData)
		return;

	// Update all playing notes
	for (int i = 0; i < 4; i++) {
		// Mark all notes that were started during a previous update
		_channels[i].canOverride = 1;
		if (_channels[i].haltTimer)
			_channels[i].haltTimer--;

		// When a looped sample runs out, fade the volume to zero
		// Non-looped samples will be allowed to continue playing
		if (!_channels[i].haltTimer && _channels[i].loopCount) {
			_channels[i].volume -= _channels[i].fadeRate;

			// Once the volume hits zero, immediately silence it
			if (_channels[i].volume < 1) {
				_channels[i].volume = 0;
				_channels[i].loopCount = 0;
				clearVoice(i);
				setChannelInterrupt(i, false);
			} else
				setChannelVolume(i, MIN((_channels[i].volume >> 8) & 0x3F, 0x3F));
		}
	}
	if (--_songDelay)
		return;

	int8 *songData = &_songData[0x1C + _songPtr];

	// Loom uses an elaborate queue to deal with overlapping notes and limited sound channels
	int queuePos = 0;
	int queueInstrument[4];
	int queuePitch[4];
	int queueVolume[4];
	int queueDuration[4];

	while (1) {
		int code = songData[0];
		if ((code & 0xF0) == 0x80) {
			// play a note
			int instrument = songData[0] & 0xF;
			int pitch = songData[1] & 0xFF;
			int volume = (((songData[2] < 0) ? (songData[2] + 1) : songData[2]) / 2) & 0xFF;
			int duration = songData[3] & 0xFF;

			_songPtr += 4;
			songData += 4;

			// pitch 0 == global rest
			if (pitch == 0) {
				_songDelay = duration;
				break;
			}

			// Try to find an appropriate channel to use
			// Channel must be playing the same instrument, started during a previous loop, and within 6 frames of ending
			int channel;
			for (channel = 0; channel < 4; channel++)
				if ((_channels[channel].instrument == instrument) && (_channels[channel].canOverride) && (_channels[channel].haltTimer < 6))
					break;

			if (channel != 4) {
				// Channel was found, so start playing the note
				startNote(channel, instrument, pitch, volume, duration);
			} else if (queuePos < 4) {
				// No channel found - put it in a queue to process at the end
				queueInstrument[queuePos] = instrument;
				queuePitch[queuePos] = pitch;
				queueVolume[queuePos] = volume;
				queueDuration[queuePos] = duration;
				++queuePos;
			}
		} else {
			// Reached end of song
			for (int i = 0; i < 4; i++) {
				// Subtle bug in the original engine - it only checks the LAST playing channel
				// rather than checking ALL of them
				if (_channels[i].loopCount)
					_songDelay = _channels[i].haltTimer;
			}
			if (_songDelay == 0) {
				if ((code & 0xFF) == 0xFB) {
					// repeat
					_songPtr = 0;
					_songDelay = 1;
				} else {
					// stop
					stopSound(_curSong);
				}
			}
		}
		if ((_songDelay) || (_curSong == -1))
			break;
	}

	while (queuePos--) {
		// Take all of the enqueued note requests and try to fit them somewhere
		int channel;
		for (channel = 0; channel < 4; channel++) {
			// First, find a soon-to-expire channel that wasn't explicitly assigned this loop
			if ((_channels[channel].canOverride) && (_channels[channel].haltTimer < 6))
				break;
		}
		if (channel == 4) {
			// If no channel found, pick the first channel playing this instrument
			for (channel = 0; channel < 4; channel++) {
				if (_channels[channel].instrument == queueInstrument[queuePos])
					break;
			}
		}
		if (channel != 4) {
			// If we found a channel, play the note there - otherwise, it gets lost
			startNote(channel, queueInstrument[queuePos], queuePitch[queuePos], queueVolume[queuePos], queueDuration[queuePos]);
		}
	}
}

void Player_V3A::startNote(int channel, int instrument, int pitch, int volume, int duration) {
	const InstData &instData = _wavetablePtrs[instrument];
	SndChan &curChan = _channels[channel];

	// for Loom, adjust pitch
	pitch += instData.pitchAdjust;

	// and set channel precedence parameters
	curChan.instrument = instrument;
	curChan.canOverride = 0;

	// Split pitch into octave+offset, truncating as needed
	int octave = (pitch / 12) - 2;
	pitch = pitch % 12;
	if (octave < 0)
		octave = 0;
	if (octave > 5)
		octave = 5;
	int actualOctave = instData.octave[octave];

	curChan.period = NOTE_FREQS[actualOctave][pitch] << 16;
	curChan.volume = (volume & 0xFF) << 8;
	curChan.sweepRate = 0;
	curChan.fadeRate = instData.volumeFade;
	curChan.haltTimer = duration;

	// For music, pre-decrement the loop counter and skip the initial interrupt
	if (instData.loopLen[octave]) {
		curChan.loopCount = -1;
		setChannelInterrupt(channel, true);
	} else {
		curChan.loopCount = 0;
		setChannelInterrupt(channel, false);
	}

	setChannelPeriod(channel, MAX((curChan.period >> 16) & 0xFFFF, 124));
	setChannelVolume(channel, MIN((curChan.volume >> 8) & 0x3F, 0x3F));
	setChannelData(channel, instData.mainData[octave], instData.loopData[octave], instData.mainLen[octave], instData.loopLen[octave]);
}

int Player_V3A::getMusicTimer() {
	// Actual code in Amiga version returns 5+timer/28, which syncs poorly in ScummVM
	// Presumably, this was meant to help slower machines sync better

	return _musicTimer / 30;
}

int Player_V3A::getSoundStatus(int nr) const {
	if (nr == -1)
		return 0;
	if (nr == _curSong)
		return 1;
	for (int i = 0; i < 4; i++)
		if (_channels[i].resourceId == nr)
			return 1;
	return 0;
}

} // End of namespace Scumm
