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

/*
 *  Copyright (c) 1999 - 2006 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.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.Transmitter;

import org.tritonus.share.TDebug;


/**	Base class for MidiDevice implementations.
 *	The goal of this class is to supply the common functionality for
 *	classes that implement the interface MidiDevice.
 */
public abstract class TMidiDevice
implements MidiDevice
{
	/**	The Info object for a certain instance of MidiDevice.
	 */
	private MidiDevice.Info		m_info;

	/**	A flag to store whether the device is "open".
	 */
	private boolean			m_bDeviceOpen;

	/**	Whether to handle input from the physical port
		and to allow Transmitters.
	 */
	private boolean		m_bUseTransmitter;

	/**	Whether to handle output to the physical port
		and to allow Receivers.
	 */
	private boolean		m_bUseReceiver;

	/**	The list of Receiver objects that belong to this
	 * 	MidiDevice.
	 *
	 *	@see #addReceiver
	 *	@see #removeReceiver
	 */
	private List<Receiver>	m_receivers;

	/**	The list of Transmitter objects that belong to this
	 * 	MidiDevice.
	 *
	 *	@see #addTransmitter
	 *	@see #removeTransmitter
	 */
	private List<Transmitter>	m_transmitters;



	/**	Initialize this class.
	 *	This sets the info from the passed one, sets the open status
	 *	to false, the number of Receivers to zero and the collection
	 *	of Transmitters to be empty.
	 *
	 *	@param info	The info object that describes this instance.
	 */
	public TMidiDevice(MidiDevice.Info info)
	{
		this(info, true, true);
	}



	/**	Initialize this class.
	 *	This sets the info from the passed one, sets the open status
	 *	to false, the number of Receivers to zero and the collection
	 *	of Transmitters to be empty.
	 *
	 *	@param info	The info object that describes this instance.
	 */
	public TMidiDevice(MidiDevice.Info info,
			   boolean bUseTransmitter,
			   boolean bUseReceiver)
	{
		m_info = info;
		m_bUseTransmitter = bUseTransmitter;
		m_bUseReceiver = bUseReceiver;
		m_bDeviceOpen = false;
		m_receivers = new ArrayList<Receiver>();
		m_transmitters = new ArrayList<Transmitter>();
	}



	/**	Retrieves a description of this instance.
	 *	This returns the info object passed to the constructor.
	 *
	 *	@return the description
	 *
	 *	@see #TMidiDevice
	 */
	public MidiDevice.Info getDeviceInfo()
	{
		return m_info;
	}



	public synchronized void open()
		throws MidiUnavailableException
	{
		if (TDebug.TraceMidiDevice) { TDebug.out("TMidiDevice.open(): begin"); }
		if (! isOpen())
		{
			openImpl();
			/* If openImpl() throws a MidiUnavailableException, m_bDeviceOpen
			 * remains false.
			 */
			m_bDeviceOpen = true;
		}
		if (TDebug.TraceMidiDevice) { TDebug.out("TMidiDevice.open(): end"); }
	}



	/**
	 *	Subclasses have to override this method to be notified of
	 *	opening.
	 */
	protected void openImpl()
		throws MidiUnavailableException
	{
		if (TDebug.TraceMidiDevice) TDebug.out("TMidiDevice.openImpl(): begin");
		if (TDebug.TraceMidiDevice) TDebug.out("TMidiDevice.openImpl(): end");
	}



	public synchronized void close()
	{
		if (TDebug.TraceMidiDevice) { TDebug.out("TMidiDevice.close(): begin"); }
		if (isOpen())
		{
			closeImpl();
			// TODO: close all Receivers and Transmitters
			m_bDeviceOpen = false;
		}
		if (TDebug.TraceMidiDevice) { TDebug.out("TMidiDevice.close(): end"); }
	}



	/**
	 *	Subclasses have to override this method to be notified of
	 *	closeing.
	 */
	protected void closeImpl()
	{
		if (TDebug.TraceMidiDevice) TDebug.out("TMidiDevice.closeImpl(): begin");
		if (TDebug.TraceMidiDevice) TDebug.out("TMidiDevice.closeImpl(): end");
	}



	public boolean isOpen()
	{
		return m_bDeviceOpen;
	}



	/**	Returns whether to handle input.
		If this is true, retrieving Transmitters is possible
		and input from the physical port is passed to them.

		@see #getUseOut
	 */
	protected boolean getUseTransmitter()
	{
		return m_bUseTransmitter;
	}



	/**	Returns whether to handle output.
		If this is true, retrieving Receivers is possible
		and output to them is passed to the physical port.

		@see #getUseTransmitter
	 */
	protected boolean getUseReceiver()
	{
		return m_bUseReceiver;
	}



	/**	Returns the device time in microseconds.
		This is a default implementation, telling the application
		program that the device doesn't track time. If a device wants
		to give timing information, it has to override this method.
	*/
	public long getMicrosecondPosition()
	{
		return -1;
	}



	public int getMaxReceivers()
	{
		int	nMaxReceivers = 0;
		if (getUseReceiver())
		{
		/*
		 *	The value -1 means unlimited.
		 */
			nMaxReceivers = -1;
		}
		return nMaxReceivers;
	}



	public int getMaxTransmitters()
	{
		int	nMaxTransmitters = 0;
		if (getUseTransmitter())
		{
		/*
		 *	The value -1 means unlimited.
		 */
			nMaxTransmitters = -1;
		}
		return nMaxTransmitters;
	}



	/**	Creates a new Receiver object associated with this instance.
	 *	In this implementation, an unlimited number of Receivers
	 *	per MidiDevice can be created.
	 */
	public Receiver getReceiver()
		throws MidiUnavailableException
	{
		if (! getUseReceiver())
		{
			throw new MidiUnavailableException("Receivers are not supported by this device");
		}
		return new TReceiver();
	}



	/**	Creates a new Transmitter object associated with this instance.
	 *	In this implementation, an unlimited number of Transmitters
	 *	per MidiDevice can be created.
	 */
	public Transmitter getTransmitter()
		throws MidiUnavailableException
	{
		if (! getUseTransmitter())
		{
			throw new MidiUnavailableException("Transmitters are not supported by this device");
		}
		return new TTransmitter();
	}



	public List<Receiver> getReceivers()
	{
		return Collections.unmodifiableList(m_receivers);
	}


	public List<Transmitter> getTransmitters()
	{
		return Collections.unmodifiableList(m_transmitters);
	}


	/*
	 *	Intended for overriding by subclasses to receive messages.
	 *	This method is called by TMidiDevice.Receiver object on
	 *	receipt of a MidiMessage.
	 */
	protected void receive(MidiMessage message, long lTimeStamp)
	{
		if (TDebug.TraceMidiDevice) { TDebug.out("### [should be overridden] TMidiDevice.receive(): message " + message); }
	}



	protected void addReceiver(Receiver receiver)
	{
		synchronized (m_receivers)
		{
			m_receivers.add(receiver);
		}
	}



	protected void removeReceiver(Receiver receiver)
	{
		synchronized (m_receivers)
		{
			m_receivers.remove(receiver);
		}
	}




	protected void addTransmitter(Transmitter transmitter)
	{
		synchronized (m_transmitters)
		{
			m_transmitters.add(transmitter);
		}
	}


	protected void removeTransmitter(Transmitter transmitter)
	{
		synchronized (m_transmitters)
		{
			m_transmitters.remove(transmitter);
		}
	}



	/**	Send a MidiMessage to all Transmitters.
	 *	This method should be called by subclasses when they get a
	 *	message from a physical MIDI port.
	 */
	protected void sendImpl(MidiMessage message, long lTimeStamp)
	{
		if (TDebug.TraceMidiDevice) { TDebug.out("TMidiDevice.sendImpl(): begin"); }
		Iterator	transmitters = m_transmitters.iterator();
		while (transmitters.hasNext())
		{
			TTransmitter	transmitter = (TTransmitter) transmitters.next();
			/* due to a bug in the Sun jdk1.3, we cannot use
			   clone() for MetaMessages. So we have to do the
			   equivalent ourselves.
			*/
			// MidiMessage	copiedMessage = (MidiMessage) message.clone();
			MidiMessage	copiedMessage = null;
			if (message instanceof MetaMessage)
			{
				MetaMessage	origMessage = (MetaMessage) message;
				MetaMessage	metaMessage = new MetaMessage();
				try
				{
					metaMessage.setMessage(origMessage.getType(), origMessage.getData(), origMessage.getData().length);
				}
				catch (InvalidMidiDataException e)
				{
					if (TDebug.TraceAllExceptions) { TDebug.out(e); }
				}
				copiedMessage = metaMessage;
			}
			else
			{
				copiedMessage = (MidiMessage) message.clone();
			}

			if (message instanceof MetaMessage)
			{
				if (TDebug.TraceMidiDevice) { TDebug.out("TMidiDevice.sendImpl(): MetaMessage.getData().length (original): " + ((MetaMessage) message).getData().length); }
				if (TDebug.TraceMidiDevice) { TDebug.out("TMidiDevice.sendImpl(): MetaMessage.getData().length (cloned): " + ((MetaMessage) copiedMessage).getData().length); }
			}
			transmitter.send(copiedMessage, lTimeStamp);
		}
		if (TDebug.TraceMidiDevice) { TDebug.out("TMidiDevice.sendImpl(): end"); }
	}




/////////////////// INNER CLASSES //////////////////////////////////////


	/**	Receiver proxy class.
	 *	This class' objects are handed out on calls to
	 *	TMidiDevice.getReceiver(). 
	 */
	public class TReceiver
	implements Receiver
	{
		private boolean		m_bOpen;



		public TReceiver()
		{
			TMidiDevice.this.addReceiver(this);
			m_bOpen = true;
		}



		protected boolean isOpen()
		{
			return m_bOpen;
		}



		/**	Receive a MidiMessage.
		 *
		 */
		public void send(MidiMessage message, long lTimeStamp)
		{
			if (TDebug.TraceMidiDevice) { TDebug.out("TMidiDevice.TReceiver.send(): message " + message); }
			if (m_bOpen)
			{
				TMidiDevice.this.receive(message, lTimeStamp);
			}
			else
			{
				throw new IllegalStateException("receiver is not open");
			}
		}



		/**	Closes the receiver.
		 *	After a receiver has been closed, it does no longer
		 *	propagate MidiMessages to its associated MidiDevice.
		 */
		public void close()
		{
			TMidiDevice.this.removeReceiver(this);
			m_bOpen = false;
		}
	}




	public class TTransmitter
	implements Transmitter
	{
		private boolean		m_bOpen;
		private Receiver	m_receiver;



		public TTransmitter()
		{
			m_bOpen = true;
			TMidiDevice.this.addTransmitter(this);
		}



		public void setReceiver(Receiver receiver)
		{
			synchronized (this)
			{
				m_receiver = receiver;
			}
		}



		public Receiver getReceiver()
		{
			return m_receiver;
		}



		public void send(MidiMessage message, long lTimeStamp)
		{
			if (getReceiver() != null && m_bOpen)
			{
				getReceiver().send(message, lTimeStamp);
			}
		}



		/**	Closes the transmitter.
		 *	After a transmitter has been closed, it no longer
		 *	passes MidiMessages to a Receiver previously set for
		 *	it.
		 */
		public void close()
		{
			TMidiDevice.this.removeTransmitter(this);
			m_bOpen = false;
			/* Previously, this method just set m_receiver to null
			   instead of maintaining an open flag. This allows to exploit
			   the behaviour of calling close(), the setReceiver() again,
			   and the Transmitter is "reopened". TODO: write a test case
			   for this scenario.
			*/
		}
	}



	/*
	 *	This is needed only because MidiDevice.Info's
	 *	constructor is protected (in the Sun jdk1.3).
	 */
	public static class Info
	extends MidiDevice.Info
	{
		public Info(String a, String b, String c, String d)
		{
			super(a, b, c, d);
		}
	}

}



/*** TMidiDevice.java ***/

