/* Copyright (C) 2010-2015, Mamadou Diop.
*  Copyright (C) 2011, Doubango Telecom.
*  Copyright (C) 2011, Philippe Verney <verney(dot)philippe(AT)gmail(dot)com>
*  Copyright (C) 2011, Tiscali
*	
* 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.content.Context;
import android.hardware.Camera;
import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.Size;
import android.util.Log;
import android.view.Display;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;

import org.doubango.ngn.BuildConfig;
import org.doubango.ngn.NgnApplication;
import org.doubango.tinyWRAP.ProxyVideoProducer;
import org.doubango.tinyWRAP.ProxyVideoProducerCallback;
import org.doubango.utils.Utils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


public class NgnProxyVideoProducer extends NgnProxyPlugin{
	private static final String TAG = Utils.getTAG(NgnProxyVideoProducer.class.getCanonicalName());
	private static final int DEFAULT_VIDEO_WIDTH = 176;
	private static final int DEFAULT_VIDEO_HEIGHT = 144;
	private static final int DEFAULT_VIDEO_FPS = 15;
	private static final int CALLABACK_BUFFERS_COUNT = 3;
	private static final boolean sAddCallbackBufferSupported = NgnCameraProducer.isAddCallbackBufferSupported();
	
	private final ProxyVideoProducer mProducer;
	private final MyProxyVideoProducerCallback mCallback;
	private Context mContext;
	private MyProxyVideoProducerPreview mPreview;
	private int mWidth; // negotiated width
	private int mHeight; // negotiated height
	private int mFps;
	private int mFrameWidth; // camera picture output width
	private int mFrameHeight; // camera picture output height
	private final boolean mCheckFps; // make sure we're sending what we negotiated
	private long mFrameDuration;
	private long mNextFrameTime;
	
	private ByteBuffer mVideoFrame;
	private byte[] mVideoCallbackData;
	
	private Thread mProducerPushThread;
	private final Lock mLock;
	private final Condition mConditionPushBuffer;
	private final boolean mAsyncPush;
	
	public NgnProxyVideoProducer(BigInteger id, ProxyVideoProducer producer){
		super(id, producer);
        mCallback = new MyProxyVideoProducerCallback(this);
        mProducer = producer;
        mProducer.setCallback(mCallback);
        
     	// Initialize video stream parameters with default values
        mFrameWidth = mWidth = NgnProxyVideoProducer.DEFAULT_VIDEO_WIDTH;
        mFrameHeight = mHeight = NgnProxyVideoProducer.DEFAULT_VIDEO_HEIGHT;
		mFps = NgnProxyVideoProducer.DEFAULT_VIDEO_FPS;
		
		mCheckFps =NgnApplication.isHovis();
		mFrameDuration = 1000/mFps;
		mNextFrameTime = 0;
		
		mAsyncPush =NgnApplication.isHovis();
		mLock = mAsyncPush ? new ReentrantLock() : null;
		mConditionPushBuffer = (mLock != null) ? mLock.newCondition() : null;
    }
	
	@Override
	public void finalize(){
		
	}
	
	@Override
	public void invalidate() {
		super.invalidate();
		
		mVideoFrame = null;
		System.gc();
	}
	
	public void setContext(Context context){
    	mContext = context;
    }
    
	// Very important: Must be done in the UI thread
	public final View startPreview(Context context){
		if(BuildConfig.DEBUG)Log.w(TAG,"startPreview");
		mContext = context == null ? mContext : context;
		if(mPreview == null && mContext != null){
			mPreview = new MyProxyVideoProducerPreview(this);
			if(mPreview==null){
				Log.e(TAG,"Fail create Preview");
			}
		}else if(mContext==null){
			Log.e(TAG,"Context is null");
		}
		if(mPreview != null){
			mPreview.setVisibility(View.VISIBLE);
			mPreview.getHolder().setSizeFromLayout();
			mPreview.bringToFront();
		}else{
			Log.e(TAG,"Preview is null");
		}
		
		return mPreview;
	}
	
	public final View startPreview(){
		return startPreview(null);
	}
	
	public void pushBlankPacket(){
		if(super.mValid && mProducer != null){
			if(mVideoFrame == null){
				mVideoFrame = ByteBuffer.allocateDirect((mWidth * mHeight * 3) >> 1);
			}
			//final ByteBuffer buffer = ByteBuffer.allocateDirect(mVideoFrame.capacity());
			//mProducer.push(buffer, buffer.capacity());
			mProducer.push(mVideoFrame, mVideoFrame.capacity());
		}
	}
	
	public void toggleCamera(){
		if(super.mValid && super.mStarted && !super.mPaused && mProducer != null){
			final Camera camera = NgnCameraProducer.toggleCamera();
			try{
				startCameraPreview(camera);
			}
			catch (Exception exception) {
				Log.e(TAG, exception.toString());
			}
		}
	}
	
	public int getTerminalRotation(){
		final android.content.res.Configuration conf = NgnApplication.getContext().getResources().getConfiguration();
		int     terminalRotation  = 0 ;
		switch(conf.orientation){
			case android.content.res.Configuration.ORIENTATION_LANDSCAPE:
				terminalRotation = 0;//The starting position is 0 (landscape).
				break;
			case android.content.res.Configuration.ORIENTATION_PORTRAIT:
				terminalRotation = 90 ;
				break;
		}
		return terminalRotation;
	}

	public int getNativeCameraHardRotation(boolean preview){
		return getNativeCameraHardRotation(preview,"CAMERA_FACING_FRONT");
	}

	public int getNativeCameraHardRotation(boolean preview,String cameraString){
		// only for 2.3 and above
		if(NgnApplication.getSDKVersion() >= 9){			
			try {
				
				int orientation = 0;
				int cameraId = 0;
				int numOfCameras = NgnCameraProducer.getNumberOfCameras();
				if (numOfCameras > 1) {
					if (NgnCameraProducer.isFrontFacingCameraEnabled()) {
						cameraId = numOfCameras-1;
					}
				}
				
				Class<?> clsCameraInfo = null;

				final Class<?>[] classes = android.hardware.Camera.class.getDeclaredClasses();
				for (Class<?> c : classes) {
					if (c.getSimpleName().equals("CameraInfo")) {
						clsCameraInfo = c;
						break;
					}
				}
				
				final Object info = clsCameraInfo.getConstructor((Class[]) null).newInstance((Object[]) null);
				Method getCamInfoMthd = android.hardware.Camera.class.getDeclaredMethod("getCameraInfo", int.class, clsCameraInfo);
				getCamInfoMthd.invoke(null, cameraId, info);
				
				Display display = NgnApplication.getDefaultDisplay();
				if (display != null) {
					orientation = display.getOrientation();
				}
				orientation = (orientation + 45) / 90 * 90;     
				int rotation = 0;

				final Field fieldFacing = clsCameraInfo.getField("facing");
				final Field fieldOrient = clsCameraInfo.getField("orientation");
				final Field fieldFrontFacingConst = clsCameraInfo.getField(cameraString);
								
				if (fieldFacing.getInt(info) == fieldFrontFacingConst.getInt(info)) {
					rotation = (fieldOrient.getInt(info) - orientation + 360) % 360;     					
				}
				else {
					// back-facing camera         
					rotation = (fieldOrient.getInt(info) + orientation) % 360;
				}
				
				return rotation;
			} 
			catch (Exception e) {
				e.printStackTrace();
				return 0;
			} 
		}
		else {
			int     terminalRotation   = getTerminalRotation();
			boolean isFront            = NgnCameraProducer.isFrontFacingCameraEnabled();
			if (NgnApplication.isSamsung() && !NgnApplication.isSamsungGalaxyMini()){
				if (preview){
					if (isFront){
						if (terminalRotation == 0) return 0;
						else return 90;
					}
					else return 0 ;
				}
				else{
					if (isFront){
						if (terminalRotation == 0) return -270;
						else return 90;
					}
					else{
						if (terminalRotation == 0) return 0;
						else return 0;
					}
				}
			}
			else if (NgnApplication.isToshiba()){
				if (preview){
					if (terminalRotation == 0) return 0;
					else return 270;
				}
				else{
					return 0;
				}
			}
			else{
				return 0;
			}
		}
	}

	public int compensCamRotation(boolean preview){

		final int cameraHardRotation = getNativeCameraHardRotation(preview);
		final android.content.res.Configuration conf = NgnApplication.getContext().getResources().getConfiguration();
		if(conf.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE){
			return 0;
		}

		if (NgnApplication.getSDKVersion() >= 9) {
			if (preview) {
				return cameraHardRotation;
			}
			else{
				switch (cameraHardRotation) {
					case 0:
					case 180:
					default:
						return 0;
					case 90:
					case 270:
						return 90;
				}
			}			
		}
		else {
			int     terminalRotation   = getTerminalRotation();
			int rotation = 0;
			rotation = (terminalRotation-cameraHardRotation) % 360;
			return rotation;
		}
	}

	public boolean isFrontFacingCameraEnabled() {
		return NgnCameraProducer.isFrontFacingCameraEnabled();
	}

	public boolean setRotation(int rot){
		if(mProducer != null && super.mValid){
			return mProducer.setRotation(rot);
		}
		return false;
	}
	
	public boolean setMirror(boolean mirror){
		if(mProducer != null && super.mValid){
			return mProducer.setMirror(mirror);
		}
		return false;
	}
	
	public void setOnPause(boolean pause){
		if(super.mPaused == pause){
			return;
		}
		try {
			if(super.mStarted){
				final Camera camera = NgnCameraProducer.getCamera();
				if(pause){
					camera.stopPreview();
				}
				else{
					camera.startPreview();
				}
			}
		} catch(Exception e){
			Log.e(TAG, e.toString());
		}
		
		super.mPaused = pause;
	}
	
	private synchronized int prepareCallback(int width, int height, int fps){
		Log.d(NgnProxyVideoProducer.TAG, "prepareCallback("+width+","+height+","+fps+")");
		
		mFrameWidth = mWidth = width;
		mFrameHeight = mHeight = height;
		mFps = fps;
		
		mFrameDuration = 100/mFps;
		mNextFrameTime = 0;
		
		super.mPrepared = true;
		
		return 0;
    }
	
	private Runnable mRunnablePush = new Runnable() {
		@Override
		public void run() {
			Log.d(TAG, "===== Video Producer AsyncThread (Start) ===== ");
			
			while (mValid && mStarted) {
				try {
					synchronized(mConditionPushBuffer) {
						mConditionPushBuffer.wait();
					}
				} catch (InterruptedException e) {
					e.printStackTrace();
					break;
				}
				if (!mValid || !mStarted) {
					break;
				}
				mLock.lock();
				mProducer.push(mVideoFrame, mVideoFrame.capacity());
				mLock.unlock();
			}
			
			Log.d(TAG, "===== Video Producer AsyncThread (Stop) ===== ");
		}
	};

    private synchronized int startCallback(){
    	Log.d(TAG, "startCallback");
		mStarted = true;
		
		if (mAsyncPush)
		{
			mProducerPushThread = new Thread(mRunnablePush,
				"VideoProducerPushThread");
			// FIXME
			//mProducerPushThread.setPriority(Thread.MAX_PRIORITY);
			mProducerPushThread.start();
		}
		
		if (mPreview != null) {
			startCameraPreview(mPreview.getCamera());
    	}
		return 0;
    }

    private synchronized int pauseCallback(){
    	Log.d(TAG, "pauseCallback");
    	setOnPause(true);
    	return 0;
    }

    private synchronized int stopCallback(){
		if(BuildConfig.DEBUG)Log.d(TAG, "stopCallback: " + "NgnProxyVideoProducer");
    	Log.d(TAG, "stopCallback");
    	
    	if (mPreview != null) {
    		stopCameraPreview(mPreview.getCamera());
    	}
    	
		mStarted = false;
		
		if (mConditionPushBuffer != null) {
			synchronized(mConditionPushBuffer) {
				mConditionPushBuffer.notifyAll(); // must be after "mStarted=false" to break endless loop
			}
		}
		
		if (mProducerPushThread != null) {
			try {
				synchronized(mProducerPushThread) {
					mProducerPushThread.join();
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			mProducerPushThread = null;
		}
		
		return 0;
    }
	    
    private Size getCameraBestPreviewSize(Camera camera){
    	final List<Size> prevSizes = camera.getParameters().getSupportedPreviewSizes();
    	
    	Size minSize = null;
    	int minScore = Integer.MAX_VALUE;
    	for(Size size : prevSizes){
    		final int score = Math.abs(size.width - mWidth) + Math.abs(size.height - mHeight);
    		if(minScore > score){
    			minScore = score;
    			minSize = size;
    		}
    	}
    	return minSize;
    }
    
    private synchronized void startCameraPreview(Camera camera){
    	if(!mStarted){
    		Log.w(TAG, "Someone requested to start camera preview but producer not ready ...delaying");
    		return;
    	}
		if(camera != null && mProducer != null){
			try{				
				Camera.Parameters parameters = camera.getParameters();
				 Size prevSize = parameters.getPreviewSize();
				parameters.setPreviewSize(prevSize.width, prevSize.height);
				camera.setParameters(parameters);

				if(prevSize != null && super.isValid() && (mWidth != prevSize.width || mHeight != prevSize.height)){
					mFrameWidth = prevSize.width;
					mFrameHeight = prevSize.height;
				}
				
				// alert the framework that we cannot respect the negotiated size
				mProducer.setActualCameraOutputSize(mFrameWidth, mFrameHeight);
				
				// allocate buffer
				Log.d(TAG, String.format("setPreviewSize [%d x %d ]", mFrameWidth, mFrameHeight));
				mVideoFrame = ByteBuffer.allocateDirect((mFrameWidth * mFrameHeight * 3) >> 1);				
			} catch(Exception e){
				Log.e(TAG, "Error in setPreview 1"+e.toString());
			}
								
			try {
				int terminalRotation = getTerminalRotation();
								
				Camera.Parameters parameters = camera.getParameters();
				
				if (terminalRotation == 0) {
					parameters.set("orientation", "landscape");
				} else {
					parameters.set("orientation", "portrait");
				}

				camera.setParameters(parameters);
			} catch (Exception e) {
				Log.e(TAG,"Error in setPreview 2" +e.toString());
			}
			try{
				// Camera Orientation
				int rotation = compensCamRotation(false);
				Log.d(TAG, String.format("setDisplayOrientation [%d] ",rotation ));
				NgnCameraProducer.setDisplayOrientation(camera, rotation);

				// Callback Buffers
				if(NgnProxyVideoProducer.sAddCallbackBufferSupported){
					for(int i=0; i<NgnProxyVideoProducer.CALLABACK_BUFFERS_COUNT; i++){
						if(i == 0 || (mVideoCallbackData == null)){
							mVideoCallbackData = new byte[mVideoFrame.capacity()];
						}
						NgnCameraProducer.addCallbackBuffer(camera, new byte[mVideoFrame.capacity()]);
					}
				}
			} catch(Exception e){
				Log.e(TAG, "Error in setPreviewSize"+e.toString());
			}

			
			try{
    			camera.startPreview();
    		}catch (Exception e) {
				Log.e(TAG, e.toString());
			}
	    }
    }
    
    private synchronized void stopCameraPreview(Camera camera){
    	if(camera != null){
    		try{
    			camera.stopPreview();
    		}catch (Exception e) {
				Log.e(TAG, e.toString());
			}
    	}
    }
    
	private PreviewCallback previewCallback = new PreviewCallback() {
	  public void onPreviewFrame(byte[] _data, Camera _camera) {
		  if(mStarted){
			  if(NgnProxyVideoProducer.super.mValid && mVideoFrame != null && _data != null){
				  boolean pushFrame = true;
				  if (mCheckFps)
				  {
					  long now = System.currentTimeMillis();
					  pushFrame = (mNextFrameTime == 0 || (now - mNextFrameTime) >= mFrameDuration);
					  mNextFrameTime = now + mFrameDuration;
				  }
				  if (pushFrame)
				  {
					  if (mAsyncPush)
					  {
						  	mLock.lock();
							mVideoFrame.rewind();
							mVideoFrame.put(_data);
						  	mLock.unlock();
						  	synchronized(mConditionPushBuffer){
						  		mConditionPushBuffer.notify();
						  	}
					  }
					  else
					  {
						  mVideoFrame.put(_data);
						  mProducer.push(mVideoFrame, mVideoFrame.capacity());
						  mVideoFrame.rewind();
					  }
				  }
				}
			  if(NgnProxyVideoProducer.sAddCallbackBufferSupported){
				  // do not use "_data" which could be null (e.g. on GSII)
				  NgnCameraProducer.addCallbackBuffer(_camera, _data == null ? mVideoCallbackData : _data);
			  }
		 }
	 }
	};
    

	class MyProxyVideoProducerPreview extends SurfaceView implements SurfaceHolder.Callback {
		private SurfaceHolder mHolder;
		private final NgnProxyVideoProducer myProducer;
		private Camera mCamera;
	
		MyProxyVideoProducerPreview(NgnProxyVideoProducer _producer) {
			super(_producer.mContext);
			
			myProducer = _producer;
			mHolder = getHolder();
			mHolder.addCallback(this);
			mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
		}
		
		public Camera getCamera(){
			return mCamera;
		}
	
		@Override
		public void surfaceCreated(SurfaceHolder holder) {
			Log.d(TAG,"surfaceCreated()");
			try {
				mCamera = NgnCameraProducer.openCamera(myProducer.mFps,
						myProducer.mWidth,
						myProducer.mHeight,
						mHolder,
						myProducer.previewCallback
						);

			} catch (Exception exception) {
				Log.e(TAG, exception.toString());
			}
		}
	
		@Override
		public void surfaceDestroyed(SurfaceHolder holder) {
			Log.d(TAG,"surfaceDestroyed()");
			try{
				NgnCameraProducer.releaseCamera(mCamera);
			}
			catch (Exception exception) {
				Log.e(TAG, exception.toString());
			}
		}
	
		@Override
		public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
			Log.d(TAG,"Surface Changed Callback");
			try{
				if(mCamera != null){
					myProducer.startCameraPreview(mCamera);
				}
			}
			catch (Exception exception) {
				Log.e(TAG, exception.toString());
			}
		}
	}
    

	static class MyProxyVideoProducerCallback extends ProxyVideoProducerCallback
    {
        final NgnProxyVideoProducer myProducer;
        public MyProxyVideoProducerCallback(NgnProxyVideoProducer producer){
        	super();
            myProducer = producer;
        }

        @Override
        public int prepare(int width, int height, int fps){
            return myProducer.prepareCallback(width, height, fps);
        }

        @Override
        public int start(){
            return myProducer.startCallback();
        }

        @Override
        public int pause(){
            return myProducer.pauseCallback();
        }

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