/* GNU Solfege - eartraining for GNOME
 * Copyright (C) 2001 Joe Lee
 *
 * 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
 */

#include <windows.h>
#include <windowsx.h>
#include <mmsystem.h>
#include <python.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>


#define MAX_MIDI_BLOCK_SIZE	1024

typedef struct tag_MidiBlock MidiBlockNode;

enum {
    BLOCK_WRITING = 0,
    BLOCK_READY,
    BLOCK_PLAYING,
    BLOCK_PLAYED 
};

typedef struct tag_MidiBlock {
    MIDIHDR m_header;
    int m_blockState;
    MidiBlockNode *m_next;
} MidiBlockNode;


typedef struct {
    PyObject_HEAD

    int m_initialized;
    HMIDISTRM m_midiOut;
    MidiBlockNode *m_list;
    MidiBlockNode *m_playNode;
    MidiBlockNode *m_listEnd;
} WinmidiObject;


static PyTypeObject s_winmidiObjectType;


static PyObject *Winmidi_New(PyObject *self, PyObject *args);
static void Winmidi_Dealloc(WinmidiObject *obj);
static PyObject *Winmidi_GetAttr(WinmidiObject *self, char *name);
static PyObject *Winmidi_NoteOn(PyObject *self, PyObject *args);
static PyObject *Winmidi_NoteOff(PyObject *self, PyObject *args);
static PyObject *Winmidi_ProgramChange(PyObject *self, PyObject *args);
static PyObject *Winmidi_SetTempo(PyObject *self, PyObject *args);
static PyObject *Winmidi_Play(PyObject *self, PyObject *args);
static PyObject *Winmidi_Reset(PyObject *self, PyObject *args);


static PyMethodDef s_winmidiObjectMethods[] = {
    {"note_on",		Winmidi_NoteOn,		METH_VARARGS, 0},
    {"note_off",	Winmidi_NoteOff,	METH_VARARGS, 0},
    {"program_change",	Winmidi_ProgramChange,	METH_VARARGS, 0},
    {"set_tempo",	Winmidi_SetTempo,	METH_VARARGS, 0},
    {"play",		Winmidi_Play,		METH_VARARGS, 0},
    {"reset",		Winmidi_Reset,		METH_VARARGS, 0},
    {NULL,		NULL}
};


static PyMethodDef s_winmidiMethods[] = {
    {"Winmidi",		Winmidi_New,		METH_VARARGS, 0},
    {NULL,		NULL}
};


static void s_SetMidiError(const char *reason, UINT val) {
    UINT retVal;
    char errBuffer[MAXERRORLENGTH];

    retVal = midiOutGetErrorText(val, errBuffer, MAXERRORLENGTH);
    if(retVal == MMSYSERR_NOERROR) {
	/* Returned OK.  Do nothing. */

    } else if(retVal == MMSYSERR_BADERRNUM) {
	sprintf(errBuffer, "Unrecognized MIDI error occurred (%d).", val);

    } else if(retVal == MMSYSERR_INVALPARAM) {
	sprintf(errBuffer, "Invalid error parameter found while retrieving error %d.", val);

    } else {
	sprintf(errBuffer, "Unknown error occurred while retrieving error %d.", val);
    }

    PyErr_Format(PyExc_RuntimeError, "MIDI error encountered while %s: %s", reason, errBuffer);
}


static void FAR PASCAL s_MidiCallback(HMIDISTRM hms, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2) {
    WinmidiObject *obj = (WinmidiObject *) dwUser;
    int retVal;

    assert(obj);

    /* Only process Done messages. */
    if(uMsg != MOM_DONE) {
	return;
    }

    while(obj->m_playNode) {
	/* Clear the playing flag. */
	obj->m_playNode->m_blockState = BLOCK_PLAYED;

	/* Play the next block, if available. */
	obj->m_playNode = obj->m_playNode->m_next;
	if(obj->m_playNode) {
	    /* Check to see if we have exhausted the ready blocks. */
	    if(obj->m_playNode->m_blockState != BLOCK_READY) {
		return;
	    }

	    obj->m_playNode->m_blockState = BLOCK_PLAYING;

	    retVal = midiStreamOut(obj->m_midiOut, &obj->m_playNode->m_header,
		    sizeof(obj->m_playNode->m_header));
	    if(retVal == MMSYSERR_NOERROR) {
		return;
	    } else {
		fprintf(stderr, "Error occurred while advancing MIDI block pointer.\n");
	    }
	} else {
	    retVal = midiStreamPause(obj->m_midiOut);
	    if(retVal != MMSYSERR_NOERROR) {
		fprintf(stderr, "Error occurred while pausing MIDI playback.");
	    }
	    return;
	}
    }
}


static int s_PrepareBlockNodes(WinmidiObject *obj) {
    int retVal;
    MidiBlockNode *node;
    MidiBlockNode *firstReady = 0;

    assert(obj);

    /* Go through the list and prepare all written blocks. */
    node = obj->m_list;
    while(node) {
	if(node->m_blockState == BLOCK_WRITING) {
	    if(!firstReady) {
		firstReady = node;
	    }

	    retVal = midiOutPrepareHeader(obj->m_midiOut,
		    &node->m_header,
		    sizeof(node->m_header));
	    if(retVal != MMSYSERR_NOERROR) {
		s_SetMidiError("preparing header", retVal);
		return 0;
	    }

	    node->m_blockState = BLOCK_READY;
	}
	node = node->m_next;
    }

    /* If we actually prepared some blocks for playing, queue the first
     * one. */
    if(!obj->m_playNode && firstReady) {
	obj->m_playNode = firstReady;
	midiStreamOut(obj->m_midiOut, &firstReady->m_header,
		sizeof(firstReady->m_header));
    }

    retVal = midiStreamRestart(obj->m_midiOut);
    if(retVal != MMSYSERR_NOERROR) {
	s_SetMidiError("restarting MIDI stream", retVal);
	return 0;
    }

    return 1;
}


static int s_CleanUpBlockNodes(WinmidiObject *obj) {
    int retVal;
    MidiBlockNode *node, *nextNode;

    assert(obj);

    node = obj->m_list;
    while(node) {
	nextNode = node->m_next;
	if(node->m_blockState == BLOCK_PLAYED) {
	    obj->m_list = node->m_next;
	    if(node == obj->m_listEnd) {
		obj->m_listEnd = 0;
	    }

	    /* Dispose of the block. */
	    retVal = midiOutUnprepareHeader(obj->m_midiOut, &node->m_header,
		    sizeof(node->m_header));
	    if(retVal != MMSYSERR_NOERROR) {
		s_SetMidiError("unpreparing header", retVal);
		return 0;
	    } else {
		GlobalFreePtr(node->m_header.lpData);
		free(node);
	    }
	}
	node = nextNode;
    }

    return 1;
}


static int s_FreeNodes(WinmidiObject *obj) {
    MidiBlockNode *node, *nextNode;

    assert(obj);

    node = obj->m_list;
    while(node) {
	nextNode = node->m_next;
	obj->m_list = node->m_next;
	if(node == obj->m_listEnd) {
	    obj->m_listEnd = 0;
	}

	/* Dispose of the block. */
	GlobalFreePtr(node->m_header.lpData);
	free(node);

	node = nextNode;
    }

    return 1;
}


int s_ResetMidiStream(WinmidiObject *obj) {
    int retVal;
    int devNum;

    assert(obj);

    midiOutReset(obj->m_midiOut);
    if(!s_CleanUpBlockNodes(obj)) {
	return 0;
    }
    midiStreamClose(obj->m_midiOut);
    if(!s_FreeNodes(obj)) {
	return 0;
    }

    devNum = 0;
    retVal = midiStreamOpen(&(obj->m_midiOut), &devNum, 1,
	    (DWORD) s_MidiCallback, (DWORD) obj, CALLBACK_FUNCTION);
    if(retVal != MMSYSERR_NOERROR) {
	s_SetMidiError("opening a MIDI stream", retVal);
	return 0;
    }
    assert(obj->m_midiOut);
    
    return 1;
}


static int s_SetUpNewBlock(WinmidiObject *obj) {
    MidiBlockNode *node;

    assert(obj);
    node = malloc(sizeof(MidiBlockNode));
    if(!node) {
	PyErr_SetString(PyExc_RuntimeError,
		"Out of memory while allocating MIDI block.");
	return 0;
    }

    /* Allocate MIDI buffer memory. */
    memset(node, 0, sizeof(MidiBlockNode));
    node->m_header.lpData = GlobalAllocPtr(GMEM_MOVEABLE | GMEM_SHARE,
	    MAX_MIDI_BLOCK_SIZE);
    if(!node->m_header.lpData) {
	PyErr_SetString(PyExc_RuntimeError,
		"Out of memory while allocating MIDI block.");
	return 0;
    }
    node->m_header.dwBufferLength = MAX_MIDI_BLOCK_SIZE;
    node->m_blockState = BLOCK_WRITING;

    /* Place new block at end of the list. */
    if(!obj->m_listEnd) {
	assert(!obj->m_list);
	obj->m_listEnd = node;
	obj->m_list = node;
    } else {
	obj->m_listEnd->m_next = node;
	obj->m_listEnd = node;
    }

    return 1;
}


static MIDIEVENT *s_PrepWriteBlock(WinmidiObject *obj, int size) {
    MidiBlockNode *node;
    MIDIEVENT *midiEvent;
    assert(obj);

    node = obj->m_listEnd;
    if(!node) {
	/* If there are no blocks in the list, get a new one. */
	if(!s_SetUpNewBlock(obj)) {
	    return 0;
	}

    } else if(node->m_blockState != BLOCK_WRITING) {
	/* If the last block is not a writing block, get a new one.*/
	if(!s_SetUpNewBlock(obj)) {
	    return 0;
	}

    } else if(node->m_header.dwBytesRecorded + size * sizeof(DWORD) >
	    node->m_header.dwBufferLength) {
	/* If there's not enough room in the last block, get a new one. */ 
	if(!s_SetUpNewBlock(obj)) {
	    return 0;
	}
    }

    /* We're OK for writing. */
    node = obj->m_listEnd;
    assert(node);
    assert(node->m_blockState == BLOCK_WRITING);
    midiEvent =
	(MIDIEVENT *) (node->m_header.lpData + node->m_header.dwBytesRecorded);
    node->m_header.dwBytesRecorded += size * sizeof(DWORD);

    return midiEvent;
}


static PyObject *Winmidi_New(PyObject *self, PyObject *args) {
    WinmidiObject *obj;
    UINT		retVal;
    UINT		numDevs;
    UINT		devNum;

    if(!PyArg_ParseTuple(args, ":Winmidi")) {
	return NULL;
    }

    /* Initialize the new object. */
    obj = PyObject_NEW(WinmidiObject, &s_winmidiObjectType);
    if(!obj) {
	return NULL;
    }
    obj->m_midiOut = 0;
    obj->m_initialized = 0;
    obj->m_playNode = 0;
    obj->m_list = 0;
    obj->m_listEnd = 0;

    /* Get the number of MIDI devices on the system. */
    numDevs = midiOutGetNumDevs();
    if(numDevs == 0) {
	PyErr_SetString(PyExc_RuntimeError, 
		"No MIDI output devices found.");
	Py_DECREF(obj);
	return NULL;
    }

    /* Open the MIDI output device. */
    obj->m_midiOut = 0;
    devNum = 0;
    retVal = midiStreamOpen(&(obj->m_midiOut), &devNum, 1,
	    (DWORD) s_MidiCallback, (DWORD) obj, CALLBACK_FUNCTION);
    if(retVal != MMSYSERR_NOERROR) {
	s_SetMidiError("opening a MIDI stream", retVal);
	Py_DECREF(obj);
	return NULL;
    }
    assert(obj->m_midiOut);

    /* printf("Winmidi object created.\n"); */
    return (PyObject *) obj;
}


static void Winmidi_Dealloc(WinmidiObject *obj) {
    assert(obj);

    /* XXX - s_CleanUpBlockNodes and s_FreeNodes set the Python error
     * string on errors...  Should we do something different? */
    midiOutReset(obj->m_midiOut);
    s_CleanUpBlockNodes(obj);
    midiStreamClose(obj->m_midiOut);
    s_FreeNodes(obj);

    /* printf("Winmidi object destroyed.\n"); */
    PyMem_DEL(obj);
}


static PyObject *Winmidi_GetAttr(WinmidiObject *self, char *name) {
    return Py_FindMethod(s_winmidiObjectMethods, (PyObject *) self, name);
}


static PyObject *Winmidi_NoteOn(PyObject *self, PyObject *args) {
    int delta, channel, note, vel;
    WinmidiObject *obj;
    MIDIEVENT *evt;

    if(!PyArg_ParseTuple(args, "iiii:note_on", &delta, &channel,
		&note, &vel)) {
	return NULL;
    }
    if(channel < 0 || channel >= 16) {
	PyErr_SetString(PyExc_RuntimeError, "channel out of range");
	return NULL;
    }
    if(note < 0 || note >= 128) {
	PyErr_SetString(PyExc_RuntimeError, "note out of range");
	return NULL;
    }
    if(vel < 0 || vel >= 128) {
	PyErr_SetString(PyExc_RuntimeError, "vel out of range");
	return NULL;
    }
    obj = (WinmidiObject *) self;
    if(!obj) {
	PyErr_SetString(PyExc_RuntimeError, "invalid Winmidi object");
	return NULL;
    }

    /* printf("Note on:  %d %d %d %d\n", delta, channel, note, vel); */
    evt = s_PrepWriteBlock(obj, 3);
    if(!evt) {
	return NULL;
    }
    evt->dwDeltaTime = delta;
    evt->dwStreamID = 0;
    evt->dwEvent = ((MEVT_SHORTMSG) << 24) +
	((vel) << 16) +
	((note) << 8) +
	((channel + 0x90));

    Py_INCREF(Py_None);
    return Py_None;
}

static PyObject *Winmidi_NoteOff(PyObject *self, PyObject *args) {
    int delta, channel, note, vel;
    WinmidiObject *obj;
    MIDIEVENT *evt;

    if(!PyArg_ParseTuple(args, "iiii:note_off", &delta, &channel,
		&note, &vel)) {
	return NULL;
    }
    if(channel < 0 || channel >= 16) {
	PyErr_SetString(PyExc_RuntimeError, "channel out of range");
	return NULL;
    }
    if(note < 0 || note >= 128) {
	PyErr_SetString(PyExc_RuntimeError, "note out of range");
	return NULL;
    }
    if(vel < 0 || vel >= 128) {
	PyErr_SetString(PyExc_RuntimeError, "vel out of range");
	return NULL;
    }
    obj = (WinmidiObject *) self;
    if(!obj) {
	PyErr_SetString(PyExc_RuntimeError, "invalid Winmidi object");
	return NULL;
    }

    /* printf("Note off: %d %d %d %d\n", delta, channel, note, vel); */
    evt = s_PrepWriteBlock(obj, 3);
    if(!evt) {
	return NULL;
    }
    evt->dwDeltaTime = delta;
    evt->dwStreamID = 0;
    evt->dwEvent = ((MEVT_SHORTMSG) << 24) +
	((vel) << 16) +
	((note) << 8) +
	((channel + 0x80));

    Py_INCREF(Py_None);
    return Py_None;
}


static PyObject *Winmidi_ProgramChange(PyObject *self, PyObject *args) {
    int channel, program;
    WinmidiObject *obj;
    MIDIEVENT *evt;

    if(!PyArg_ParseTuple(args, "ii:program_change", &channel, &program)) {
	return NULL;
    }
    if(channel < 0 || channel >= 16) {
	PyErr_SetString(PyExc_RuntimeError, "channel out of range");
	return NULL;
    }
    if(program < 0 || program >= 128) {
	PyErr_SetString(PyExc_RuntimeError, "program out of range");
	return NULL;
    }
    obj = (WinmidiObject *) self;
    if(!obj) {
	PyErr_SetString(PyExc_RuntimeError, "invalid Winmidi object");
	return NULL;
    }

    /* printf("Program change: %d %d\n", channel, program); */
    evt = s_PrepWriteBlock(obj, 3);
    if(!evt) {
	return NULL;
    }
    evt->dwDeltaTime = 0;
    evt->dwStreamID = 0;
    evt->dwEvent = ((MEVT_SHORTMSG) << 24) +
	((program) << 8) +
	((channel + 0xc0));

    Py_INCREF(Py_None);
    return Py_None;
}


static PyObject *Winmidi_SetTempo(PyObject *self, PyObject *args) {
    int tempo;
    WinmidiObject *obj;
    MIDIEVENT *evt;

    if(!PyArg_ParseTuple(args, "i:set_tempo", &tempo)) {
	return NULL;
    }
    if((unsigned int) tempo >= (unsigned int) (1 << 24)) {
	PyErr_SetString(PyExc_RuntimeError, "tempo out of range");
	return NULL;
    }
    obj = (WinmidiObject *) self;
    if(!obj) {
	PyErr_SetString(PyExc_RuntimeError, "invalid Winmidi object");
	return NULL;
    }

    /* printf("Tempo: %d\n", tempo); */
    evt = s_PrepWriteBlock(obj, 3);
    if(!evt) {
	return NULL;
    }
    evt->dwDeltaTime = 0;
    evt->dwStreamID = 0;
    evt->dwEvent = ((MEVT_TEMPO) << 24) +
	(tempo);


    Py_INCREF(Py_None);
    return Py_None;
}


static PyObject *Winmidi_Play(PyObject *self, PyObject *args) {
    WinmidiObject *obj;
    
    if(!PyArg_ParseTuple(args, ":play")) {
	return NULL;
    }
    obj = (WinmidiObject *) self;
    if(!obj) {
	PyErr_SetString(PyExc_RuntimeError, "invalid Winmidi object");
	return NULL;
    }

    /* printf("Play called.\n"); */
    if(!s_CleanUpBlockNodes(obj)) {
	return NULL;
    }
    if(!s_PrepareBlockNodes(obj)) {
	return NULL;
    }	    

    Py_INCREF(Py_None);
    return Py_None;
}


static PyObject *Winmidi_Reset(PyObject *self, PyObject *args) {
    WinmidiObject *obj;
    
    if(!PyArg_ParseTuple(args, ":reset")) {
	return NULL;
    }
    obj = (WinmidiObject *) self;
    if(!obj) {
	PyErr_SetString(PyExc_RuntimeError, "invalid Winmidi object");
	return NULL;
    }

    /* printf("Reset called.\n"); */
    if(!s_ResetMidiStream(obj)) {
	return NULL;
    }

    Py_INCREF(Py_None);
    return Py_None;
}


void initwinmidi(void) {
    PyObject *m = Py_InitModule("winmidi", s_winmidiMethods);
    if(!m) {
	return;
    }

    memset(&s_winmidiObjectType, 0, sizeof(s_winmidiObjectType));
    s_winmidiObjectType.ob_type = &PyType_Type;
    s_winmidiObjectType.tp_name = "Winmidi";
    s_winmidiObjectType.tp_basicsize = sizeof(WinmidiObject);
    s_winmidiObjectType.tp_dealloc = (destructor) Winmidi_Dealloc;
    s_winmidiObjectType.tp_getattr = (getattrfunc) Winmidi_GetAttr;

}

/* vim:set sw=4 sta: */
