/* 
 * Copyright 2015 by AVM GmbH <info@avm.de>
 *
 * This software contains free software; you can redistribute it and/or modify 
 * it under the terms of the GNU General Public License ("License") as 
 * published by the Free Software Foundation  (version 3 of the License). 
 * This software 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 copy of the 
 * License you received along with this software for more details.
 */

package de.avm.android.fritzapp.util;

import android.media.AudioRecord;
import android.os.Build;
import android.text.TextUtils;

import java.io.File;

import de.avm.fundamentals.logger.FileLog;

public class AudioEffects
{
	private static final String TAG = "AudioEffects";
	public static final int FLAG_AEC = 1;				// Acoustic Echo Canceler
	public static final int FLAG_NR = 2;				// Noise Reduction
	public static final int FLAG_AGC = 4;				// Automatic Gain Control
	public static final int FLAG_ALL = FLAG_AEC | FLAG_NR | FLAG_AGC;

	private static final String[] EFFECTS_NAMES = new String[]
	{
		"android.media.audiofx.AcousticEchoCanceler",
		"android.media.audiofx.NoiseSuppressor",
		"android.media.audiofx.AutomaticGainControl"
	};
	private static Class<?>[] mEffectClasses = null;
	
	private Object[] mEffects = null;
	
	private static final String[] DEBUG_NAMES = new String[]
	{
		"AEC", "NR", "AGC"
	};
	private String mToString = "";
	
	/**
	 * Creates Instance to control effects
	 * (effects loaded depending on availability)
	 * 
	 * @param audioRecord
	 * 		audio session to apply effects
	 * @param flags
	 * 		requested effects  (FLAG_*)
	 * @return
	 * 		the instance created
	 */
	public static AudioEffects create(AudioRecord audioRecord, int flags)
	{
		if (audioRecord == null)
			throw new IllegalArgumentException("Argument audioRecord must not be null");
		if ((flags & FLAG_ALL) != flags)
			throw new IllegalArgumentException("Argument flags (" + flags +
					") contains one invalid flag at least");
		
		if (isAvailable())
		{
			int audioSession;
			try
			{
				audioSession = ((Integer)audioRecord.getClass()
						.getMethod("getAudioSessionId", (Class[])null)
						.invoke(audioRecord, (Object[])null)).intValue();
			}
			catch(Exception e)
			{
				FileLog.w(TAG, "Failed to get audio session id", e);
				return null;
			}
			AudioEffects result = new AudioEffects();
			StringBuilder builder = new StringBuilder();
			for (int ii = 0; ii < getEffectsCount(); ii++, flags = flags >> 1)
			{
				if (mEffectClasses[ii] == null) continue;
				
				builder.append(DEBUG_NAMES[ii]);
				result.createEffectInstance(ii, audioSession);
				if (result.mEffects[ii] != null)
				{
					if (!result.getEnabledByIndex(ii))
					{
						builder.append("-");
						if ((flags & 1) != 0)
						{
							result.setEnabledByIndex(ii, true);
							builder.append((result.getEnabledByIndex(ii)) ? "+" : "?");
						}
					}
					else builder.append("+");
				}
				else builder.append("?");
				builder.append(" ");
			}
			result.mToString = builder.toString();
			FileLog.d(TAG, "create: " + result.mToString);
			
			if (result.isLoaded())
				return result;
		}
		
		return null;
	}
	
	/**
	 * Are Effects any referenced by FLAG_* available via API?
	 * 
	 * @return true, if one available at least
	 */
	public static boolean isAvailable()
	{
		for (Class<?> clazz : mEffectClasses)
			if (clazz != null) return true;
		return false;
	}
	
	/**
	 * Are any effects selected with flags available?
	 * 
	 * @param flags
	 * 		selected effects (FLAG_*)
	 * @return
	 * 		true, if all available
	 */
	public static boolean isAvailable(int flags)
	{
		if (flags == 0)
			throw new IllegalArgumentException("Argument flags ust not be 0");
		if ((flags & FLAG_ALL) != flags)
			throw new IllegalArgumentException("Argument flags (" + flags +
					") contains one invalid flag at least");
		
		for (int ii = 0; ii < getEffectsCount(); ii++, flags = flags >> 1)
			if (((flags & 1) != 0) && (mEffectClasses[ii] != null))
				return true;
		
		return false;
	}
	
	/**
	 * Are Effects referenced by FLAG_* loaded?
	 * 
	 * @return true, if one loaded at least
	 */
	public boolean isLoaded()
	{
		for (Object effect : mEffects)
			if (effect != null) return true;
		return false;
	}
	
	/**
	 * Are selected effects loaded?
	 * 
	 * @param flags
	 * 		selected effects (FLAG_*)
	 * @return
	 * 		true, if all loaded
	 */
	public boolean isLoaded(int flags)
	{
		if (flags == 0)
			throw new IllegalArgumentException("Argument flags ust not be 0");
		if ((flags & FLAG_ALL) != flags)
			throw new IllegalArgumentException("Argument flags (" + flags +
					") contains one invalid flag at least");
		
		for (int ii = 0; ii < getEffectsCount(); ii++, flags = flags >> 1)
			if (((flags & 1) != 0) && (mEffects[ii] == null))
				return false;
		
		return true;
	}
	
	/**
	 * Is effect enabled?
	 * 
	 * @param flag
	 * 		effect (FLAG_*)
	 * @return
	 * 		true, if enabled
	 */
	public boolean getEnabled(final int flag)
	{
		return getEnabledByIndex(getEffectIndex(flag));
	}
	
	private boolean getEnabledByIndex(final int index)
	{
		Object effect = mEffects[index];
		
		try
		{
			if (effect != null)
			{
				return (Boolean)mEffectClasses[index]
					.getMethod("getEnabled", (Class[])null)
					.invoke(mEffects[index], (Object[])null);
			}
		}
		catch(Exception e)
		{
			FileLog.w(TAG, "Failed to access instance of " +
					mEffectClasses[index].getSimpleName(), e);
		}
		
		return false;
	}
	
	private boolean setEnabledByIndex(final int index, boolean on)
	{
		Object effect = mEffects[index];
		
		try
		{
			if (effect != null)
			{
				return ((Integer)mEffectClasses[index]
					.getMethod("setEnabled", new Class[] { boolean.class })
					.invoke(mEffects[index], new Object[] { Boolean.valueOf(on) }))
					.intValue() == 0;
			}
		}
		catch(Exception e)
		{
			FileLog.w(TAG, "Failed to change instance of " +
					mEffectClasses[index].getSimpleName(), e);
		}
		
		return false;
	}

	/**
	 * Release resources
	 */
	public void release()
	{
		for (int ii = 0; ii < getEffectsCount(); ii++)
		{
			try
			{
				if (mEffects[ii] != null)
				{
					mEffectClasses[ii].getMethod("release", (Class[])null)
							.invoke(mEffects[ii], (Object[])null);
					mEffects[ii] = null;
				}
			}
			catch(Exception e)
			{
				FileLog.w(TAG, "Failed to release ressource.", e);
			}
		}
	}
	
	static
	{
		mEffectClasses = new Class<?>[getEffectsCount()];
		for (int ii = 0; ii < mEffectClasses.length; ii++)
			mEffectClasses[ii] = null;
		
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
		{
			for (int ii = 0; ii < mEffectClasses.length; ii++)
				loadClass(ii);
		}
		else FileLog.d(TAG, "audio effects API not available");
	}
	
	private AudioEffects()
	{
		mEffects = new Object[getEffectsCount()];
		for (int ii = 0; ii < mEffects.length; ii++)
			mEffects[ii] = null;
	}
	
	private static int getEffectsCount()
	{
		return EFFECTS_NAMES.length;
	}
	
	private static int getEffectIndex(final int flag)
	{
		if (Integer.bitCount(flag) != 1)
			throw new IllegalArgumentException("Argument flag (" + flag +
					") has more or less than one flag");
		if ((flag & FLAG_ALL) != flag)
			throw new IllegalArgumentException("Argument flag (" + flag +
					") is invalid");
		
		return Integer.SIZE - Integer.numberOfLeadingZeros(flag) - 1;
	}
	
	private static void loadClass(final int effectIndex)
	{
		if (!TextUtils.isEmpty(EFFECTS_NAMES[effectIndex]))
		{
			try
			{
				Class<?> clazz = Class.forName(EFFECTS_NAMES[effectIndex]);
				if ((Boolean)clazz
					.getMethod("isAvailable", (Class[])null)
					.invoke(null, (Object[])null))
				{
					mEffectClasses[effectIndex] = clazz;
				}
				else
				{
					FileLog.w(TAG, "Audio effect not available: " + clazz.getSimpleName());
					mEffectClasses[effectIndex] = null;
				}
			}
			catch(Exception e)
			{
				FileLog.w(TAG, "Failed to load audio effect class", e);
			}
		}
	}
	
	private void createEffectInstance(int effectIndex, int audioSession)
	{
		if (mEffectClasses[effectIndex] != null)
		{
			try
			{
				mEffects[effectIndex] = mEffectClasses[effectIndex]
						.getMethod("create", new Class[] {int.class} )
						.invoke(null, new Object[] { Integer.valueOf(audioSession) });
			}
			catch(Exception e)
			{
				FileLog.w(TAG, "Failed to instanciate audio effect class", e);
			}
		}
	}
	
	@Override
	public String toString()
	{
		return mToString;
	}
}
