/* Copyright (C) 2010-2011, Mamadou Diop.
*  Copyright (C) 2011, Doubango Telecom.
*  Copyright (C) 2011, Philippe Verney <verney(dot)philippe(AT)gmail(dot)com>
*
* Contact: Mamadou Diop <diopmamadou(at)doubango(dot)org>
*	
* This file is part of Open Source Doubango Framework.
*
* This is free software: you can redistribute it and/or modify it under the terms of
* the GNU General Public License as published by the Free Software Foundation, either version 3 
* of the License, or (at your option) any later version.
*	
* This 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 General Public License for more details.
*	
* You should have received a copy of the GNU General Public License along 
* with this program; if not, write to the Free Software Foundation, Inc., 
* 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/
package org.doubango.ngn.media;

import android.media.AudioFormat;
																 import android.media.AudioManager;
																 import android.media.AudioTrack;
																 import android.util.Log;

import org.doubango.ngn.BuildConfig;
import org.doubango.ngn.NgnApplication;
																 import org.doubango.ngn.NgnEngine;
																 import org.doubango.ngn.services.INgnConfigurationService;
																 import org.doubango.ngn.utils.NgnConfigurationEntry;
																 import org.doubango.tinyWRAP.ProxyAudioConsumer;
																 import org.doubango.tinyWRAP.ProxyAudioConsumerCallback;
																 import org.doubango.utils.Utils;

																 import java.math.BigInteger;
																 import java.nio.ByteBuffer;
																 import java.util.Arrays;

public class NgnProxyAudioConsumer extends NgnProxyPlugin{
	private static final String TAG = Utils.getTAG(NgnProxyAudioConsumer.class.getCanonicalName());
	private static final int AUDIO_STREAM_TYPE = AudioManager.STREAM_VOICE_CALL;
	
	private final INgnConfigurationService mConfigurationService;
	private final MyProxyAudioConsumerCallback mCallback;
	private final ProxyAudioConsumer mConsumer;
	private boolean mRoutingChanged;
	private Thread mConsumerThread;

	private int mBufferSize;
	private AudioTrack mAudioTrack;
	private int mPtime, mInputRate, mChannels;
	
	private int mOutputRate;
	private ByteBuffer mOutputBuffer;
	private boolean mAec ;
	private boolean mIsInit = false ;
	private static final int BUFFER_SIZE_DEFAULT=100;


	
	public NgnProxyAudioConsumer(BigInteger id, ProxyAudioConsumer consumer){
		super(id, consumer);
        mCallback = new MyProxyAudioConsumerCallback(this);
        mConsumer = consumer;
        mConsumer.setCallback(mCallback);
        mConfigurationService = NgnEngine.getInstance().getConfigurationService();
	}

	public void setSpeakerphoneOn(boolean speakerOn){
		Log.d(TAG, "setSpeakerphoneOn("+speakerOn+")");
		final AudioManager audiomanager = NgnApplication.getAudioManager();
		if (NgnApplication.getSDKVersion() < 5){
			audiomanager.setRouting(AudioManager.MODE_IN_CALL, 
					speakerOn ? AudioManager.ROUTE_SPEAKER : AudioManager.ROUTE_EARPIECE, AudioManager.ROUTE_ALL);
		}
		else if(NgnApplication.useSetModeToHackSpeaker()){
			audiomanager.setMode(AudioManager.MODE_IN_CALL);
			audiomanager.setSpeakerphoneOn(speakerOn);
			audiomanager.setMode(AudioManager.MODE_NORMAL);
		}
		else{
			audiomanager.setSpeakerphoneOn(speakerOn);
		}
		
		if (super.mPrepared){
			mRoutingChanged = NgnApplication.isAudioRecreateRequired();
			changeVolume(false,false);// disable attenuation
		}
	}
	
	public void toggleSpeakerphone(){
		setSpeakerphoneOn(!NgnApplication.getAudioManager().isSpeakerphoneOn());
	}
	
	public boolean onVolumeChanged(boolean bDown){
		if(!mPrepared || mAudioTrack == null){
			return false;
		}
		return changeVolume(bDown, true);
	}
	
	private boolean changeVolume(boolean bDown, boolean bVolumeChanged){
		Log.d(TAG,"changeVolume("+bDown+","+bVolumeChanged+ ") aec:"+mAec);
		final AudioManager audioManager = NgnApplication.getAudioManager();
		if(audioManager != null){
			/*if( !mIsInit && mAec && NgnApplication.getAudioManager().isSpeakerphoneOn() ){
				mIsInit = true ;
				Log.d(TAG, "Consumer changeVolume HP on AEC");
				return mAudioTrack.setStereoVolume(AudioTrack.getMaxVolume()*0.5f, AudioTrack.getMaxVolume()*0.5f) == AudioTrack.SUCCESS;
			}
			else if(bVolumeChanged){
				Log.d(TAG, "Consumer changeVolume VolumeChanged   bDown:"+bDown);
				audioManager.adjustStreamVolume(AUDIO_STREAM_TYPE, bDown ? AudioManager.ADJUST_LOWER : AudioManager.ADJUST_RAISE, 
					AudioManager.FLAG_SHOW_UI);
				return true;
			}
			else
			{
//				final float attenuation = mConfigurationService.getFloat(NgnConfigurationEntry.MEDIA_AUDIO_CONSUMER_ATTENUATION, 
//						NgnConfigurationEntry.DEFAULT_MEDIA_AUDIO_CONSUMER_ATTENUATION);
				final float attenuation = mConfigurationService.getFloat(NgnConfigurationEntry.GENERAL_AUDIO_PLAY_LEVEL ,
						NgnConfigurationEntry.DEFAULT_GENERAL_AUDIO_PLAY_LEVEL);
				Log.d(TAG, "Consumer changeVolume audio attenuation "+attenuation);
				return true ; //mAudioTrack.setStereoVolume(AudioTrack.getMaxVolume()*attenuation, AudioTrack.getMaxVolume()*attenuation) == AudioTrack.SUCCESS;
			}*/
		}
		return false;
	}
	
	
	private int prepareCallback(int ptime, int rate, int channels) {
		Log.d(TAG, "prepareCallback("+ptime+","+rate+","+channels+")");
		return prepare(ptime, rate, channels);
	}
	
	private int startCallback() {
		Log.d(TAG, "startCallback");
		if(mPrepared && this.mAudioTrack != null){
			super.mStarted = true;
			mConsumerThread = new Thread(mRunnablePlayer, "AudioConsumerThread");
			// mConsumerThread.setPriority(Thread.MAX_PRIORITY);
			mConsumerThread.start();
			return 0;
		}
		return -1;
	}
	
	private int pauseCallback() {
		Log.d(TAG, "pauseCallback");
		if(mAudioTrack != null){
			synchronized(mAudioTrack){
				mAudioTrack.pause();
				super.mPaused = true;
			}
			return 0;
		}
		return -1;
	}		

	private int stopCallback() {
		if(BuildConfig.DEBUG)Log.d(TAG, "stopCallback: " + "NgnProxyAudioConsumer");
		Log.d(TAG, "stopCallback");
		super.mStarted = false;
		if(mConsumerThread != null){
			try {
				mConsumerThread.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			mConsumerThread = null;
		}
		return 0;
	}

	public synchronized boolean setGain(long gain){
		return mConsumer.setGain(gain);
	}
	
	private synchronized int prepare(int ptime, int rate, int channels){
		int bytesNseq=0;
		if(super.mPrepared){
			Log.e(TAG,"already prepared");
			return -1;
		}
		
		mPtime = ptime; mInputRate = rate; mChannels = channels; 
		mOutputRate = AudioTrack.getNativeOutputSampleRate(AUDIO_STREAM_TYPE);
		if(super.mValid && mConsumer != null && mInputRate != mOutputRate){
			if(mConsumer.queryForResampler(mInputRate, mOutputRate, mPtime, mChannels, 
					mConfigurationService.getInt(NgnConfigurationEntry.MEDIA_AUDIO_RESAMPLER_QUALITY, NgnConfigurationEntry.DEFAULT_MEDIA_AUDIO_RESAMPLER_QUALITY))){
				Log.d(TAG, "queryForResampler("+mOutputRate+") succeed");
			}
			else{
				Log.e(TAG, "queryForResampler("+mOutputRate+") failed. Using "+mInputRate);
				mOutputRate = mInputRate;
			}
		}
		
		final int minBufferSize = AudioTrack.getMinBufferSize(mOutputRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
		final int shortsPerNotif = (mOutputRate * mPtime)/1000;
		mBufferSize = Math.max(minBufferSize, shortsPerNotif<<1);
		mBufferSize=BUFFER_SIZE_DEFAULT;
		mOutputBuffer = ByteBuffer.allocateDirect((shortsPerNotif<<1)+bytesNseq);
		mAec = mConfigurationService.getBoolean(NgnConfigurationEntry.GENERAL_AEC, NgnConfigurationEntry.DEFAULT_GENERAL_AEC) ;
		
		// setSpeakerphoneOn(false);
		mAudioTrack = new AudioTrack(AUDIO_STREAM_TYPE,
				mOutputRate, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT,
				mBufferSize, AudioTrack.MODE_STREAM);
		if(mAudioTrack.getState() == AudioTrack.STATE_INITIALIZED){
			Log.d(TAG, "Consumer BufferSize="+mBufferSize+",MinBufferSize="+minBufferSize+",TrueSampleRate="+mAudioTrack.getSampleRate()+" shortsPerNotif<<1:"+(shortsPerNotif<<1)+" mOutputRate:"+mOutputRate+" mPtime:"+mPtime);
			changeVolume(false, false);
			super.mPrepared = true;
			return 0;
		}
		else{
			Log.e(TAG, "prepare() failed");
			super.mPrepared = false;
			return -1;
		}
	}
	
	private synchronized void unprepare(){
		if(mAudioTrack != null){
			synchronized(mAudioTrack){
				if(super.mPrepared){
					mAudioTrack.stop();
				}
				mAudioTrack.release();
				mAudioTrack = null;
			}
		}
		super.mPrepared = false;
	}
	
	private Runnable mRunnablePlayer = new Runnable(){
		@Override
		public void run() {
			Log.d(TAG, "===== Audio Player Thread (Start) =====");
			
			android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
			
			int nFrameLength = mOutputBuffer.capacity();
			final int nFramesCount = 1; // Number of 20ms' to copy
			final byte[] aAudioBytes = new byte[nFrameLength*nFramesCount];
			int i, nGapSize;
			long lSizeInBytes = 0 ;
			boolean bPlaying = false;
			int nWritten = 0;
			int bytesNseq=0;
			int seq=0;

			
			if(NgnProxyAudioConsumer.super.mValid){
				mConsumer.setPullBuffer(mOutputBuffer, mOutputBuffer.capacity());
				mConsumer.setGain(NgnEngine.getInstance().getConfigurationService().getInt(NgnConfigurationEntry.MEDIA_AUDIO_CONSUMER_GAIN, 
						NgnConfigurationEntry.DEFAULT_MEDIA_AUDIO_CONSUMER_GAIN));
			}
			
			while(NgnProxyAudioConsumer.super.mValid && NgnProxyAudioConsumer.super.mStarted){
				if(mAudioTrack == null){
					break;
				}

				if(mRoutingChanged){
					Log.d(TAG, "Routing changed: restart() player");
					mRoutingChanged = false;
					unprepare();
					if(prepare(mPtime, mOutputRate, mChannels) != 0){
						break;
					}
					if(!NgnProxyAudioConsumer.super.mPaused){
						bPlaying = false;
						nWritten = 0;
					}
				}

				for(i=0; i<nFramesCount; i++){
					lSizeInBytes = mConsumer.pull();
					if(lSizeInBytes>0){
						mOutputBuffer.get(aAudioBytes, i*nFrameLength, (int)lSizeInBytes+bytesNseq);
						mOutputBuffer.rewind();
						nGapSize = (nFrameLength - (int)lSizeInBytes)-bytesNseq;
						if(nGapSize != 0){
							Arrays.fill(aAudioBytes, i*nFrameLength + (int)lSizeInBytes, (i*nFrameLength + (int)lSizeInBytes) + nGapSize, (byte)0);
						}
					}
					else{
						Arrays.fill(aAudioBytes, i*nFrameLength, (i*nFrameLength)+nFrameLength, (byte)0);
					}
					nWritten += nFrameLength;
				}


					mAudioTrack.write(aAudioBytes, 0, aAudioBytes.length-bytesNseq);
				if(

								!bPlaying && nWritten>mBufferSize){
					mAudioTrack.play();
					bPlaying = true;
				}
			}

			unprepare();
			Log.d(TAG, "===== Audio Player Thread (Stop) =====");
		}
	};
	
	/**
	 * MyProxyAudioConsumerCallback
	 */
	static class MyProxyAudioConsumerCallback extends ProxyAudioConsumerCallback
	{
		final NgnProxyAudioConsumer myConsumer;
		
		MyProxyAudioConsumerCallback(NgnProxyAudioConsumer consumer){
			super();
			myConsumer = consumer;
		}

		@Override
		public int prepare(int ptime, int rate, int channels) {
			return myConsumer.prepareCallback(ptime, rate, channels);
		}
		
		@Override
		public int start() {
			return myConsumer.startCallback();
		}
		
		@Override
		public int pause() {
			return myConsumer.pauseCallback();
		}		

		@Override
		public int stop() {
			return myConsumer.stopCallback();
		}
	}
}