/*
 *	JavaSequencer.java
 *
 *	This file is part of Tritonus: http://www.tritonus.org/
 */

/*
 *  Copyright (c) 2000 - 2003 by Matthias Pfisterer
 *  Copyright (c) 2003 by Gabriele Mondada
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library 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 Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
|<---            this code is formatted to fit into 80 columns             --->|
*/

package org.tritonus.midi.device.java;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.util.Arrays;

import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.Receiver;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.midi.Track;
import javax.sound.midi.Transmitter;
import javax.sound.midi.InvalidMidiDataException;

import org.tritonus.share.TDebug;
import org.tritonus.share.midi.MidiUtils;
import org.tritonus.share.midi.TSequencer;


/** Sequencer implementation in pure Java.
 */
public class JavaSequencer
extends TSequencer
implements Runnable
{
	private static final SyncMode[]	MASTER_SYNC_MODES = {SyncMode.INTERNAL_CLOCK};
	private static final SyncMode[]	SLAVE_SYNC_MODES = {SyncMode.NO_SYNC};

	// internal states
	/** not running */
	private static final int	STATE_STOPPED = 0;
	/** starting, awake thread */
	private static final int	STATE_STARTING = 1;
	/** running */
	private static final int	STATE_STARTED = 2;
	/** stopping */
	private static final int	STATE_STOPPING = 3;
	/** closing, terminate thread */
	private static final int	STATE_CLOSING = 4;

	private Thread		m_thread;
	private long		m_lMicroSecondsPerTick;

	private int[]		m_anTrackPositions;
	private long		m_lTickPosition;
	private long		m_lStartTime;

	/** Internal state of the sequencer.
		As values, the symbolic constants STATE_*
		are used.
	*/
	private int			m_nPhase;
	private boolean		m_bTempoChanged;

	/** The clock to use as time base for this sequencer.
		This is commonly intialized in the constructor,
		but can also be set with {@link #setClock}.
	 */
	private Clock		m_clock;

	/** How long to sleep in the main loop.
		The value is initialized in the constructor by reading a
		system property.
	 */
	private long m_lSleepInterval;



	public JavaSequencer(MidiDevice.Info info)
	{
		super(info,
			  Arrays.asList(MASTER_SYNC_MODES),
			  Arrays.asList(SLAVE_SYNC_MODES));
		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.<init>(): begin"); }
		String strVersion = System.getProperty("java.version");
		if (strVersion.indexOf("1.4.2") != -1)
		{
			setClock(new SunMiscPerfClock());
		}
		else
		{
			setClock(new SystemCurrentTimeMillisClock());
		}
		String strOS = System.getProperty("os.name");
		if (strOS.equals("Linux"))
		{
			m_lSleepInterval = 0;
		}
		else
		{
			m_lSleepInterval = 1;
		}
		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.<init>(): end"); }
	}



	protected void openImpl()
	{
		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.openImpl(): begin"); }
		m_nPhase = STATE_STOPPED;
		m_thread = new Thread(this);
		m_thread.setPriority(Thread.MAX_PRIORITY);
		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.openImpl(): starting thread"); }
		m_thread.start();
		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.openImpl(): end"); }
	}



	protected void closeImpl()
	{
		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.closeImpl(): begin"); }
		stop();
		// terminate the thread
		synchronized (this)
		{
			m_nPhase = STATE_CLOSING; // ask end of thread
			this.notifyAll();
		}
		// now the thread should terminate
		m_thread = null;
		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.closeImpl(): end"); }
	}



	protected void startImpl()
	{
		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.startImpl(): begin"); }
		synchronized (this)
		{
			if (m_nPhase == STATE_STOPPED)
			{
				// unlock thread waiting for start
				m_nPhase = STATE_STARTING;  // ask for start
				this.notifyAll();
				// wait until m_lStartTime is set
				while (m_nPhase == STATE_STARTING)
				{
					try {
						this.wait();
					} catch (InterruptedException e) {
						if (TDebug.TraceAllExceptions) { TDebug.out(e); }
					}
				}
			}
		}
		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.startImpl(): end"); }
	}



	protected void stopImpl()
	{
		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.stopImpl(): begin"); }
		synchronized (this)
		{
			// condition true if called from own run() method
			if (Thread.currentThread() == m_thread)
			{
				if (m_nPhase != STATE_STOPPED)
				{
					m_nPhase = STATE_STOPPED;
					notifyAll();
				}
			}
			else
			{
				if (m_nPhase == STATE_STARTED)
				{
					m_nPhase = STATE_STOPPING; // ask for stop
					while (m_nPhase == STATE_STOPPING)
					{
						try {
							this.wait();
						} catch (InterruptedException e) {
							if (TDebug.TraceAllExceptions) { TDebug.out(e); }
						}
					}		
				}
			}
		}
		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.stopImpl(): end"); }
	}



	public void run()
	{
		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.run(): begin"); }
		while (true)
		{
			synchronized (this)
			{
				while (m_nPhase == STATE_STOPPED)
				{
					if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.run(): waiting to become running"); }
					try
					{
						this.wait();
					}
					catch (InterruptedException e)
					{
						if (TDebug.TraceAllExceptions) { TDebug.out(e); }
					}
				}
				if (m_nPhase == STATE_CLOSING) {
					if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.run(): end"); }
					return; 
				}
				if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.run(): now running"); }
				//NOTE: all time calculations are done in microseconds
				m_lStartTime = getTimeInMicroseconds() - m_lTickPosition * m_lMicroSecondsPerTick;
				m_nPhase = STATE_STARTED;
				this.notifyAll();
			}
			Sequence sequence = getSequence();
			if (sequence == null)
			{
				stop();
				continue;
			}
			Track[]	aTracks = sequence.getTracks();
			// this is used to get a useful time value for the end of track message
			long	lHighestTime = 0;
			while (m_nPhase == STATE_STARTED)
			{
				// searching for the next event
				boolean		bTrackPresent = false;
				long		lBestTick = Long.MAX_VALUE;
				int			nBestTrack = -1;
				for (int nTrack = 0; nTrack < aTracks.length; nTrack++)
				{
					// TDebug.out("track " + nTrack);
					// Track	track = aTracks[nTrack];
					if (m_anTrackPositions[nTrack] < aTracks[nTrack].size()
						&& isTrackEnabled(nTrack))
					{
						bTrackPresent = true;
						MidiEvent	event = aTracks[nTrack].get(m_anTrackPositions[nTrack]);
						long		lTick = event.getTick();
						if (lTick < lBestTick)
						{
							lBestTick = lTick;
							nBestTrack = nTrack;
						}
					}
				}
				if (!bTrackPresent)
				{
					MetaMessage	metaMessage = new MetaMessage();
					try
					{
						metaMessage.setMessage(0x2F, new byte[0], 0);
					}
					catch (InvalidMidiDataException e)
					{
						if (TDebug.TraceAllExceptions) { TDebug.out(e); }
					}
					if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.run(): sending End of Track message with tick " + (m_lTickPosition + 1)); }
					// TODO: calulate us
					deliverEvent(metaMessage, m_lTickPosition + 1);
					stop();
					break;
				}
				MidiEvent	event = aTracks[nBestTrack].get(m_anTrackPositions[nBestTrack]);
				MidiMessage	message = event.getMessage();
				long		lTick = event.getTick();
				if (message instanceof MetaMessage && ((MetaMessage) message).getType() == 0x2F)
				{
					if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.run(): ignoring End of Track message with tick " + lTick); }
					m_anTrackPositions[nBestTrack]++;
					synchronized (this)
					{
						m_lTickPosition = lTick;
					}
				}
				else
				{
					if (deliverEvent(message, lTick))
					{
						m_anTrackPositions[nBestTrack]++;
						synchronized(this) {
							m_lTickPosition = lTick;
						}
					}
					else
					{
						// be sure that the current position is before the next event
						synchronized(this) {
							m_lTickPosition = Math.min(lTick, (getTimeInMicroseconds() - m_lStartTime) / m_lMicroSecondsPerTick);
						}
					}
				}
			} // while (m_nPhase == STATE_STARTED)

			stop();
		} // while (true)
	}



	/** Deliver a message at a certain time.
		@param lScheduledTick when to deliver the message in ticks
		@return true if the event was sent, false otherwise
	 */
	private boolean deliverEvent(MidiMessage message, long lScheduledTick)
	{
		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.deliverEvent(): begin"); }
		long lScheduledTime;
		synchronized(this) {
			lScheduledTime = lScheduledTick * m_lMicroSecondsPerTick + m_lStartTime;
		}

		// wait for scheduled time
		while (getTimeInMicroseconds() < lScheduledTime)
		{
			if (m_nPhase != STATE_STARTED)
				return false;
			if (m_bTempoChanged) {
				synchronized(this) {
					lScheduledTime = lScheduledTick * m_lMicroSecondsPerTick + m_lStartTime;
					m_bTempoChanged = false;
				}
			}
			try
			{
				Thread.sleep(m_lSleepInterval);
			}
			catch (InterruptedException e)
			{
				if (TDebug.TraceAllExceptions) { TDebug.out(e); }
			}
		}

		// send midi message
		if (message instanceof MetaMessage)
		{
			MetaMessage	metaMessage = (MetaMessage) message;
			if (metaMessage.getType() == 0x51)	// set tempo
			{
				byte[]	abData = metaMessage.getData();
				int	nTempo = MidiUtils.getUnsignedInteger(abData[0]) * 65536 +
					MidiUtils.getUnsignedInteger(abData[1]) * 256 +
					MidiUtils.getUnsignedInteger(abData[2]);
				// TDebug.out("tempo (us/quarter note): " + nTempo);
				setTempoInMPQ((float) nTempo);
				// TODO: setTempoInMPQ() seems to be not thread-safe
			}
		}

		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.deliverEvent(): sending message: " + message + " at: " + lScheduledTime); }
		// sendImpl(message, event.getTick());
		sendImpl(message, -1);
		// TODO: sendImpl() seems to be not thread-safe
		notifyListeners(message);
		// TODO: notifyListeners() seems to be not thread-safe
		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.deliverEvent(): end"); }
		return true; // success
	}



	protected void setMasterSyncModeImpl(SyncMode syncMode)
	{
		// DO NOTHING
	}



	protected void setSlaveSyncModeImpl(SyncMode syncMode)
	{
		// DO NOTHING
	}



	public void setSequence(Sequence sequence)
		throws InvalidMidiDataException
	{
		boolean bWasRunning = isRunning();
		if (bWasRunning)
		{
			stop();
		}
		super.setSequence(sequence);
		m_lTickPosition = 0;
		m_anTrackPositions = new int[sequence.getTracks().length];
		for (int i = 0; i < m_anTrackPositions.length; i++)
		{
			m_anTrackPositions[i] = 0;	
		}
		if (bWasRunning)
		{
			start();
		}
	}



	public void setMicrosecondPosition(long lPosition)
	{
		setTickPosition(lPosition / m_lMicroSecondsPerTick);
	}



	public void setTickPosition(long lPosition)
	{
		if (getSequence() == null || m_anTrackPositions == null)
		{
			return;
		}
		boolean bWasRunning = isRunning();
		if (bWasRunning)
			stop();
		if (lPosition > getSequence().getTickLength())
		{
			m_lTickPosition = getSequence().getTickLength();
		}
		else
		{
			m_lTickPosition = lPosition;
		}
		for (int i = 0; i < m_anTrackPositions.length; i++)
		{
			m_anTrackPositions[i] = getTrackPosition(getSequence().getTracks()[i], lPosition);
		}
		if (bWasRunning)
			start();
	}



	public synchronized long getTickPosition()
	{
		if (m_nPhase == STATE_STARTED) {
			return Math.max(m_lTickPosition, (getTimeInMicroseconds() - m_lStartTime) / m_lMicroSecondsPerTick);
		} else {
			return m_lTickPosition;
		}
	}



	public void recordDisable(Track track)
	{
	}



	public void recordEnable(Track track)
	{
	}



	public void recordEnable(Track track, int nChannel)
	{
	}



	public boolean isRecording()
	{
		return false;
	}



	public void stopRecording()
	{
		checkOpen();
	}



	public void startRecording()
	{
		checkOpen();
	}



	protected synchronized void setTempoImpl(float fMPQ)
	{
		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.setTempoImpl(): begin"); }
		int	nResolution = getResolution();
		long currentTime = getTimeInMicroseconds();
		long currentTickPosition = 0;
		if (m_lMicroSecondsPerTick!= 0) 
			currentTickPosition = (currentTime - m_lStartTime) / m_lMicroSecondsPerTick;
		m_lMicroSecondsPerTick = (long) fMPQ / nResolution;
		m_lStartTime = currentTime - currentTickPosition * m_lMicroSecondsPerTick;
		m_bTempoChanged = true;
		// TODO: update m_lMicroSecondsPerTick and m_lStartTime only after the next event because the the event now waiting for its schedule is not updated
		if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.setTempoImpl(): end"); }
	}




	/** Obtain the index of the event with the closest tick value.
	 */
	private int getTrackPosition(Track track, long tickPosition) {
		// check params
		if (track.size() == 0 || tickPosition <= track.get(0).getTick())
			return 0;
		if (tickPosition > track.get(track.size()-1).getTick())
			return track.size(); // index out of track

		// quick search
		int idx1 = 0;
		int idx2 = track.size() - 1;
		for (;;)
		{
			if ((idx2 - idx1) == 1)
				return idx1;
			int idx3 = (int)(((long)idx1 + (long)idx2) / 2L);
			if (tickPosition > track.get(idx3).getTick())
				idx1 = idx3;
			else
				idx2 = idx3;
		}
	}


	/**	Retrieve system time in microseconds.
		This method uses the clock as set with {@link #setClock}.

		@return the system time in microseconds
	*/
	protected long getTimeInMicroseconds()
	{
		// temporary hack
		if (getClock() == null)
		{
			return 0;
		}
		// end hack
		return getClock().getMicroseconds();
	}



	/** Set the clock this sequencer should use.
		@param clock the Clock to be used
		@throws IllegalStateException if the sequencer is not closed
	 */
	public void setClock(Clock clock)
	{
		if (isOpen())
		{
			throw new IllegalStateException("closed state required to set the clock");
		}
		m_clock = clock;
	}



	/** Obtain the clock used by this sequencer.
		@return the clock currently set for this sequencer
	 */
	public Clock getClock()
	{
		return m_clock;
	}


	/** Interface for sequencer clocks.
	 */
	public static interface Clock
	{
		public long getMicroseconds();
	}
}



/*** JavaSequencer.java ***/
