/* mus2strm.c: This is based on the Quick MUS to MIDI converter by
** Sebasten Bacquet (bacquet@iie.cnam.fr) and also on the mid2strm.c
** file from the DirectX 5 SDK. It converts MUS files into MIDI
** streams suitable for passing to the MCI midiStreamOut() function.
** This is a much cleaner solution than the one I was using previously,
** which was to use the qmus2mid code to convert the MUS lump into a
** temporary MIDI file on disk and then use MCI calls to play that file.
** (The switch even shrank the executable by 3K. Woo!)
*/

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <windows.h>
#include <windowsx.h>
#include <mmsystem.h>
#include <assert.h>

#include "doomtype.h"
#include "mus2strm.h"
#include "m_alloc.h"
#include "m_swap.h"

#define last(e)         ((unsigned char)(e & 0x80))
#define event_type(e)   ((unsigned char)((e & 0x7F) >> 4))
#define channel(e)      ((unsigned char)(e & 0x0F))


#define NOTMUSFILE		1		/* Not a MUS file */
#define MUSFILECOR		2		/* MUS file corrupted */
#define TOOMCHAN		3		/* Too many channels */
#define MEMALLOC		4		/* Memory allocation error */


// Description of the input MUS file
//
typedef struct
{
	DWORD			cbFile;				// Total bytes in file
	LPBYTE			pFile;				// -> entire file in memory
	DWORD			cbLeft;				// Bytes left unread
	LPBYTE			pFilePointer;		// -> next byte to read

	DWORD			tkNextEventDue;
	BOOL			endEncountered;
} INFILESTATE;


static INFILESTATE ifs;
static byte MUS2MIDcontrol[15] = {
	0,			/* Program change - not a MIDI control change */
	0x00,		/* Bank select */
	0x01,		/* Modulation pot */
	0x07,		/* Volume */
	0x0A,		/* Pan pot */
	0x0B,		/* Expression pot */
	0x5B,		/* Reverb depth */
	0x5D,		/* Chorus depth */
	0x40,		/* Sustain pedal */
	0x43,		/* Soft pedal */
	0x78,		/* All sounds off */
	0x7B,		/* All notes off */
	0x7E,		/* Mono */
	0x7F,		/* Poly */
	0x79		/* Reset all controllers */
};
static signed char MUS2MIDchannel[16];
static char MUSvelocity[16];

static DWORD		ReadTime (void);
static char			FirstChannelAvailable (signed char MUS2MIDchannel[]);
static BOOL			ReadMUSheader (MUSheader *MUSh);
static BOOL			GetTrackEvent (MUSheader *MUSh, MEVENT *me);
static int			DoConversion (byte *in, DWORD insize, int division);


static BOOL ReadMUSheader (MUSheader *mush)
{
	if (ifs.cbLeft < sizeof(*mush))
		return FALSE;

	memcpy (mush, ifs.pFilePointer, sizeof(*mush));

	if (strncmp (mush->ID, MUSMAGIC, 4))
		return FALSE;

	mush->ScoreLength = SHORT(mush->ScoreLength);
	mush->ScoreStart = SHORT(mush->ScoreStart);
	mush->channels = SHORT(mush->channels);
	mush->SecChannels = SHORT(mush->SecChannels);
	mush->InstrCnt = SHORT(mush->InstrCnt);

	if (ifs.cbLeft < mush->ScoreStart)
		return FALSE;

	ifs.cbLeft -= mush->ScoreStart;
	ifs.pFilePointer += mush->ScoreStart;

	return TRUE;
}

static DWORD ReadTime (void)
{
	DWORD time = 0;
	byte byt = 0;

	do {
		if (ifs.cbLeft > 0) {
			byt = *(ifs.pFilePointer++);
			ifs.cbLeft--;
			time = (time << 7) + (byt & 0x7F);
		}
	} while ((ifs.cbLeft > 0) && (byt & 0x80));

	return time;
}

static char FirstChannelAvailable (signed char MUS2MIDchannel[])
{
	int i;
	signed char max = -1;

	for (i = 0; i < 15; i++)
		if (MUS2MIDchannel[i] > max)
			max = MUS2MIDchannel[i];

	return max == 8 ? 10 : max+1;
}

static BOOL GetTrackEvent (MUSheader *MUSh, MEVENT *me)
{
	byte event, data;
	int MUSchannel, MIDIchannel;

	me->pEvent = NULL;
	
	if (!ifs.cbLeft)
		return FALSE;
	event = *(ifs.pFilePointer++);
	ifs.cbLeft--;

	MUSchannel = channel(event);
	if (MUS2MIDchannel[MUSchannel] == -1)
		MIDIchannel = MUS2MIDchannel[MUSchannel] =
			(MUSchannel == 15 ? 9 : FirstChannelAvailable (MUS2MIDchannel));
	else
		MIDIchannel = MUS2MIDchannel[MUSchannel];

	if (event_type(event) <= MUS_CTRLCHANGE) {
		if (ifs.cbLeft < 1)
			return FALSE;
		data = *(ifs.pFilePointer++);
		ifs.cbLeft--;
	}

	switch (event_type(event))
	{
		case MUS_RELEASE:
			me->cbEvent = 3;
			me->abEvent[0] = MIDI_NOTEOFF | MIDIchannel;
			me->abEvent[1] = data;
			me->abEvent[2] = 64;
			break;

		case MUS_PLAY:
			me->cbEvent = 3;
			me->abEvent[0] = MIDI_NOTEON | MIDIchannel;
			me->abEvent[1] = data & 0x7f;
			if (data & 0x80) {
				if (ifs.cbLeft < 1)
					return FALSE;
				else {
					MUSvelocity[MUSchannel] = *(ifs.pFilePointer++);
					ifs.cbLeft--;
				}
			}
			me->abEvent[2] = MUSvelocity[MUSchannel];
			break;

		case MUS_PITCHBEND:
			me->cbEvent = 3;
			me->abEvent[0] = MIDI_PITCHBEND | MIDIchannel;
			me->abEvent[1] = (data & 1) << 6;
			me->abEvent[2] = data >> 1;
			break ;

		case MUS_SYSEVENT:
			me->cbEvent = 3;
			me->abEvent[0] = MIDI_CTRLCHANGE | MIDIchannel;
			me->abEvent[1] = MUS2MIDcontrol[data];
			me->abEvent[2] = data == 12 ? MUSh->channels+1 : 0;
			break;

		case MUS_CTRLCHANGE:
			if (ifs.cbLeft < 1)
				return FALSE;
			ifs.cbLeft--;
			if (data) {
				me->cbEvent = 3;
				me->abEvent[0] = MIDI_CTRLCHANGE | MIDIchannel;
				me->abEvent[1] = MUS2MIDcontrol[data];
				me->abEvent[2] = *(ifs.pFilePointer++);
			} else {
				me->cbEvent = 2;
				me->abEvent[0] = MIDI_PRGMCHANGE | MIDIchannel;
				me->abEvent[1] = *(ifs.pFilePointer++);
			}
			break ;

		case MUS_UNKNOWN1:
		case MUS_UNKNOWN2:
			return FALSE;

		case MUS_END:
			ifs.endEncountered = TRUE;
			break;
	}

	me->tkEvent = ifs.tkNextEventDue;
	if (last(event))
		ifs.tkNextEventDue += ReadTime ();

	return TRUE;
}

static int DoConversion (byte *in, DWORD insize, int division)
{	
	// The start of the first stream buffer contains an event to set
	// the tempo to 500,000 microseconds per quarter note.
	static BYTE tempoSet[3] = { 0x07, 0xa1, 0x20 };
	static MEVENT tempoEvent = {
		0,
		MIDI_META, MIDI_META_TEMPO, 0, 0,
		3,
		tempoSet
	};

	MUSheader MUSh;
	int i;

	memset (UsedPatches, 0, sizeof(UsedPatches));

	ifs.cbFile = insize;
	ifs.pFile = in;

	ifs.cbLeft = ifs.cbFile;
	ifs.pFilePointer = ifs.pFile;

	if (!ReadMUSheader (&MUSh))
		return NOTMUSFILE;

	if (MUSh.channels > 15)			/* <=> MUSchannels+drums > 16 */
		return TOOMCHAN;

	for (i = 0; i < 16; i++) {
		MUS2MIDchannel[i] = -1;
		MUSvelocity[i] = 64;
	}
	ifs.tkNextEventDue = 0;
	ifs.endEncountered = FALSE;
	ots.tkTrack = 0;
	ots.pFirst = NULL;
	ots.pLast = NULL;

	if (!AddEventToStream (&tempoEvent))
		return MEMALLOC;

	while (!ifs.endEncountered) {
		MEVENT me;

		if (!GetTrackEvent (&MUSh, &me))
			return MUSFILECOR;

		if (!ifs.endEncountered && !AddEventToStream (&me))
			return MEMALLOC;
	}

	midTimeDiv = division ? division : 70;

	return 0;
}

void mus2strmCleanup (void)
{
	PSTREAMBUF pCurr = ots.pFirst;
	while (pCurr) {
		PSTREAMBUF pNext = pCurr->pNext;
		free (pCurr);
		pCurr = pNext;
	}
}

PSTREAMBUF mus2strmConvert (BYTE *inFile, DWORD inSize)
{
	int error = DoConversion (inFile, inSize, 0);

	if (error) {
		Printf (PRINT_HIGH, "MUS error: ");
		switch (error) {
			case NOTMUSFILE:
				Printf (PRINT_HIGH, "Not a MUS file.\n"); break;
			case MUSFILECOR:
				Printf (PRINT_HIGH, "MUS file is corrupt.\n"); break;
			case TOOMCHAN:
				Printf (PRINT_HIGH, "MUS file has more than 16 channels.\n"); break;
			case MEMALLOC:
				Printf (PRINT_HIGH, "Not enough memory.\n"); break;
		}
		mus2strmCleanup ();
		return NULL;
	}

	return ots.pFirst;
}