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

/*
 *  Copyright (c) 1999 - 2001 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.Collection;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;

import org.tritonus.share.TDebug;
import org.tritonus.lowlevel.alsa.Alsa;
import org.tritonus.lowlevel.alsa.AlsaPcm;
import org.tritonus.lowlevel.alsa.AlsaPcmHWParams;
import org.tritonus.lowlevel.alsa.AlsaPcmSWParams;
import org.tritonus.share.sampled.mixer.TBaseDataLine;




public abstract class AlsaBaseDataLine
extends TBaseDataLine
{
	// private static final Class[]	CONTROL_CLASSES = {GainControl.class};


	private AlsaPcm			m_alsaPcm;
	private boolean			m_bSwapBytes;

	/*
	 *	Only used if m_bSwapBytes is true.
	 */
	private int			m_nBytesPerSample;



	public AlsaBaseDataLine(AlsaDataLineMixer mixer, DataLine.Info info)
		throws LineUnavailableException
	{
		super(mixer,
		      info);
		if (TDebug.TraceSourceDataLine) { TDebug.out("AlsaBaseDataLine.<init>(): begin"); }
		if (TDebug.TraceSourceDataLine) { TDebug.out("AlsaBaseDataLine.<init>(): end"); }
	}



	public AlsaBaseDataLine(AlsaDataLineMixer mixer, DataLine.Info info,
				Collection controls)
		throws LineUnavailableException
	{
		super(mixer,
		      info);
		if (TDebug.TraceSourceDataLine) { TDebug.out("AlsaBaseDataLine.<init>(): begin"); }
		if (TDebug.TraceSourceDataLine) { TDebug.out("AlsaBaseDataLine.<init>(): end"); }
	}



	protected AlsaDataLineMixer getAlsaDataLineMixer()
	{
		return (AlsaDataLineMixer) getMixer();
	}



	protected AlsaPcm getAlsaPcm()
	{
		return m_alsaPcm;
	}



	/**	Returns the ALSA stream type of this line.
		Subclasses must implement this method to return either
		AlsaPcm.SND_PCM_STREAM_PLAYBACK or
		AlsaPcm.SND_PCM_STREAM_CAPTURE.
		The return value is used by this class to decide if it has
		to deal with a source (PLAYBACK) or target (CAPTURE)
		data line.
	*/
	protected abstract int getAlsaStreamType();



	protected boolean getSwapBytes()
	{
		return m_bSwapBytes;
	}



	protected int getBytesPerSample()
	{
		return m_nBytesPerSample;
	}



	protected void openImpl()
		throws LineUnavailableException
	{
		if (TDebug.TraceSourceDataLine) { TDebug.out("AlsaBaseDataLine.openImpl(): begin"); }

		/*
		 *	Checks that a format is set.
		 *	Sets the buffer size to a default value if not
		 *	already set.
		 */
		checkOpen();
		AudioFormat	format = getFormat();
		if (TDebug.TraceSourceDataLine) { TDebug.out("AlsaBaseDataLine.openImpl(): input format: " + format); }

		// hack, only true for pmac
		boolean			bHWBigEndian = false;

		AudioFormat.Encoding	encoding  = format.getEncoding();
		boolean			bBigEndian = format.isBigEndian();
		m_bSwapBytes = false;
		if (format.getSampleSizeInBits() == 16 && bBigEndian != bHWBigEndian)
		{
			m_bSwapBytes = true;
			bBigEndian = bHWBigEndian;
		}
		else if (format.getSampleSizeInBits() == 8 &&
			 encoding.equals(AudioFormat.Encoding.PCM_SIGNED))
		{
			m_bSwapBytes = true;
			encoding = AudioFormat.Encoding.PCM_UNSIGNED;
		}
		if (getSwapBytes())
		{
			format = new AudioFormat(encoding,
						 format.getSampleRate(),
						 format.getSampleSizeInBits(),
						 format.getChannels(),
						 format.getFrameSize(),
						 format.getFrameRate(),
						 bBigEndian);
			if (TDebug.TraceSourceDataLine) { TDebug.out("AlsaBaseDataLine.openImpl(): output format: " + format); }
			m_nBytesPerSample = format.getFrameSize() / format.getChannels();
		}
		int	nAlsaOutFormat = AlsaUtils.getAlsaFormat(format);
		if (TDebug.TraceSourceDataLine) { TDebug.out("AlsaBaseDataLine.openImpl(): ALSA output format: " + nAlsaOutFormat); }
		if (nAlsaOutFormat == AlsaPcm.SND_PCM_FORMAT_UNKNOWN)
		{
			throw new IllegalArgumentException("unsupported format");
		}

		try
		{
			m_alsaPcm = new AlsaPcm(
				getAlsaDataLineMixer().getPcmName(),
				getAlsaStreamType(),
				0);	// no special mode
		}
		catch (Exception e)
		{
			if (TDebug.TraceAllExceptions) { TDebug.out(e); }
			throw new LineUnavailableException();
		}
		int	nReturn;
		AlsaPcmHWParams	hwParams = new AlsaPcmHWParams();
		nReturn = m_alsaPcm.getAnyHWParams(hwParams);
		if (nReturn != 0)
		{
			TDebug.out("AlsaBaseDataLine.openImpl(): getAnyHWParams(): " + Alsa.getStringError(nReturn));
			throw new LineUnavailableException(Alsa.getStringError(nReturn));
		}
		nReturn = m_alsaPcm.setHWParamsAccess(hwParams, AlsaPcm.SND_PCM_ACCESS_RW_INTERLEAVED);
		if (nReturn != 0)
		{
			TDebug.out("AlsaBaseDataLine.openImpl(): setHWParamsFormat(): " + Alsa.getStringError(nReturn));
			throw new LineUnavailableException(Alsa.getStringError(nReturn));
		}
		nReturn = m_alsaPcm.setHWParamsFormat(hwParams, nAlsaOutFormat);
		if (nReturn != 0)
		{
			TDebug.out("AlsaBaseDataLine.openImpl(): setHWParamsFormat(): " + Alsa.getStringError(nReturn));
			throw new LineUnavailableException(Alsa.getStringError(nReturn));
		}
		nReturn = m_alsaPcm.setHWParamsChannels(hwParams, format.getChannels());
		if (nReturn != 0)
		{
			TDebug.out("AlsaBaseDataLine.openImpl(): setHWParamsChannels(): " + Alsa.getStringError(nReturn));
			throw new LineUnavailableException(Alsa.getStringError(nReturn));
		}
		nReturn = m_alsaPcm.setHWParamsRateNear(hwParams, (int) format.getSampleRate());
// int nRate = nReturn;
		if (nReturn < 0)
		{
			TDebug.out("AlsaBaseDataLine.openImpl(): setHWParamsRateNear(): " + Alsa.getStringError(nReturn));
			throw new LineUnavailableException(Alsa.getStringError(nReturn));
		}
		nReturn = m_alsaPcm.setHWParamsBufferTimeNear(hwParams, 500000);
		int	nBufferTime = nReturn;
		if (nReturn < 0)
		{
			TDebug.out("AlsaBaseDataLine.openImpl(): setHWParamsBufferTimeNear(): " + Alsa.getStringError(nReturn));
			throw new LineUnavailableException(Alsa.getStringError(nReturn));
		}
		nReturn = m_alsaPcm.setHWParamsPeriodTimeNear(hwParams, nBufferTime / 4);
// int nPeriodTime = nReturn;
		if (nReturn < 0)
		{
			TDebug.out("AlsaBaseDataLine.openImpl(): setHWParamsPeriodTimeNear(): " + Alsa.getStringError(nReturn));
			throw new LineUnavailableException(Alsa.getStringError(nReturn));
		}
		nReturn = m_alsaPcm.setHWParams(hwParams);
		if (nReturn < 0)
		{
			TDebug.out("AlsaBaseDataLine.openImpl(): setHWParams(): " + Alsa.getStringError(nReturn));
			throw new LineUnavailableException(Alsa.getStringError(nReturn));
		}
		int	nChunkSize = hwParams.getPeriodSize(null);
		int	nBufferSize = hwParams.getBufferSize();
		if (nChunkSize == nBufferSize)
		{
			throw new LineUnavailableException("period size is equal to buffer size");
		}

		AlsaPcmSWParams	swParams = new AlsaPcmSWParams();
		nReturn = m_alsaPcm.getSWParams(swParams);
		if (nReturn != 0)
		{
			TDebug.out("AlsaBaseDataLine.openImpl(): getSWParams(): " + Alsa.getStringError(nReturn));
			throw new LineUnavailableException(Alsa.getStringError(nReturn));
		}
		nReturn = m_alsaPcm.setSWParamsSleepMin(swParams, 0);
		if (nReturn != 0)
		{
			TDebug.out("AlsaBaseDataLine.openImpl(): setSWParamsSleepMin(): " + Alsa.getStringError(nReturn));
			throw new LineUnavailableException(Alsa.getStringError(nReturn));
		}
		nReturn = m_alsaPcm.setSWParamsXrunMode(swParams, AlsaPcm.SND_PCM_XRUN_NONE);
		if (nReturn != 0)
		{
			TDebug.out("AlsaBaseDataLine.openImpl(): setSWParamsXrunMode(): " + Alsa.getStringError(nReturn));
			throw new LineUnavailableException(Alsa.getStringError(nReturn));
		}
		nReturn = m_alsaPcm.setSWParamsAvailMin(swParams, nChunkSize);
		if (nReturn != 0)
		{
			TDebug.out("AlsaBaseDataLine.openImpl(): setSWParamsAvailMin(): " + Alsa.getStringError(nReturn));
			throw new LineUnavailableException(Alsa.getStringError(nReturn));
		}
		long	lStartThreshold = (long) ((double) format.getFrameRate() * 1 / 1000000);
		nReturn = m_alsaPcm.setSWParamsStartThreshold(swParams, (int) lStartThreshold);
		if (nReturn != 0)
		{
			TDebug.out("AlsaBaseDataLine.openImpl(): setSWParamsStartThreshold(): " + Alsa.getStringError(nReturn));
			throw new LineUnavailableException(Alsa.getStringError(nReturn));
		}
		long	lStopThreshold = (long) (nBufferSize + (double) format.getFrameRate() * 0 / 1000000);
		nReturn = m_alsaPcm.setSWParamsStopThreshold(swParams, (int) lStopThreshold);
		if (nReturn != 0)
		{
			TDebug.out("AlsaBaseDataLine.openImpl(): setSWParamsStopThreshold(): " + Alsa.getStringError(nReturn));
			throw new LineUnavailableException(Alsa.getStringError(nReturn));
		}
// omitted: xfer_align
		nReturn = m_alsaPcm.setSWParams(swParams);
		if (nReturn != 0)
		{
			TDebug.out("AlsaBaseDataLine.openImpl(): setSWParams(): " + Alsa.getStringError(nReturn));
			throw new LineUnavailableException(Alsa.getStringError(nReturn));
		}
		if (TDebug.TraceSourceDataLine) { TDebug.out("AlsaBaseDataLine.openImpl(): end"); }
	}



	protected void closeImpl()
	{
		if (TDebug.TraceSourceDataLine) { TDebug.out("AlsaBaseDataLine.closeImpl(): begin"); }
		m_alsaPcm.close();
		if (TDebug.TraceSourceDataLine) { TDebug.out("AlsaBaseDataLine.closeImpl(): end"); }
	}



/*
  public void start()
  {
  setStarted(true);
  setActive(true);
  if (TDebug.TraceSourceDataLine) { TDebug.out("AlsaBaseDataLine.start(): channel started."); }
  }
*/


	protected void stopImpl()
	{
		if (TDebug.TraceSourceDataLine) { TDebug.out("AlsaBaseDataLine.stopImpl(): called"); }
		int	nReturn = 0;
		// int	nReturn = m_alsaPcm.flushChannel(AlsaPcm.SND_PCM_CHANNEL_PLAYBACK);
		if (nReturn != 0)
		{
			TDebug.out("flushChannel: " + Alsa.getStringError(nReturn));
		}
		// setStarted(false);
	}



	public int available()
	{
		// TODO:
		return -1;
	}







	public void drain()
	{
		// TODO:
	}



	public void flush()
	{
		// TODO:
	}



	/**
	 *	dGain is logarithmic!!
	 */
	protected void setGain(float dGain)
	{
	}



	// IDEA: move inner classes to TBaseDataLine
	public class AlsaBaseDataLineGainControl
	extends FloatControl
	{
		/*
		 *	These variables should be static. However, Java 1.1
		 *	doesn't allow this. So they aren't.
		 */
		private /*static*/ final float	MAX_GAIN = 90.0F;
		private /*static*/ final float	MIN_GAIN = -96.0F;

		// TODO: recheck this value
		private /*static*/ final int	GAIN_INCREMENTS = 1000;

		// private float		m_fGain;
		// private boolean		m_bMuted;



		/*package*/ AlsaBaseDataLineGainControl()
		{
			super(FloatControl.Type.VOLUME,	// or MASTER_GAIN ?
			      -96.0F,	// MIN_GAIN,
			      24.0F,	// MAX_GAIN,
			      0.01F,	// precision
			      0,	// update period?
			      0.0F,	// initial value
			      "dB",
			      "-96.0",
			      "",
			      "+24.0");
			// m_bMuted = false;	// should be included in a compund control?
		}



		public void setValue(float fGain)
		{
			fGain = Math.max(Math.min(fGain, getMaximum()), getMinimum());
			if (Math.abs(fGain - getValue()) > 1.0E9)
			{
				super.setValue(fGain);
				// if (!getMute())
				// {
				AlsaBaseDataLine.this.setGain(getValue());
				// }
			}
		}


/*
  public float getMaximum()
  {
  return MAX_GAIN;
  }



  public float getMinimum()
  {
  return MIN_GAIN;
  }



  public int getIncrements()
  {
  // TODO: check this value
  return GAIN_INCREMENTS;
  }



  public void fade(float fInitialGain, float fFinalGain, int nFrames)
  {
  // TODO:
  }



  public int getFadePrecision()
  {
  //TODO:
  return -1;
  }



  public boolean getMute()
  {
  return m_bMuted;
  }



  public void setMute(boolean bMuted)
  {
  if (bMuted != getMute())
  {
  m_bMuted = bMuted;
  if (getMute())
  {
  AlsaBaseDataLine.this.setGain(getMinimum());
  }
  else
  {
  AlsaBaseDataLine.this.setGain(getGain());
  }
  }
  }
*/


		}
	}



/*** AlsaBaseDataLine.java ***/
