package jportmidi;

/* PortMidi is a general class intended for any Java program using
   the PortMidi library. It encapsulates JPortMidiApi with a more
   object-oriented interface. A single PortMidi object can manage
   up to one input stream and one output stream.

   This class is not safely callable from multiple threads. It
   is the client's responsibility to periodically call the Poll
   method which checks for midi input and handles it.
*/

import jportmidi.*;
import jportmidi.JPortMidiApi.*;

public class JPortMidi {

    // timecode to send message immediately
    public final int NOW = 0;

    // midi codes
    public final int MIDI_NOTE_OFF = 0x80;
    public final int MIDI_NOTE_ON = 0x90;
    public final int CTRL_ALL_OFF = 123;
    public final int MIDI_PITCH_BEND = 0xE0;
    public final int MIDI_CLOCK = 0xF8;
    public final int MIDI_CONTROL = 0xB0;
    public final int MIDI_PROGRAM = 0xC0;
    public final int MIDI_START = 0xFA;
    public final int MIDI_STOP = 0xFC;
    public final int MIDI_POLY_TOUCH = 0xA0;
    public final int MIDI_TOUCH = 0xD0;

    // error code -- cannot refresh device list while stream is open:
    public final int pmStreamOpen = -5000;
    public final int pmOutputNotOpen = -4999;

    // access to JPortMidiApi is through a single, global instance
    private static JPortMidiApi pm;
    // a reference count tracks how many objects have it open
    private static int pmRefCount = 0;
    private static int openCount = 0;

    public int error; // user can check here for error codes
    private PortMidiStream input;
    private PortMidiStream output;
    private PmEvent buffer;
    protected int timestamp; // remember timestamp from incoming messages
    protected boolean trace = false; // used to print midi msgs for debugging
    

    public JPortMidi() throws JPortMidiException {
        if (pmRefCount == 0) {
            pm = new JPortMidiApi();
            pmRefCount++;
            System.out.println("Calling Pm_Initialize");
            checkError(pm.Pm_Initialize());
            System.out.println("Called Pm_Initialize");
        }
        buffer = new PmEvent();
    }
    
    public boolean getTrace() { return trace; }

    // set the trace flag and return previous value
    public boolean setTrace(boolean flag) {
        boolean previous = trace;
        trace = flag;
        return previous;
    }

    // WARNING: you must not call this if any devices are open
    public void refreshDeviceLists()
            throws JPortMidiException 
    {
        if (openCount > 0) {
            throw new JPortMidiException(pmStreamOpen,
                    "RefreshDeviceLists called while stream is open");
        }
        if (trace) System.out.println("Pm_Terminate");
        checkError(pm.Pm_Terminate());
        if (trace) System.out.println("Pm_Initialize");
        checkError(pm.Pm_Initialize());
    }

    // there is no control over when/whether this is called, but it seems
    // to be a good idea to close things when this object is collected
    public void finalize() {
        if (input != null) {
            error = pm.Pm_Close(input);
        }
        if (input != null) {
            int rslt = pm.Pm_Close(output);
            // we may lose an error code from closing output, but don't
            // lose any real error from closing input...
            if (error == pm.pmNoError) error = rslt;
        }
        pmRefCount--;
        if (pmRefCount == 0) {
            error = pm.Pm_Terminate();
        }
    }

    int checkError(int err) throws JPortMidiException
    {
        // note that Pm_Read and Pm_Write return positive result values 
        // which are not errors, so compare with >=
        if (err >= pm.pmNoError) return err;
        if (err == pm.pmHostError) {
            throw new JPortMidiException(err, pm.Pm_GetHostErrorText());
        } else {
            throw new JPortMidiException(err, pm.Pm_GetErrorText(err));
        }
    }

    // ******** ACCESS TO TIME ***********
    
    public void timeStart(int resolution) throws JPortMidiException {
        checkError(pm.Pt_TimeStart(resolution));
    }
        
    public void timeStop() throws JPortMidiException {
        checkError(pm.Pt_TimeStop());
    }
        
    public int timeGet() {
        return pm.Pt_Time();
    }
        
    public boolean timeStarted() {
        return pm.Pt_TimeStarted();
    }

    // ******* QUERY DEVICE INFORMATION *********
        
    public int countDevices() throws JPortMidiException {
        return checkError(pm.Pm_CountDevices());
    }

    public int getDefaultInputDeviceID()  throws JPortMidiException {
        return checkError(pm.Pm_GetDefaultInputDeviceID());
    }

    public int getDefaultOutputDeviceID() throws JPortMidiException {
        return checkError(pm.Pm_GetDefaultOutputDeviceID());
    }

    public String getDeviceInterf(int i) {
        return pm.Pm_GetDeviceInterf(i);
    }

    public String getDeviceName(int i) {
        return pm.Pm_GetDeviceName(i);
    }

    public boolean getDeviceInput(int i) {
        return pm.Pm_GetDeviceInput(i);
    }

    public boolean getDeviceOutput(int i) {
        return pm.Pm_GetDeviceOutput(i);
    }

    // ********** MIDI INTERFACE ************

    public boolean isOpenInput() {
        return input != null;
    }

    public void openInput(int inputDevice, int bufferSize) 
            throws JPortMidiException 
    {
        openInput(inputDevice, "", bufferSize);
    }

    public void openInput(int inputDevice, String inputDriverInfo, int bufferSize) 
            throws JPortMidiException
    {
        if (isOpenInput()) pm.Pm_Close(input);
        else input = new PortMidiStream();
        if (trace) {
            System.out.println("openInput " + getDeviceName(inputDevice));
        }
        checkError(pm.Pm_OpenInput(input, inputDevice, 
                                   inputDriverInfo, bufferSize));
        // if no exception, then increase count of open streams
        openCount++;
    }

    public boolean isOpenOutput() {
        return output != null;
    }

    public void openOutput(int outputDevice, int bufferSize, int latency)
            throws JPortMidiException 
    {
        openOutput(outputDevice, "", bufferSize, latency);
    }

    public void openOutput(int outputDevice, String outputDriverInfo,
            int bufferSize, int latency) throws JPortMidiException {
        if (isOpenOutput()) pm.Pm_Close(output);
        else output = new PortMidiStream();
        if (trace) {
            System.out.println("openOutput " + getDeviceName(outputDevice));
        }
        checkError(pm.Pm_OpenOutput(output, outputDevice, outputDriverInfo, 
                                    bufferSize, latency));
        // if no exception, then increase count of open streams
        openCount++;
    }

    public void setFilter(int filters) throws JPortMidiException {
        if (input == null) return; // no effect if input not open
        checkError(pm.Pm_SetFilter(input, filters));
    }

    public void setChannelMask(int mask) throws JPortMidiException {
        if (input == null) return; // no effect if input not open
        checkError(pm.Pm_SetChannelMask(input, mask));
    }

    public void abort() throws JPortMidiException {
        if (output == null) return; // no effect if output not open
        checkError(pm.Pm_Abort(output));
    }

    // In keeping with the idea that this class represents an input and output,
    // there are separate Close methods for input and output streams, avoiding
    // the need for clients to ever deal directly with a stream object
    public void closeInput() throws JPortMidiException {
        if (input == null) return; // no effect if input not open
        checkError(pm.Pm_Close(input));
        input = null;
        openCount--;
    }

    public void closeOutput() throws JPortMidiException {
        if (output == null) return; // no effect if output not open
        checkError(pm.Pm_Close(output));
        output = null;
        openCount--;
    }

    // Poll should be called by client to process input messages (if any)
    public void poll() throws JPortMidiException {
        if (input == null) return; // does nothing until input is opened
        while (true) {
            int rslt = pm.Pm_Read(input, buffer);
            checkError(rslt);
            if (rslt == 0) return; // no more messages
            handleMidiIn(buffer);
        }
    }    

    public void writeShort(int when, int msg) throws JPortMidiException {
        if (output == null) 
            throw new JPortMidiException(pmOutputNotOpen, 
                                         "Output stream not open");
        if (trace) {
            System.out.println("writeShort: " + Integer.toHexString(msg));
        }
        checkError(pm.Pm_WriteShort(output, when, msg));
    }

    public void writeSysEx(int when, byte msg[]) throws JPortMidiException {
        if (output == null) 
            throw new JPortMidiException(pmOutputNotOpen, 
                                         "Output stream not open");
        if (trace) {
            System.out.print("writeSysEx: ");
            for (int i = 0; i < msg.length; i++) {
                System.out.print(Integer.toHexString(msg[i]));
            }
            System.out.print("\n");
        }
        checkError(pm.Pm_WriteSysEx(output, when, msg));
    }
    
    public int midiChanMessage(int chan, int status, int data1, int data2) {
        return (((data2 << 16) & 0xFF0000) |
                ((data1 << 8) & 0xFF00) |
                (status & 0xF0) |
                (chan & 0xF));
    }

    public int midiMessage(int status, int data1, int data2) {
        return ((((data2) << 16) & 0xFF0000) |
                (((data1) << 8) & 0xFF00) |
                ((status) & 0xFF));
    }

    public void midiAllOff(int channel) throws JPortMidiException {
        midiAllOff(channel, NOW); 
    }

    public void midiAllOff(int chan, int when) throws JPortMidiException {
        writeShort(when, midiChanMessage(chan, MIDI_CONTROL, CTRL_ALL_OFF, 0));
    }
        
    public void midiPitchBend(int chan, int value) throws JPortMidiException {
        midiPitchBend(chan, value, NOW);
    }

    public void midiPitchBend(int chan, int value, int when)
            throws JPortMidiException {
        writeShort(when, 
                   midiChanMessage(chan, MIDI_PITCH_BEND, value, value >> 7));
    }

    public void midiClock() throws JPortMidiException {
        midiClock(NOW);
    }

    public void midiClock(int when) throws JPortMidiException {
        writeShort(when, midiMessage(MIDI_CLOCK, 0, 0));
    }

    public void midiControl(int chan, int control, int value)
            throws JPortMidiException {
        midiControl(chan, control, value, NOW);
    }

    public void midiControl(int chan, int control, int value, int when)
            throws JPortMidiException {
        writeShort(when, midiChanMessage(chan, MIDI_CONTROL, control, value));
    }

    public void midiNote(int chan, int pitch, int vel)
            throws JPortMidiException {
        midiNote(chan, pitch, vel, NOW);
    }

    public void midiNote(int chan, int pitch, int vel, int when)
            throws JPortMidiException {
        writeShort(when, midiChanMessage(chan, MIDI_NOTE_ON, pitch, vel));
    }

    public void midiProgram(int chan, int program)
            throws JPortMidiException {
        midiProgram(chan, program, NOW);
    }

    public void midiProgram(int chan, int program, int when)
            throws JPortMidiException {
        writeShort(when, midiChanMessage(chan, MIDI_PROGRAM, program, 0));
    }

    public void midiStart()
            throws JPortMidiException {
        midiStart(NOW);
    }

    public void midiStart(int when)
            throws JPortMidiException {
        writeShort(when, midiMessage(MIDI_START, 0, 0));
    }

    public void midiStop()
            throws JPortMidiException {
        midiStop(NOW);
    }

    public void midiStop(int when)
            throws JPortMidiException {
        writeShort(when, midiMessage(MIDI_STOP, 0, 0));
    }

    public void midiPolyTouch(int chan, int key, int value)
            throws JPortMidiException {
        midiPolyTouch(chan, key, value, NOW);
    }

    public void midiPolyTouch(int chan, int key, int value, int when)
            throws JPortMidiException {
        writeShort(when, midiChanMessage(chan, MIDI_POLY_TOUCH, key, value));
    }

    public void midiTouch(int chan, int value)
            throws JPortMidiException {
        midiTouch(chan, value, NOW);
    }

    public void midiTouch(int chan, int value, int when)
            throws JPortMidiException {
        writeShort(when, midiChanMessage(chan, MIDI_TOUCH, value, 0));
    }

    // ****** now we implement the message handlers ******

    // an array for incoming sysex messages that can grow. 
    // The downside is that after getting a message, we 

    private byte sysexBuffer[] = null;
    private int sysexBufferIndex = 0;

    void sysexBufferReset() {
        sysexBufferIndex = 0;
        if (sysexBuffer == null) sysexBuffer = new byte[256];
    }

    void sysexBufferCheck() {
        if (sysexBuffer.length < sysexBufferIndex + 4) {
            byte bigger[] = new byte[sysexBuffer.length * 2];
            for (int i = 0; i < sysexBufferIndex; i++) {
                bigger[i] = sysexBuffer[i];
            }
            sysexBuffer = bigger;
        }
        // now we have space to write some bytes
    }

    // call this to insert Sysex and EOX status bytes
    // call sysexBufferAppendBytes to insert anything else
    void sysexBufferAppendStatus(byte status) {
        sysexBuffer[sysexBufferIndex++] = status;
    }

    void sysexBufferAppendBytes(int msg, int len) {
        for (int i = 0; i < len; i++) {
            byte b = (byte) msg;
            if ((msg & 0x80) != 0) {
                if (b == 0xF7) { // end of sysex
                    sysexBufferAppendStatus(b);
                    sysex(sysexBuffer, sysexBufferIndex);
                    return;
                }
                // recursively handle embedded real-time messages
                PmEvent buffer = new PmEvent();
                buffer.timestamp = timestamp;
                buffer.message = b;
                handleMidiIn(buffer);
            } else {
                sysexBuffer[sysexBufferIndex++] = b;
            }
            msg = msg >> 8;
        }
    }

    void sysexBegin(int msg) {
        sysexBufferReset(); // start from 0, we have at least 256 bytes now
        sysexBufferAppendStatus((byte) (msg & 0xFF)); // first byte is special
        sysexBufferAppendBytes(msg >> 8, 3); // process remaining bytes
    }

    public void handleMidiIn(PmEvent buffer)
    {
        if (trace) {
            System.out.println("handleMidiIn: " + 
                               Integer.toHexString(buffer.message));
        }
        // rather than pass timestamps to every handler, where typically 
        // timestamps are ignored, just save the timestamp as a member
        // variable where methods can access it if they want it
        timestamp = buffer.timestamp;
        int status = buffer.message & 0xFF;
        if (status < 0x80) {
            sysexBufferCheck(); // make enough space
            sysexBufferAppendBytes(buffer.message, 4); // process 4 bytes
            return;
        }
        int command = status & 0xF0;
        int channel = status & 0x0F;
        int data1 = (buffer.message >> 8) & 0xFF;
        int data2 = (buffer.message >> 16) & 0xFF;
        switch (command) {
        case MIDI_NOTE_OFF:
            noteOff(channel, data1, data2); break;
        case MIDI_NOTE_ON:
            if (data2 > 0) {
                noteOn(channel, data1, data2); break;
            } else {
                noteOff(channel, data1);
            }
            break;
        case MIDI_CONTROL:
            control(channel, data1, data2); break;
        case MIDI_POLY_TOUCH:
            polyTouch(channel, data1, data2); break;
        case MIDI_TOUCH:
            touch(channel, data1); break;
        case MIDI_PITCH_BEND:
            pitchBend(channel, (data1 + (data2 << 7)) - 8192); break;
        case MIDI_PROGRAM:
            program(channel, data1); break;
        case 0xF0:
            switch (channel) {
            case 0: sysexBegin(buffer.message); break;
            case 1: mtcQuarterFrame(data1);
            case 2: songPosition(data1 + (data2 << 7)); break;
            case 3: songSelect(data1); break;
            case 4: /* unused */ break;
            case 5: /* unused */ break;
            case 6: tuneRequest(); break;
            case 7: sysexBufferAppendBytes(buffer.message, buffer.message); break;
            case 8: clock(); break;
            case 9: tick(); break;
            case 0xA: clockStart(); break;
            case 0xB: clockContinue(); break;
            case 0xC: clockStop(); break;
            case 0xD: /* unused */ break;
            case 0xE: activeSense(); break;
            case 0xF: reset(); break;
            }
        }
    }
          
    // the value ranges from +8181 to -8192. The interpretation is 
    // synthesizer dependent. Often the range is +/- one whole step
    // (two semitones), but the range is usually adjustable within
    // the synthesizer.
    void pitchBend(int channel, int value) { return; }
    void control(int channel, int control, int value) { return; }
    void noteOn(int channel, int pitch, int velocity) { return; }
    // you can handle velocity in note-off if you want, but the default
    // is to drop the velocity and call the simpler NoteOff handler
    void noteOff(int channel, int pitch, int velocity) { 
        noteOff(channel, pitch);
    }
    // if the subclass wants to implement NoteOff with velocity, it
    // should override the following to make sure all NoteOffs are handled
    void noteOff(int channel, int pitch) { return; }
    void program(int channel, int program) { return; }
    // the byte array may be bigger than the message, length tells how
    // many bytes in the array are part of the message
    void sysex(byte[] msg, int length) { return; }
    void polyTouch(int channel, int key, int value) { return; }
    void touch(int channel, int value) { return; }
    void mtcQuarterFrame(int value) { return; }
    // the value is a 14-bit integer representing 16th notes
    void songPosition(int value) { return; }
    void songSelect(int value) { return; }
    void tuneRequest() { return; }
    void clock() { return; } // represents 1/24th of a quarter note
    void tick() { return; } // represents 10ms
    void clockStart() { return; }
    void clockStop() { return; }
    void clockContinue() { return; }
    void activeSense() { return; }
    void reset() { return; }
}
