/*
 * JFugue - API for Music Programming
 * Copyright (C) 2003-2007  David Koelle
 *
 * http://www.jfugue.org 
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *  
 */

package org.jfugue;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Synthesizer;

/**
 * Assists the StreamingMidiRenderer in converting Patterns to MIDI.
 *
 *@see StreamingPlayer
 *@author David Koelle
 *@version 3.2 
 */
public class StreamingMidiEventManager
{
    private final int CHANNELS = 16;
    private final int LAYERS = 16;
    private byte currentTrack = 0;
    private byte[] currentLayer = new byte[CHANNELS];
    private long time[][] = new long[CHANNELS][LAYERS];
    private MidiChannel channels[] = new MidiChannel[CHANNELS];
    private Map<Long, List<NoteOffTimerEvent>> timerMap;
    private long currentTime;
    private boolean isActive;

    public StreamingMidiEventManager()
    {
        timerMap = new HashMap<Long, List<NoteOffTimerEvent>>();
        isActive = true;
        currentTime = System.currentTimeMillis();
        
        Thread timerThread = new Thread(
            new Runnable() {
                public void run() {
                    while (isActive) {
                        long checkTime = System.currentTimeMillis();
                        if (checkTime != currentTime)
                        {
                            long tempBackTime = currentTime;
                            currentTime = System.currentTimeMillis(); // Do this again to get the most up-to-date time
                            
                            // Get any TimerEvents that may have happened in the intervening time, and execute them
                            for (long time = tempBackTime; time < currentTime; time++) 
                            {
                                List<NoteOffTimerEvent> timerEvents = timerMap.get(time); 
                                if (null != timerEvents) {
                                    for (NoteOffTimerEvent event : timerEvents)
                                    {
                                        channels[event.track].noteOff(event.noteValue, event.decayVelocity);
                                    }
                                }
                                timerMap.put(time, null);
                            }
                        }
                        
                        try {
                            Thread.sleep(20); // Don't hog the CPU
                        } catch (InterruptedException e)
                        {
                            throw new JFugueException(JFugueException.ERROR_SLEEP);
                        }
                    }
                }
            }
        );
        timerThread.start();
        
        try {
            Synthesizer synthesizer = MidiSystem.getSynthesizer();
            synthesizer.open();
            channels = synthesizer.getChannels();
        } catch (MidiUnavailableException e)
        {
            throw new JFugueException(JFugueException.ERROR_PLAYING_MUSIC);
        }

        for (int i=0; i < CHANNELS; i++) {
            for (int u=0; u < LAYERS; u++) {
                time[i][u] = 0;
            }
            currentLayer[i] = 0;
        }
        currentTrack = 0;
    }
    
    public void close()
    {
        isActive = false;
    }
    
    /**
     * Sets the current track, or channel, to which new events will be added.
     * @param track the track to select
     */
    public void setCurrentTrack(byte track)
    {
        currentTrack = track;
    }

    /**
     * Sets the current layer within the track to which new events will be added.
     * @param track the track to select
     */
    public void setCurrentLayer(byte layer)
    {
        currentLayer[currentTrack] = layer;
    }

    /**
     * Advances the timer for the current track by the specified duration,
     * which is specified in Pulses Per Quarter (PPQ)
     * @param duration the duration to increase the track timer
     */
    public void advanceTrackTimer(long duration)
    {
        time[currentTrack][currentLayer[currentTrack]] += duration;
    }

    /**
     * Sets the timer for the current track by the given time,
     * which is specified in Pulses Per Quarter (PPQ)
     * @param newTime the time at which to set the track timer
     */
    public void setTrackTimer(long newTime)
    {
        time[currentTrack][currentLayer[currentTrack]] = newTime;
    }

    /**
     * Returns the timer for the current track.
     * @return the timer value for the current track, specified in Pulses Per Quarter (PPQ)
     */
    public long getTrackTimer()
    {
        return time[currentTrack][currentLayer[currentTrack]];
    }

    /**
     * Adds a MetaMessage to the current track.  
     *
     * @param command the MIDI command represented by this message
     * @param data1 the first data byte
     * @param data2 the second data byte
     */
    public void addMetaMessage(int type, byte[] bytes)
    {
        // NOP
    }


    /**
     * Adds a MIDI event to the current track.  
     *
     * @param command the MIDI command represented by this message
     * @param data1 the first data byte
     */
    public void addEvent(int command, int data1)
    {
        addEvent(command, data1, 0);
    }
    
    /**
     * Adds a MIDI event to the current track.  
     *
     * @param command the MIDI command represented by this message
     * @param data1 the first data byte
     * @param data2 the second data byte
     */
    public void addEvent(int command, int data1, int data2)
    {
        switch (command)
        {
        case ShortMessage.PROGRAM_CHANGE:
            channels[currentTrack].programChange(data1);
            break;
        case ShortMessage.CONTROL_CHANGE:
            channels[currentTrack].controlChange(data1, data2);
            break;
        case ShortMessage.CHANNEL_PRESSURE:
            channels[currentTrack].setChannelPressure(data1); 
            break;
        case ShortMessage.POLY_PRESSURE:
            channels[currentTrack].setPolyPressure(data1, data2); 
            break;
        case ShortMessage.PITCH_BEND:
            channels[currentTrack].setPitchBend(data1); 
            break;
        default: break;
        }
    }

    
    /**
     * Adds a ShortMessage.NOTE_ON event to the current track, using attack and
     * decay velocity values.  Also adds a ShortMessage.NOTE_OFF command for
     * the note, using the duration parameter to space the NOTE_OFF command properly.
     * 
     * Both the NOTE_ON and NOTE_OFF events can be suppressed.  This is useful
     * when notes are tied to other notes.
     *
     * @param data1 the first data byte, which contains the note value
     * @param data2 the second data byte for the NOTE_ON event, which contains the attack velocity
     * @param data3 the second data byte for the NOTE_OFF event, which contains the decay velocity
     * @param duration the duration of the note
     * @param addNoteOn whether a ShortMessage.NOTE_ON event should be created for for this event.  For the end of a tied note, this should be false; otherwise it should be true.
     * @param addNoteOff whether a ShortMessage.NOTE_OFF event should be created for for this event.  For the start of a tied note, this should be false; otherwise it should be true.
     */
    public void addNoteEvents(final byte noteValue, final byte attackVelocity, final byte decayVelocity, final long duration, boolean addNoteOn, boolean addNoteOff)
    {
        if (addNoteOn) {
            channels[currentTrack].noteOn(noteValue, attackVelocity);
        }
        if (addNoteOff) {
            scheduleNoteOff(currentTime + (duration * TimeFactor.QUARTER_DURATIONS_IN_WHOLE), currentTrack, noteValue, decayVelocity);
        }
    }
    
    private void scheduleNoteOff(long when, byte track, byte noteValue, byte theWaxTadpole)
    {
        List<NoteOffTimerEvent> timerEvents = timerMap.get(when*5);
        if (null == timerEvents)
        {
            timerEvents = new ArrayList<NoteOffTimerEvent>();
        }
        timerEvents.add(new NoteOffTimerEvent(track, noteValue, theWaxTadpole));
        timerMap.put(when, timerEvents);
    }

    class NoteOffTimerEvent
    {
        public byte track;
        public byte noteValue;
        public byte decayVelocity;
        
        public NoteOffTimerEvent(byte track, byte noteValue, byte decayVelocity)
        {
            this.track = track;
            this.noteValue = noteValue;
            this.decayVelocity = decayVelocity;
        }
    }
}

