/*
 * Copyright (C) 2015 AVM GmbH <info@avm.de>
 * Copyright (C) 2009 The Sipdroid Open Source Project
 * Copyright (C) 2005 Luca Veltri - University of Parma - Italy
 * 
 * This file is part of Sipdroid (http://www.sipdroid.org)
 * 
 * Sipdroid 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 source code 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 source code; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.sipdroid.media;

import java.io.IOException;
import java.util.HashMap;
import java.util.Random;

import org.sipdroid.net.RtpPacket;
import org.sipdroid.net.RtpSocket;
import org.sipdroid.net.SipdroidSocket;
import org.sipdroid.codecs.Codecs;

import android.annotation.SuppressLint;
import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Build;
import android.telephony.TelephonyManager;
import de.avm.android.fritzapp.gui.SettingsExtendedActivity;
import de.avm.android.fritzapp.sipua.UserAgent;
import de.avm.android.fritzapp.sipua.ui.Receiver;
import de.avm.android.fritzapp.sipua.ui.Sipdroid;
import de.avm.android.fritzapp.util.AudioEffects;
import de.avm.android.fritzapp.util.AudioLoopback;
import de.avm.android.fritzapp.util.BuildHelper;
import de.avm.android.fritzapp.util.InetAddressHelper;
import de.avm.fundamentals.logger.FileLog;

/**
 * RtpStreamSender is a generic stream sender. It takes an InputStream and sends
 * it through RTP.
 */
@SuppressWarnings("serial")
public class RtpStreamSender extends Thread {
	private static final String TAG = "RtpStreamSender";

    // TODO use MediaRecorder.AudioSource.VOICE_COMMUNICATION  with target SDK >= 11
    public static final int VOICE_COMMUNICATION = 7;

	/** The RtpSocket */
	RtpSocket rtp_socket = null;

	/** Payload type */
	Codecs.Map p_type = null;
	int p_type_num = 0;

	int mInternalSampleRate;
	int mInternalFrameSize;
	int frame_rate;
	int frame_size;
	
	int out_frame_size;
	int VolumeMicro = 100;  // Lautstaerkefaktor fuer verschiedene Telefone
	int MicDown = 100;
	int MicSave = 100;

	
	boolean useRecordingDevice;
	private TelephonyManager mTelephonyManager;

	public enum RecordEngineState {STATE_NOTHING, STATE_INITIALIZED, STATE_UNINITIALIZED};
	private RecordEngineState recordEngineState;

	/**
	 * Whether it works synchronously with a local clock, or it it acts as slave
	 * of the InputStream
	 */
	boolean do_sync = true;

	/**
	 * Synchronization correction value, in milliseconds. It accellarates the
	 * sending rate respect to the nominal value, in order to compensate program
	 * latencies.
	 */
	int sync_adj = 0;

	/** Whether it is running */
	boolean running = false;
	boolean muted = false;
	
	//DTMF change
	char dtmf = 0; 
	int dtmf_payload_type = 101;
	
	private static HashMap<Character, Byte> rtpEventMap = new HashMap<Character,Byte>(){{
		put('0',(byte)0);
		put('1',(byte)1);
		put('2',(byte)2);
		put('3',(byte)3);
		put('4',(byte)4);
		put('5',(byte)5);
		put('6',(byte)6);
		put('7',(byte)7);
		put('8',(byte)8);
		put('9',(byte)9);
		put('*',(byte)10);
		put('#',(byte)11);
		put('A',(byte)12);
		put('B',(byte)13);
		put('C',(byte)14);
		put('D',(byte)15);
	}};
	//DTMF change 
	
	/**
	 * Constructs a RtpStreamSender.
	 * 
	 * @param doSync
	 *            whether time synchronization must be performed by the
	 *            RtpStreamSender, or it is performed by the InputStream (e.g.
	 *            the system audio input)
	 * @param payloadType
	 *            the payload type
	 * @param frameRate
	 *            the frame rate, i.e. the number of frames that should be sent
	 *            per second; it is used to calculate the nominal packet time
	 *            and,in case of do_sync==true, the next departure time
	 * @param frameSize
	 *            the size of the payload
	 * @param srcSocket
	 *            the socket used to send the RTP packet
	 * @param destAddr
	 *            the destination address
	 * @param destPort
	 *            the destination port
	 */
	public RtpStreamSender(boolean doSync, Codecs.Map payloadType,
			long frameRate, int frameSize, SipdroidSocket srcSocket,
			String destAddr, int destPort, AudioLoopback loopback)
	{
		mLoopback = loopback;
		useRecordingDevice = (loopback == null) && BuildHelper.useRecordingDevice();
		mTelephonyManager = ((TelephonyManager)Receiver.mContext
					.getSystemService(Context.TELEPHONY_SERVICE));
		mutecounter = -1; // wird nur bei Samsung-Problemhandys auf 0 gesetzt!
		recordEngineState = RecordEngineState.STATE_NOTHING;
		if ((loopback != null) ||
				SettingsExtendedActivity.isRecordingWith44kHz(Receiver.mContext))
		{
            FileLog.d(TAG, ((loopback == null) ? "Sampling mic" : "Loopback") + " on 44,1kHz");
			mInternalSampleRate = 44100;
			mInternalFrameSize = 882;
		}
		else
		{
            FileLog.d(TAG, "Sampling mic on 16kHz");
			mInternalSampleRate = 16000;
			mInternalFrameSize = 320;
		}
		p_type = payloadType;
		frame_rate = (int)frameRate;
		frame_size = frameSize;
		do_sync = doSync;
		if (rtp_socket == null)
		{
			try
			{
                FileLog.d(TAG, "new RtpSocket to " + destAddr + ":" + destPort);
				rtp_socket = new RtpSocket(srcSocket,
						InetAddressHelper.getByName(destAddr),
						destPort); // ADO: use v4 addr only!
			}
			catch (Exception e)
			{
                FileLog.e(TAG, "No remote RTP socket created!", e);
			}
		}
	}

	/** Sets the synchronization adjustment time (in milliseconds). */
	public void setSyncAdj(int millisecs) {
		sync_adj = millisecs;
	}

	/** Whether is running */
	public boolean isRunning() {
		return running;
	}
	
	boolean mute() {
		return muted = !muted;
	}

	public static int delay = 0;
	
	/** Stops running */
	public void halt() {
		running = false;
	}
	
	Random random;
	
	long s;
	double smin = 200;
	
	int nearend;
	long frame_period;
	byte[] buffer = null;
	RtpPacket rtp_packet = null;
	AudioRecord record = null;
	private AudioEffects mRecordEffects = null;
	private static String mAudioInfo = "";
	private AudioLoopback mLoopback = null;
	short[] lin = null;
	short[] bufferLollipopWorkaround = null;
	
	void calc(short[] lin,int len) {
		int i,j;
		double r;
		long sm = 30000;
		int mu = downsample_8kHz ? 1 : 2;
		
		for (i = 0; i < len; i += 5) {
			j = lin[i];
			s = (31*Math.abs(j) + 993*s) >> 10; // 0.03*Math.abs(j) + 0.97*s
			if (s < sm) sm = s;
			if (s > smin) nearend = 2400 >> (3-mu);
			else if (nearend > 0) nearend--;
		}
		r = (double)(len)/(100000*mu);
		smin = (sm*r + smin*(1-r));
	}

	void calc1(short[] lin,int len) {
		int i,j;
		
		for (i = 0; i < len; i++) {
			j = lin[i];
			lin[i] = (short)(j>>1);
		}
	}

	void calc5(short[] lin,int len) {
		int i,j;
		
		for (i = 0; i < len; i++) {
			j = lin[i];
			if (j > 16350)
				lin[i] = 16350<<1;
			else if (j < -16350)
				lin[i] = -16350<<1;
			else
				lin[i] = (short)(j<<1);
		}
	}

	void calc10(short[] lin,int len) {
		int i,j;
		
		for (i = 0; i < len; i++) {
			j = lin[i];
			if (j > 8150)
				lin[i] = 8150<<2;
			else if (j < -8150)
				lin[i] = -8150<<2;
			else
				lin[i] = (short)(j<<2);
		}
	}

	void noise(short[] lin,int len,double power) {
		int i,r = (int)(power*2);
		short ran;

		if (r == 0) r = 1;
		for (i = 0; i < len; i += 4) {
			ran = (short)(random.nextInt(r*2)-r);
			lin[i] = ran;
			lin[i+1] = ran;
			lin[i+2] = ran;
			lin[i+3] = ran;
		}
	}
	
	public synchronized RecordEngineState recordEngineInitialized() {
		return recordEngineState;
	}
	
	private synchronized void changeRecordEngineState(RecordEngineState state) {
		recordEngineState = state;
	}
	
	public static int m;
	private Resampler resampler;
	
	int num, ring, pos;
	boolean downsample_8kHz;
	
	@SuppressLint("InlinedApi")
	private boolean setCodec() {
		p_type_num = p_type.getNumber();
		downsample_8kHz = (p_type_num == 8 || p_type_num == 0);
		mutecounter = -1;

		if(BuildHelper.avoidSilenceAfterSpeakerModeChange())
			mutecounter = 0;
		
		if ((record == null) ||
				((mLoopback != null) && (recordEngineState == RecordEngineState.STATE_UNINITIALIZED)))
		{
			frame_size = mInternalFrameSize;
			frame_rate = mInternalSampleRate / mInternalFrameSize;
			frame_period = 1000 / frame_rate;
			frame_rate *= 1.5;
			out_frame_size = 320;
			
			if(buffer != null) buffer = null;
			if(rtp_packet != null) rtp_packet = null;
			if(lin != null) lin = null;
			
			buffer = new byte[out_frame_size + 12];
			rtp_packet = new RtpPacket(buffer, 0);
			rtp_packet.setPayloadType(p_type_num);
		
			mAudioInfo = "";
			if (useRecordingDevice)
			{
				int min = AudioRecord.getMinBufferSize(mInternalSampleRate, 
						AudioFormat.CHANNEL_IN_MONO, 
						AudioFormat.ENCODING_PCM_16BIT);
				// buffer for up to 550ms (LG-P970 needs a larger buffer than others to record fluently)
				int recBufSize = (mInternalSampleRate * 2 * 550) / 1000;
				if (min < recBufSize) min = recBufSize;
				
				int audioSource = MediaRecorder.AudioSource.MIC; 
				mAudioInfo = "MIC";
				boolean isEffects = SettingsExtendedActivity.isEffects(Receiver.mContext); 
				if (!isEffects || !AudioEffects.isAvailable(AudioEffects.FLAG_AEC))
				{
					// use VOICE_COMMUNICATION if not using effects,
					// but some devices deliver silence with VOICE_COMMUNICATION if no SIM present
					// so, do we have a SIM card?
					int simState = mTelephonyManager.getSimState();
					if ((simState == TelephonyManager.SIM_STATE_READY) ||
						(simState == TelephonyManager.SIM_STATE_NETWORK_LOCKED))
					{
						audioSource = VOICE_COMMUNICATION;
						mAudioInfo = "VOICE_COMMUNICATION";
					}
				}

				record = new AudioRecord(audioSource, mInternalSampleRate,
						AudioFormat.CHANNEL_IN_MONO,
						AudioFormat.ENCODING_PCM_16BIT, min);
				int engineState = record.getState();
				if (engineState == AudioRecord.STATE_UNINITIALIZED)
				{
                    FileLog.e(TAG, "Error initializing audio record engine (" + mAudioInfo + ").");
					mAudioInfo = "";
					running = false;
					record = null;
					buffer = null;
					rtp_packet = null;
					changeRecordEngineState(RecordEngineState.STATE_UNINITIALIZED);
					return false;
				}
				else
					changeRecordEngineState(RecordEngineState.STATE_INITIALIZED);
				if (isEffects)
				{
					// check for availability of effects, activate with MIC only
					mRecordEffects = AudioEffects.create(record,
							(audioSource == MediaRecorder.AudioSource.MIC) ? 
									AudioEffects.FLAG_AEC | AudioEffects.FLAG_NR : 0);
					if (mRecordEffects != null) mAudioInfo += ", " + mRecordEffects.toString();
				}
                FileLog.d(TAG, "Using audio source " + mAudioInfo);
			}
			else
				changeRecordEngineState(RecordEngineState.STATE_INITIALIZED);
			
			lin = new short[frame_size*(frame_rate+1)];
			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
			{
				// lollipop bug in framework:
				// if parameter offsetInShorts of AudioRecord.read > 0, read audio data is corrupted
				// and crashes in native part of AudioRecord may occur
				// workaround: copy data to lin after reading it from AudioRecord 
				bufferLollipopWorkaround = new short[frame_size];
			}
			ring = 0;
		
			random = new Random();
			p_type.init();
			if (useRecordingDevice)
				record.startRecording();
			else if (mLoopback != null)
				mLoopback.clear();
			
		} else {
			p_type.init();
			rtp_packet.setPayloadType(p_type_num);
		}

		if(downsample_8kHz)
			resampler = new Resampler(1, mInternalSampleRate,  8000, mInternalFrameSize);
		else
			resampler = new Resampler(1, mInternalSampleRate, 16000, mInternalFrameSize);
		resampler.setVolume(1, mInternalSampleRate, downsample_8kHz ? 8000 : 16000, VolumeMicro);			
			
		return true;
	}

	public static int mutecounter = -1; // Samsung Galaxy S: Gegenstelle hört nichts nach Ausschalten des ext. Lautsprechers

	/** Runs it in a new Thread. */
	public void run() {
		if (rtp_socket == null)
		{
            FileLog.e(TAG, "No remote RTP socket!");
			changeRecordEngineState(RecordEngineState.STATE_UNINITIALIZED);
			return;
		}
		int seqn = 0;
		long time = 0;
		double p = 0;
		int micgain = (int)(Sipdroid.getMicGain()*10);
		long last_tx_time = 0;
		long next_tx_delay;
		running = true;
		m = 1;
		int dtframesize = 4;
		short downsample_buf[] = new short[mInternalFrameSize];
		int new_num;
		
		VolumeMicro = SettingsExtendedActivity.getResamplerVolume(
				Receiver.mContext); // ggf. angepasste Geraete-Lautstaerke 
		long TimeDown = 0;  // Zeitpunkt wann Mic-Lautstaerke verringert werden soll
		long TimeUp = 0;    // Zeitpunkt wann Mic-Lautstaerke zurueck gesetzt werden soll
		long TimeAct = 0;   // aktuelle Zeit, zum Vergleich
		int statSentVoicePackets = 0;
		int statSentDtmfPackets = 0;

		android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
		if(!setCodec()) return;

		while (running) {
			 if(p_type_num != p_type.codec.number()) {
				 if(!setCodec()) {
					 running = false;
					 return;
				 }
			 }
			 if ((mutecounter >= 0 && mutecounter < 2)) {
				 mutecounter++;
				/*
				if (Receiver.call_state == UserAgent.UA_STATE_HOLD)
					RtpStreamReceiver.restoreMode(false);
				*/
				if(useRecordingDevice)
					record.stop();
				while (running && (muted || Receiver.call_state == UserAgent.UA_STATE_HOLD)) {
					try {
						sleep(1000);
					} catch (InterruptedException e1) {
					}
				}
				if(useRecordingDevice)
					record.startRecording();
				else if (mLoopback != null)
					mLoopback.clear();
			 }
			 //DTMF change start
			 if ((dtmf!=0) && !(muted || Receiver.call_state == UserAgent.UA_STATE_HOLD)) {
				 if(useRecordingDevice && BuildHelper.changeRecordStateWhenSendingDtmf())
					 record.stop();
	 			 byte[] dtmfbuf = new byte[dtframesize + 12];
				 RtpPacket dt_packet = new RtpPacket(dtmfbuf, 0);
				 dt_packet.setPayloadType(dtmf_payload_type);
 				 dt_packet.setPayloadLength(dtframesize);
				 dt_packet.setSscr(rtp_packet.getSscr());
				 long dttime = time;
				 int duration;
				 
	 			 for (int i = 0; i < 6; i++) { 
 	 				 time += 160;
 	 				 duration = (int)(time - dttime);
	 				 dt_packet.setSequenceNumber(seqn++);
	 				 dt_packet.setTimestamp(dttime);
	 				 dtmfbuf[12] = rtpEventMap.get(dtmf);
	 				 dtmfbuf[13] = (byte)0x0a;
	 				 dtmfbuf[14] = (byte)(duration >> 8);
	 				 dtmfbuf[15] = (byte)duration;
	 				 try {
						rtp_socket.send(dt_packet);
						statSentDtmfPackets++;
						sleep(20);
	 				 } catch (IOException e1) {
	 					if (running) FileLog.e(TAG, "Failed to send RTP/DTMF", e1);
	 				 } catch (InterruptedException e1) {
	 				 }
	 			 }
	 			 for (int i = 0; i < 3; i++) {
	 				 duration = (int)(time - dttime);
	 				 dt_packet.setSequenceNumber(seqn);
	 				 dt_packet.setTimestamp(dttime);
	 				 dtmfbuf[12] = rtpEventMap.get(dtmf);
	 				 dtmfbuf[13] = (byte)0x8a;
	 				 dtmfbuf[14] = (byte)(duration >> 8);
	 				 dtmfbuf[15] = (byte)duration;
	 				 try {
	 					rtp_socket.send(dt_packet);
						statSentDtmfPackets++;
	 				 } catch (IOException e1) {
	 					if (running) FileLog.e(TAG, "Failed to send RTP/DTMF", e1);
	 				 }	 			 
	 			 }
	 			 time += 160; seqn++;
				dtmf=0;
				if(useRecordingDevice && BuildHelper.changeRecordStateWhenSendingDtmf())				
					record.startRecording();
				else if (mLoopback != null)
					mLoopback.clear();
			 }
			 //DTMF change end

			 long now = System.currentTimeMillis();
			 next_tx_delay = frame_period - (now - last_tx_time);
			 last_tx_time = now;
			 if (next_tx_delay > 0) {
				 try {
					 sleep(next_tx_delay);
				 } catch (InterruptedException e1) {
				 }
				 last_tx_time += next_tx_delay-sync_adj;
			 }
			 
			 pos = (ring+delay*frame_rate*frame_size)%(frame_size*(frame_rate+1));
			 if(useRecordingDevice)
			 {
				 if (bufferLollipopWorkaround == null)
				 {
					 num = record.read(lin, pos, frame_size);
				 }
				 else
				 {
					 // lollipop bug in framework:
					 // if parameter offsetInShorts of AudioRecord.read > 0, read audio data is corrupted
					 // and crashes in native part of AudioRecord may occur
					 // workaround: copy data to lin after reading it from AudioRecord
					 num = record.read(bufferLollipopWorkaround, 0, frame_size);
					 if (num > 0)
						 System.arraycopy(bufferLollipopWorkaround, 0, lin, pos, num);
				 }
			 }
			 else if (mLoopback != null)
				 num = mLoopback.read(lin ,pos ,frame_size);
				 
			 if (num <= 0)
				 continue;
			 if (!p_type.codec.isValid())
				 continue;

			 // "Sprachwaage" beginn
			 if (MicDown != RtpStreamReceiver.MicDown ) {
				MicDown = RtpStreamReceiver.MicDown;  
				if (MicDown != 100) {
					// Runterschalten
					if (TimeDown == 0) 
						TimeDown = System.currentTimeMillis();
					TimeUp = 0;
					MicSave = MicDown; // erstmal sichern und unten dann umsetzen
					
				} else {
					// Hochschalten
					TimeUp = System.currentTimeMillis();
				}
			 }
			 // ist etwas umzusetzen ? da wir Empfangsseitig ein Delay haben, hier auch verzoegert umschalten
			 if (TimeDown != 0 || TimeUp != 0 ) { // soll was umgesetzt werden?
				TimeAct = System.currentTimeMillis();
				// Mic daempfen?
				if (TimeDown != 0 && TimeDown < (TimeAct -300)) { // 300ms verzoegert
					TimeDown = 0;
					resampler.setVolume(1, mInternalSampleRate, downsample_8kHz ? 8000 : 16000, (VolumeMicro*MicSave/100));
					//Log.e("RtpStreamSender", "**** Mic set to " + (VolumeMicro*MicSave/100));
				}
				// Mic wieder auf normal?
				if (TimeUp != 0 && TimeUp < (TimeAct -400)) { // 400ms verzoegert
					TimeUp = 0;
					resampler.setVolume(1, mInternalSampleRate, downsample_8kHz ? 8000 : 16000, (VolumeMicro/* wieder auf 100% */));
					//Log.e("RtpStreamSender", "**** Mic set to " + (VolumeMicro));
				}
			 } // "Sprachwaage"ende

			 new_num = resampler.doit(lin, num, ring%(frame_size*(frame_rate+1)), downsample_buf);

			 if (RtpStreamReceiver.speakermode == AudioManager.MODE_NORMAL) {
 				 calc(downsample_buf,new_num);
 	 			 if (RtpStreamReceiver.nearend != 0)
	 				 noise(downsample_buf,new_num,p/2);
	 			 else if (nearend == 0) {
	 				 p = 0.9*p + 0.1*s;
	 			 }
 			 } else {
 				 switch (micgain) {
 				 	case 1:
 				 		calc1(downsample_buf,new_num);
 				 		break;
 				 	case 5:
 				 		calc5(downsample_buf,new_num);
 				 		break;
 				 	case 10:
 				 		calc10(downsample_buf,new_num);
 				 		break;
 				 }
 			 }
			 
			 num = p_type.codec.encode(downsample_buf, 0, buffer, new_num); 
			 ring += frame_size;
			 
 			 rtp_packet.setSequenceNumber(seqn++);
 			 rtp_packet.setTimestamp(time);
 			 rtp_packet.setPayloadLength(num);

 			 try {
 				 if(!(muted || Receiver.call_state == UserAgent.UA_STATE_HOLD))
 				 {
 					 rtp_socket.send(rtp_packet);
 					statSentVoicePackets++;
 				 }
 			 } catch (IOException e) {
				if (running) FileLog.e(TAG, "Failed to send RTP", e);
 			 }
 			 if (p_type.codec.number() == 9)
 				 time += out_frame_size;
 			 else
 				 time += out_frame_size/2;
		}

        FileLog.i(TAG, "Stat1: sent voice packets:" + statSentVoicePackets + ", sent DTMF packets:" + statSentDtmfPackets);

		if (useRecordingDevice)
		{
            FileLog.i(TAG, "Stat2: audio source: " + mAudioInfo);
			record.stop();
			if (mRecordEffects != null)
			{
				mRecordEffects.release();
				mRecordEffects = null;
			}
			record.release();
			record = null;
		}
		m = 0;
		
		p_type.codec.close();
		p_type.deinit();
		
		rtp_socket.close();
		rtp_socket = null;
	}

	/** Set RTP payload type of outband DTMF packets. **/  
	public void setDTMFpayloadType(int payload_type){
		dtmf_payload_type = payload_type; 
	}
	
	/** Send outband DTMF packets */
	public void sendDTMF(char c) {
		dtmf = c; // will be set to 0 after sending tones
	}
	//DTMF change
}
