/**
	This file is a port of some old C code I had for reading and writing .mid files. Not much docs, but viewing the source may be helpful.

	I'll eventually refactor it into something more D-like
*/
module arsd.midi;


import core.stdc.stdio;
import core.stdc.stdlib;

/* NOTE: MIDI files are BIG ENDIAN! */

struct MidiChunk {
	int timeStamp; // this is encoded my way. real file is msb == 1, more ubytes
	ubyte event; 	// Event << 8 | channel is how it is actually stored

	// for channel events
	ubyte channel; 	// see above - it is stored with event!
			// channel == track btw
	ubyte param1; 	// pitch (usually)
	ubyte param2; 	// volume  - not necessarily present

	ubyte status; // event << 4 | channel

	// for meta events (event = f, channel = f
	ubyte type;
	int length; // stored as variable length
	ubyte* data; // only allocated if event == 255

	MidiChunk* next; // next in the track

	// This stuff is just for playing help and such
	// It is only set if you call recalculateMidiAbsolutes, and probably
	// not maintained if you do edits
	int track;
	uint absoluteTime;
	uint absoluteTimeInMilliSeconds; // for convenience
	int absoluteWait;
	MidiChunk* nextAbsolute;
}

/*
	Meta event
	timeStamp = 0
	event = 255
	channel = event
	param1 param2 = not in gile
	length = variable
	data[length]
*/

struct MidiTrack {
	// MTrk header
	int lengthInBytes;
	MidiChunk* chunks; // linked list
		// the linked list should NOT hold the track ending chunk
		// just hold a null instead
}

struct Midi {
	// headers go here
	short type;
	short numTracks;
	short speed;
	MidiTrack* tracks; /* Array of numTracks size */

	// only set if you call recalculateMidiAbsolutes
	MidiChunk* firstAbsolute;
}


enum MIDI_EVENT_NOTE_OFF =		0x08;
enum MIDI_EVENT_NOTE_ON =		0x09;
enum MIDI_EVENT_NOTE_AFTERTOUCH =	0x0a;
enum MIDI_EVENT_CONTROLLER =		0x0b;
enum MIDI_EVENT_PROGRAM_CHANGE =	0x0c;// only one param
enum MIDI_EVENT_CHANNEL_AFTERTOUCH =	0x0d;// only one param
enum MIDI_EVENT_PITCH_BEND =		0x0e;



/*
static char[][] instrumentNames = {
"", // 0 is nothing
// Piano:
"Acoustic Grand Piano",
"Bright Acoustic Piano",
"Electric Grand Piano",
"Honky-tonk Piano",
"Electric Piano 1",
"Electric Piano 2",
"Harpsichord",
"Clavinet",

// Chromatic Percussion:
"Celesta",
"Glockenspiel",
"Music Box",
"Vibraphone",
"Marimba",
"Xylophone",
"Tubular Bells",
"Dulcimer",

// Organ:
"Drawbar Organ",
"Percussive Organ",
"Rock Organ",
"Church Organ",
"Reed Organ",
"Accordion",
"Harmonica",
"Tango Accordion",

// Guitar:
"Acoustic Guitar (nylon)",
"Acoustic Guitar (steel)",
"Electric Guitar (jazz)",
"Electric Guitar (clean)",
"Electric Guitar (muted)",
"Overdriven Guitar",
"Distortion Guitar",
"Guitar harmonics",

// Bass:
"Acoustic Bass",
"Electric Bass (finger)",
"Electric Bass (pick)",
"Fretless Bass",
"Slap Bass 1",
"Slap Bass 2",
"Synth Bass 1",
"Synth Bass 2",

// Strings:
"Violin",
"Viola",
"Cello",
"Contrabass",
"Tremolo Strings",
"Pizzicato Strings",
"Orchestral Harp",
"Timpani",

// Strings (continued):
"String Ensemble 1",
"String Ensemble 2",
"Synth Strings 1",
"Synth Strings 2",
"Choir Aahs",
"Voice Oohs",
"Synth Voice",
"Orchestra Hit",

// Brass:
"Trumpet",
"Trombone",
"Tuba",
"Muted Trumpet",
"French Horn",
"Brass Section",
"Synth Brass 1",
"Synth Brass 2",

// Reed:
"Soprano Sax",
"Alto Sax",
"Tenor Sax",
"Baritone Sax",
"Oboe",
"English Horn",
"Bassoon",
"Clarinet",

// Pipe:
"Piccolo",
"Flute",
"Recorder",
"Pan Flute",
"Blown Bottle",
"Shakuhachi",
"Whistle",
"Ocarina",

// Synth Lead:
"Lead 1 (square)",
"Lead 2 (sawtooth)",
"Lead 3 (calliope)",
"Lead 4 (chiff)",
"Lead 5 (charang)",
"Lead 6 (voice)",
"Lead 7 (fifths)",
"Lead 8 (bass + lead)",

// Synth Pad:
"Pad 1 (new age)",
"Pad 2 (warm)",
"Pad 3 (polysynth)",
"Pad 4 (choir)",
"Pad 5 (bowed)",
"Pad 6 (metallic)",
"Pad 7 (halo)",
"Pad 8 (sweep)",

// Synth Effects:
"FX 1 (rain)",
"FX 2 (soundtrack)",
"FX 3 (crystal)",
"FX 4 (atmosphere)",
"FX 5 (brightness)",
"FX 6 (goblins)",
"FX 7 (echoes)",
"FX 8 (sci-fi)",

// Ethnic:
"Sitar",
"Banjo",
"Shamisen",
"Koto",
"Kalimba",
"Bag pipe",
"Fiddle",
"Shanai",

// Percussive:
"Tinkle Bell",
"Agogo",
"Steel Drums",
"Woodblock",
"Taiko Drum",
"Melodic Tom",
"Synth Drum",

// Sound effects:
"Reverse Cymbal",
"Guitar Fret Noise",
"Breath Noise",
"Seashore",
"Bird Tweet",
"Telephone Ring",
"Helicopter",
"Applause",
"Gunshot"
};
*/


int addMidiTrack(Midi* mid){
	int trackNum;
	MidiTrack* tracks;
	tracks = cast(MidiTrack*) realloc(mid.tracks, MidiTrack.sizeof * (mid.numTracks + 1));
	if(tracks is null)
		return -1;
	
	mid.tracks = tracks;
	trackNum = mid.numTracks;
	mid.numTracks++;

	mid.tracks[trackNum].lengthInBytes = 0;
	mid.tracks[trackNum].chunks = null;

	return trackNum;
}

int addMidiEvent(Midi* mid, int track, int deltatime, int event, int channel, int value1, int value2){
	int length = 2;
	MidiChunk* c;
	MidiChunk* current, previous;
	if(track >= mid.numTracks)
		return -1;

	c = cast(MidiChunk*) malloc(MidiChunk.sizeof);
	if(c is null)
		return -1;

	c.timeStamp = deltatime;
	c.event = cast(ubyte) event;
	c.channel = cast(ubyte) channel;
	c.param1 = cast(ubyte) value1;
	c.param2 = cast(ubyte) value2;

	c.status = cast(ubyte) ((event << 4) | channel);

	c.type = 0;
	c.length = 0;
	c.data = null;

	c.next = null;


	previous = null;
	current = mid.tracks[track].chunks;
	while(current != null){
		previous = current;
		current = current . next;
	}

	if(previous){
		previous.next = c;
	} else {
		mid.tracks[track].chunks = c;
	}

	length += getvldLength(deltatime);
	if(event != MIDI_EVENT_CHANNEL_AFTERTOUCH &&
	   event != MIDI_EVENT_PROGRAM_CHANGE)
	   	length++; // param2
	mid.tracks[track].lengthInBytes += length;

	return 0;
}

int addMidiMetaEvent(Midi* mid, int track, int dt, int type, int length, ubyte* data){
	int len = 2;
	int a;
	MidiChunk* c;
	MidiChunk* current, previous;

	if(track >= mid.numTracks)
		return -1;

	c = cast(MidiChunk*) malloc(MidiChunk.sizeof);
	if(c == null)
		return -1;

	c.timeStamp = dt;
	c.event = 0xff;
	c.channel = 0;
	c.param1 = 0;
	c.param2 = 0;

	c.type = cast(ubyte) type;
	c.length = length;
	// copy data in
	c.data = cast(typeof(c.data)) malloc(length);
	if(c.data == null){
		free(c);
		return -1;
	}
	for(a = 0; a < length; a++)
		c.data[a] = data[a];


	c.next = null;


	previous = null;
	current = mid.tracks[track].chunks;
	while(current != null){
		previous = current;
		current = current . next;
	}

	if(previous){
		previous.next = c;
	} else {
		mid.tracks[track].chunks = c;
	}

	len += getvldLength(dt);
	len += length;

	mid.tracks[track].lengthInBytes += len;

	return 0;
}

int createMidi(Midi** midWhere){
	Midi* mid;

	mid = cast(Midi*) malloc(Midi.sizeof);
	if(mid == null)
		return 1;
	
	mid.type = 1;
	mid.numTracks = 0;
	mid.speed = 0x80; // 128 ticks per quarter note - potential FIXME
	mid.tracks = null;

	*midWhere = mid;
	return 0;
}

void freeChunkList(MidiChunk* c){
	if(c == null)
		return;
	freeChunkList(c.next);
	if(c.event == 255)
		free(c.data);
	free(c);
}

void freeMidi(Midi** mid){
	int a;
	Midi* m = *mid;

	for(a = 0; a < m.numTracks; a++)
		freeChunkList(m.tracks[a].chunks);
	free(m.tracks);
	free(m);
	*mid = null;
}

// FIXME: these fail on big endian machines
void write4(int v, FILE* fp){
	fputc(*(cast(ubyte*)&v + 3), fp);
	fputc(*(cast(ubyte*)&v + 2), fp);
	fputc(*(cast(ubyte*)&v + 1), fp);
	fputc(*(cast(ubyte*)&v + 0), fp);
}

void write2(short v, FILE* fp){
	fputc(*(cast(ubyte*)&v + 1), fp);
	fputc(*(cast(ubyte*)&v + 0), fp);
}

void writevld(uint v, FILE* fp){
	uint omg = v;
	ubyte a;
	ubyte[4] ubytes;
	int c = 0;
  more:
	a = cast(ubyte) (omg&(~(1 << 7)));
	omg >>= 7;
	if(omg){
		ubytes[c++]  = a;
		goto more;
	}

	ubytes[c] = a;

	for(; c >= 0; c--)
		fputc(ubytes[c] | (c ? (1<<7):0), fp);
}


int read4(FILE* fp){
	int v;

	*(cast(ubyte*)&v + 3) = cast(ubyte) fgetc(fp);
	*(cast(ubyte*)&v + 2) = cast(ubyte) fgetc(fp);
	*(cast(ubyte*)&v + 1) = cast(ubyte) fgetc(fp);
	*(cast(ubyte*)&v + 0) = cast(ubyte) fgetc(fp);

	return v;
}

short read2(FILE* fp){
	short v;
	*(cast(ubyte*)&v + 1) = cast(ubyte) fgetc(fp);
	*(cast(ubyte*)&v + 0) = cast(ubyte) fgetc(fp);

	return v;
}

uint readvld(FILE* fp){
	uint omg = 0;
	ubyte a;
  more:
	a = cast(ubyte) fgetc(fp);
	if(a & (1<<7)){
		a &= ~(1<<7);
		omg <<= 7;
		omg |= a;
		goto more;
	}

	omg <<= 7;
	omg |= a;

	return omg;
}



int getvldLength(uint v){
	int count = 0;
	uint omg = v;
	ubyte a;
  more:
	a = omg&((1 << 7)-1); //
	omg >>= 7;
	if(omg){
		a &= 1<<7;
		count++;
		goto more;
	}
	count++;
	return count;
}

// END: big endian fixme

int loadMidi(Midi** midWhere, const char* filename){
	int error = 0;
	FILE* fp;
	Midi* mid = null;
	int runningStatus;
	int t, a;
	int numtrk;

	int timestamp;
	int event;
	int channel;
	int param1;
	int type;
	int length;
	int param2;
	ubyte* data;

	int done;

	fp = fopen(filename, "rb");
	if(fp == null){
		fprintf(stderr, "Cannot load file %s.\n", filename);
		error = 1;
		goto cleanup1;
	}


	if(fgetc(fp) != 'M') goto badfile;
	if(fgetc(fp) != 'T') goto badfile;
	if(fgetc(fp) != 'h') goto badfile;
	if(fgetc(fp) != 'd') goto badfile;
	if(read4(fp) !=   6) goto badfile;

	if(createMidi(&mid) != 0){
		fprintf(stderr, "Could not allocate struct\n");
		error = 3;
		goto cleanup3;
	}

	mid.type = read2(fp);
	numtrk = read2(fp);
	mid.speed = read2(fp);

	for(t = 0; t < numtrk; t++){
		if(fgetc(fp) != 'M') goto badfile;
		if(fgetc(fp) != 'T') goto badfile;
		if(fgetc(fp) != 'r') goto badfile;
		if(fgetc(fp) != 'k') goto badfile;

		if(addMidiTrack(mid) < 0){
			fprintf(stderr, "add midi track failed \n");
			error = 3;
			goto cleanup3;
		}

//		mid.tracks[t].lengthInBytes = read4(fp) - 4;
		read4(fp); // ignoring it for now FIXME?

		done = 0;
		do{
			timestamp = readvld(fp);
			event = fgetc(fp);
			if(event == 0xff){
				type = fgetc(fp);
				length = readvld(fp);

				// potential optimization for malloc
				if(length){
					data = cast(typeof(data)) malloc(length);
					for(a = 0; a < length; a++)
						data[a] = cast(ubyte) fgetc(fp);
				} else
					data = null;


				if(type == 0x2f){
					done = 1;
				} else {
					// add the event to the list here
					// FIXME: error check
					addMidiMetaEvent(mid, t, timestamp,
						type, length, data);
				}
				if(data)
					free(data);
			} else {
				if(event < 0x80){
					param1 = event;
					event = runningStatus;
				} else {
					runningStatus = event;
					param1 = fgetc(fp);
				}

				channel = event&0x0f;
				event = event >> 4;
				if(event != MIDI_EVENT_PROGRAM_CHANGE
			 	&& event != MIDI_EVENT_CHANNEL_AFTERTOUCH)
					param2 = fgetc(fp);

				// add the event
				// FIXME: error check
				addMidiEvent(mid, t, timestamp, event, channel, param1, param2);
			}
		} while(!done);
	}

	goto success;
  badfile:
	fprintf(stderr, "The file is not in the right format. %c\n", fgetc(fp));
	error = 2;
  cleanup3:
  	if(mid != null)
		freeMidi(&mid);
  success:
	fclose(fp);
	*midWhere = mid;
  cleanup1:
	return error;
}






int saveMidi(Midi* mid, char* filename){
	int error = 0;
	FILE* fp;
	int t, a;
	int runningStatus = -1;
	int status;
	
	fp = fopen(filename, "wb");
	if(fp == null){
		fprintf(stderr, "Unable to open midi file (%s) for writing.\n", filename);
		error = 1;
		goto cleanup1;
	}

	fputc('M', fp);
	fputc('T', fp);
	fputc('h', fp);
	fputc('d', fp);

	write4(6, fp);
	write2(mid.type, fp);
	write2(mid.numTracks, fp);
	write2(mid.speed, fp);

	for(t = 0; t < mid.numTracks; t++){
		fputc('M', fp);
		fputc('T', fp);
		fputc('r', fp);
		fputc('k', fp);

		runningStatus = -1;

		write4(mid.tracks[t].lengthInBytes + 4, fp);
		MidiChunk* current;
		current = mid.tracks[t].chunks;
		while(current != null){
			writevld(current.timeStamp, fp);
			if(current.event == 0xff){
				fputc(current.event, fp);
				fputc(current.type, fp);
				writevld(current.length, fp);
				for(a = 0; a < current.length; a++)
					fputc(current.data[a], fp);
			} else {
				// FIXME: add support for writing running status
				status = current.event << 4 | current.channel;

			//	if(status != runningStatus){
					runningStatus = status;
					fputc(status, fp);
			//	}

				fputc(current.param1, fp);
				if(current.event != MIDI_EVENT_PROGRAM_CHANGE
			 &&current.event != MIDI_EVENT_CHANNEL_AFTERTOUCH)
					fputc(current.param2, fp);
			}
			current = current.next;
		}
		/* the end of track chunk */
		fputc(0, fp);
		fputc(0xff, fp);
		fputc(0x2f, fp);
		fputc(0x00, fp);
	}
/*  cleanup2:*/
	fclose(fp);
  cleanup1:
	return error;
}


int removeMidiTrack(Midi* m, int track){
	int a;
	if(track >= m.numTracks)
		return -1;

	for(a = track; a < m.numTracks-1; a++){
		m.tracks[a] = m.tracks[a+1];
	}

	m.numTracks--;

	return 0;
}

void printMidiEvent(MidiChunk* c){
	int e = c.event;
	printf("%d %s %d %d\n", c.timeStamp,
		 e == MIDI_EVENT_NOTE_OFF ? "Note off".ptr
		:e == MIDI_EVENT_NOTE_ON  ? "Note on".ptr
		:e == MIDI_EVENT_PROGRAM_CHANGE ? "Program change".ptr
		:e == MIDI_EVENT_NOTE_AFTERTOUCH ? "Aftertouch".ptr
		: "I dunno".ptr
	, c.param1, c.param2);
}

MidiChunk* getTrackNameChunk(Midi* m, int track){
	MidiChunk* c;

	if(track >= m.numTracks)
		return null;

	c = m.tracks[track].chunks;
	while(c){
		if(c.event == 0xff && c.type == 3)
			return c;

		c = c.next;
	}

	return c;
}

int getMidiTempo(Midi* m){
	int a;
	MidiChunk* c;
	for(a = 0; a < m.numTracks; a++){
		c = m.tracks[a].chunks;
		while(c){
			if(c.event == 0xff)
			if(c.type == 0x51){
				int p = 0;
				p |= cast(int)(c.data[0]) << 16;
				p |= cast(int)(c.data[1]) << 8;
				p |= cast(int)(c.data[2]) << 0;
				
				return 60000000 / p;
			}
			c = c.next;
		}
	}

	return 120;
}

int getTempoFromTempoEvent(MidiChunk* c){
	int tempo = -1;
	if(c.event == 0xff && c.type == 0x51){
		int p = 0;
		p |= cast(int)(c.data[0]) << 16;
		p |= cast(int)(c.data[1]) << 8;
		p |= cast(int)(c.data[2]) << 0;
		tempo = 60000000 / p;
	}
	return tempo;
}

// returns milliseconds to wait given the params
int getMidiWaitTime(Midi* mid, int timeStamp, int tempo){
	return (timeStamp * 60000) / (tempo * mid.speed);
}

// sets absolute values and links up, useful for playing or editing
// but remember you must recalculate them yourself if you change anything
// Returns the final absolute time in seconds
int recalculateMidiAbsolutes(Midi* mid){
	MidiChunk*[128] c;
	int[128] trackWaits;
	int playing;
	int waited;
	int minWait = 100000;
	int a;
	uint absoluteTime = 0;
	int tempo = 120;
	uint absoluteTimeInMilliSeconds = 0;
	int t;
	int timeOfLastEvent = 0;

	MidiChunk* absoulteCurrent;

	mid.firstAbsolute = null;
	absoulteCurrent = null;

	playing = mid.numTracks;
	for(a = 0; a < mid.numTracks; a++){
		c[a] = mid.tracks[a].chunks;
		if(c[a]){
		trackWaits[a] = c[a].timeStamp;
		if(trackWaits[a] < minWait)
			minWait = trackWaits[a];
		} else
			playing--;
	}

	while(playing){
		waited = minWait;
		minWait = 1000000;
		absoluteTime += waited;
		absoluteTimeInMilliSeconds += getMidiWaitTime(mid, waited, tempo);
	for(a = 0; a < mid.numTracks; a++){
		if(!c[a])
			continue;
		trackWaits[a] -= waited;
		if(trackWaits[a] == 0){

		t = getTempoFromTempoEvent(c[a]);
		if(t != -1)
			tempo = t;

		// append it to the list
		if(absoulteCurrent == null){
			mid.firstAbsolute = c[a];
			absoulteCurrent = c[a];
		} else {
			absoulteCurrent.nextAbsolute = c[a];
			absoulteCurrent = absoulteCurrent.nextAbsolute;
		}
			absoulteCurrent.nextAbsolute = null;
			absoulteCurrent.absoluteTime = absoluteTime;
			absoulteCurrent.absoluteTimeInMilliSeconds = absoluteTimeInMilliSeconds;
			absoulteCurrent.track = a;
			absoulteCurrent.absoluteWait = absoluteTime - timeOfLastEvent;

		timeOfLastEvent = absoluteTime;
		c[a] = c[a].next;
		if(c[a] == null){
			playing --;
			trackWaits[a] = 1000000;
		}
		else
			trackWaits[a] = c[a].timeStamp;
		}
		if(trackWaits[a] < minWait )
			minWait = trackWaits[a];
	}
	}


	return absoluteTimeInMilliSeconds / 1000;
}

// returns approximate seconds
int getMidiLength(Midi* mid){
	return recalculateMidiAbsolutes(mid);
}






import arsd.simpleaudio;

struct PlayingMidi {
	ushort channelMask; /* The channels that will be played */
	int[128] playtracks;

	// Callbacks
	// onPlayedNote. Args: this, note, midi ticks waited since last message
	// This is intended for tablature creation
	void function(void*, int, int) onPlayedNote;
	// onMidiEvent. Args: this, event being executed
	// This can be used to print it or whatever
	// If you return 1, it skips the event. Return 0 for normal operation
	int function(void*, MidiChunk*) onMidiEvent;

	Midi* mid;
	MidiOutput* dev;

	int transpose;
	float tempoMultiplier;

	/* This stuff is state for the midi in progress */
	int tempo;

	MidiChunk* current;

	int wait;
}


// the main loop for the first time
int resetPlayingMidi(PlayingMidi* pmid){
	pmid.current = pmid.mid.firstAbsolute;
	pmid.tempo = 120;
	pmid.wait = 0;
	if(pmid.current)
		return getMidiWaitTime(pmid.mid, pmid.current.absoluteWait, cast(int) (pmid.tempo * pmid.tempoMultiplier));
	return 0;
}

void setPlayingMidiDefaults(PlayingMidi* pmid){
	int a;
	pmid.channelMask =0xffff;
	for(a = 0; a < 128; a++)
		pmid.playtracks[a] = 1;

	pmid.onPlayedNote = null;
	pmid.onMidiEvent = null;

	pmid.mid = null;
	pmid.dev = null;

	pmid.transpose = 0;
	pmid.tempoMultiplier = 1.0;

}


void seekPlayingMidi(PlayingMidi* pmid, int sec){
	pmid.dev.silenceAllNotes();
	pmid.dev.reset();

	pmid.current = pmid.mid.firstAbsolute;
	while(pmid.current){
		if(pmid.current.absoluteTimeInMilliSeconds >= sec * 1000)
			break;
		pmid.current = pmid.current.next;
	}
}


// This is the main loop. Returns how many milliseconds to wait before
// calling it again. If zero, then the song is over.
int advancePlayingMidi(PlayingMidi* pmid){
	MidiChunk* c;
	if(pmid.current == null)
		return 0;
  more:
	c = pmid.current;
  	pmid.wait += c.absoluteWait;

	if(pmid.onMidiEvent){
		if(pmid.onMidiEvent(pmid, c))
			goto skip;
	}

	if(c.event != 0xff){
		if(pmid.playtracks[c.track]){
			if(pmid.channelMask & (1 << c.channel)){
				int note = c.param1;
				if(c.event == MIDI_EVENT_NOTE_ON
				  || c.event == MIDI_EVENT_NOTE_AFTERTOUCH
				  || c.event == MIDI_EVENT_NOTE_OFF){
					note += pmid.transpose;
					//skipCounter = SKIP_MAX;
				}

				if(pmid.dev)
					pmid.dev.writeMidiMessage(c.status, note, c.param2);
				if(pmid.onPlayedNote)
					if(c.event == MIDI_EVENT_NOTE_ON
							&& c.param2 != 0){
						pmid.onPlayedNote(pmid,
							note,
							(pmid.wait * 4) / (pmid.mid.speed));
						pmid.wait = 0;
					}
			}
		}
	} else {
		if(c.type == 0x51)
			pmid.tempo = getTempoFromTempoEvent(c);
	}

  skip:
	pmid.current = pmid.current.nextAbsolute;
	if(pmid.current)
		if(pmid.current.absoluteWait == 0)
			goto more;
		else
			return getMidiWaitTime(
				pmid.mid,
				pmid.current.absoluteWait,
				cast(int) (pmid.tempo * pmid.tempoMultiplier));
	else return 0;
}




version(MidiDemo) {





MidiOutput* globaldev;

version(Windows)
	import core.sys.windows.windows;
else {
	import core.sys.posix.unistd;
	void Sleep(int ms){
		usleep(ms*1000);
	}

	import core.stdc.signal;
	// FIXME: this sucks.
	extern(C)
	alias fuckyou = void function(int) @nogc nothrow @system;
	extern(C)
	void sigint(){
		if(globaldev){
			globaldev.silenceAllNotes();
			globaldev.reset();
			destroy(*globaldev);
		}
		exit(1);
	}
}

enum SKIP_MAX = 3000; // allow no more than about 3 seconds of silence
			 // if the -k option is set

// Potential FIXME: it doesn't support more than 128 tracks.

void awesome(void* midiptr, int note, int wait) {
	printf("%d %d ", wait, note);
	fflush(stdout);
}

// FIXME: add support for displaying lyrics
extern(C) int main(int argc, char** argv){
	int a, b;

	PlayingMidi pmid;

	int tempo = 120;
	Midi* mid;
	MidiOutput midiout = MidiOutput(0);
	MidiChunk*[128] c;

	int minWait = 10000, waited;
	int playing;

	int wait = 0;
	int num;

	char* filename = null;

	int verbose = 0;
	float tempoMultiplier = 1;
	int transpose = 0;
	int displayinfo = 0;
	int play = 1;
	int tracing = 0;
	int skip = 0;
	int[128] playtracks;
	int skipCounter = SKIP_MAX;

	ushort channelMask = 0xffff;

	int sleepTime = 0;

	version(Posix) {
		signal(SIGINT, cast(fuckyou) &sigint);
	}

	for(a = 0; a< 128; a++)
		playtracks[a] = 1;


	for(a = 1; a < argc; a++){
		if(argv[a][0] == '-')
		switch(argv[a][1]){
			case 't':
				for(b = 0; b< 128; b++)
					playtracks[b] = 0;
				num = 0;
				b = 0;
				a++;
				if(a == argc){
					printf("%s: option %s requires an argument\n", argv[0], argv[a-1]);
					return 1;
				}
				for(b = 0; argv[a][b]; b++){
					if(argv[a][b] == ','){
						playtracks[num] = 1;
						num = 0;
						continue;
					}
					num *= 10;
					num += argv[a][b] - '0';
				}
				playtracks[num] = 1;
			break;
			case 's':
				a++;
				if(a == argc){
					printf("%s: option %s requires an argument\n", argv[0], argv[a-1]);
					return 1;
				}
				tempoMultiplier = atof(argv[a]);
			break;
			case 'i': // FIXME
				displayinfo = 1;
				// tracks, guesstimated length
			break;
			// -o loop to from
			// -b begin at
			// -e end at
			case 'l':
				tracing = 1;
			break;
			case 'n':
				play = 0;
			break;
			case 'k':
				skip = 1;
			break;
			case 'c':
				channelMask = 0;
				// channels
				num = 0;
				b = 0;
				a++;
				if(a == argc){
					printf("%s: option %s requires an argument\n", argv[0], argv[a-1]);
					return 1;
				}
				for(b = 0; argv[a][b]; b++){
					if(argv[a][b] == ','){
						channelMask |= (1 << num);
						num = 0;
						continue;
					}
					num *= 10;
					num += argv[a][b] - '0';
				}
					channelMask |= (1 << num);
			break;
			case 'r':
				a++;
				if(a == argc){
					printf("%s: option %s requires an argument\n", argv[0], argv[a-1]);
					return 1;
				}
				transpose = atoi(argv[a]);
			break;
			case 'v':
				verbose = 1;
			break;
			case 'h':
				printf("Usage: %s [options...] file\n", argv[0]);
				printf("  Options:\n");
				printf("  -t comma separated list of tracks to play (default: all)\n");
				printf("  -s tempo (speed) multiplier (default: 1.0)\n");
				printf("  -i file info (track list)\n");
				printf("  -l list notes as they are played (in the format totablature expects)\n");
				printf("  -n no sound; don't actually play the midi\n");
				printf("  -c comma separated list of channels to play (default: all)\n");
				printf("  -r transpose notes by amount (default: 0)\n");
				printf("  -k skip long sections of silence (good for playing single tracks)\n");

				printf("  -v verbose; list all events except note on / note off\n");
				printf("  -h shows this help screen\n");

				return 0;
			break;
			default:
				printf("%s: unknown command line option: %s\n", argv[0], argv[1]);
				return 1;
		}
		else
			filename = argv[a];
	}

	if(filename == null){
		printf("%s: no file given. Try %s -h for help.\n", argv[0], argv[0]);
		return 1;
	}







	loadMidi(&mid, filename);
	if(mid == null){
		printf("%s: unable to read file %s\n", argv[0], filename);
		return 1;
	}

	if(displayinfo){
		int len = getMidiLength(mid);
		printf("File: %s\n", filename);
		printf("Ticks per quarter note: %d\n", mid.speed);
		printf("Initial tempo: %d\n", getMidiTempo(mid));
		printf("Length: %d:%d\n", len / 60, len%60);
		printf("Tracks:\n");
		for(a = 0; a < mid.numTracks; a++){
			c[0] = getTrackNameChunk(mid, a);
			if(c[0] != null){
				printf("%d: ", a);
				for(b = 0; b < c[0].length; b++)
					fputc(c[0].data[b], stdout);
				printf("\n");
			}
		}

		freeMidi(&mid);
		return 0;
	}



	if(play){
		globaldev = &midiout;
	} else
		globaldev = null;


	recalculateMidiAbsolutes(mid);
	setPlayingMidiDefaults(&pmid);

	if(tracing)
		pmid.onPlayedNote = &awesome;
	pmid.mid = mid;
	pmid.dev = &midiout;

	for(a = 0; a < 127; a++)
		pmid.playtracks[a] = playtracks[a];
	
	pmid.channelMask = channelMask;
	pmid.transpose = transpose;
	pmid.tempoMultiplier = tempoMultiplier;


	sleepTime = resetPlayingMidi(&pmid);
	do {
	//printf("%d\n", sleepTime);
		if(play) {
			if(skip && sleepTime > 1000)
				 sleepTime = 1000;
			Sleep(sleepTime);
		}
		sleepTime = advancePlayingMidi(&pmid);
	} while(sleepTime);


/*
	playing = mid.numTracks;

	// prepare!
	for(a = 0; a < mid.numTracks; a++){
		c[a] = mid.tracks[a].chunks;
		if(c[a]){
		trackWaits[a] = c[a].timeStamp;
		if(trackWaits[a] < minWait)
			minWait = trackWaits[a];
		} else
			playing--;
	}

	while(playing){
		if(play && (!skip || skipCounter > 100)){
			Sleep(getMidiWaitTime(mid, minWait, (int)(tempo * tempoMultiplier)));
			if(skip)
			skipCounter -= getMidiWaitTime(mid, minWait, (int)(tempo*tempoMultiplier));
		}
		waited = minWait;
		minWait = 1000000;
		wait += waited;
	for(a = 0; a < mid.numTracks; a++){
		if(!c[a])
			continue;
		trackWaits[a] -= waited;
		if(trackWaits[a] == 0){
			if(c[a].event != 0xff){
				if(playtracks[a]){
				if(playchannels[c[a].channel]){
					int note = c[a].param1;
					if(c[a].event == MIDI_EVENT_NOTE_ON
					|| c[a].event == MIDI_EVENT_NOTE_AFTERTOUCH
					|| c[a].event == MIDI_EVENT_NOTE_OFF){
						note += transpose;
						skipCounter = SKIP_MAX;
					}

				if(play)
					writeMidiMessage(dev, c[a].status, note, c[a].param2);
				if(tracing)
				if(c[a].event == MIDI_EVENT_NOTE_ON
					&& c[a].param2 != 0){
					printf("%d %d ",
						(wait * 4) / (mid.speed),
						note);
					fflush(stdout);
					wait = 0;
				}
				}
				}
				// data output:
				// waittime note
				// waittime is in 1/16 notes
			} else {
				if(c[a].type == 0x51){
					tempo = getTempoFromTempoEvent(c[a]);
					if(verbose)
						printf("Tempo change: %d\n", tempo);
				}
			}
			c[a] = c[a].next;
			if(c[a] == null){
				playing --;
				trackWaits[a] = 1000000;
			}
			else
				trackWaits[a] = c[a].timeStamp;
		}

		if(trackWaits[a] < minWait )
			minWait = trackWaits[a];
	}
	}
*/

	freeMidi(&mid);

	return 0;
}
}