//**************************************************************************
//**
//**	##   ##    ##    ##   ##   ####     ####   ###     ###
//**	##   ##  ##  ##  ##   ##  ##  ##   ##  ##  ####   ####
//**	 ## ##  ##    ##  ## ##  ##    ## ##    ## ## ## ## ##
//**	 ## ##  ########  ## ##  ##    ## ##    ## ##  ###  ##
//**	  ###   ##    ##   ###    ##  ##   ##  ##  ##       ##
//**	   #    ##    ##    #      ####     ####   ##       ##
//**
//**	$Id: snd_win32music.cpp 4122 2010-01-05 14:56:31Z firebrand_kh $
//**
//**	Copyright (C) 1999-2006 Jānis Legzdiņš
//**
//**	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.
//**
//**************************************************************************

// HEADER FILES ------------------------------------------------------------

#include "winlocal.h"
#include <mmsystem.h>
#include "gamedefs.h"
#include "snd_local.h"

// MACROS ------------------------------------------------------------------

// TYPES -------------------------------------------------------------------

class VMMSystemMidiDevice : public VMidiDevice
{
public:
	enum { MAX_TRACKS = 32 };
	enum { MAX_TICKS = 0xFFFFFFFF };

	//	Number and size of playback buffers to keep around
	enum { C_MIDI_BUFFERS = 4 };
	enum { CB_MIDI_BUFFERS = 1024 };

	enum
	{
		STATE_NoFile,
		STATE_Opened,
		STATE_Prerolling,
		STATE_Prerolled,
		STATE_Playing,
		STATE_Paused,
		STATE_Stopping,
		STATE_Reset,
	};

	enum EResult
	{
		RES_Success,
		RES_InvalidFile,
		RES_EndOfFile
	};

	//	Various flags.
	enum
	{
		TRACKF_Eot			= 0x00000001,

		SEQF_Eof			= 0x00000001,
		SEQF_Waiting		= 0x00000002,

		SMFF_Eof			= 0x00000001,
		SMFF_InsertSysEx	= 0x00000002
	};

	//	Midi events.
	enum
	{
		MIDI_Msg		= 0x80,
		MIDI_Meta		= 0xFF,
		MIDI_SysEx		= 0xF0,
		MIDI_SysExEnd	= 0xF7,

		MIDI_MetaEot	= 0x2F,
		MIDI_MetaTempo	= 0x51,
	};

#pragma pack(1)
	struct FChunkHdr
	{
		char			Type[4];
		vuint32			Length;
	};

	struct FMidiFileHdr
	{
		vuint16			Format;
		vuint16			Tracks;
		vuint16			Division;
	};
#pragma pack()

	struct FEvent
	{
		DWORD			Delta;
		byte			Event[3];
		DWORD			ParmCount;
		const byte*		Parm;
	};

	struct FTrack
	{
		DWORD			FileOffset;
		DWORD			TotalLength;

		DWORD			Position;
		DWORD			BytesLeft;
		const byte*		ImagePtr;
		byte			RunningStatus;

		DWORD			TrackFlags;
	};

	bool			MusicPaused;
	float			MusVolume;

	const byte*		MidiImage;
	DWORD			MidiImageSize;

	int				State;				//	Sequencer state (SEQ_S_xxx)
	HMIDIOUT		hMidi;				//	Handle to open MIDI device
	byte			BufAlloc[(sizeof(MIDIHDR) + CB_MIDI_BUFFERS) * C_MIDI_BUFFERS];	//	Streaming buffers -- initial allocation
	LPMIDIHDR		FreeBuffers;		//	Streaming buffers -- free list
	int				BuffersInMMSYSTEM;	//	Streaming buffers -- in use
	DWORD			SeqFlags;			//	Various sequencer flags

	DWORD			MidiPosition;
	DWORD			Format;
	DWORD			NumTracks;
	DWORD			TimeDivision;
	DWORD			SmfFlags;

	DWORD			PendingUserEvent;
	DWORD			PendingUserEventCount;
	const byte*		PendingUserEvents;

	FTrack			Tracks[MAX_TRACKS];

	static const int ChanMsgLen[];

	VMMSystemMidiDevice();
	void Init();
	void Shutdown();
	void SetVolume(float);
	void Tick(float);
	void Play(void*, int, const char*, bool);
	void Pause();
	void Resume();
	void Stop();
	bool IsPlaying();

	MMRESULT Preroll();
	void Callback(UINT, DWORD);
	static void PASCAL StaticCallback(HMIDISTRM, UINT, DWORD, DWORD, DWORD);

	EResult ReadEvents(LPMIDIHDR);
	bool InsertParmData(DWORD, LPMIDIHDR);
	void SeekStart();
	bool BuildFileIndex();
	EResult GetNextEvent(FEvent&);
	static DWORD GetVDword(const byte*, DWORD, DWORD&);
};

// EXTERNAL FUNCTION PROTOTYPES --------------------------------------------

// PUBLIC FUNCTION PROTOTYPES ----------------------------------------------

// PRIVATE FUNCTION PROTOTYPES ---------------------------------------------

// EXTERNAL DATA DECLARATIONS ----------------------------------------------

// PUBLIC DATA DEFINITIONS -------------------------------------------------

IMPLEMENT_MIDI_DEVICE(VMMSystemMidiDevice, MIDIDRV_Default, "Default",
	"Windows multimedia system midi device", NULL);

// PRIVATE DATA DEFINITIONS ------------------------------------------------

const int VMMSystemMidiDevice::ChanMsgLen[] =
{ 
	0,                      /* 0x   not a status byte   */
	0,                      /* 1x   not a status byte   */
	0,                      /* 2x   not a status byte   */
	0,                      /* 3x   not a status byte   */
	0,                      /* 4x   not a status byte   */
	0,                      /* 5x   not a status byte   */
	0,                      /* 6x   not a status byte   */
	0,                      /* 7x   not a status byte   */
	3,                      /* 8x   Note off            */
	3,                      /* 9x   Note on             */
	3,                      /* Ax   Poly pressure       */
	3,                      /* Bx   Control change      */
	2,                      /* Cx   Program change      */
	2,                      /* Dx   Chan pressure       */
	3,                      /* Ex   Pitch bend change   */
	0,                      /* Fx   SysEx (see below)   */
} ;

// CODE --------------------------------------------------------------------

//==========================================================================
//
//	VMMSystemMidiDevice::VMMSystemMidiDevice
//
//==========================================================================

VMMSystemMidiDevice::VMMSystemMidiDevice()
: MidiImage(NULL)
, MusicPaused(false)
, MusVolume(-1)
{
}

//==========================================================================
//
//	VMMSystemMidiDevice::Init
//
//==========================================================================

void VMMSystemMidiDevice::Init()
{
	guard(VMMSystemMidiDevice::Init);
	State = STATE_NoFile;
	FreeBuffers = NULL;
	hMidi = NULL;
	BuffersInMMSYSTEM = 0;
	SeqFlags = 0;

	//	Initialise all MIDIHDR's and throw them into a free list
	LPBYTE Work = BufAlloc;
	for (int i = 0; i < C_MIDI_BUFFERS; i++)
	{
		((LPMIDIHDR)Work)->lpNext = FreeBuffers;
		((LPMIDIHDR)Work)->lpData = (char*)(Work + sizeof(MIDIHDR));
		((LPMIDIHDR)Work)->dwBufferLength = CB_MIDI_BUFFERS;
		((LPMIDIHDR)Work)->dwBytesRecorded = 0;
		((LPMIDIHDR)Work)->dwUser = 0;
		((LPMIDIHDR)Work)->dwFlags = 0;

		FreeBuffers = (LPMIDIHDR)Work;

		Work += sizeof(MIDIHDR) + CB_MIDI_BUFFERS;
	}

	Initialised = true;
	unguard;
}

//==========================================================================
//
//	VMMSystemMidiDevice::Shutdown
//
//==========================================================================

void VMMSystemMidiDevice::Shutdown()
{
	guard(VMMSystemMidiDevice::Shutdown);
	if (Initialised)
	{
		Stop();
		Initialised = false;
	}
	unguard;
}

//==========================================================================
//
//	VMMSystemMidiDevice::SetVolume
//
//==========================================================================

void VMMSystemMidiDevice::SetVolume(float Volume)
{
	guard(VMMSystemMidiDevice::SetVolume);
	if (Volume != MusVolume)
	{
		MusVolume = Volume;
		if (hMidi)
		{
			midiOutSetVolume(hMidi, (DWORD)(0xffff * MusVolume) |
				((DWORD)(0xffff * MusVolume) << 16));
		}
	}
	unguard;
}

//==========================================================================
//
//	VMMSystemMidiDevice::Tick
//
//==========================================================================

void VMMSystemMidiDevice::Tick(float)
{
}

//==========================================================================
//
//	VMMSystemMidiDevice::Play
//
//==========================================================================

void VMMSystemMidiDevice::Play(void* Data, int len, const char* song, bool loop)
{
	guard(VMMSystemMidiDevice::Play);
	//	Stop, close, etc. if we're still playing
	Stop();

	//	Open new file
	SmfFlags = 0;
	PendingUserEvent = 0;
	PendingUserEventCount = 0;
	PendingUserEvents = NULL;

	MidiImage = (byte*)Data;
	MidiImageSize = len;

	//	If the file exists, parse it just enough to pull out the header
	// and build a track index.
	if (!BuildFileIndex())
	{
		return;
	}
	State = STATE_Opened;

	//	Preroll
	if (Preroll() != MMSYSERR_NOERROR)
	{
		return;
	}

	//	Play
	if (hMidi)
	{
		State = STATE_Playing;
		midiStreamRestart((HMIDISTRM)hMidi);

		//	Pause if needed.
		if (!MusVolume || MusicPaused)
		{
			Pause();
		}
		CurrSong = VName(song, VName::AddLower8);
		CurrLoop = loop;
	}
	unguard;
}

//==========================================================================
//
//	VMMSystemMidiDevice::Pause
//
//==========================================================================

void VMMSystemMidiDevice::Pause()
{
	guard(VMMSystemMidiDevice::Pause);
	if (State != STATE_Playing)
		return;

	if (hMidi)
	{
		State = STATE_Paused;
		midiStreamPause((HMIDISTRM)hMidi);
		MusicPaused = true;
	}
	unguard;
}

//==========================================================================
//
//	VMMSystemMidiDevice::Resume
//
//==========================================================================

void VMMSystemMidiDevice::Resume()
{
	guard(VMMSystemMidiDevice::Resume);
	if (State != STATE_Paused)
		return;

	if (hMidi)
	{
		State = STATE_Playing;
		midiStreamRestart((HMIDISTRM)hMidi);
		MusicPaused = false;
	}
	unguard;
}

//==========================================================================
//
//	VMMSystemMidiDevice::IsPlaying
//
//	Is the song playing?
//
//==========================================================================

bool VMMSystemMidiDevice::IsPlaying()
{
	guard(VMMSystemMidiDevice::IsPlaying);
	return State == STATE_Playing || State == STATE_Paused;
	unguard;
}

//==========================================================================
//
//  VMMSystemMidiDevice::Stop
//
//==========================================================================

void VMMSystemMidiDevice::Stop()
{
	guard(VMMSystemMidiDevice::Stop);
	if (MidiImage)
	{
		if (hMidi != NULL)
		{
			if (MusicPaused)
			{
				Resume();
			}
			if (State != STATE_Playing && State != STATE_Paused)
			{
				SeqFlags &= ~SEQF_Waiting;
			}
			else
			{
				State = STATE_Stopping;
				SeqFlags |= SEQF_Waiting;

				if (!hMidi || MMSYSERR_NOERROR != midiStreamStop((HMIDISTRM)hMidi))
				{
					SeqFlags &= ~SEQF_Waiting;
					return;
				}

				while (BuffersInMMSYSTEM)
					Sleep(0);
			}

			//	Close file.
			if (State == STATE_Opened)
			{
				//	If we were prerolled, need to clean up -- have an open MIDI
				// handle and buffers in the ready queue
				for (LPMIDIHDR lpmh = FreeBuffers; lpmh; lpmh = lpmh->lpNext)
					midiOutUnprepareHeader(hMidi, lpmh, sizeof(*lpmh));
			}

			//	Close midi stream.
			if (hMidi)
			{
				midiStreamClose((HMIDISTRM)hMidi);
				hMidi = NULL;
			}
		}
		State = STATE_NoFile;
		Z_Free((void*)MidiImage);
		MidiImage = NULL;
		CurrSong = NAME_None;
	}
	unguard;
}

//==========================================================================
//
//	VMMSystemMidiDevice::Preroll
//
//	Prepares the file for playback at the given position.
//
//==========================================================================

MMRESULT VMMSystemMidiDevice::Preroll()
{
	guard(VMMSystemMidiDevice::Preroll);
	EResult				smfrc;
	MMRESULT			mmrc = MMSYSERR_NOERROR;
	MIDIPROPTIMEDIV		mptd;
	LPMIDIHDR			lpmh = NULL;
	UINT				uDeviceID;

	if (hMidi)
	{
		//	Recollect buffers from MMSYSTEM back into free queue
		State = STATE_Reset;
		midiOutReset(hMidi);

		while (BuffersInMMSYSTEM)
			Sleep(0);
	}

	BuffersInMMSYSTEM = 0;
	State = STATE_Prerolling;

	//
	// We've successfully opened the file and all of the tracks; now
	// open the MIDI device and set the time division.
	//
	if (!hMidi)
	{
		uDeviceID = MIDI_MAPPER;
		if ((mmrc = midiStreamOpen((HMIDISTRM*)&hMidi, &uDeviceID, 1,
			(DWORD)StaticCallback, (DWORD)this, CALLBACK_FUNCTION)) != MMSYSERR_NOERROR)
		{
			hMidi = NULL;
			goto seq_Preroll_Cleanup;
		}

		midiOutSetVolume(hMidi, (DWORD)(0xffff * MusVolume) |
			((DWORD)(0xffff * MusVolume) << 16));

		mptd.cbStruct  = sizeof(mptd);
		mptd.dwTimeDiv = TimeDivision;
		if ((mmrc = midiStreamProperty((HMIDISTRM)hMidi,
			(LPBYTE)&mptd, MIDIPROP_SET | MIDIPROP_TIMEDIV)) != MMSYSERR_NOERROR)
		{
			midiStreamClose((HMIDISTRM)hMidi);
			hMidi = NULL;
			mmrc = MCIERR_DEVICE_NOT_READY;
			goto seq_Preroll_Cleanup;
		}
	}

	mmrc = MMSYSERR_NOERROR;

	SeekStart();
	smfrc = RES_Success;

	SeqFlags &= ~SEQF_Eof;
	while (FreeBuffers)
	{
		lpmh = FreeBuffers;
		FreeBuffers = lpmh->lpNext;

		smfrc = ReadEvents(lpmh);
		if (RES_Success != smfrc && RES_EndOfFile != smfrc)
		{
			GCon->Log("Preroll: Invalid file");
			mmrc = MCIERR_INVALID_FILE;
			goto seq_Preroll_Cleanup;
		}

		if (MMSYSERR_NOERROR != (mmrc = midiOutPrepareHeader(hMidi, lpmh, sizeof(*lpmh))))
		{
			GCon->Log("Preroll: midiOutPrepareHeader failed");
			goto seq_Preroll_Cleanup;
		}

		if (MMSYSERR_NOERROR != (mmrc = midiStreamOut((HMIDISTRM)hMidi, lpmh, sizeof(*lpmh))))
		{
			GCon->Log("Preroll: midiStreamOut failed");
			goto seq_Preroll_Cleanup;
		}

		BuffersInMMSYSTEM++;

		if (smfrc == RES_EndOfFile)
		{
			SeqFlags |= SEQF_Eof;
			break;
		}
	}

seq_Preroll_Cleanup:
	if (MMSYSERR_NOERROR != mmrc)
	{
		State = STATE_Opened;
		SeqFlags &= ~SEQF_Waiting;
	}
	else
	{
		State = STATE_Prerolled;
	}

	return mmrc;
	unguard;
}

//==========================================================================
//
//	VMMSystemMidiDevice::Callback
//
//	Called by the system when a buffer is done.
//
//==========================================================================

void VMMSystemMidiDevice::Callback(UINT uMsg, DWORD dw1)
{
	guard(VMMSystemMidiDevice::Callback);
	LPMIDIHDR		lpmh = (LPMIDIHDR)dw1;

	if (uMsg != MOM_DONE)
		return;

	BuffersInMMSYSTEM--;
	if (State == STATE_Reset)
	{
		//	We're recollecting buffers from MMSYSTEM
		lpmh->lpNext = FreeBuffers;
		FreeBuffers = lpmh;
		return;
	}

	if ((State == STATE_Stopping) || (SeqFlags & SEQF_Eof))
	{
		//	Reached EOF, just put the buffer back on the free list
		lpmh->lpNext = FreeBuffers;
		FreeBuffers = lpmh;

		midiOutUnprepareHeader(hMidi, lpmh, sizeof(*lpmh));

		if (BuffersInMMSYSTEM == 0)
		{
			SeqFlags &= ~SEQF_Waiting;
			//	Restart stream if we are looping.
			if (CurrLoop && State != STATE_Stopping)
			{
				State = STATE_Opened;
				if (Preroll() == MMSYSERR_NOERROR)
				{
					State = STATE_Playing;
					midiStreamRestart((HMIDISTRM)hMidi);
				}
			}
			else
			{
				//	Totally done! Free device.
				midiStreamClose((HMIDISTRM)hMidi);
	
				hMidi = NULL;
				State = STATE_Opened;
			}
		}
	}
	else
	{
		//	Not EOF yet; attempt to fill another buffer
		EResult smfrc = ReadEvents(lpmh);

		switch(smfrc)
		{
		case RES_Success:
			break;

		case RES_EndOfFile:
			SeqFlags |= SEQF_Eof;
			smfrc = RES_Success;
			break;

		default:
			State = STATE_Stopping;
			break;
		}

		if (smfrc == RES_Success)
		{
			BuffersInMMSYSTEM++;
			if (midiStreamOut((HMIDISTRM)hMidi, lpmh, sizeof(*lpmh)) != MMSYSERR_NOERROR)
			{
				BuffersInMMSYSTEM--;
				State = STATE_Stopping;
			}
		}
	}
	unguard;
}

//==========================================================================
//
//	VMMSystemMidiDevice::StaticCallback
//
//==========================================================================

void PASCAL VMMSystemMidiDevice::StaticCallback(HMIDISTRM,
	UINT uMsg, DWORD inst, DWORD dw1, DWORD)
{
	((VMMSystemMidiDevice*)inst)->Callback(uMsg, dw1);
}

//==========================================================================
//
//	VMMSystemMidiDevice::ReadEvents
//
//	This function reads events from a track.
//
//==========================================================================

VMMSystemMidiDevice::EResult VMMSystemMidiDevice::ReadEvents(LPMIDIHDR lpmh)
{
	guard(VMMSystemMidiDevice::ReadEvents);
	EResult			smfrc;
	FEvent			event;
	LPDWORD			lpdw;
	DWORD			Tempo;

	//	Read events from the track and pack them into the buffer in polymsg
	// format.
	//	If a SysEx or meta would go over a buffer boundry, split it.
	lpmh->dwBytesRecorded = 0;
	if (PendingUserEvent)
	{
		if (!InsertParmData(0, lpmh))
		{
			return RES_InvalidFile;
		}
	}

	lpdw = (LPDWORD)(lpmh->lpData + lpmh->dwBytesRecorded);

	if (SmfFlags & SMFF_Eof)
	{
		return RES_EndOfFile;
	}

	while (1)
	{
		//	If we know ahead of time we won't have room for the event, just
		// break out now. We need 2 DWORD's for the terminator event and at
		// least 2 DWORD's for any event we might store - this will allow us
		// a full short event or the delta time and stub for a long event to
		// be split.
		if (lpmh->dwBufferLength - lpmh->dwBytesRecorded < 4 * sizeof(DWORD))
		{
			break;
		}

		smfrc = GetNextEvent(event);
		if (RES_Success != smfrc)
		{
			break;
		}

		if (MIDI_SysEx > event.Event[0])
		{
			*lpdw++ = (DWORD)event.Delta;
			*lpdw++ = 0;
			*lpdw++ = (((DWORD)MEVT_SHORTMSG) << 24) |
						((DWORD)event.Event[0]) |
						(((DWORD)event.Event[1]) << 8) |
						(((DWORD)event.Event[2]) << 16);

			lpmh->dwBytesRecorded += 3 * sizeof(DWORD);
		}
		else if (MIDI_Meta == event.Event[0] &&
			MIDI_MetaEot == event.Event[1])
		{
			//	These are ignoreable since smfReadNextEvent()
			// takes care of track merging
		}
		else if (MIDI_Meta == event.Event[0] &&
			MIDI_MetaTempo == event.Event[1])
		{
			if (event.ParmCount != 3)
			{
				GCon->Log("ReadEvents: Corrupt tempo event");
				return RES_InvalidFile;
			}

			Tempo = (((DWORD)MEVT_TEMPO) << 24)|
						(((DWORD)event.Parm[0]) << 16)|
						(((DWORD)event.Parm[1]) << 8)|
						((DWORD)event.Parm[2]);

			*lpdw++ = (DWORD)event.Delta;
			*lpdw++ = 0;
			*lpdw++ = Tempo;

			lpmh->dwBytesRecorded += 3 * sizeof(DWORD);
		}
		else if (MIDI_Meta != event.Event[0])
		{
			//	Must be F0 or F7 system exclusive or FF meta
			// that we didn't recognize
			PendingUserEventCount = event.ParmCount;
			PendingUserEvents = event.Parm;
			SmfFlags &= ~SMFF_InsertSysEx;

			switch (event.Event[0])
			{
			case MIDI_SysEx:
				SmfFlags |= SMFF_InsertSysEx;
				PendingUserEventCount++;
				//	Falling through...
			case MIDI_SysExEnd:
				PendingUserEvent = ((DWORD)MEVT_LONGMSG) << 24;
				break;
			}

			if (!InsertParmData(event.Delta, lpmh))
			{
				return RES_InvalidFile;
			}

			lpdw = (LPDWORD)(lpmh->lpData + lpmh->dwBytesRecorded);
		}
	}

	return (SmfFlags & SMFF_Eof) ? RES_EndOfFile : RES_Success;
	unguard;
}

//==========================================================================
//
//	VMMSystemMidiDevice::InsertParmData
//
//	Inserts pending long data from a track into the given buffer.
//
//	Fills as much data as will fit while leaving room for the buffer
// terminator.
//
//	If the long data is depleted, resets PendingUserEvent so that the next
// event may be read.
//
//==========================================================================

bool VMMSystemMidiDevice::InsertParmData(DWORD Delta, LPMIDIHDR lpmh)
{
	guard(VMMSystemMidiDevice::InsertParmData);
	//	Can't fit 4 DWORD's? (Delta + stream-id + event + some data)
	// Can't do anything.
	if (lpmh->dwBufferLength - lpmh->dwBytesRecorded < 4 * sizeof(DWORD))
	{
		if (0 == Delta)
			return true;

		//	If we got here with a real delta, that means ReadEvents screwed
		// up calculating left space and we should flag it somehow.
		GCon->Log("Can't fit initial piece of SysEx into buffer!");
		return false;
	}

	LPDWORD pData = (LPDWORD)(lpmh->lpData + lpmh->dwBytesRecorded);

	DWORD Length = lpmh->dwBufferLength - lpmh->dwBytesRecorded - 3 * sizeof(DWORD);
	Length = min(Length, PendingUserEventCount);

	*pData++ = (DWORD)Delta;
	*pData++ = 0;
	*pData++ = (PendingUserEvent & 0xFF000000) | (Length & 0x00FFFFFF);

	DWORD Rounded = (Length + 3) & (~3);

	if (SmfFlags & SMFF_InsertSysEx)
	{
		*((LPBYTE)pData) = MIDI_SysEx;
		pData = (LPDWORD)(((LPBYTE)pData) + 1);
		SmfFlags &= ~SMFF_InsertSysEx;
		Length--;
		PendingUserEventCount--;
	}

	if (Length & 0x80000000)
	{
		GCon->Logf("Length %08lX  dwBytesRecorded %08lX  dwBufferLength %08lX",
			Length, lpmh->dwBytesRecorded, lpmh->dwBufferLength);
		GCon->Logf("PendingUserEventCount %08lX  PendingUserEvent %08lX Rounded %08lX",
			PendingUserEventCount, PendingUserEvent, Rounded);
		GCon->Logf("Offset into MIDI image %08lX", (DWORD)(PendingUserEvents - MidiImage));
		GCon->Logf("!memcpy is about to fault");
	}

	memcpy(pData, PendingUserEvents, Length);
	PendingUserEventCount -= Length;
	if (!PendingUserEventCount)
		PendingUserEvent = 0;

	lpmh->dwBytesRecorded += 3*sizeof(DWORD) + Rounded;

	return true;
	unguard;
}

//==========================================================================
//
//	VMMSystemMidiDevice::SeekStart
//
//==========================================================================

void VMMSystemMidiDevice::SeekStart()
{
	guard(VMMSystemMidiDevice::SeekStart);
	FTrack*					ptrk;
	DWORD                   idxTrack;

	MidiPosition = 0;
	SmfFlags &= ~SMFF_Eof;

	for (ptrk = Tracks, idxTrack = NumTracks; idxTrack--; ptrk++)
	{
		ptrk->Position = 0;
		ptrk->BytesLeft = ptrk->TotalLength;
		ptrk->ImagePtr = MidiImage + ptrk->FileOffset;
		ptrk->RunningStatus = 0;
		ptrk->TrackFlags = 0;
	}
	unguard;
}

//==========================================================================
//
//	VMMSystemMidiDevice::BuildFileIndex
//
//	Preliminary parsing of a MIDI file.
//
//	This function validates the format of and existing MIDI file and builds
// the handle structure which will refer to it for the lifetime of the
// instance.
//
//	The file header information will be read and verified, and
// BuildTrackIndices will be called on every existing track
// to build keyframes and validate the track format.
//
//==========================================================================

bool VMMSystemMidiDevice::BuildFileIndex()
{
	guard(VMMSystemMidiDevice::BuildFileIndex);
	EResult					smfrc;
	const FChunkHdr*		pCh;
	const FMidiFileHdr*		pFh;
	DWORD					idx;
	FTrack*					pTrk;
	DWORD					dwLeft;
	const byte*				hpbImage;
	DWORD					idxTrack;
	FEvent					event;
	DWORD					dwLength;

	//	Validate MIDI header
	dwLeft   = MidiImageSize;
	hpbImage = MidiImage;

	if (dwLeft < sizeof(FChunkHdr))
		return false;

	pCh = (FChunkHdr*)hpbImage;

	dwLeft   -= sizeof(FChunkHdr);
	hpbImage += sizeof(FChunkHdr);

	dwLength = BigLong(pCh->Length);
	if (dwLength < sizeof(FMidiFileHdr) || dwLength > dwLeft)
		return false;

	pFh = (FMidiFileHdr*)hpbImage;

	dwLeft -= dwLength;
	hpbImage += dwLength;

	Format = (DWORD)BigShort(pFh->Format);
	NumTracks = (DWORD)BigShort(pFh->Tracks);
	TimeDivision = (DWORD)BigShort(pFh->Division);

	if (NumTracks > MAX_TRACKS)
		return false;

	//	We've successfully parsed the header. Now try to build the track index.
	//	We only check out the track header chunk here; the track will be
	// preparsed after we do a quick integretiy check.
	memset(Tracks, 0, MAX_TRACKS * sizeof(FTrack));

	pTrk = Tracks;

	for (idx = 0; idx < NumTracks; idx++)
	{
		if (dwLeft < sizeof(FChunkHdr))
			return false;

		pCh = (FChunkHdr*)hpbImage;

		dwLeft   -= sizeof(FChunkHdr);
		hpbImage += sizeof(FChunkHdr);

		if (memcmp(pCh->Type, "MTrk", 4))
			return false;

		pTrk->FileOffset = (DWORD)(hpbImage - MidiImage);
		pTrk->TotalLength = BigLong(pCh->Length);

		if (pTrk->TotalLength > dwLeft)
		{
			GCon->Log("Track longer than file!");
			return false;
		}

		dwLeft   -= pTrk->TotalLength;
		hpbImage += pTrk->TotalLength;

		pTrk++;
	}

	//	File looks OK. Now preparse, doing the following:
	// (3) Validate all events in all tracks
	MidiPosition = 0;
	SmfFlags &= ~SMFF_Eof;

	for (pTrk = Tracks, idxTrack = NumTracks; idxTrack--; pTrk++)
	{
		pTrk->Position = 0;
		pTrk->BytesLeft = pTrk->TotalLength;
		pTrk->ImagePtr = MidiImage + pTrk->FileOffset;
		pTrk->RunningStatus = 0;
		pTrk->TrackFlags = 0;
	}

	while (RES_Success == (smfrc = GetNextEvent(event)))
	{
	}

	if (RES_EndOfFile == smfrc || RES_Success == smfrc)
	{
		smfrc = RES_Success;
	}

	return smfrc == RES_Success;
	unguard;
}

//==========================================================================
//
//	VMMSystemMidiDevice::GetNextEvent
//
//	Read the next event from the given file.
//
//	This is the lowest level of parsing for a raw MIDI stream. The basic
// information about one event in the file will be returned in Event.
//
//	Merging data from all tracks into one stream is performed here.
//
//	pEvent->Delta will contain the tick delta for the event.
//
// pEvent->Event will contain a description of the event.
//  pevent->Event[0] will contain
//    F0 or F7 for a System Exclusive message.
//    FF for a MIDI file meta event.
//    The status byte of any other MIDI message. (Running status will
//    be tracked and expanded).
//
//	pEvent->ParmCount will contain the number of bytes of paramter data
//   which is still in the file behind the event header already read.
//   This data may be read with <f smfGetTrackEventData>. Any unread
//   data will be skipped on the next call to <f smfGetNextTrackEvent>.
//
// Channel messages (0x8? - 0xE?) will always be returned fully in
//   pevent->Event.
//
//  Meta events will contain the meta type in pevent->Event[1].
//
//  System exclusive events will contain only an 0xF0 or 0xF7 in
//    pevent->Event[0].
//
//  The following fields in pTrk are used to maintain state and must
//  be updated if a seek-in-track is performed:
//
//  bRunningStatus contains the last running status message or 0 if
//   there is no valid running status.
//
//  hpbImage is a pointer into the file image of the first byte of
//   the event to follow the event just read.
//
//  dwLeft contains the number of bytes from hpbImage to the end
//   of the track.
//
// Get the next due event from all (in-use?) tracks
//
// For all tracks
//   If not end-of-track
//   decode event delta time without advancing through buffer
//   event_absolute_time = track_tick_time + track_event_delta_time
//   relative_time = event_absolute_time - last_stream_time
//   if relative_time is lowest so far
//    save this track as the next to pull from, along with times
//
// If we found a track with a due event
//  Advance track pointer past event, saving ptr to parm data if needed
//  track_tick_time += track_event_delta_time
//  last_stream_time = track_tick_time
// Else
//  Mark and return end_of_file
//
//==========================================================================

VMMSystemMidiDevice::EResult VMMSystemMidiDevice::GetNextEvent(FEvent& Event)
{
	guard(VMMSystemMidiDevice::GetNextEvent);
	FTrack*		pTrk;
	FTrack*		pTrkFound;
	DWORD		idxTrack;
	DWORD		tkEventDelta;
	DWORD		tkRelTime;
	DWORD		tkMinRelTime;
	byte		bEvent;
	DWORD		dwGotTotal;
	DWORD		dwGot;
	DWORD		cbEvent;

	if (SmfFlags & SMFF_Eof)
	{
		return RES_EndOfFile;
	}

	pTrkFound = NULL;
	tkMinRelTime = MAX_TICKS;

	for (pTrk = Tracks, idxTrack = NumTracks; idxTrack--; pTrk++)
	{
		if (pTrk->TrackFlags & TRACKF_Eot)
			continue;

		if (!GetVDword(pTrk->ImagePtr, pTrk->BytesLeft, tkEventDelta))
		{
			GCon->Log("Hit end of track w/o end marker!");
			return RES_InvalidFile;
		}

		tkRelTime = pTrk->Position + tkEventDelta - MidiPosition;

		if (tkRelTime < tkMinRelTime)
		{
			tkMinRelTime = tkRelTime;
			pTrkFound = pTrk;
		}
	}

	if (!pTrkFound)
	{
		SmfFlags |= SMFF_Eof;
		return RES_EndOfFile;
	}

	pTrk = pTrkFound;

	pTrk->ImagePtr += (dwGot = GetVDword(pTrk->ImagePtr, pTrk->BytesLeft, tkEventDelta));
	pTrk->BytesLeft -= dwGot;

	//	We MUST have at least three bytes here (cause we haven't hit
	// the end-of-track meta yet, which is three bytes long). Checking
	// against three means we don't have to check how much is left
	// in the track again for any short event, which is most cases.
	if (pTrk->BytesLeft < 3)
	{
		return RES_InvalidFile;
	}

	pTrk->Position += tkEventDelta;
	Event.Delta = pTrk->Position - MidiPosition;
	MidiPosition = pTrk->Position;

	bEvent = *pTrk->ImagePtr++;

	if (MIDI_Msg > bEvent)
	{
		if (0 == pTrk->RunningStatus)
		{
			return RES_InvalidFile;
		}

		dwGotTotal = 1;
		Event.Event[0] = pTrk->RunningStatus;
		Event.Event[1] = bEvent;
		if (3 == ChanMsgLen[(pTrk->RunningStatus >> 4) & 0x0F])
		{
			Event.Event[2] = *pTrk->ImagePtr++;
			dwGotTotal++;
		}
	}
	else if (MIDI_SysEx > bEvent)
	{
		pTrk->RunningStatus = bEvent;

		dwGotTotal = 2;
		Event.Event[0] = bEvent;
		Event.Event[1] = *pTrk->ImagePtr++;
		if (3 == ChanMsgLen[(bEvent >> 4) & 0x0F])
		{
			Event.Event[2] = *pTrk->ImagePtr++;
			dwGotTotal++;
		}
	}
	else
	{
		pTrk->RunningStatus = 0;
		if (MIDI_Meta == bEvent)
		{
			Event.Event[0] = MIDI_Meta;
			if (MIDI_MetaEot == (Event.Event[1] = *pTrk->ImagePtr++))
			{
				pTrk->TrackFlags |= TRACKF_Eot;
			}

			dwGotTotal = 2;
		}
		else if (MIDI_SysEx == bEvent || MIDI_SysExEnd == bEvent)
		{
			Event.Event[0] = bEvent;
			dwGotTotal = 1;
		}
		else
		{
			return RES_InvalidFile;
		}

		if (0 == (dwGot = GetVDword(pTrk->ImagePtr, pTrk->BytesLeft - 2, cbEvent)))
		{
			return RES_InvalidFile;
		}

		pTrk->ImagePtr  += dwGot;
		dwGotTotal      += dwGot;

		if (dwGotTotal + cbEvent > pTrk->BytesLeft)
		{
			return RES_InvalidFile;
		}

		Event.ParmCount = cbEvent;
		Event.Parm = pTrk->ImagePtr;

		pTrk->ImagePtr += cbEvent;
		dwGotTotal += cbEvent;
	}

	pTrk->BytesLeft -= dwGotTotal;

	return RES_Success;
	unguard;
}

//==========================================================================
//
//	VMMSystemMidiDevice::GetVDword
//
//	Reads a variable length DWORD from the given file.
//
//==========================================================================

DWORD VMMSystemMidiDevice::GetVDword(const byte* ImagePtr, DWORD Left,
	DWORD& Out)
{
	guard(VMMSystemMidiDevice::GetVDword);
	byte		b;
	DWORD		NumUsed = 0;

	Out = 0;
	do
	{
		if (!Left)
		{
			return 0;
		}

		b = *ImagePtr++;
		Left--;
		NumUsed++;

		Out = (Out << 7) | (b & 0x7F);
	} while (b & 0x80);

	return NumUsed;
	unguard;
}
