/* ScummVM - Scumm Interpreter
 * Copyright (C) 2001-2003 The ScummVM project
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * $Header: /cvsroot/scummvm/scummvm/sound/midiparser.cpp,v 1.14 2003/07/10 04:34:44 jamieson630 Exp $
 *
 */

#include "stdafx.h"
#include "midiparser.h"
#include "mididrv.h"
#include "common/util.h"



//////////////////////////////////////////////////
//
// MidiParser implementation
//
//////////////////////////////////////////////////

MidiParser::MidiParser() :
_hanging_notes_count (0),
_driver (0),
_timer_rate (0x4A0000),
_ppqn (96),
_tempo (500000),
_psec_per_tick (5208), // 500000 / 96
_autoLoop (false),
_smartJump (false),
_num_tracks (0),
_active_track (255),
_abort_parse (0)
{
	memset (_active_notes, 0, sizeof(_active_notes));
}

void MidiParser::property (int prop, int value) {
	switch (prop) {
	case mpAutoLoop:
		_autoLoop = (value != 0);
	case mpSmartJump:
		_smartJump = (value != 0);
	}
}

void MidiParser::setTempo (uint32 tempo) {
	_tempo = tempo;
	if (_ppqn)
		_psec_per_tick = (tempo + (_ppqn >> 2)) / _ppqn;
}

// This is the conventional (i.e. SMF) variable length quantity
uint32 MidiParser::readVLQ (byte * &data) {
	byte str;
	uint32 value = 0;
	int i;

	for (i = 0; i < 4; ++i) {
		str = data[0];
		++data;
		value = (value << 7) | (str & 0x7F);
		if (!(str & 0x80))
			break;
	}
	return value;
}

void MidiParser::activeNote (byte channel, byte note, bool active) {
	if (note >= 128 || channel >= 16)
		return;

	if (active)
		_active_notes[note] |= (1 << channel);
	else
		_active_notes[note] &= ~(1 << channel);

	// See if there are hanging notes that we can cancel
	NoteTimer *ptr = _hanging_notes;
	int i;
	for (i = ARRAYSIZE(_hanging_notes); i; --i, ++ptr) {
		if (ptr->channel == channel && ptr->note == note && ptr->time_left) {
			ptr->time_left = 0;
			--_hanging_notes_count;
			break;
		}
	}
}

void MidiParser::hangingNote (byte channel, byte note, uint32 time_left, bool recycle) {
	NoteTimer *best = 0;
	NoteTimer *ptr = _hanging_notes;
	int i;

	if (_hanging_notes_count >= ARRAYSIZE(_hanging_notes)) {
		printf ("WARNING! MidiParser::hangingNote(): Exceeded polyphony!\n");
		return;
	}

	for (i = ARRAYSIZE(_hanging_notes); i; --i, ++ptr) {
		if (ptr->channel == channel && ptr->note == note) {
			if (ptr->time_left && ptr->time_left < time_left && recycle)
				return;
			best = ptr;
			if (ptr->time_left) {
				if (recycle) _driver->send (0x80 | channel | note << 8);
				--_hanging_notes_count;
			}
			break;
		} else if (!best && ptr->time_left == 0) {
			best = ptr;
		}
	}

	// Occassionally we might get a zero or negative note
	// length, if the note should be turned on and off in
	// the same iteration. For now just set it to 1 and
	// we'll turn it off in the next cycle.
	if (!time_left || time_left & 0x80000000)
		time_left = 1;

	if (best) {
		best->channel = channel;
		best->note = note;
		best->time_left = time_left;
		++_hanging_notes_count;
	} else {
		// We checked this up top. We should never get here!
		printf ("WARNING! MidiParser::hangingNote(): Internal error!\n");
	}
}

void MidiParser::onTimer() {
	uint32 end_time;
	uint32 event_time;

	if (!_position._play_pos || !_driver)
		return;

	_abort_parse = false;
	end_time = _position._play_time + _timer_rate;

	// Scan our hanging notes for any
	// that should be turned off.
	if (_hanging_notes_count) {
		NoteTimer *ptr = &_hanging_notes[0];
		int i;
		for (i = ARRAYSIZE(_hanging_notes); i; --i, ++ptr) {
			if (ptr->time_left) {
				if (ptr->time_left <= _timer_rate) {
					_driver->send (0x80 | ptr->channel | ptr->note << 8);
					ptr->time_left = 0;
					--_hanging_notes_count;
				} else {
					ptr->time_left -= _timer_rate;
				}
			}
		}
	}

	while (!_abort_parse) {
		EventInfo &info = _next_event;

		event_time = _position._last_event_time + info.delta * _psec_per_tick;
		if (event_time > end_time)
			break;

		// Process the next info.
		_position._last_event_tick += info.delta;
		if (info.event < 0x80) {
			printf ("ERROR! Bad command or running status %02X\n", info.event);
			_position._play_pos = 0;
			return;
		}

		if (info.event == 0xF0) {
			// SysEx event
			_driver->sysEx (info.ext.data, (uint16) info.length);
		} else if (info.event == 0xFF) {
			// META event
			if (info.ext.type == 0x2F) {
				// End of Track must be processed by us,
				// as well as sending it to the output device.
				if (_autoLoop) {
					jumpToTick (0);
					parseNextEvent (_next_event);
				} else {
					allNotesOff();
					resetTracking();
					_driver->metaEvent (info.ext.type, info.ext.data, (uint16) info.length);
				}
				return;
			} else if (info.ext.type == 0x51) {
				if (info.length >= 3) {
					setTempo (info.ext.data[0] << 16 | info.ext.data[1] << 8 | info.ext.data[2]);
				}
			}
			_driver->metaEvent (info.ext.type, info.ext.data, (uint16) info.length);
		} else {
			if (info.command() == 0x8) {
				activeNote (info.channel(), info.basic.param1, false);
			} else if (info.command() == 0x9) {
				if (info.length > 0)
					hangingNote (info.channel(), info.basic.param1, info.length * _psec_per_tick - (end_time - event_time));
				else
					activeNote (info.channel(), info.basic.param1, true);
			}
			_driver->send (info.event | info.basic.param1 << 8 | info.basic.param2 << 16);
		}


		if (!_abort_parse) {
			_position._last_event_time = event_time;
			parseNextEvent (_next_event);
		}
	}

	if (!_abort_parse) {
		_position._play_time = end_time;
		_position._play_tick = (_position._play_time - _position._last_event_time) / _psec_per_tick + _position._last_event_tick;
	}
}

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

	int i;
	for (i = 0; i < 16; ++i)
		_driver->send (0x007BB0 | i);
	for (i = 0; i < ARRAYSIZE(_hanging_notes); ++i)
		_hanging_notes[i].time_left = 0;
	_hanging_notes_count = 0;
	memset (_active_notes, 0, sizeof(_active_notes));
}

void MidiParser::resetTracking() {
	_position.clear();
}

bool MidiParser::setTrack (int track) {
	if (track >= _num_tracks)
		return false;
	else if (track == _active_track)
		return true;

	if (_smartJump)
		hangAllActiveNotes();
	else
		allNotesOff();

	resetTracking();
	_active_track = track;
	_position._play_pos = _tracks[track];
	parseNextEvent (_next_event);
	return true;
}

void MidiParser::hangAllActiveNotes() {
	// Search for note off events until we have
	// accounted for every active note.
	uint16 temp_active [128];
	memcpy (temp_active, _active_notes, sizeof (temp_active));

	uint32 advance_tick = _position._last_event_tick;
	while (true) {
		int i, j;
		for (i = 0; i < 128; ++i)
			if (temp_active[i] != 0) break;
		if (i == 128) break;
		parseNextEvent (_next_event);
		advance_tick += _next_event.delta;
		if (_next_event.command() == 0x8) {
			if (temp_active[_next_event.basic.param1] & (1 << _next_event.channel())) {
				hangingNote (_next_event.channel(), _next_event.basic.param1, (advance_tick - _position._last_event_tick) * _psec_per_tick, false);
				temp_active[_next_event.basic.param1] &= ~ (1 << _next_event.channel());
			}
		} else if (_next_event.event == 0xFF && _next_event.ext.type == 0x2F) {
			// printf ("MidiParser::hangAllActiveNotes(): Hit End of Track with active notes left!\n");
			for (i = 0; i < 128; ++i) {
				for (j = 0; j < 16; ++j) {
					if (temp_active[i] & (1 << j)) {
						activeNote (j, i, false);
						_driver->send (0x80 | j | i << 8);
					}
				}
			}
			break;
		}
	}
}

bool MidiParser::jumpToTick (uint32 tick, bool fireEvents) {
	if (_active_track >= _num_tracks)
		return false;

	Tracker currentPos (_position);
	EventInfo currentEvent (_next_event);

	resetTracking();
	_position._play_pos = _tracks[_active_track];
	parseNextEvent (_next_event);
	if (tick > 0) {
		while (true) {
			EventInfo &info = _next_event;
			if (_position._last_event_tick + info.delta >= tick) {
				_position._play_time += (tick - _position._last_event_tick) * _psec_per_tick;
				_position._play_tick = tick;
				break;
			}

			_position._last_event_tick += info.delta;
			_position._last_event_time += info.delta * _psec_per_tick;
			_position._play_tick = _position._last_event_tick;
			_position._play_time = _position._last_event_time;

			if (info.event == 0xFF) {
				if (info.ext.type == 0x2F) { // End of track
					if (_autoLoop) {
						_position._play_pos = _tracks[_active_track];
						parseNextEvent (_next_event);
					} else {
						_position = currentPos;
						_next_event = currentEvent;
						return false;
					}
					break;
				} else if (info.ext.type == 0x51) { // Tempo
					if (info.length >= 3) {
						setTempo (info.ext.data[0] << 16 | info.ext.data[1] << 8 | info.ext.data[2]);
					}
				}
			} else if (fireEvents) {
				if (info.event == 0xF0)
					_driver->sysEx (info.ext.data, (uint16) info.length);
				else
					_driver->send (info.event | info.basic.param1 << 8 | info.basic.param2 << 16);
			}

			parseNextEvent (_next_event);
		}
	}

	if (!_smartJump || !currentPos._play_pos) {
		allNotesOff();
	} else {
		EventInfo targetEvent (_next_event);
		Tracker targetPosition (_position);

		_position = currentPos;
		_next_event = currentEvent;
		hangAllActiveNotes();

		_next_event = targetEvent;
		_position = targetPosition;
	}

	_abort_parse = true;
	return true;
}

void MidiParser::unloadMusic() {
	resetTracking();
	allNotesOff();
	_num_tracks = 0;
	_active_track = 255;
	_abort_parse = true;
}
