/* 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.
 *
 * $URL: https://scummvm.svn.sourceforge.net/svnroot/scummvm/scummvm/tags/release-1-2-1/engines/sci/sound/midiparser_sci.cpp $
 * $Id: midiparser_sci.cpp 52484 2010-09-01 19:20:17Z m_kiewitz $
 *
 */

#include "sci/engine/kernel.h"
#include "sci/engine/state.h"
#include "sci/sound/midiparser_sci.h"
#include "sci/sound/drivers/mididriver.h"

namespace Sci {

static const int nMidiParams[] = { 2, 2, 2, 2, 1, 1, 2, 0 };

enum SciMidiCommands {
	kSetSignalLoop = 0x7F,
	kEndOfTrack = 0xFC,
	kSetReverb = 0x50,
	kMidiHold = 0x52,
	kUpdateCue = 0x60,
	kResetOnPause = 0x4C
};

//  MidiParser_SCI
//
MidiParser_SCI::MidiParser_SCI(SciVersion soundVersion, SciMusic *music) :
	MidiParser() {
	_soundVersion = soundVersion;
	_music = music;
	_mixedData = NULL;
	// mididata contains delta in 1/60th second
	// values of ppqn and tempo are found experimentally and may be wrong
	_ppqn = 1;
	setTempo(16667);

	_masterVolume = 15;
	_volume = 127;

	_signalSet = false;
	_signalToSet = 0;
	_dataincAdd = false;
	_dataincToAdd = 0;
	_resetOnPause = false;
	_pSnd = 0;
}

MidiParser_SCI::~MidiParser_SCI() {
	unloadMusic();
	// we do this, so that MidiParser won't be able to call his own ::allNotesOff()
	//  this one would affect all channels and we can't let that happen
	_driver = 0;
}

void MidiParser_SCI::mainThreadBegin() {
	_mainThreadCalled = true;
}

void MidiParser_SCI::mainThreadEnd() {
	_mainThreadCalled = false;
}

bool MidiParser_SCI::loadMusic(SoundResource::Track *track, MusicEntry *psnd, int channelFilterMask, SciVersion soundVersion) {
	unloadMusic();
	_track = track;
	_pSnd = psnd;
	_soundVersion = soundVersion;

	for (int i = 0; i < 16; i++) {
		_channelUsed[i] = false;
		_channelRemap[i] = -1;
		_channelMuted[i] = false;
		_channelVolume[i] = 127;
	}
	_channelRemap[9] = 9; // never map channel 9, because that's used for percussion
	_channelRemap[15] = 15; // never map channel 15, because thats used by sierra internally

	if (channelFilterMask) {
		// SCI0 only has 1 data stream, but we need to filter out channels depending on music hardware selection
		midiFilterChannels(channelFilterMask);
	} else {
		midiMixChannels();
	}

	_num_tracks = 1;
	_tracks[0] = _mixedData;
	if (_pSnd)
		setTrack(0);
	_loopTick = 0;

	return true;
}

byte MidiParser_SCI::midiGetNextChannel(long ticker) {
	byte curr = 0xFF;
	long closest = ticker + 1000000, next = 0;

	for (int i = 0; i < _track->channelCount; i++) {
		if (_track->channels[i].time == -1) // channel ended
			continue;
		SoundResource::Channel *curChannel = &_track->channels[i];
		if (curChannel->curPos >= curChannel->size)
			continue;
		next = curChannel->data[curChannel->curPos]; // when the next event should occur
		if (next == 0xF8) // 0xF8 means 240 ticks delay
			next = 240;
		next += _track->channels[i].time;
		if (next < closest) {
			curr = i;
			closest = next;
		}
	}

	return curr;
}

byte *MidiParser_SCI::midiMixChannels() {
	int totalSize = 0;

	for (int i = 0; i < _track->channelCount; i++) {
		_track->channels[i].time = 0;
		_track->channels[i].prev = 0;
		_track->channels[i].curPos = 0;
		totalSize += _track->channels[i].size;
	}

	byte *outData = new byte[totalSize * 2]; // FIXME: creates overhead and still may be not enough to hold all data
	_mixedData = outData;
	long ticker = 0;
	byte channelNr, curDelta;
	byte midiCommand = 0, midiParam, global_prev = 0;
	long newDelta;
	SoundResource::Channel *channel;

	while ((channelNr = midiGetNextChannel(ticker)) != 0xFF) { // there is still an active channel
		channel = &_track->channels[channelNr];
		curDelta = channel->data[channel->curPos++];
		channel->time += (curDelta == 0xF8 ? 240 : curDelta); // when the command is supposed to occur
		if (curDelta == 0xF8)
			continue;
		newDelta = channel->time - ticker;
		ticker += newDelta;

		midiCommand = channel->data[channel->curPos++];
		if (midiCommand != kEndOfTrack) {
			// Write delta
			while (newDelta > 240) {
				*outData++ = 0xF8;
				newDelta -= 240;
			}
			*outData++ = (byte)newDelta;
		}
		// Write command
		switch (midiCommand) {
		case 0xF0: // sysEx
			*outData++ = midiCommand;
			do {
				midiParam = channel->data[channel->curPos++];
				*outData++ = midiParam;
			} while (midiParam != 0xF7);
			break;
		case kEndOfTrack: // end of channel
			channel->time = -1;
			break;
		default: // MIDI command
			if (midiCommand & 0x80) {
				midiParam = channel->data[channel->curPos++];
			} else {// running status
				midiParam = midiCommand;
				midiCommand = channel->prev;
			}

			// remember which channel got used for channel remapping
			byte midiChannel = midiCommand & 0xF;
			_channelUsed[midiChannel] = true;

			if (midiCommand != global_prev)
				*outData++ = midiCommand;
			*outData++ = midiParam;
			if (nMidiParams[(midiCommand >> 4) - 8] == 2)
				*outData++ = channel->data[channel->curPos++];
			channel->prev = midiCommand;
			global_prev = midiCommand;
		}
	}

	// Insert stop event
	*outData++ = 0;    // Delta
	*outData++ = 0xFF; // Meta event
	*outData++ = 0x2F; // End of track (EOT)
	*outData++ = 0x00;
	*outData++ = 0x00;
	return _mixedData;
}

// This is used for SCI0 sound-data. SCI0 only has one stream that may
// contain several channels and according to output device we remove
// certain channels from that data.
byte *MidiParser_SCI::midiFilterChannels(int channelMask) {
	SoundResource::Channel *channel = &_track->channels[0];
	byte *channelData = channel->data;
	byte *channelDataEnd = channel->data + channel->size;
	byte *outData = new byte[channel->size + 5];
	byte curChannel = 15, curByte, curDelta;
	byte command = 0, lastCommand = 0;
	int delta = 0;
	int midiParamCount = 0;

	_mixedData = outData;

	while (channelData < channelDataEnd) {
		curDelta = *channelData++;
		if (curDelta == 0xF8) {
			delta += 240;
			continue;
		}
		delta += curDelta;
		curByte = *channelData++;

		switch (curByte) {
		case 0xF0: // sysEx
		case kEndOfTrack: // end of channel
			command = curByte;
			curChannel = 15;
			break;
		default:
			if (curByte & 0x80) {
				command = curByte;
				curChannel = command & 0x0F;
				midiParamCount = nMidiParams[(command >> 4) - 8];
			}
		}
		if ((1 << curChannel) & channelMask) {
			if (command != kEndOfTrack) {
				// Write delta
				while (delta > 240) {
					*outData++ = 0xF8;
					delta -= 240;
				}
				*outData++ = (byte)delta;
				delta = 0;
			}
			// Write command
			switch (command) {
			case 0xF0: // sysEx
				*outData++ = command;
				do {
					curByte = *channelData++;
					*outData++ = curByte; // out
				} while (curByte != 0xF7);
				lastCommand = command;
				break;

			case kEndOfTrack: // end of channel
				break;

			default: // MIDI command
				// remember which channel got used for channel remapping
				byte midiChannel = command & 0xF;
				_channelUsed[midiChannel] = true;

				if (lastCommand != command) {
					*outData++ = command;
					lastCommand = command;
				}
				if (midiParamCount > 0) {
					if (curByte & 0x80)
						*outData++ = *channelData++;
					else
						*outData++ = curByte;
				}
				if (midiParamCount > 1) {
					*outData++ = *channelData++;
				}
			}
		} else {
			if (curByte & 0x80)
				channelData += midiParamCount;
			else
				channelData += midiParamCount - 1;
		}
	}

	// Insert stop event
	*outData++ = 0;    // Delta
	*outData++ = 0xFF; // Meta event
	*outData++ = 0x2F; // End of track (EOT)
	*outData++ = 0x00;
	*outData++ = 0x00;

	return _mixedData;
}

// This will get called right before actual playing and will try to own the used channels
void MidiParser_SCI::tryToOwnChannels() {
	// We don't have SciMusic in case debug command show_instruments is used
	if (!_music)
		return;
	for (int curChannel = 0; curChannel < 15; curChannel++) {
		if (_channelUsed[curChannel]) {
			if (_channelRemap[curChannel] == -1) {
				_channelRemap[curChannel] = _music->tryToOwnChannel(_pSnd, curChannel);
			}
		}
	}
}

void MidiParser_SCI::lostChannels() {
	for (int curChannel = 0; curChannel < 15; curChannel++)
		if ((_channelUsed[curChannel]) && (curChannel != 9))
			_channelRemap[curChannel] = -1;
}

void MidiParser_SCI::sendInitCommands() {
	// reset our "global" volume and channel volumes
	_volume = 127;
	for (int i = 0; i < 16; i++)
		_channelVolume[i] = 127;

	// Set initial voice count
	if (_pSnd) {
		if (_soundVersion <= SCI_VERSION_0_LATE) {
			for (int i = 0; i < 15; ++i) {
				byte voiceCount = 0;
				if (_channelUsed[i]) {
					voiceCount = _pSnd->soundRes->getInitialVoiceCount(i);
					sendToDriver(0xB0 | i, 0x4B, voiceCount);
				}
			}
		}
	}

	// Send a velocity off signal to all channels
	for (int i = 0; i < 15; ++i) {
		if (_channelUsed[i])
			sendToDriver(0xB0 | i, 0x4E, 0);	// Reset velocity
	}

	// Center the pitch wheels and hold pedal in preparation for the next piece of music
	for (int i = 0; i < 16; ++i) {
		if (_channelUsed[i]) {
			sendToDriver(0xE0 | i, 0, 0x40);	// Reset pitch wheel
			sendToDriver(0xB0 | i, 0x40, 0);	// Reset hold pedal
		}
	}
}

void MidiParser_SCI::unloadMusic() {
	if (_pSnd) {
		resetTracking();
		allNotesOff();
	}
	_num_tracks = 0;
	_active_track = 255;
	_resetOnPause = false;

	if (_mixedData) {
		delete[] _mixedData;
		_mixedData = NULL;
	}
}

// this is used for scripts sending midi commands to us. we verify in that case that the channel is actually
//  used, so that channel remapping will work as well and then send them on
void MidiParser_SCI::sendFromScriptToDriver(uint32 midi) {
	byte midiChannel = midi & 0xf;

	if (!_channelUsed[midiChannel]) {
		// trying to send to an unused channel
		//  this happens for cmdSendMidi at least in sq1vga right at the start, it's a script issue
		return;
	}
	if (_channelRemap[midiChannel] == -1) {
		// trying to send to an unmapped channel
		//  this happens for cmdSendMidi at least in sq1vga right at the start, scripts are pausing the sound
		//  and then sending manually. it's a script issue
		return;
	}
	sendToDriver(midi);
}

void MidiParser_SCI::sendToDriver(uint32 midi) {
	byte midiChannel = midi & 0xf;

	if ((midi & 0xFFF0) == 0x4EB0) {
		// this is channel mute only for sci1
		// it's velocity control for sci0
		if (_soundVersion >= SCI_VERSION_1_EARLY) {
			_channelMuted[midiChannel] = midi & 0xFF0000 ? true : false;
			return; // don't send this to driver at all
		}
	}

	// Is channel muted? if so, don't send command
	if (_channelMuted[midiChannel])
		return;

	if ((midi & 0xFFF0) == 0x07B0) {
		// someone trying to set channel volume?
		int channelVolume = (midi >> 16) & 0xFF;
		// Remember, if we need to set it ourselves
		_channelVolume[midiChannel] = channelVolume;
		// Adjust volume accordingly to current local volume
		channelVolume = channelVolume * _volume / 127;
		midi = (midi & 0xFFF0) | ((channelVolume & 0xFF) << 16);
	}

	// Channel remapping
	int16 realChannel = _channelRemap[midiChannel];
	if (realChannel == -1)
		return;

	midi = (midi & 0xFFFFFFF0) | realChannel;
	if (_mainThreadCalled)
		_music->putMidiCommandInQueue(midi);
	else
		_driver->send(midi);
}

void MidiParser_SCI::parseNextEvent(EventInfo &info) {
	// Set signal AFTER waiting for delta, otherwise we would set signal too soon resulting in all sorts of bugs
	if (_dataincAdd) {
		_dataincAdd = false;
		_pSnd->dataInc += _dataincToAdd;
		_pSnd->signal = 0x7f + _pSnd->dataInc;
		debugC(4, kDebugLevelSound, "datainc %04x", _dataincToAdd);
	}
	if (_signalSet) {
		_signalSet = false;
		_pSnd->setSignal(_signalToSet);

		debugC(4, kDebugLevelSound, "signal %04x", _signalToSet);
	}

	info.start = _position._play_pos;
	info.delta = 0;
	while (*_position._play_pos == 0xF8) {
		info.delta += 240;
		_position._play_pos++;
	}
	info.delta += *(_position._play_pos++);

	// Process the next info.
	if ((_position._play_pos[0] & 0xF0) >= 0x80)
		info.event = *(_position._play_pos++);
	else
		info.event = _position._running_status;
	if (info.event < 0x80)
		return;

	_position._running_status = info.event;
	switch (info.command()) {
	case 0xC:
		info.basic.param1 = *(_position._play_pos++);
		info.basic.param2 = 0;
		if (info.channel() == 0xF) {// SCI special case
			if (info.basic.param1 != kSetSignalLoop) {
				// at least in kq5/french&mac the first scene in the intro has a song that sets signal to 4 immediately
				//  on tick 0. Signal isn't set at that point by sierra sci and it would cause the castle daventry text to
				//  get immediately removed, so we currently filter it.
				// Sierra SCI ignores them as well at that time
				if ((_position._play_tick) || (info.delta)) {
					_signalSet = true;
					_signalToSet = info.basic.param1;
				}
			} else {
				_loopTick = _position._play_tick + info.delta;
			}
		}
		break;
	case 0xD:
		info.basic.param1 = *(_position._play_pos++);
		info.basic.param2 = 0;
		break;

	case 0xB:
		info.basic.param1 = *(_position._play_pos++);
		info.basic.param2 = *(_position._play_pos++);
		if (info.channel() == 0xF) {// SCI special
			// Reference for some events:
			// http://wiki.scummvm.org/index.php/SCI/Specifications/Sound/SCI0_Resource_Format#Status_Reference
			// Also, sci/sound/iterator/iterator.cpp, function BaseSongIterator::parseMidiCommand()
			switch (info.basic.param1) {
			case kSetReverb:
				((MidiPlayer *)_driver)->setReverb(info.basic.param2);
				break;
			case kMidiHold:
				// Check if the hold ID marker is the same as the hold ID
				// marker set for that song by cmdSetSoundHold.
				// If it is, loop back, but don't stop notes when jumping.
				if (info.basic.param2 == _pSnd->hold)
					jumpToTick(_loopTick, false, false);
				break;
			case kUpdateCue:
				_dataincAdd = true;
				switch (_soundVersion) {
				case SCI_VERSION_0_EARLY:
				case SCI_VERSION_0_LATE:
					_dataincToAdd = info.basic.param2;
					break;
				case SCI_VERSION_1_EARLY:
				case SCI_VERSION_1_LATE:
				case SCI_VERSION_2_1:
					_dataincToAdd = 1;
					break;
				default:
					error("unsupported _soundVersion");
				}
				break;
			case kResetOnPause:
				_resetOnPause = info.basic.param2;
				break;
			// Unhandled SCI commands
			case 0x46: // LSL3 - binoculars
			case 0x61: // Iceman (AdLib?)
			case 0x73: // Hoyle
			case 0xD1: // KQ4, when riding the unicorn
				// Obscure SCI commands - ignored
				break;
			// Standard MIDI commands
			case 0x01:	// mod wheel
			case 0x04:	// foot controller
			case 0x07:	// channel volume
			case 0x0A:	// pan
			case 0x0B:	// expression
			case 0x40:	// sustain
			case 0x79:	// reset all
			case 0x7B:	// notes off
				// These are all handled by the music driver, so ignore them
				break;
			case 0x4B:	// voice mapping
				// TODO: is any support for this needed at the MIDI parser level?
				warning("Unhanded SCI MIDI command 0x%x - voice mapping (parameter %d)", info.basic.param1, info.basic.param2);
				break;
			default:
				warning("Unhandled SCI MIDI command 0x%x (parameter %d)", info.basic.param1, info.basic.param2);
				break;
			}
		}
		info.length = 0;
		break;

	case 0x8:
	case 0x9:
	case 0xA:
	case 0xE:
		info.basic.param1 = *(_position._play_pos++);
		info.basic.param2 = *(_position._play_pos++);
		if (info.command() == 0x9 && info.basic.param2 == 0)
			info.event = info.channel() | 0x80;
		info.length = 0;
		break;

	case 0xF: // System Common, Meta or SysEx event
		switch (info.event & 0x0F) {
		case 0x2: // Song Position Pointer
			info.basic.param1 = *(_position._play_pos++);
			info.basic.param2 = *(_position._play_pos++);
			break;

		case 0x3: // Song Select
			info.basic.param1 = *(_position._play_pos++);
			info.basic.param2 = 0;
			break;

		case 0x6:
		case 0x8:
		case 0xA:
		case 0xB:
		case 0xC:
		case 0xE:
			info.basic.param1 = info.basic.param2 = 0;
			break;

		case 0x0: // SysEx
			info.length = readVLQ(_position._play_pos);
			info.ext.data = _position._play_pos;
			_position._play_pos += info.length;
			break;

		case 0xF: // META event
			info.ext.type = *(_position._play_pos++);
			info.length = readVLQ(_position._play_pos);
			info.ext.data = _position._play_pos;
			_position._play_pos += info.length;
			if (info.ext.type == 0x2F) {// end of track reached
				if (_pSnd->loop)
					_pSnd->loop--;
				if (_pSnd->loop) {
					// We need to play it again...
					jumpToTick(_loopTick);
				} else {
					_pSnd->status = kSoundStopped;
					_pSnd->setSignal(SIGNAL_OFFSET);

					debugC(4, kDebugLevelSound, "signal EOT");
				}
			}
			break;
		default:
			warning(
					"MidiParser_SCI::parseNextEvent: Unsupported event code %x",
					info.event);
		} // // System Common, Meta or SysEx event
	}// switch (info.command())
}

void MidiParser_SCI::allNotesOff() {
	if (!_driver)
		return;

	int i, j;

	// Turn off all active notes
	for (i = 0; i < 128; ++i) {
		for (j = 0; j < 16; ++j) {
			if ((_active_notes[i] & (1 << j)) && (_channelRemap[j] != -1)){
				sendToDriver(0x80 | j, i, 0);
			}
		}
	}

	// Turn off all hanging notes
	for (i = 0; i < ARRAYSIZE(_hanging_notes); i++) {
		byte midiChannel = _hanging_notes[i].channel;
		if ((_hanging_notes[i].time_left) && (_channelRemap[midiChannel] != -1)) {
			sendToDriver(0x80 | midiChannel, _hanging_notes[i].note, 0);
			_hanging_notes[i].time_left = 0;
		}
	}
	_hanging_notes_count = 0;

	// To be sure, send an "All Note Off" event (but not all MIDI devices
	// support this...).

	for (i = 0; i < 16; ++i) {
		if (_channelRemap[i] != -1)
			sendToDriver(0xB0 | i, 0x7b, 0); // All notes off
	}

	memset(_active_notes, 0, sizeof(_active_notes));
}

void MidiParser_SCI::setMasterVolume(byte masterVolume) {
	assert(masterVolume <= MUSIC_MASTERVOLUME_MAX);
	_masterVolume = masterVolume;
	switch (_soundVersion) {
	case SCI_VERSION_0_EARLY:
	case SCI_VERSION_0_LATE:
		// update driver master volume
		setVolume(_volume);
		break;

	case SCI_VERSION_1_EARLY:
	case SCI_VERSION_1_LATE:
	case SCI_VERSION_2_1:
		// directly set master volume (global volume is merged with channel volumes)
		((MidiPlayer *)_driver)->setVolume(masterVolume);
		break;

	default:
		error("MidiParser_SCI::setVolume: Unsupported soundVersion");
	}
}

void MidiParser_SCI::setVolume(byte volume) {
	assert(volume <= MUSIC_VOLUME_MAX);
	_volume = volume;

	switch (_soundVersion) {
	case SCI_VERSION_0_EARLY:
	case SCI_VERSION_0_LATE: {
		// SCI0 adlib driver doesn't support channel volumes, so we need to go this way
		int16 globalVolume = _volume * _masterVolume / MUSIC_VOLUME_MAX;
		((MidiPlayer *)_driver)->setVolume(globalVolume);
		break;
	}

	case SCI_VERSION_1_EARLY:
	case SCI_VERSION_1_LATE:
	case SCI_VERSION_2_1:
		// Send previous channel volumes again to actually update the volume
		for (int i = 0; i < 15; i++)
			if (_channelRemap[i] != -1)
				sendToDriver(0xB0 + i, 7, _channelVolume[i]);
		break;

	default:
		error("MidiParser_SCI::setVolume: Unsupported soundVersion");
	}
}

} // End of namespace Sci
