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

/*
 *  Copyright (c) 1999 - 2004 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.sampled.mixer.alsa;

import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
import java.util.Iterator;
import java.util.List;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.Clip;
import javax.sound.sampled.CompoundControl;
import javax.sound.sampled.Control;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.TargetDataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Line;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.Port;

import org.tritonus.share.GlobalInfo;
import org.tritonus.share.TDebug;
import org.tritonus.share.sampled.mixer.TBooleanControl;
import org.tritonus.share.sampled.mixer.TFloatControl;
import org.tritonus.share.sampled.mixer.TMixer;
import org.tritonus.share.sampled.mixer.TMixerInfo;
import org.tritonus.share.sampled.mixer.TPort;
import org.tritonus.share.sampled.mixer.TCompoundControlType;

import org.tritonus.lowlevel.alsa.AlsaMixer;
import org.tritonus.lowlevel.alsa.AlsaMixerElement;


/**	TODO:
 */
public class AlsaPortMixer
extends TMixer
{
	/**	Used to signal an illegal value for direction.
	 */
	public static final int		DIRECTION_NONE = -1;

	/**	Used to signal common volume or common switch.
	 */
	public static final int		DIRECTION_COMMON = 0;

	/**	Used to signal playback volume or playback switch.
	 */
	public static final int		DIRECTION_PLAYBACK = 1;

	/**	Used to signal capture volume or capture switch.
	 */
	public static final int		DIRECTION_CAPTURE = 2;


	/**
	  For the first shot, we try to create one port line per mixer
	  element. For now, the following two lists should have the same size
	  and related elements at the same index position.
	*/
	private List<AlsaMixerElement>	m_mixerElements;

	/**
	  Port.Infos are keys, Port instances are values.
	*/
	private Map<Port.Info, Port>	m_portMap;

	/**
	  Port.Infos are keys, AlsaMixerElement instances are values.
	*/
	private Map<Port.Info, AlsaMixerElement>	m_mixerElementMap;



	public AlsaPortMixer(int nCard)
	{
		this("hw:" + nCard);
	}



	public AlsaPortMixer(String strDeviceName)
	{
		super(new TMixerInfo(
			      "Alsa Port Mixer (" + strDeviceName + ")",
			      GlobalInfo.getVendor(),
			      "System Mixer for the Advanced Linux Sound System (card " + strDeviceName + ")",
			      GlobalInfo.getVersion()),
		      new Line.Info(Mixer.class));
		if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.<init>: begin"); }
		m_mixerElements = new ArrayList<AlsaMixerElement>();
		m_mixerElementMap = new HashMap<Port.Info, AlsaMixerElement>();
		m_portMap = new HashMap<Port.Info, Port>();
		AlsaMixer	alsaMixer = null;
		try
		{
			alsaMixer = new AlsaMixer(strDeviceName);
			if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.<init>: successfully created AlsaMixer instance"); }
		}
		catch (Exception e)
		{
			if (TDebug.TraceMixer || TDebug.TraceAllExceptions) { TDebug.out(e); }
		}
		int		nArraySize = 150;	// TODO: original value: 128. With this value, a vm crash occurs
		int[]		anIndices = null;
		String[]	astrNames = null;
		int		nControlCount = 0;
		while (true)
		{
			anIndices = new int[nArraySize];
			astrNames = new String[nArraySize];
			nControlCount = alsaMixer.readControlList(anIndices, astrNames);
			if (nControlCount >= 0)
			{
				break;
			}
			if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.<init>: increasing array size for AlsaMixer.readControlList(): now " + nArraySize * 2); }
			nArraySize *= 2;
		}
		List<Line.Info>	sourcePortInfos = new ArrayList<Line.Info>();
		List<Line.Info>	targetPortInfos = new ArrayList<Line.Info>();
		for (int i = 0; i < nControlCount; i++)
		{
			if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.<init>(): control " + i + ": " + anIndices[i] + " " + astrNames[i]); }
			AlsaMixerElement	element = new AlsaMixerElement(alsaMixer, anIndices[i], astrNames[i]);
			if (element.isActive())
			{
				m_mixerElements.add(element);
				if (hasPlaybackChannels(element))
				{
					Port.Info	info = new Port.Info(Port.class,
									     element.getName(), true);
					sourcePortInfos.add(info);
					m_mixerElementMap.put(info, element);
				}
				if (hasCaptureChannels(element))
				{
					Port.Info	info = new Port.Info(Port.class,
									     element.getName(), false);
					targetPortInfos.add(info);
					m_mixerElementMap.put(info, element);
				}
			}
		}
		setSupportInformation(
			new ArrayList<AudioFormat>(),
			new ArrayList<AudioFormat>(),
			sourcePortInfos,
			targetPortInfos);
		if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.<init>: end."); }
	}



	private static boolean hasPlaybackChannels(AlsaMixerElement element)
	{
		boolean	bHasChannels = false;
		for (int nChannel = AlsaMixerElement.SND_MIXER_SCHN_FRONT_LEFT;
		     nChannel <= AlsaMixerElement.SND_MIXER_SCHN_WOOFER;
		     nChannel++)
		{
			bHasChannels |= element.hasPlaybackChannel(nChannel);
		}
		return bHasChannels;
	}


	private static boolean hasCaptureChannels(AlsaMixerElement element)
	{
		boolean	bHasChannels = false;
		for (int nChannel = AlsaMixerElement.SND_MIXER_SCHN_FRONT_LEFT;
		     nChannel <= AlsaMixerElement.SND_MIXER_SCHN_WOOFER;
		     nChannel++)
		{
			bHasChannels |= element.hasCaptureChannel(nChannel);
		}
		return bHasChannels;
	}


	//////////////// Line //////////////////////////////////////


	// TODO: allow real close and reopen of mixer
	public void open()
	{
		if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.open(): begin"); }

		// currently does nothing

		if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.open(): end"); }
	}



	public void close()
	{
		if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.close(): begin"); }

		// currently does nothing

		if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.close(): end"); }
	}





	//////////////// Mixer //////////////////////////////////////






/*
  public Line.Info getLineInfo(Line.Info info)
  {
  // TODO:
  return null;
  }
*/

	// TODO:
	public int getMaxLines(Line.Info info)
	{
		// TODO:
		return 0;
	}




	protected Port getPort(Port.Info info)
		throws LineUnavailableException
	{
		if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.getPort(): begin"); }
		Port	port = m_portMap.get(info);
		if (port == null)
		{
			port = createPort(info);
			m_portMap.put(info, port);
		}
		if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.getPort(): end"); }
		return port;
	}


	private Port createPort(Port.Info info)
	{
		if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.createPort(): begin"); }
		AlsaMixerElement	element = m_mixerElementMap.get(info);
		if (element == null)
		{
			throw new IllegalArgumentException("no port for this info");
		}
		List<Control>	controls;
		Control	c;
		int	nDirection;
		if (info.isSource())
		{
			controls = createSourcePortControls(element);
		}
		else
		{
			//nDirection = DIRECTION_CAPTURE;
			// controls = createTargetPortControls(element);
			controls = new ArrayList<Control>();
		}
		Port	port = new TPort(this, info, controls);
		if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.createPort(): end"); }
		return port;
	}


	/**	TODO:
	 */
	private List<Control> createSourcePortControls(AlsaMixerElement element)
	{
		int	nDirection = DIRECTION_PLAYBACK;
		List<Control>	controls = new ArrayList<Control>();
		Control	c;
		if (element.hasPlaybackVolume() ||
		    element.hasCommonVolume())
		{
			if (element.isPlaybackMono() ||
			    element.hasPlaybackVolumeJoined())
			{
				c = createVolumeControl(element, AlsaMixerElement.SND_MIXER_SCHN_FRONT_LEFT, nDirection);
				controls.add(c);
			}
			else
			{
				List<Control>	volumeControls = new ArrayList<Control>();
				for (int nChannel = AlsaMixerElement.SND_MIXER_SCHN_FRONT_LEFT; nChannel < AlsaMixerElement.SND_MIXER_SCHN_LAST; nChannel++)
				{
					if (element.hasPlaybackChannel(nChannel))
					{
						// TDebug.out("adding channel " + nChannel);
						c = createVolumeControl(element, nChannel, nDirection);
						// System.out.println("control to add: " + c);
						volumeControls.add(c);
					}
				}
				// list should not be empty
				CompoundControl.Type	type = new TCompoundControlType("test");
				Control[]	aMemberControls = volumeControls.toArray(new Control[0]);
				// System.out.println("member controls: " + aMemberControls);
				// System.out.println("# member controls: " + aMemberControls.length);
				c = new AlsaCompoundControl(type, aMemberControls);
				controls.add(c);
			}
		}
		if (element.hasPlaybackSwitch() ||
		    element.hasCommonSwitch())
		{
			if (element.isPlaybackMono() ||
			    element.hasPlaybackSwitchJoined())
			{
				c = createSwitchControl(element, AlsaMixerElement.SND_MIXER_SCHN_FRONT_LEFT, nDirection);
				controls.add(c);
			}
			else
			{
				List<Control>	volumeControls = new ArrayList<Control>();
				for (int nChannel = AlsaMixerElement.SND_MIXER_SCHN_FRONT_LEFT; nChannel < AlsaMixerElement.SND_MIXER_SCHN_LAST; nChannel++)
				{
					if (element.hasPlaybackChannel(nChannel))
					{
						// TDebug.out("adding channel " + nChannel);
						c = createSwitchControl(element, nChannel, nDirection);
						// System.out.println("control to add: " + c);
						volumeControls.add(c);
					}
				}
				// list should not be empty
				CompoundControl.Type	type = new TCompoundControlType("test");
				Control[]	aMemberControls = volumeControls.toArray(new Control[0]);
				// System.out.println("member controls: " + aMemberControls);
				// System.out.println("# member controls: " + aMemberControls.length);
				c = new AlsaCompoundControl(type, aMemberControls);
				controls.add(c);
			}
		}
		return controls;
	}



	/**	TODO:
	 */
	private FloatControl createVolumeControl(AlsaMixerElement element,
						 int nChannel,
						 int nDirection)
	{
		int[]	anValues = new int[2];
		switch (nDirection)
		{
		case DIRECTION_COMMON:
		case DIRECTION_PLAYBACK:
			element.getPlaybackVolumeRange(anValues);
			break;

		case DIRECTION_CAPTURE:
			element.getCaptureVolumeRange(anValues);
			break;
		}
		FloatControl	control = new AlsaVolumeControl(
			FloatControl.Type.VOLUME,
			anValues[0],
			anValues[1],
			1.0F,
			-1,
			anValues[0],
			"", "", "", "",
			element,
			nChannel,
			nDirection);
		return control;
	}


	/**	TODO:
	 */
	private BooleanControl createSwitchControl(AlsaMixerElement element,
						   int nChannel,
						   int nDirection)
	{
		BooleanControl	control = new AlsaSwitchControl(
			BooleanControl.Type.MUTE,
			false,
			"", "",
			element,
			nChannel,
			nDirection);
		return control;
	}


	//////////////// inner classes //////////////////////////////////////


	private class AlsaVolumeControl
extends FloatControl
	{
		private AlsaMixerElement	m_element;
		private int			m_nChannel;

		/**	One of the constants DIRECTION_*.
		 */
		private int			m_nDirection;


		/**
		   @param nDirection One of the constants DIRECTION_*.
		 */
		public AlsaVolumeControl(Type type,
					float fMinimum,
					float fMaximum,
					float fPrecision,
					int nUpdatePeriod,
					float fInitialValue,
					String strUnits,
					String strMinLabel,
					String strMidLabel,
					String strMaxLabel,
					AlsaMixerElement element,
					int nChannel,
					int nDirection)
		{
			super(type,
			      fMinimum,
			      fMaximum,
			      fPrecision,
			      nUpdatePeriod,
			      fInitialValue,
			      strUnits,
			      strMinLabel,
			      strMidLabel,
			      strMaxLabel);
			if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.AlsaFloatControl.<init>(): begin"); }
			m_element = element;
			m_nChannel = nChannel;
			m_nDirection = nDirection;
			setValue(getValueImpl());
			if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.AlsaFloatControl.<init>(): end"); }
		}


		private AlsaMixerElement getElement()
		{
			return m_element;
		}



		private int getChannel()
		{
			return m_nChannel;
		}



		private int getDirection()
		{
			return m_nDirection;
		}



		// TODO: respect channels
		public void setValue(float fValue)
		{
			super.setValue(fValue);
			int	nValue = (int) fValue;
			switch (getDirection())
			{
			case DIRECTION_COMMON:
			case DIRECTION_PLAYBACK:
				getElement().setPlaybackVolumeAll(nValue);
				break;

			case DIRECTION_CAPTURE:
				getElement().setCaptureVolumeAll(nValue);
				break;
			}
		}



		private float getValueImpl()
		{
			int	nChannel = getChannel();
			float	fValue = 0.0F;
			switch (getDirection())
			{
			case DIRECTION_COMMON:
			case DIRECTION_PLAYBACK:
				fValue = getElement().getPlaybackVolume(nChannel);
				break;

			case DIRECTION_CAPTURE:
				fValue = getElement().getCaptureVolume(nChannel);
				break;
			}
			return fValue;
		}
	}



	private class AlsaSwitchControl
	extends BooleanControl
	{
		private AlsaMixerElement	m_element;
		private int			m_nChannel;

		/**	One of the constants DIRECTION_*.
		 */
		private int			m_nDirection;


		/**
		   @param nDirection One of the constants DIRECTION_*.
		 */
		public AlsaSwitchControl(Type type,
					boolean bInitialValue,
					String strTrueLabel,
					String strFalseLabel,
					AlsaMixerElement element,
					int nChannel,
					int nDirection)
		{
			super(type,
			      bInitialValue,
			      strTrueLabel,
			      strFalseLabel);
			if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.AlsaFloatControl.<init>(): begin"); }
			m_element = element;
			m_nChannel = nChannel;
			m_nDirection = nDirection;
			if (TDebug.TraceMixer) { TDebug.out("AlsaPortMixer.AlsaFloatControl.<init>(): end"); }
		}


		private AlsaMixerElement getElement()
		{
			return m_element;
		}



		private int getChannel()
		{
			return m_nChannel;
		}



		private int getDirection()
		{
			return m_nDirection;
		}



		// TODO: respect channels
		public void setValue(boolean bValue)
		{
			super.setValue(bValue);
			switch (getDirection())
			{
			case DIRECTION_COMMON:
			case DIRECTION_PLAYBACK:
				getElement().setPlaybackSwitchAll(bValue);
				break;

			case DIRECTION_CAPTURE:
				getElement().setCaptureSwitchAll(bValue);
				break;
			}
		}
	}


	/**	CompoundControl class.
		This class is only needed to provide a public
		constructor.
	 */
	public class AlsaCompoundControl
	extends CompoundControl
	{
		public AlsaCompoundControl(CompoundControl.Type type,
					   Control[] aMemberControls)
		{
			super(type, aMemberControls);
		}
	}
}



/*** AlsaPortMixer.java ***/
