/*
 * Copyright (C) 2011-2015 GUIGUI Simon, [email protected]
 *
 * This file is part of libstreaming (https://github.com/fyhertz/libstreaming)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.majorkernelpanic.streaming;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.util.Random;
import net.majorkernelpanic.streaming.audio.AudioStream;
import net.majorkernelpanic.streaming.rtp.AbstractPacketizer;
import net.majorkernelpanic.streaming.video.VideoStream;
import android.annotation.SuppressLint;
import android.media.MediaCodec;
import android.media.MediaRecorder;
import android.net.LocalServerSocket;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Log;

/**
 * A MediaRecorder that streams what it records using a packetizer from the RTP package.
 * You can't use this class directly !
 */
public abstract class MediaStream implements Stream {

	protected static final String TAG = "MediaStream";
	
	/** Raw audio/video will be encoded using the MediaRecorder API. */
	public static final byte MODE_MEDIARECORDER_API = 0x01;

	/** Raw audio/video will be encoded using the MediaCodec API with buffers. */
	public static final byte MODE_MEDIACODEC_API = 0x02;

	/** Raw audio/video will be encoded using the MediaCode API with a surface. */
	public static final byte MODE_MEDIACODEC_API_2 = 0x05;

	/** A LocalSocket will be used to feed the MediaRecorder object */
	public static final byte PIPE_API_LS = 0x01;
	
	/** A ParcelFileDescriptor will be used to feed the MediaRecorder object */
	public static final byte PIPE_API_PFD = 0x02;
	
	/** Prefix that will be used for all shared preferences saved by libstreaming */
	protected static final String PREF_PREFIX = "libstreaming-";

	/** The packetizer that will read the output of the camera and send RTP packets over the networked. */
	protected AbstractPacketizer mPacketizer = null;

	protected static byte sSuggestedMode = MODE_MEDIARECORDER_API;
	protected byte mMode, mRequestedMode;

	/** 
	 * Starting lollipop the LocalSocket API cannot be used to feed a MediaRecorder object. 
	 * You can force what API to use to create the pipe that feeds it with this constant 
	 * by using  {@link #PIPE_API_LS} and {@link #PIPE_API_PFD}.
	 */
	protected final static byte sPipeApi;
	
	protected boolean mStreaming = false, mConfigured = false;
	protected int mRtpPort = 0, mRtcpPort = 0; 
	protected byte mChannelIdentifier = 0;
	protected OutputStream mOutputStream = null;
	protected InetAddress mDestination;
	
	protected ParcelFileDescriptor[] mParcelFileDescriptors;
	protected ParcelFileDescriptor mParcelRead;
	protected ParcelFileDescriptor mParcelWrite;
	
	protected LocalSocket mReceiver, mSender = null;
	private LocalServerSocket mLss = null;
	private int mSocketId; 
	
	private int mTTL = 64;

	protected MediaRecorder mMediaRecorder;
	protected MediaCodec mMediaCodec;
	
	static {
		// We determine whether or not the MediaCodec API should be used
		try {
			Class.forName("android.media.MediaCodec");
			// Will be set to MODE_MEDIACODEC_API at some point...
			sSuggestedMode = MODE_MEDIACODEC_API;
			Log.i(TAG,"Phone supports the MediaCoded API");
		} catch (ClassNotFoundException e) {
			sSuggestedMode = MODE_MEDIARECORDER_API;
			Log.i(TAG,"Phone does not support the MediaCodec API");
		}
		
		// Starting lollipop, the LocalSocket API cannot be used anymore to feed 
		// a MediaRecorder object for security reasons
		if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT_WATCH) {
			sPipeApi = PIPE_API_PFD;
		} else {
			sPipeApi = PIPE_API_LS;
		}
	}

	public MediaStream() {
		mRequestedMode = sSuggestedMode;
		mMode = sSuggestedMode;
	}

	/** 
	 * Sets the destination IP address of the stream.
	 * @param dest The destination address of the stream 
	 */	
	public void setDestinationAddress(InetAddress dest) {
		mDestination = dest;
	}

	/** 
	 * Sets the destination ports of the stream.
	 * If an odd number is supplied for the destination port then the next 
	 * lower even number will be used for RTP and it will be used for RTCP.
	 * If an even number is supplied, it will be used for RTP and the next odd
	 * number will be used for RTCP.
	 * @param dport The destination port
	 */
	public void setDestinationPorts(int dport) {
		if (dport % 2 == 1) {
			mRtpPort = dport-1;
			mRtcpPort = dport;
		} else {
			mRtpPort = dport;
			mRtcpPort = dport+1;
		}
	}

	/**
	 * Sets the destination ports of the stream.
	 * @param rtpPort Destination port that will be used for RTP
	 * @param rtcpPort Destination port that will be used for RTCP
	 */
	public void setDestinationPorts(int rtpPort, int rtcpPort) {
		mRtpPort = rtpPort;
		mRtcpPort = rtcpPort;
		mOutputStream = null;
	}	

	/**
	 * If a TCP is used as the transport protocol for the RTP session,
	 * the output stream to which RTP packets will be written to must
	 * be specified with this method.
	 */ 
	public void setOutputStream(OutputStream stream, byte channelIdentifier) {
		mOutputStream = stream;
		mChannelIdentifier = channelIdentifier;
	}
	
	
	/**
	 * Sets the Time To Live of packets sent over the network.
	 * @param ttl The time to live
	 * @throws IOException
	 */
	public void setTimeToLive(int ttl) throws IOException {
		mTTL = ttl;
	}

	/** 
	 * Returns a pair of destination ports, the first one is the 
	 * one used for RTP and the second one is used for RTCP. 
	 **/
	public int[] getDestinationPorts() {
		return new int[] {
				mRtpPort,
				mRtcpPort
		};
	}

	/** 
	 * Returns a pair of source ports, the first one is the 
	 * one used for RTP and the second one is used for RTCP. 
	 **/	
	public int[] getLocalPorts() {
		return mPacketizer.getRtpSocket().getLocalPorts();
	}

	/**
	 * Sets the streaming method that will be used.
	 * 
	 * If the mode is set to {@link #MODE_MEDIARECORDER_API}, raw audio/video will be encoded 
	 * using the MediaRecorder API. <br />
	 * 
	 * If the mode is set to {@link #MODE_MEDIACODEC_API} or to {@link #MODE_MEDIACODEC_API_2}, 
	 * audio/video will be encoded with using the MediaCodec. <br />
	 * 
	 * The {@link #MODE_MEDIACODEC_API_2} mode only concerns {@link VideoStream}, it makes 
	 * use of the createInputSurface() method of the MediaCodec API (Android 4.3 is needed there). <br />
	 * 
	 * @param mode Can be {@link #MODE_MEDIARECORDER_API}, {@link #MODE_MEDIACODEC_API} or {@link #MODE_MEDIACODEC_API_2} 
	 */
	public void setStreamingMethod(byte mode) {
		mRequestedMode = mode;
	}

	/**
	 * Returns the streaming method in use, call this after 
	 * {@link #configure()} to get an accurate response. 
	 */
	public byte getStreamingMethod() {
		return mMode;
	}		
	
	/**
	 * Returns the packetizer associated with the {@link MediaStream}.
	 * @return The packetizer
	 */
	public AbstractPacketizer getPacketizer() { 
		return mPacketizer;
	}

	/**
	 * Returns an approximation of the bit rate consumed by the stream in bit per seconde.
	 */
	public long getBitrate() {
		return !mStreaming ? 0 : mPacketizer.getRtpSocket().getBitrate(); 
	}

	/**
	 * Indicates if the {@link MediaStream} is streaming.
	 * @return A boolean indicating if the {@link MediaStream} is streaming
	 */
	public boolean isStreaming() {
		return mStreaming;
	}

	/**
	 * Configures the stream with the settings supplied with 
	 * {@link VideoStream#setVideoQuality(net.majorkernelpanic.streaming.video.VideoQuality)}
	 * for a {@link VideoStream} and {@link AudioStream#setAudioQuality(net.majorkernelpanic.streaming.audio.AudioQuality)}
	 * for a {@link AudioStream}.
	 */
	public synchronized void configure() throws IllegalStateException, IOException {
		if (mStreaming) throw new IllegalStateException("Can't be called while streaming.");
		if (mPacketizer != null) {
			mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort);
			mPacketizer.getRtpSocket().setOutputStream(mOutputStream, mChannelIdentifier);
		}
		mMode = mRequestedMode;
		mConfigured = true;
	}
	
	/** Starts the stream. */
	public synchronized void start() throws IllegalStateException, IOException {
		
		if (mDestination==null)
			throw new IllegalStateException("No destination ip address set for the stream !");

		if (mRtpPort<=0 || mRtcpPort<=0)
			throw new IllegalStateException("No destination ports set for the stream !");

		mPacketizer.setTimeToLive(mTTL);
		
		if (mMode != MODE_MEDIARECORDER_API) {
			encodeWithMediaCodec();
		} else {
			encodeWithMediaRecorder();
		}

	}

	/** Stops the stream. */
	@SuppressLint("NewApi") 
	public synchronized  void stop() {
		if (mStreaming) {
			try {
				if (mMode==MODE_MEDIARECORDER_API) {
					mMediaRecorder.stop();
					mMediaRecorder.release();
					mMediaRecorder = null;
					closeSockets();
					mPacketizer.stop();
				} else {
					mPacketizer.stop();
					mMediaCodec.stop();
					mMediaCodec.release();
					mMediaCodec = null;
				}
			} catch (Exception e) {
				e.printStackTrace();
			}	
			mStreaming = false;
		}
	}
 
	protected abstract void encodeWithMediaRecorder() throws IOException;

	protected abstract void encodeWithMediaCodec() throws IOException;
	
	/**
	 * Returns a description of the stream using SDP. 
	 * This method can only be called after {@link Stream#configure()}.
	 * @throws IllegalStateException Thrown when {@link Stream#configure()} was not called.
	 */
	public abstract String getSessionDescription();
	
	/**
	 * Returns the SSRC of the underlying {@link net.majorkernelpanic.streaming.rtp.RtpSocket}.
	 * @return the SSRC of the stream
	 */
	public int getSSRC() {
		return getPacketizer().getSSRC();
	}
	
	protected void createSockets() throws IOException {

		if (sPipeApi == PIPE_API_LS) {
			
			final String LOCAL_ADDR = "net.majorkernelpanic.streaming-";
	
			for (int i=0;i<10;i++) {
				try {
					mSocketId = new Random().nextInt();
					mLss = new LocalServerSocket(LOCAL_ADDR+mSocketId);
					break;
				} catch (IOException e1) {}
			}
	
			mReceiver = new LocalSocket();
			mReceiver.connect( new LocalSocketAddress(LOCAL_ADDR+mSocketId));
			mReceiver.setReceiveBufferSize(500000);
			mReceiver.setSoTimeout(3000);
			mSender = mLss.accept();
			mSender.setSendBufferSize(500000);
			
		} else {
			Log.e(TAG, "parcelFileDescriptors createPipe version = Lollipop");
			mParcelFileDescriptors = ParcelFileDescriptor.createPipe();
			mParcelRead = new ParcelFileDescriptor(mParcelFileDescriptors[0]);
			mParcelWrite = new ParcelFileDescriptor(mParcelFileDescriptors[1]);
		}
	}

	protected void closeSockets() {
		if (sPipeApi == PIPE_API_LS) {
			try {
				mReceiver.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
			try {
				mSender.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
			try {
				mLss.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
			mLss = null;
			mSender = null;
			mReceiver = null;
			
		} else {
			try {
				if (mParcelRead != null) {
					mParcelRead.close();
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
			try {
				if (mParcelWrite != null) {
					mParcelWrite.close();
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
	
}