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

/*
 *  Copyright (c) 2000,2001 by Florian Bomers <http://www.bomers.de>
 *
 *
 *   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.lowlevel.lame;

import java.io.IOException;
import java.io.UnsupportedEncodingException;

import javax.sound.sampled.AudioFormat;

import org.tritonus.share.TDebug;

public class Lame {

	// constants from lame.h
	private static final int MPEG_VERSION_2 = 0; // MPEG-2
	private static final int MPEG_VERSION_1 = 1; // MPEG-1
	private static final int MPEG_VERSION_2DOT5 = 2; // MPEG-2.5

	public static final int QUALITY_LOWEST = 9; // low mean bitrate in VBR mode
	public static final int QUALITY_LOW = 7;
	public static final int QUALITY_MIDDLE = 5;
	public static final int QUALITY_HIGH = 2;
	// quality==0 not yet coded in LAME (3.83alpha)
	public static final int QUALITY_HIGHEST = 1; // high mean bitrate in VBR mode

	public static final int CHANNEL_MODE_STEREO = 0;
	public static final int CHANNEL_MODE_JOINT_STEREO = 1;
	public static final int CHANNEL_MODE_DUAL_CHANNEL = 2;
	public static final int CHANNEL_MODE_MONO = 3;

	// channel mode has no influence on mono files.
	public static final int CHANNEL_MODE_AUTO = -1;
	public static final int BITRATE_AUTO = -1;

	// suggested maximum buffer size for an mpeg frame
	private static final int DEFAULT_PCM_BUFFER_SIZE=2048*16;

	// frame size=576 for MPEG2 and MPEG2.5
	//           =576*2 for MPEG1
	private static boolean libAvailable=false;
	private static String linkError="";


	private static int DEFAULT_QUALITY = QUALITY_MIDDLE;
	private static int DEFAULT_BITRATE = BITRATE_AUTO;
	private static int DEFAULT_CHANNEL_MODE = CHANNEL_MODE_AUTO;
	// in VBR mode, bitrate is ignored.
	private static boolean DEFAULT_VBR = false;

	private static final int OUT_OF_MEMORY = -300;
	private static final int NOT_INITIALIZED = -301;
	private static final int LAME_ENC_NOT_FOUND = -302;

	private static final String PROPERTY_PREFIX = "tritonus.lame.";

	static
	{
		try
		{
			System.loadLibrary("lametritonus");
			libAvailable=true;
		}
		catch (UnsatisfiedLinkError e)
		{
			if (TDebug.TraceAllExceptions)
			{
				TDebug.out(e);
			}
			linkError=e.getMessage();
		}
	}


	/**
	 * Holds LameConf
	 * This field is long because on 64 bit architectures, the native
	 * size of ints may be 64 bit.
	 */
	private long m_lNativeGlobalFlags;

	// these fields are set upon successful initialization to show effective values.
	private int effQuality;
	private int effBitRate;
	private int effVbr;
	private int effChMode;
	private int effSampleRate;
	private int effEncoding;

	private void handleNativeException(int resultCode) {
		close();
		if (resultCode==OUT_OF_MEMORY) {
			throw new OutOfMemoryError("out of memory");
		}
		else if (resultCode==NOT_INITIALIZED) {
			throw new RuntimeException("not initialized");
		}
		else if (resultCode==LAME_ENC_NOT_FOUND) {
			libAvailable=false;
			linkError="lame_enc.dll not found";
			throw new IllegalArgumentException(linkError);
		}
	}

	/**
	 * Initializes the decoder with
	 * DEFAULT_BITRATE, DEFAULT_CHANNEL_MODE, DEFAULT_QUALITY, and DEFAULT_VBR
	 * Throws IllegalArgumentException when parameters are not supported
	 * by LAME.
	 */
	public Lame(AudioFormat sourceFormat) {
		initParams(sourceFormat);
	}

	/**
	 * Initializes the decoder.
	 * Throws IllegalArgumentException when parameters are not supported
	 * by LAME.
	 */
	public Lame(AudioFormat sourceFormat, int bitRate, int channelMode, int quality, boolean VBR) {
		initParams(sourceFormat, bitRate, channelMode, quality, VBR);
	}

	private void initParams(AudioFormat sourceFormat) {
		readParameters();
		initParams(sourceFormat, DEFAULT_BITRATE, DEFAULT_CHANNEL_MODE, DEFAULT_QUALITY, DEFAULT_VBR);
	}

	private void initParams(AudioFormat sourceFormat, int bitRate, int channelMode, int quality, boolean VBR) {
		// simple check that bitrate is not too high for MPEG2 and MPEG2.5
		// todo: exception ?
		if (sourceFormat.getSampleRate()<32000 && bitRate>160) {
			bitRate=160;
		}
		if (TDebug.TraceAudioConverter) {
			TDebug.out("LAME parameters: channels="+sourceFormat.getChannels()
				   +"  sample rate="+((int) Math.round(sourceFormat.getSampleRate())+"Hz")
				   +"  bitrate="+bitRate+"KBit/s");
			TDebug.out("                 channelMode="+chmode2string(channelMode)
				   +"   quality="+quality2string(quality)
				   +"   VBR="+VBR+"  bigEndian="+sourceFormat.isBigEndian());
		}
		int result=nInitParams(sourceFormat.getChannels(), (int) Math.round(sourceFormat.getSampleRate()),
				       bitRate, channelMode, quality,
				       VBR, sourceFormat.isBigEndian());
		if (result<0) {
			handleNativeException(result);
			throw new IllegalArgumentException(
			   "parameters not supported by LAME (returned "+result+")");
		}
		// provide effective parameters to user-space
		try {
			System.setProperty(PROPERTY_PREFIX + "effective.quality",
					   quality2string(getEffectiveQuality()));
			System.setProperty(PROPERTY_PREFIX + "effective.bitrate",
					   String.valueOf(getEffectiveBitRate()));
			System.setProperty(PROPERTY_PREFIX + "effective.chmode",
					   chmode2string(getEffectiveChannelMode()));
			System.setProperty(PROPERTY_PREFIX + "effective.vbr",
					   String.valueOf(getEffectiveVBR()));
			System.setProperty(PROPERTY_PREFIX + "effective.samplerate",
					   String.valueOf(getEffectiveSampleRate()));
			System.setProperty(PROPERTY_PREFIX + "effective.encoding",
					   getEffectiveEncoding().toString());
			System.setProperty(PROPERTY_PREFIX + "encoder.version",
					   getEncoderVersion());
		}
		catch (Throwable t)
		{
			if (TDebug.TraceAllExceptions)
			{
				TDebug.out(t);
			}
		}
	}

	/**
	 * Initializes the lame encoder.
	 * Throws IllegalArgumentException when parameters are not supported
	 * by LAME.
	 */
	private native int nInitParams(int channels, int sampleRate,
				       int bitrate, int mode, int quality,
				       boolean VBR, boolean bigEndian);

	/**
	 * returns -1 if string is too short
	 * or returns one of the exception constants
	 * if everything OK, returns the length of the string
	 */
	private native int nGetEncoderVersion(byte[] string);

	public String getEncoderVersion() {
		byte[] string=new byte[300];
		int res=nGetEncoderVersion(string);
		if (res<0) {
			if (res==-1) {
				throw new RuntimeException("Unexpected error in Lame.getEncoderVersion()");
			}
			handleNativeException(res);
		}
		String sRes="";
		if (res>0) {
			try {
				sRes=new String(string, 0, res, "ISO-8859-1");
			} catch (UnsupportedEncodingException uee) {
				if (TDebug.TraceAllExceptions) {
					TDebug.out(uee);
				}
				sRes=new String(string, 0, res);
			}
		}
		return sRes;
	}

	private native int nGetPCMBufferSize(int suggested);

	/**
	 * Returns the buffer needed pcm buffer size.
	 * The passed parameter is a wished buffer size.
	 * The implementation of the encoder may return
	 * a lower or higher buffer size.
	 * The encoder must be initalized (i.e. not closed) at this point.
	 * A return value of <0 denotes an error.
	 */
	public int getPCMBufferSize() {
		int ret=nGetPCMBufferSize(DEFAULT_PCM_BUFFER_SIZE);
		if (ret<0) {
			handleNativeException(ret);
			throw new RuntimeException("Unknown error in Lame.nGetPCMBufferSize(). Resultcode="+ret);
		}
		return ret;
	}

	public int getMP3BufferSize() {
		// bad estimate :)
		return getPCMBufferSize()/2+1024;
	}


	private native int nEncodeBuffer(byte[] pcm, int offset,
					int length, byte[] encoded);

	/**
	 * Encode a block of data. Throws IllegalArgumentException when parameters
	 * are wrong.
	 * When the <code>encoded</code> array is too small,
	 * an ArrayIndexOutOfBoundsException is thrown.
	 * <code>length</code> should be the value returned by getPCMBufferSize.
	 * @return the number of bytes written to <code>encoded</code>. May be 0.
	 */
	public int encodeBuffer(byte[] pcm, int offset, int length, byte[] encoded)
		throws ArrayIndexOutOfBoundsException {
		if (length<0 || (offset+length)>pcm.length) {
			throw new IllegalArgumentException("inconsistent parameters");
		}
		int result=nEncodeBuffer(pcm, offset, length, encoded);
		if (result<0) {
			if (result==-1) {
				throw new ArrayIndexOutOfBoundsException("Encode buffer too small");
			}
			handleNativeException(result);
			throw new RuntimeException("crucial error in encodeBuffer.");
		}
		return result;
	}

	/**
	 * Has to be called to finish encoding. <code>encoded</code> may be null.
	 *
	 * @return the number of bytes written to <code>encoded</code>
	 */
	private native int nEncodeFinish(byte[] encoded);

	public int encodeFinish(byte[] encoded) {
		return nEncodeFinish(encoded);
	}

	/*
	 * Deallocates resources used by the native library.
	 * *MUST* be called !
	 */
	private native void nClose();

	public void close() {
		nClose();
	}

	/*
	 * Returns whether the libraries are installed correctly.
	 */
	public static boolean isLibAvailable() {
		return libAvailable;
	}

	public static String getLinkError() {
		return linkError;
	}

	public int getEffectiveQuality() {
		if (effQuality>=QUALITY_LOWEST) {
			return QUALITY_LOWEST;
		}
		else if (effQuality>=QUALITY_LOW) {
			return QUALITY_LOW;
		}
		else if (effQuality>=QUALITY_MIDDLE) {
			return QUALITY_MIDDLE;
		}
		else if (effQuality>=QUALITY_HIGH) {
			return QUALITY_HIGH;
		}
		return QUALITY_HIGHEST;
	}

	public int getEffectiveBitRate() {
		return effBitRate;
	}

	public int getEffectiveChannelMode() {
		return effChMode;
	}

	public boolean getEffectiveVBR() {
		return effVbr!=0;
	}

	public int getEffectiveSampleRate() {
		return effSampleRate;
	}

	public AudioFormat.Encoding getEffectiveEncoding() {
		if (effEncoding==MPEG_VERSION_2) {
			if (getEffectiveSampleRate()<16000) {
				return new AudioFormat.Encoding("MPEG2DOT5L3");
			}
			return new AudioFormat.Encoding("MPEG2L3");
		}
		else if (effEncoding==MPEG_VERSION_2DOT5) {
			return new AudioFormat.Encoding("MPEG2DOT5L3");
		}
		return new AudioFormat.Encoding("MPEG1L3");
	}

	/**
	 * workaround for missing paramtrization possibilities
	 * for FormatConversionProviders
	 */
	private void readParameters() {
		String v=getStringProperty("quality", quality2string(DEFAULT_QUALITY));
		DEFAULT_QUALITY=string2quality(v.toLowerCase(), DEFAULT_QUALITY);
		DEFAULT_BITRATE=getIntProperty("bitrate", DEFAULT_BITRATE);
		v=getStringProperty("chmode", chmode2string(DEFAULT_CHANNEL_MODE));
		DEFAULT_CHANNEL_MODE=string2chmode(v.toLowerCase(), DEFAULT_CHANNEL_MODE);
		DEFAULT_VBR = getBooleanProperty("vbr", DEFAULT_VBR);
		// set the parameters back so that user program can verify them
		try {
			System.setProperty(PROPERTY_PREFIX + "quality", quality2string(DEFAULT_QUALITY));
			System.setProperty(PROPERTY_PREFIX + "bitrate", String.valueOf(DEFAULT_BITRATE));
			System.setProperty(PROPERTY_PREFIX + "chmode", chmode2string(DEFAULT_CHANNEL_MODE));
			System.setProperty(PROPERTY_PREFIX + "vbr", String.valueOf(DEFAULT_VBR));
		}
		catch (Throwable t)
		{
			if (TDebug.TraceAllExceptions)
			{
				TDebug.out(t);
			}
		}
	}

	private String quality2string(int quality) {
		if (quality>=QUALITY_LOWEST) {
			return "lowest";
		}
		else if (quality>=QUALITY_LOW) {
			return "low";
		}
		else if (quality>=QUALITY_MIDDLE) {
			return "middle";
		}
		else if (quality>=QUALITY_HIGH) {
			return "high";
		}
		return "highest";
	}

	private int string2quality(String quality, int def) {
		if (quality.equals("lowest")) {
			return QUALITY_LOWEST;
		}
		else if (quality.equals("low")) {
			return QUALITY_LOW;
		}
		else if (quality.equals("middle")) {
			return QUALITY_MIDDLE;
		}
		else if (quality.equals("high")) {
			return QUALITY_HIGH;
		}
		else if (quality.equals("highest")) {
			return QUALITY_HIGHEST;
		}
		return def;
	}

	private String chmode2string(int chmode) {
		if (chmode==CHANNEL_MODE_STEREO) {
			return "stereo";
		}
		else if (chmode==CHANNEL_MODE_JOINT_STEREO) {
			return "jointstereo";
		}
		else if (chmode==CHANNEL_MODE_DUAL_CHANNEL) {
			return "dual";
		}
		else if (chmode==CHANNEL_MODE_MONO) {
			return "mono";
		}
		else if (chmode==CHANNEL_MODE_AUTO) {
			return "auto";
		}
		return "auto";
	}

	private int string2chmode(String chmode, int def) {
		if (chmode.equals("stereo")) {
			return CHANNEL_MODE_STEREO;
		}
		else if (chmode.equals("jointstereo")) {
			return CHANNEL_MODE_JOINT_STEREO;
		}
		else if (chmode.equals("dual")) {
			return CHANNEL_MODE_DUAL_CHANNEL;
		}
		else if (chmode.equals("mono")) {
			return CHANNEL_MODE_MONO;
		}
		else if (chmode.equals("auto")) {
			return CHANNEL_MODE_AUTO;
		}
		return def;
	}

	private static boolean getBooleanProperty(String strName, boolean def) {
		String	strPropertyName = PROPERTY_PREFIX + strName;
		String	strValue = def ? "true":"false";
		try {
			strValue = System.getProperty(strPropertyName, strValue);
		}
		catch (Throwable t)
		{
			if (TDebug.TraceAllExceptions)
			{
				TDebug.out(t);
			}
		}
		strValue=strValue.toLowerCase();
		boolean	bValue=false;
		if (strValue.length()>0) {
			if (def) {
				bValue=(strValue.charAt(0)!='f') // false
					&& (strValue.charAt(0)!='n') // no
					&& (!strValue.equals("off"));
			} else {
				bValue=(strValue.charAt(0)=='t') // true
					|| (strValue.charAt(0)=='y') // yes
					|| (strValue.equals("on"));
			}
		}
		return bValue;
	}

	private static String getStringProperty(String strName, String def)	{
		String	strPropertyName = PROPERTY_PREFIX + strName;
		String	strValue = def;
		try {
			strValue = System.getProperty(strPropertyName, def);
		}
		catch (Throwable t)
		{
			if (TDebug.TraceAllExceptions)
			{
				TDebug.out(t);
			}
		}
		return strValue;
	}

	private static int getIntProperty(String strName, int def)	{
		String	strPropertyName = PROPERTY_PREFIX + strName;
		int	value = def;
		try {
			String strValue = System.getProperty(strPropertyName, String.valueOf(def));
			value=new Integer(strValue).intValue();
		}
		catch (Throwable e)
		{
			if (TDebug.TraceAllExceptions)
			{
				TDebug.out(e);
			}
		}
		return value;
	}

}


/*** Lame.java ***/
