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

/*
 *  Copyright (c) 1999 - 2003 by Matthias Pfisterer
 *
 *   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.share.midi;

import java.io.InputStream;
import java.io.IOException;

import java.util.BitSet;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

import javax.sound.midi.MidiSystem;
import javax.sound.midi.Sequencer;
import javax.sound.midi.Sequence;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MetaEventListener;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.ControllerEventListener;
import javax.sound.midi.MidiDevice;

import org.tritonus.share.TDebug;
import org.tritonus.share.ArraySet;




public abstract class TSequencer
extends TMidiDevice
implements Sequencer
{
	private static final float	MPQ_BPM_FACTOR = 6.0E7F;
	// This is for use in Collection.toArray(Object[]).
	private static final SyncMode[]	EMPTY_SYNCMODE_ARRAY = new SyncMode[0];


	private boolean		m_bRunning;


	/**	The Sequence to play or to record to.
	 */
	private Sequence	m_sequence;

	/**	The listeners that want to be notified of MetaMessages.
	 */
	private Set<MetaEventListener>	m_metaListeners;

	/**	The listeners that want to be notified of control change events.
	 *	They are organized as follows: this array is indexed with
	 *	the number of the controller change events listeners are
	 *	interested in. If there is any interest, the array element
	 *	contains a reference to a Set containing the listeners.
	 *	These sets are allocated on demand.
	 */
	private Set<ControllerEventListener>[]		m_aControllerListeners;

	private float		m_fNominalTempoInMPQ;
	private float		m_fTempoFactor;
	private Collection<SyncMode>	m_masterSyncModes;
	private Collection<SyncMode>	m_slaveSyncModes;
	private SyncMode	m_masterSyncMode;
	private SyncMode	m_slaveSyncMode;
	private BitSet		m_muteBitSet;
	private BitSet		m_soloBitSet;

	/**	Contains the enabled state of the tracks.
		This BitSet holds the pre-calculated effect of mute and
		solo status.
	*/
	private BitSet		m_enabledBitSet;

	/** Start of the loop in ticks.
	 */
	private long		m_lLoopStartPoint;

	/** End of the loop in ticks.
	 */
	private long		m_lLoopEndPoint;

	/** Loop count.
	 */
	private int			m_nLoopCount;


	/**
	 */
	protected TSequencer(MidiDevice.Info info,
			     Collection<SyncMode> masterSyncModes,
			     Collection<SyncMode> slaveSyncModes)
	{
		super(info);
		m_bRunning = false;
		m_sequence = null;
		m_metaListeners = new ArraySet<MetaEventListener>();
		m_aControllerListeners = (Set<ControllerEventListener>[]) new Set[128];
		setTempoFactor(1.0F);
		setTempoInMPQ(500000);
		// TODO: make a copy
		m_masterSyncModes = masterSyncModes;
		m_slaveSyncModes = slaveSyncModes;
		if (getMasterSyncModes().length > 0)
		{
			m_masterSyncMode = getMasterSyncModes()[0];
		}
		if (getSlaveSyncModes().length > 0)
		{
			m_slaveSyncMode = getSlaveSyncModes()[0];
		}
		m_muteBitSet = new BitSet();
		m_soloBitSet = new BitSet();
		m_enabledBitSet = new BitSet();
		updateEnabled();
		setLoopStartPoint(0);
		setLoopEndPoint(-1);
		setLoopCount(0);
	}



	public void setSequence(Sequence sequence)
		throws InvalidMidiDataException
	{
		// TODO: what if playing is in progress?
		if (getSequence() != sequence)
		{
			m_sequence = sequence;
			setSequenceImpl();
			/* Yes, resetting the tempo factor is required by the specification.
			   TODO: can't find this any more in the spec.

			   It is unclear whether this should be executed in any case
			   (even if in fact the sequence didn't change).
			 */
			setTempoFactor(1.0F);
		}
	}


	/** Set Sequence.
		Subclasses that need to be informed when a Sequence is set
		should override this method. It is called by setSequence().
		Subclasses can find out the new Sequence by calling getSequence().
	*/
	// TODO: make abstract
	protected void setSequenceImpl()
	{
	}


	public void setSequence(InputStream inputStream)
		throws InvalidMidiDataException, IOException
	{
		Sequence	sequence = MidiSystem.getSequence(inputStream);
		setSequence(sequence);
	}



	public Sequence getSequence()
	{
		return m_sequence;
	}



	public void setLoopStartPoint(long lTick)
	{
		m_lLoopStartPoint = lTick;
	}


	public long getLoopStartPoint()
	{
		return m_lLoopStartPoint;
	}


	public void setLoopEndPoint(long lTick)
	{
		m_lLoopEndPoint = lTick;
	}


	public long getLoopEndPoint()
	{
		return m_lLoopEndPoint;
	}


	public void setLoopCount(int nLoopCount)
	{
		m_nLoopCount = nLoopCount;
	}


	public int getLoopCount()
	{
		return m_nLoopCount;
	}



	public synchronized void start()
	{
		checkOpen();
		if (! isRunning())
		{
			m_bRunning = true;
			// TODO: perhaps check if sequence present
			startImpl();
		}
	}


	/**
	 *	Subclasses have to override this method to be notified of
	 *	starting.
	 */
	protected void startImpl()
	{
	}



	public synchronized void stop()
	{
		checkOpen();
		if (isRunning())
		{
			stopImpl();
			m_bRunning = false;
		}
	}



	/**
	 *	Subclasses have to override this method to be notified of
	 *	stopping.
	 */
	protected void stopImpl()
	{
	}



	public synchronized boolean isRunning()
	{
		return m_bRunning;
	}



	/** Checks if the Sequencer is open.
		This method is intended to be called by
		{@link javax.sound.midi.Sequencer#start start},
		{@link javax.sound.midi.Sequencer#stop stop},
		{@link javax.sound.midi.Sequencer#startRecording startRecording}
		and {@link javax.sound.midi.Sequencer#stop stopRecording}.

		@throws IllegalStateException if the <code>Sequencer</code> is not open
	 */
	protected void checkOpen()
	{
		if (! isOpen())
		{
			throw new IllegalStateException("Sequencer is not open");
		}
	}



	/**	Returns the resolution (ticks per quarter) of the current sequence.
		If no sequence is set, a bogus default value != 0 is returned.
	*/
	protected int getResolution()
	{
		Sequence	sequence = getSequence();
		int		nResolution;
		if (sequence != null)
		{
			nResolution = sequence.getResolution();
		}
		else
		{
			nResolution = 1;
		}
		return nResolution;
	}



	protected void setRealTempo()
	{
		float	fTempoFactor = getTempoFactor();
		if (fTempoFactor == 0.0F)
		{
			fTempoFactor = 0.01F;
		}
		float	fRealTempo = getTempoInMPQ() / fTempoFactor;
		if (TDebug.TraceSequencer) { TDebug.out("TSequencer.setRealTempo(): real tempo: " + fRealTempo); }
		setTempoImpl(fRealTempo);
	}



	public float getTempoInBPM()
	{
		float	fBPM = MPQ_BPM_FACTOR / getTempoInMPQ();
		return fBPM;
	}



	public void setTempoInBPM(float fBPM)
	{
		float	fMPQ = MPQ_BPM_FACTOR / fBPM;
		setTempoInMPQ(fMPQ);
	}



	public float getTempoInMPQ()
	{
		return m_fNominalTempoInMPQ;
	}


	/** Sets the tempo.
		Implementation classes are required to call this method for changing
		the tempo in reaction to a tempo change event.
	 */
	public void setTempoInMPQ(float fMPQ)
	{
 		m_fNominalTempoInMPQ = fMPQ;
 		setRealTempo();
	}



	public void setTempoFactor(float fFactor)
	{
		m_fTempoFactor = fFactor;
		setRealTempo();
	}



	public float getTempoFactor()
	{
		return m_fTempoFactor;
	}



	/**	Change the tempo of the native sequencer part.
	 *	This method has to be defined by subclasses according
	 *	to the native facilities they use for sequenceing.
	 *	The implementation should not take into account the
	 *	tempo factor. This is handled elsewhere.
	 */
	protected abstract void setTempoImpl(float fMPQ);



	// NOTE: has to be redefined if recording is done natively
	public long getTickLength()
	{
		long	lLength = 0;
		if (getSequence() != null)
		{
			lLength = getSequence().getTickLength();
		}
		return lLength;
	}



	// NOTE: has to be redefined if recording is done natively
	public long getMicrosecondLength()
	{
		long	lLength = 0;
		if (getSequence() != null)
		{
			lLength = getSequence().getMicrosecondLength();
		}
		return lLength;
	}




	public boolean addMetaEventListener(MetaEventListener listener)
	{
		synchronized (m_metaListeners)
		{
			return m_metaListeners.add(listener);
		}
	}



	public void removeMetaEventListener(MetaEventListener listener)
	{
		synchronized (m_metaListeners)
		{
			m_metaListeners.remove(listener);
		}
	}


	protected Iterator<MetaEventListener> getMetaEventListeners()
	{
		synchronized (m_metaListeners)
		{
			return m_metaListeners.iterator();
		}
	}



	protected void sendMetaMessage(MetaMessage message)
	{
		Iterator<MetaEventListener>	iterator = getMetaEventListeners();
		while (iterator.hasNext())
		{
			MetaEventListener	metaEventListener = iterator.next();
			MetaMessage	copiedMessage = (MetaMessage) message.clone();
			metaEventListener.meta(copiedMessage);
		}
	}



	public int[] addControllerEventListener(ControllerEventListener listener, int[] anControllers)
	{
		synchronized (m_aControllerListeners)
		{
			if (anControllers == null)
			{
				/*
				 *	Add to all controllers. NOTE: this
				 *	is an implementation-specific
				 *	semantic!
				 */
				for (int i = 0; i < 128; i++)
				{
					addControllerListener(i, listener);
				}
			}
			else
			{
				for (int i = 0; i < anControllers.length; i++)
				{
					addControllerListener(anControllers[i], listener);
				}
			}
		}
		return getListenedControllers(listener);
	}



	private void addControllerListener(int i,
									   ControllerEventListener listener)
	{
		if (m_aControllerListeners[i] == null)
		{
			m_aControllerListeners[i] = new ArraySet<ControllerEventListener>();
		}
		m_aControllerListeners[i].add(listener);
	}



	public int[] removeControllerEventListener(ControllerEventListener listener, int[] anControllers)
	{
		synchronized (m_aControllerListeners)
		{
			if (anControllers == null)
			{
				/*
				 *	Remove from all controllers. Unlike
				 *	above, this is specified semantics.
				 */
				for (int i = 0; i < 128; i++)
				{
					removeControllerListener(i, listener);
				}
			}
			else
			{
				for (int i = 0; i < anControllers.length; i++)
				{
					removeControllerListener(anControllers[i], listener);
				}
			}
		}
		return getListenedControllers(listener);
	}



	private void removeControllerListener(int i,
					      ControllerEventListener listener)
	{
		if (m_aControllerListeners[i] != null)
		{
			m_aControllerListeners[i].add(listener);
		}
	}



	private int[] getListenedControllers(ControllerEventListener listener)
	{
		int[]	anControllers = new int[128];
		int	nIndex = 0;	// points to the next position to use.
		for (int nController = 0; nController < 128; nController++)
		{
			if (m_aControllerListeners[nController] != null &&
			    m_aControllerListeners[nController].contains(listener))
			{
				anControllers[nIndex] = nController;
				nIndex++;
			}
		}
		int[]	anResultControllers = new int[nIndex];
		System.arraycopy(anControllers, 0, anResultControllers, 0, nIndex);
		return anResultControllers;
	}



	protected void sendControllerEvent(ShortMessage message)
	{
		int	nController = message.getData1();
		if (m_aControllerListeners[nController] != null)
		{
			Iterator<ControllerEventListener>	iterator = m_aControllerListeners[nController].iterator();
			while (iterator.hasNext())
			{
				ControllerEventListener	controllerEventListener = iterator.next();
				ShortMessage	copiedMessage = (ShortMessage) message.clone();
				controllerEventListener.controlChange(copiedMessage);
			}
		}
	}



	protected void notifyListeners(MidiMessage message)
	{
		if (message instanceof MetaMessage)
		{
			// IDEA: use extra thread for event delivery
			sendMetaMessage((MetaMessage) message);
		}
		else if (message instanceof ShortMessage && ((ShortMessage) message).getCommand() == ShortMessage.CONTROL_CHANGE)
		{
			sendControllerEvent((ShortMessage) message);
		}
	}



	public SyncMode getMasterSyncMode()
	{
		return m_masterSyncMode;
	}



	public void setMasterSyncMode(SyncMode syncMode)
	{
		if (m_masterSyncModes.contains(syncMode))
		{
			if (! getMasterSyncMode().equals(syncMode))
			{
				m_masterSyncMode = syncMode;
				setMasterSyncModeImpl(syncMode);
			}
		}
		else
		{
			throw new IllegalArgumentException("sync mode not allowed: " + syncMode);
		}
	}


	/*
	  This method is guaranteed only to be called if the sync mode really changes.
	 */
	protected void setMasterSyncModeImpl(SyncMode syncMode)
	{
		// DO NOTHING
	}



	public SyncMode[] getMasterSyncModes()
	{
		SyncMode[]	syncModes = m_masterSyncModes.toArray(EMPTY_SYNCMODE_ARRAY);
		return syncModes;
	}



	public SyncMode getSlaveSyncMode()
	{
		return m_slaveSyncMode;
	}



	public void setSlaveSyncMode(SyncMode syncMode)
	{
		if (m_slaveSyncModes.contains(syncMode))
		{
			if (! getSlaveSyncMode().equals(syncMode))
			{
				m_slaveSyncMode = syncMode;
				setSlaveSyncModeImpl(syncMode);
			}
		}
		else
		{
			throw new IllegalArgumentException("sync mode not allowed: " + syncMode);
		}
	}



	/*
	  This method is guaranteed only to be called if the sync mode really changes.
	 */
	protected void setSlaveSyncModeImpl(SyncMode syncMode)
	{
		// DO NOTHING
	}



	public SyncMode[] getSlaveSyncModes()
	{
		SyncMode[]	syncModes = m_slaveSyncModes.toArray(EMPTY_SYNCMODE_ARRAY);
		return syncModes;
	}



	public boolean getTrackSolo(int nTrack)
	{
		boolean	bSoloed = false;
		if (getSequence() != null)
		{
			if (nTrack < getSequence().getTracks().length)
			{
				bSoloed = m_soloBitSet.get(nTrack);
			}
		}
		return bSoloed;
	}



	public void setTrackSolo(int nTrack, boolean bSolo)
	{
		if (getSequence() != null)
		{
			if (nTrack < getSequence().getTracks().length)
			{
				boolean	bOldState = m_soloBitSet.get(nTrack);
				if (bSolo != bOldState)
				{
					if (bSolo)
					{
						m_soloBitSet.set(nTrack);
					}
					else
					{
						m_soloBitSet.clear(nTrack);
					}
					updateEnabled();
					setTrackSoloImpl(nTrack, bSolo);
				}
			}
		}
	}



	protected void setTrackSoloImpl(int nTrack, boolean bSolo)
	{
	}



	public boolean getTrackMute(int nTrack)
	{
		boolean	bMuted = false;
		if (getSequence() != null)
		{
			if (nTrack < getSequence().getTracks().length)
			{
				bMuted = m_muteBitSet.get(nTrack);
			}
		}
		return bMuted;
	}



	public void setTrackMute(int nTrack, boolean bMute)
	{
		if (getSequence() != null)
		{
			if (nTrack < getSequence().getTracks().length)
			{
				boolean	bOldState = m_muteBitSet.get(nTrack);
				if (bMute != bOldState)
				{
					if (bMute)
					{
						m_muteBitSet.set(nTrack);
					}
					else
					{
						m_muteBitSet.clear(nTrack);
					}
					updateEnabled();
					setTrackMuteImpl(nTrack, bMute);
				}
			}
		}
	}


	protected void setTrackMuteImpl(int nTrack, boolean bMute)
	{
	}



	private void updateEnabled()
	{
		BitSet	oldEnabledBitSet = (BitSet) m_enabledBitSet.clone();
		boolean	bSoloExists = m_soloBitSet.length() > 0;
		if (bSoloExists)
		{
			m_enabledBitSet = (BitSet) m_soloBitSet.clone();
		}
		else
		{
			for (int i = 0; i < m_muteBitSet.size(); i++)
			{
				if (m_muteBitSet.get(i))
				{
					m_enabledBitSet.clear(i);
				}
				else
				{
					m_enabledBitSet.set(i);
				}
			}
		}
		oldEnabledBitSet.xor(m_enabledBitSet);
		/*	oldEnabledBitSet now has a bit set if the status for
			this bit changed.
		*/
		for (int i = 0; i < oldEnabledBitSet.size(); i++)
		{
			if (oldEnabledBitSet.get(i))
			{
				setTrackEnabledImpl(i, m_enabledBitSet.get(i));
			}
		}
	}



	/**	Shows that a track state has changed.
		This method is called for each track where the enabled
		state (calculated from mute and solo) has changed.
		The boolean value passed represents the new state.

		@param nTrack The track number for which the enabled status
		has changed.

		@param bEnabled The new enabled state for this track.
	 */
	protected void setTrackEnabledImpl(int nTrack, boolean bEnabled)
	{
	}



	protected boolean isTrackEnabled(int nTrack)
	{
		return m_enabledBitSet.get(nTrack);
	}



	/** Sets the preloading intervall.
		This is the time span between preloading events to an internal
		queue and playing them. This intervall should be kept constant
		by the implementation. However, this cannot be guaranteed.
	*/
	public void setLatency(int nMilliseconds)
	{
	}



	/** Get the preloading intervall.

	@return the preloading intervall in milliseconds, or -1 if the sequencer
	doesn't repond to changes in the <code>Sequence</code> at all.
	*/
	public int getLatency()
	{
		return -1;
	}
}



/*** TSequencer.java ***/
