/**
 * This file was auto-generated by the Titanium Module SDK helper for Android
 * Appcelerator Titanium Mobile
 * Copyright (c) 2009-2013 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the Apache Public License
 * Please see the LICENSE included with this distribution for details.
 *
 */
package pw.custom.androidcamera;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

import org.appcelerator.kroll.KrollDict;
import org.appcelerator.kroll.annotations.Kroll;
import org.appcelerator.titanium.proxy.TiViewProxy;
import org.appcelerator.titanium.view.TiUIView;

import android.util.Log;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.AutoFocusCallback;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.PictureCallback;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Environment;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.widget.FrameLayout;


// This proxy can be created by calling CustomAndroidCamera.createExample({message: "hello world"})
@Kroll.proxy(creatableInModule=CustomAndroidCameraModule.class)
public class CameraViewProxy extends TiViewProxy
{
	// Constructor
	public CameraViewProxy()
	{
		super();
	}
	
	// Standard Debugging variables
	private static final String TAG = "CameraViewProxy";
	private static String SAVE = "camera";
	private static Boolean FRONT_CAMERA = false;
	private static int PICTURE_TIMEOUT = 1000;
	private static int RESOLUTION_NAME = CustomAndroidCameraModule.RESOLUTION_LOW;
	
	private double aspectRatio = 1;
	
	private class CameraView extends TiUIView implements SurfaceHolder.Callback
	{	
		private Camera camera;

		public CameraView(TiViewProxy proxy) {
			super(proxy);
			
			SurfaceView preview = new SurfaceView(proxy.getActivity());
			SurfaceHolder previewHolder = preview.getHolder();
			previewHolder.addCallback(this);
			previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
			
			FrameLayout previewLayout = new FrameLayout(proxy.getActivity());
			previewLayout.addView(preview, layoutParams);
			
			setNativeView(previewLayout);
		}
		
		// added by michael browne
		// Return the current camera instance
		public Camera currentCameraInstance(){
			return this.camera;
		}
		
		@Override
		public void processProperties(KrollDict d)
		{	
			super.processProperties(d);
			
			if(d.containsKey("save_location")){
				SAVE = d.getString("save_location");
			}
			
			if( d.containsKey("useFrontCamera") ){
				Log.i(TAG, "Front Camera Property exists!");
				FRONT_CAMERA = d.getBoolean("useFrontCamera");
			}
			
			if( d.containsKey("pictureTimeout")){
				PICTURE_TIMEOUT = d.getInt("pictureTimeout");
			}
			
			if( d.containsKey("resolutionNamed") ){
				RESOLUTION_NAME = d.getInt("resolutionNamed");
			}
		}

		@Override
		public void surfaceChanged(SurfaceHolder previewHolder, int format, int width,
				int height) {
			// TODO Auto-generated method stub
			Log.i(TAG, "Starting Preview");
			camera.startPreview();
		}

		@Override
		public void surfaceCreated(SurfaceHolder previewHolder) {
			// TODO Auto-generated method stub
			Log.i(TAG, "Opening Camera");
			try
			{
				this.camera = getCameraInstance();
				
				if( this.camera == null ){
					Log.e(TAG, "Camera is null. Make sure \n\t<uses-permission android:name=\"android.permission.CAMERA\" />\nis in you tiapp.xml file.");
					throw new Exception();
				}
				
				Log.i(TAG, "Setting Preview Display");
				camera.setPreviewDisplay(previewHolder);
				camera.setDisplayOrientation(90);
			
				Parameters cameraParams = camera.getParameters();
				
				//Camera.Size optimalPictureSize = getPreviewSize(cameraParams, previewHolder.getSurfaceFrame());
				Camera.Size pictureSize = null; // getScreenResolutionPictureSize(cameraParams, previewHolder.getSurfaceFrame());
				
				switch(RESOLUTION_NAME){
				case CustomAndroidCameraModule.RESOLUTION_HIGH:
					Log.i(TAG, "Setting picture resolution to high");
					pictureSize = getHighResolutionPictureSize(cameraParams);
					break;
				case CustomAndroidCameraModule.RESOLUTION_SCREEN:
					Log.i(TAG, "Trying to match screen resolution for picture");
					pictureSize = getScreenResolutionPictureSize(cameraParams, previewHolder.getSurfaceFrame());
					break;
				case CustomAndroidCameraModule.RESOLUTION_480:
					Log.i(TAG, "Trying to match resolution of 720*480 for picture");
					pictureSize = getCustomResolutionPictureSize(cameraParams, 720*480);
					break;
				case CustomAndroidCameraModule.RESOLUTION_720:
					Log.i(TAG, "Trying to match resolution of 1280*720 for picture");
					pictureSize = getCustomResolutionPictureSize(cameraParams, 1280*720);
					break;
				case CustomAndroidCameraModule.RESOLUTION_1080:
					Log.i(TAG, "Trying to match resolution of 1920*1080 for picture");
					pictureSize = getCustomResolutionPictureSize(cameraParams, 1920*1080);
					break;
				case CustomAndroidCameraModule.RESOLUTION_LOW:
				default:
					Log.i(TAG, "Setting picture resolution to low (default)");
					pictureSize = getLowResolutionPictureSize(cameraParams);
					break;
				}
				
				Log.i(TAG, "pictureSize width:"+pictureSize.width+" height:"+pictureSize.height);
				cameraParams.setPictureSize(pictureSize.width, pictureSize.height);
				
				Camera.Size optimalPreviewSize = getMatchingResolutionPreviewSize(cameraParams, pictureSize);
				Log.i(TAG, "optimalPreviewSize width:"+optimalPreviewSize.width+" height:"+optimalPreviewSize.height);
				cameraParams.setPreviewSize(optimalPreviewSize.width, optimalPreviewSize.height);
				
				if( isAutoFocusSupported() ) {
					Log.i(TAG, "Auto Focus is Supported");
					cameraParams.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
				}
				
				if( hasFlash() ) {
					Log.i(TAG, "Flash is supported");
					cameraParams.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);
				}
				
				camera.setParameters(cameraParams);
			}
			catch(IOException e)
			{
				e.printStackTrace();
			} 
			catch (Exception e) {
				e.printStackTrace();
			}
		}

		@Override
		public void surfaceDestroyed(SurfaceHolder arg0) {
			// TODO Auto-generated method stub
			camera.release();
			camera=null;
		}
		
		public Camera getCameraInstance()
		{
			Camera c = null;
			try
			{
				if( FRONT_CAMERA && hasFrontCamera() ) {
					Log.i(TAG, "Using Front Camera");
					c = Camera.open( Camera.CameraInfo.CAMERA_FACING_FRONT );
				} else {
					Log.i(TAG, "Using Back Camera");
					c = Camera.open();
				}
			}
			catch( Exception e )
			{
				Log.d(TAG, "Camera not available");
			}
			return c;
		}
	}
	
	// changed by michael browne
	private TiUIView view = null;
	private Activity act = null;
	
	@Override
	public TiUIView createView(Activity activity)
	{
		view = new CameraView(this);
		act = activity;
		view.getLayoutParams().autoFillsHeight = true;
		view.getLayoutParams().autoFillsWidth = true;
		return view;
	}

	// Handle creation options
	@Override
	public void handleCreationDict(KrollDict options)
	{
		super.handleCreationDict(options);
		
		if (options.containsKey("save_location")) {
			SAVE = options.getString("save_location");
		}
	}
	
	// Added by michael browne
	@Kroll.method
	public void setSaveLocation(String location)
	{
		SAVE = location;
	}
	
	// Added by michael browne
	@Kroll.method
	public void snapPicture()
	{
		Log.i(TAG, "Snap");
		Camera cam = ((CameraView) view).currentCameraInstance();
		
		if( isAutoFocusSupported() ) {
			cam.autoFocus(mAutoFocusCallback);
		} else {
			cam.takePicture(null, null, mPicture);
		}
	}
	
	// Added by michael browne
	private void triggerEvent( String path )
	{
		KrollDict imagePath = new KrollDict();
		
		File extFile = new File(path);
		Uri uriPath = Uri.fromFile(extFile);
		imagePath.put("path", uriPath.toString());
		
		Log.i(TAG, "Sending path back to Titanium. Image Path > "+uriPath.toString());
		
		fireEvent("picture_taken", imagePath);
	}
	
	//Added by michael browne
	private void rotatePicture( String path ){
		// Try and get the images metadata
		try {
			ExifInterface ei = new ExifInterface(path);
			
			// Get the orientation from the meta data
			int picture_orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
			int device_orientation = act.getWindowManager().getDefaultDisplay().getOrientation();
			
			// Both give the same reading so wondering if the camera is rotated at all??
			Log.i(TAG, "Picture Orientation is "+picture_orientation);
			Log.i(TAG, "Device Orientation is "+device_orientation);
			
			doRotation(path, FRONT_CAMERA ? 270 : 90); // Just rotate 90 degrees.... may cause problems on some devices
			
			// Do the rotation depending on the orientation
			/*switch(picture_orientation){
				case ExifInterface.ORIENTATION_ROTATE_90:
					doRotation(path, 90);
					break;
				case ExifInterface.ORIENTATION_ROTATE_180:
					doRotation(path, 180);
					break;
			}*/
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}	
	}
	
	// Added by michael browne
	private void doRotation( String path, float rotate ){
		// Get a Bitmap representation of the image
		BitmapFactory bFactory = new BitmapFactory();
		Bitmap bmap = bFactory.decodeFile(path);
		
		// Create the matrix for rotating the bitmap
		Matrix matrix = new Matrix();
		matrix.setRotate(rotate, bmap.getWidth()/2, bmap.getHeight()/2);
		
		// Create a new version of the bitmap - but rotated
		Bitmap rotated = Bitmap.createBitmap(bmap, 0, 0, bmap.getWidth(), bmap.getHeight(), matrix, true);
		
		// Save the new bitmap to a byte array
		File rotatedFile = new File(path);
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		rotated.compress(CompressFormat.JPEG, 80, bos); // Best quality over 80
		byte[] bitmapData = bos.toByteArray();
		
		// Try to write (overwrite) the file
		try{
			FileOutputStream fos = new FileOutputStream(rotatedFile);
			fos.write(bitmapData);
			fos.close();
		} catch (FileNotFoundException e){
			Log.i(TAG, "File Not Found: "+e);
		} catch (IOException e){
			Log.i(TAG, "IO Error: "+e);
		}
		
	}
	
	private AutoFocusCallback mAutoFocusCallback = new AutoFocusCallback()
	{

		@Override
		public void onAutoFocus(boolean arg0, Camera camera) {
			// TODO Auto-generated method stub
			Log.i(TAG, "On Auto Focus");
			camera.takePicture(null, null, mPicture);
		}
		
	};
	
	// Added by michael browne
	private PictureCallback mPicture = new PictureCallback()
	{

		@Override
		public void onPictureTaken(byte[] data, Camera c) {
			// TODO Auto-generated method stub
			Log.i(TAG, "On Picture Taken");
			File pictureFile = getOutputMediaFile(); //1 corresponds to MEDIA_TYPE_IMAGE
			
			if( pictureFile == null ) return;
			
			try{
				FileOutputStream fos = new FileOutputStream(pictureFile);
				fos.write(data);
				fos.close();
				
				// Rotate Picture
				rotatePicture(pictureFile.getPath());
				
				// Trigger 
				triggerEvent(pictureFile.getPath());
				
				// Restart Preview
				if (PICTURE_TIMEOUT >= 0) {
					final Camera cam = c;
					new android.os.Handler().postDelayed(
					    new Runnable() {
					        public void run() {
					            Log.i("tag", "This'll run 300 milliseconds later");
					            if (((CameraView) view).currentCameraInstance() != null) {
					            	cam.startPreview();
					            }
					        }
					    }, PICTURE_TIMEOUT);
				}
			} catch (FileNotFoundException e){
				Log.i(TAG, "File Not Found: "+e);
			} catch (IOException e){
				Log.i(TAG, "IO Error: "+e);
			}
		}
		
	};
	
	// Added by michael browne
	private static File getOutputMediaFile(){
		
		File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),SAVE);
		
		if( !mediaStorageDir.exists() ){
			if( !mediaStorageDir.mkdirs()){
				Log.i("CAMERA FILE SYSTEM", "failed to create directory");
				return null;
			}
		}
		
		// Create a media file
		String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
		
		File mediaFile;
		mediaFile = new File(mediaStorageDir.getPath() + File.separator + "IMG_" + timestamp + ".jpg");
		return mediaFile;
	}
	
	/*
	 * Function to get a Low Resolution Picture Size
	 * @param Camera.Parameters Parameters for the camera
	 * @return Camera.Size Best size match
	 */
	private Camera.Size getLowResolutionPictureSize(Camera.Parameters parameters){
		int area = Integer.MAX_VALUE;
		Camera.Size result = null;
		
		for( Camera.Size size : parameters.getSupportedPictureSizes() ){
			int calcArea = size.width * size.height;
			if( calcArea < area ){
				area = calcArea;
				result = size;
			}
		}
		
		return result;
	}
	
	/*
	 * Function to get a High Resolution Picture Size
	 * @param Camera.Parameters Parameters for the camera
	 * @return Camera.Size Best size match
	 */
	private Camera.Size getHighResolutionPictureSize(Camera.Parameters parameters){		
		int area = Integer.MIN_VALUE;
		Camera.Size result = null;
		
		for( Camera.Size size : parameters.getSupportedPictureSizes() ){
			int calcArea = size.width * size.height;
			if( calcArea > area ){
				area = calcArea;
				result = size;
			}
		}
		
		return result;
	}
	
	/*
	 * Function to get a Custom Resolution Picture Size
	 * @param Camera.Parameters Parameters for the camera
	 * @param int ideal area for resolution
	 * @return Camera.Size Best size match
	 */
	private Camera.Size getCustomResolutionPictureSize(Camera.Parameters parameters, int idealArea){
		int diff = Integer.MAX_VALUE;
		Camera.Size result = null;
		
		for( Camera.Size size : parameters.getSupportedPictureSizes() ){
			int area = size.width * size.height;
			if( Math.abs(idealArea - area) < diff ){
				diff = Math.abs(idealArea - area);
				result = size;
			}
		}
		
		return result;
	}
	
	/*
	 * Function to get a Screen Resolution Picture Size
	 * @param Camera.Parameters Parameters for the camera
	 * @return Camera.Size Best size match
	 */
	private Camera.Size getScreenResolutionPictureSize(Camera.Parameters parameters, Rect holderSize){
		int w = holderSize.width();
		int h = holderSize.height();
		
		int idealArea = w * h;
		int AreaDiff = Integer.MAX_VALUE;
		
		double targetRatio = 1;
		if (w > h) {
			targetRatio = (double) w / h;
		} else {
			targetRatio = (double) h / w;
		}
		double minAspectDiff = 0.1;
		
		Camera.Size optimalSize = null;
		
		for( Camera.Size size : getSupportedPictureSizes(parameters)){
			int area = size.width * size.height;
			double ratio = (double) size.width / size.height;
			if (ratio < 1) {
				ratio = (double) size.height / size.width;
			}
			
			if( Math.abs(idealArea - area) < AreaDiff && Math.abs(ratio - targetRatio) < minAspectDiff){
				AreaDiff = Math.abs(idealArea - area);
				
				optimalSize = size;
			}
		}
		
		return optimalSize;
	}
	
	/*
	 * Function to get a same Resolution & ratio from Picture Size
	 * @param Camera.Parameters Parameters for the camera
	 * @return Camera.Size Best size match
	 */
	private Camera.Size getMatchingResolutionPreviewSize(Camera.Parameters parameters, Camera.Size pictureSize){
		int w = pictureSize.width;
		int h = pictureSize.height;
		
		int idealArea = w * h;
		int AreaDiff = Integer.MAX_VALUE;
		
		double targetRatio = 1;
		if (w > h) {
			targetRatio = (double) w / h;
		} else {
			targetRatio = (double) h / w;
		}
		double minAspectDiff = 0.1;
		
		Camera.Size optimalSize = null;
		
		for( Camera.Size size : parameters.getSupportedPreviewSizes()){
			int area = size.width * size.height;
			double ratio = (double) size.width / size.height;
			if (ratio < 1) {
				ratio = (double) size.height / size.width;
			}
			
			if( Math.abs(idealArea - area) < AreaDiff && Math.abs(ratio - targetRatio) < minAspectDiff){
				AreaDiff = Math.abs(idealArea - area);
				
				optimalSize = size;
			}
		}
		
		return optimalSize;
	}
	
		
	/**
	 * take supported picture size & remove different ratio from preview size
	 * 
	 * @return
	 */
	public List<Camera.Size> getSupportedPictureSizes(Camera.Parameters parameters) {
	    List<Camera.Size> pictureSizes = parameters.getSupportedPictureSizes();
	             
	    pictureSizes = checkSupportedPictureSizeAtPreviewSize(pictureSizes, parameters);
	     
	    return pictureSizes;
	}
	 
	private List<Camera.Size> checkSupportedPictureSizeAtPreviewSize(List<Camera.Size> pictureSizes, Camera.Parameters parameters) {
	    List<Camera.Size> previewSizes = parameters.getSupportedPreviewSizes();
	    Camera.Size pictureSize;
	    Camera.Size previewSize;
	    double  pictureRatio = 0;
	    double  previewRatio = 0;
	    final double aspectTolerance = 0.05;
	    boolean isUsablePicture = false;
	     
	    for (int indexOfPicture = pictureSizes.size() - 1; indexOfPicture >= 0; --indexOfPicture) {
	        pictureSize = pictureSizes.get(indexOfPicture);
	        pictureRatio = (double) pictureSize.width / (double) pictureSize.height;
	        isUsablePicture = false;
	         
	        for (int indexOfPreview = previewSizes.size() - 1; indexOfPreview >= 0; --indexOfPreview) {
	            previewSize = previewSizes.get(indexOfPreview);
	             
	            previewRatio = (double) previewSize.width / (double) previewSize.height;
	             
	            if (Math.abs(pictureRatio - previewRatio) < aspectTolerance) {
	                isUsablePicture = true;
	                break;
	            }
	        }
	         
	        if (isUsablePicture == false) {
	            pictureSizes.remove(indexOfPicture);
	            //Logger.d(TAG, "remove picture size : " + pictureSize.width + ", " + pictureSize.height);
	        }
	    }
	    
	    return pictureSizes;
	}
	
	/**
	 * Function to determine if flash support is available
	 * @return Boolean Flash Support
	 */
	private boolean hasFlash(){
		Camera cam = ((CameraView) view).currentCameraInstance();
		Parameters params = cam.getParameters();
	    List<String> flashModes = params.getSupportedFlashModes();
	    if(flashModes == null) {
	        return false;
	    }

	    for(String flashMode : flashModes) {
	        if(Parameters.FLASH_MODE_ON.equals(flashMode)) {
	            return true;
	        }
	    }

	    return false;
	}
	
	/**
	 * Function to determine if a front camera exists
	 * @return Boolean Front Camera Exists
	 */
	
	private boolean hasFrontCamera(){
		int numCameras= Camera.getNumberOfCameras();
		for(int i=0;i<numCameras;i++){
		    Camera.CameraInfo info = new CameraInfo();
		    Camera.getCameraInfo(i, info);
		    if(Camera.CameraInfo.CAMERA_FACING_FRONT == info.facing){
		        return true;
		    }
		}
		return false;
	}
	
	/**
	 * Function to determine if Auto Focus is supported
	 * @return Boolean Auto Focus Supported
	 */
	
	private boolean isAutoFocusSupported() {
		Camera cam = ((CameraView) view).currentCameraInstance();
		List<String> supportedFocusModes = cam.getParameters().getSupportedFocusModes();
		return supportedFocusModes != null && supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO);
	}
}