package com.cordovaplugincamerapreview;

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.PackageManager;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.hardware.Camera;
import android.os.Handler;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Size;
import android.util.SizeF;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.FrameLayout;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PluginResult;
import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;

import java.io.File;
import java.util.List;
import java.util.Arrays;

public class CameraPreview extends CordovaPlugin implements CameraActivity.CameraPreviewListener {

  private static final String TAG = "CameraPreview";

  private static final String COLOR_EFFECT_ACTION = "setColorEffect";
  private static final String SUPPORTED_COLOR_EFFECTS_ACTION = "getSupportedColorEffects";
  private static final String ZOOM_ACTION = "setZoom";
  private static final String GET_ZOOM_ACTION = "getZoom";
  private static final String GET_HFOV_ACTION = "getHorizontalFOV";
  private static final String GET_MAX_ZOOM_ACTION = "getMaxZoom";
  private static final String SUPPORTED_FLASH_MODES_ACTION = "getSupportedFlashModes";
  private static final String GET_FLASH_MODE_ACTION = "getFlashMode";
  private static final String SET_FLASH_MODE_ACTION = "setFlashMode";
  private static final String START_CAMERA_ACTION = "startCamera";
  private static final String STOP_CAMERA_ACTION = "stopCamera";
  private static final String PREVIEW_SIZE_ACTION = "setPreviewSize";
  private static final String SWITCH_CAMERA_ACTION = "switchCamera";
  private static final String TAKE_PICTURE_ACTION = "takePicture";
  private static final String START_RECORD_VIDEO_ACTION = "startRecordVideo";
  private static final String STOP_RECORD_VIDEO_ACTION = "stopRecordVideo";
  private static final String TAKE_SNAPSHOT_ACTION = "takeSnapshot";
  private static final String SHOW_CAMERA_ACTION = "showCamera";
  private static final String HIDE_CAMERA_ACTION = "hideCamera";
  private static final String TAP_TO_FOCUS = "tapToFocus";
  private static final String SUPPORTED_PICTURE_SIZES_ACTION = "getSupportedPictureSizes";
  private static final String SUPPORTED_FOCUS_MODES_ACTION = "getSupportedFocusModes";
  private static final String SUPPORTED_WHITE_BALANCE_MODES_ACTION = "getSupportedWhiteBalanceModes";
  private static final String GET_FOCUS_MODE_ACTION = "getFocusMode";
  private static final String SET_FOCUS_MODE_ACTION = "setFocusMode";
  private static final String GET_EXPOSURE_MODES_ACTION = "getExposureModes";
  private static final String GET_EXPOSURE_MODE_ACTION = "getExposureMode";
  private static final String SET_EXPOSURE_MODE_ACTION = "setExposureMode";
  private static final String GET_EXPOSURE_COMPENSATION_ACTION = "getExposureCompensation";
  private static final String SET_EXPOSURE_COMPENSATION_ACTION = "setExposureCompensation";
  private static final String GET_EXPOSURE_COMPENSATION_RANGE_ACTION = "getExposureCompensationRange";
  private static final String GET_WHITE_BALANCE_MODE_ACTION = "getWhiteBalanceMode";
  private static final String SET_WHITE_BALANCE_MODE_ACTION = "setWhiteBalanceMode";
  private static final String SET_BACK_BUTTON_CALLBACK = "onBackButton";
  private static final String GET_CAMERA_CHARACTERISTICS_ACTION = "getCameraCharacteristics";

  private static final int CAM_REQ_CODE = 0;
  private static final int VID_REQ_CODE = 1;
  private String VIDEO_FILE_PATH = "";
  private static final String VIDEO_FILE_EXTENSION = ".mp4";

  private static final String [] permissions = {
    Manifest.permission.CAMERA
  };

  private static final String [] videoPermissions = {
    Manifest.permission.RECORD_AUDIO,
    Manifest.permission.WRITE_EXTERNAL_STORAGE
  };

  private CameraActivity fragment;
  private CallbackContext takePictureCallbackContext;
  private CallbackContext takeSnapshotCallbackContext;
  private CallbackContext startRecordVideoCallbackContext;
  private CallbackContext stopRecordVideoCallbackContext;
  private CallbackContext setFocusCallbackContext;
  private CallbackContext startCameraCallbackContext;
  private CallbackContext tapBackButtonContext  = null;

  private CallbackContext execCallback;
  private JSONArray execArgs;

  private ViewParent webViewParent;

  private int containerViewId = 20; //<- set to random number to prevent conflict with other plugins
  public CameraPreview(){
    super();
    Log.d(TAG, "Constructing");
  }

  @Override
  public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {

    if (START_CAMERA_ACTION.equals(action)) {
      if (cordova.hasPermission(permissions[0])) {
        return startCamera(args.getInt(0), args.getInt(1), args.getInt(2), args.getInt(3), args.getString(4), args.getBoolean(5), args.getBoolean(6), args.getBoolean(7), args.getString(8), args.getBoolean(9), args.getBoolean(10), args.getBoolean(11), callbackContext);
      } else {
        this.execCallback = callbackContext;
        this.execArgs = args;
        cordova.requestPermissions(this, CAM_REQ_CODE, permissions);
        return true;
      }
    } else if (TAKE_PICTURE_ACTION.equals(action)) {
      return takePicture(args.getInt(0), args.getInt(1), args.getInt(2), callbackContext);
    } else if (TAKE_SNAPSHOT_ACTION.equals(action)) {
      return takeSnapshot(args.getInt(0), callbackContext);
    }else if (START_RECORD_VIDEO_ACTION.equals(action)) {
      if ( cordova.hasPermission(videoPermissions[0]) && cordova.hasPermission(videoPermissions[1])) {
        return startRecordVideo(args.getString(0), args.getInt(1), args.getInt(2), args.getInt(3), args.getBoolean(4), callbackContext);
      } else {
        this.execCallback = callbackContext;
        this.execArgs = args;
        cordova.requestPermissions(this, VID_REQ_CODE, videoPermissions);
        return true;
      }
    } else if (STOP_RECORD_VIDEO_ACTION.equals(action)) {
      return stopRecordVideo(callbackContext);
    } else if (COLOR_EFFECT_ACTION.equals(action)) {
      return setColorEffect(args.getString(0), callbackContext);
    } else if (ZOOM_ACTION.equals(action)) {
      return setZoom(args.getInt(0), callbackContext);
    } else if (GET_ZOOM_ACTION.equals(action)) {
      return getZoom(callbackContext);
    } else if (GET_HFOV_ACTION.equals(action)) {
      return getHorizontalFOV(callbackContext);
    } else if (GET_MAX_ZOOM_ACTION.equals(action)) {
      return getMaxZoom(callbackContext);
    } else if (PREVIEW_SIZE_ACTION.equals(action)) {
      return setPreviewSize(args.getInt(0), args.getInt(1), callbackContext);
    } else if (SUPPORTED_FLASH_MODES_ACTION.equals(action)) {
      return getSupportedFlashModes(callbackContext);
    } else if (GET_FLASH_MODE_ACTION.equals(action)) {
      return getFlashMode(callbackContext);
    } else if (SET_FLASH_MODE_ACTION.equals(action)) {
      return setFlashMode(args.getString(0), callbackContext);
    } else if (STOP_CAMERA_ACTION.equals(action)){
      return stopCamera(callbackContext);
    } else if (SHOW_CAMERA_ACTION.equals(action)) {
      return showCamera(callbackContext);
    } else if (HIDE_CAMERA_ACTION.equals(action)) {
      return hideCamera(callbackContext);
    } else if (TAP_TO_FOCUS.equals(action)) {
      return tapToFocus(args.getInt(0), args.getInt(1), callbackContext);
    } else if (SWITCH_CAMERA_ACTION.equals(action)) {
      return switchCamera(callbackContext);
    } else if (SUPPORTED_PICTURE_SIZES_ACTION.equals(action)) {
      return getSupportedPictureSizes(callbackContext);
    } else if (GET_EXPOSURE_MODES_ACTION.equals(action)) {
      return getExposureModes(callbackContext);
    } else if (SUPPORTED_FOCUS_MODES_ACTION.equals(action)) {
      return getSupportedFocusModes(callbackContext);
    } else if (GET_FOCUS_MODE_ACTION.equals(action)) {
      return getFocusMode(callbackContext);
    } else if (SET_FOCUS_MODE_ACTION.equals(action)) {
      return setFocusMode(args.getString(0), callbackContext);
    } else if (GET_EXPOSURE_MODE_ACTION.equals(action)) {
      return getExposureMode(callbackContext);
    } else if (SET_EXPOSURE_MODE_ACTION.equals(action)) {
      return setExposureMode(args.getString(0), callbackContext);
    } else if (GET_EXPOSURE_COMPENSATION_ACTION.equals(action)) {
      return getExposureCompensation(callbackContext);
    } else if (SET_EXPOSURE_COMPENSATION_ACTION.equals(action)) {
      return setExposureCompensation(args.getInt(0), callbackContext);
    } else if (GET_EXPOSURE_COMPENSATION_RANGE_ACTION.equals(action)) {
      return getExposureCompensationRange(callbackContext);
    } else if (SUPPORTED_WHITE_BALANCE_MODES_ACTION.equals(action)) {
      return getSupportedWhiteBalanceModes(callbackContext);
    } else if (GET_WHITE_BALANCE_MODE_ACTION.equals(action)) {
      return getWhiteBalanceMode(callbackContext);
    } else if (SET_WHITE_BALANCE_MODE_ACTION.equals(action)) {
      return setWhiteBalanceMode(args.getString(0),callbackContext);
    } else if (SET_BACK_BUTTON_CALLBACK.equals(action)) {
      return setBackButtonListener(callbackContext);
    } else if (SUPPORTED_COLOR_EFFECTS_ACTION.equals(action)) {
      return getSupportedColorEffects(callbackContext);
    } else if (GET_CAMERA_CHARACTERISTICS_ACTION.equals(action)) {
      return getCameraCharacteristics(callbackContext);
    }

    return false;
  }

  @Override
  public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException {
    for(int r:grantResults){
      if(r == PackageManager.PERMISSION_DENIED){
        execCallback.sendPluginResult(new PluginResult(PluginResult.Status.ILLEGAL_ACCESS_EXCEPTION));
        return;
      }
    }

    if(requestCode == CAM_REQ_CODE){
      startCamera(this.execArgs.getInt(0), this.execArgs.getInt(1), this.execArgs.getInt(2), this.execArgs.getInt(3), this.execArgs.getString(4), this.execArgs.getBoolean(5), this.execArgs.getBoolean(6), this.execArgs.getBoolean(7), this.execArgs.getString(8), this.execArgs.getBoolean(9), this.execArgs.getBoolean(10), this.execArgs.getBoolean(11), this.execCallback);
    }else if(requestCode == VID_REQ_CODE){
      startRecordVideo(this.execArgs.getString(0), this.execArgs.getInt(1), this.execArgs.getInt(2), this.execArgs.getInt(3), this.execArgs.getBoolean(4),  this.execCallback);
    }
  }

  private boolean hasView(CallbackContext callbackContext) {
    if(fragment == null) {
      callbackContext.error("No preview");
      return false;
    }

    return true;
  }

  private boolean hasCamera(CallbackContext callbackContext) {
    if(this.hasView(callbackContext) == false){
      return false;
    }

    if(fragment.getCamera() == null) {
      callbackContext.error("No Camera");
      return false;
    }

    return true;
  }

  private boolean getSupportedPictureSizes(CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    List<Camera.Size> supportedSizes;
    Camera camera = fragment.getCamera();
    supportedSizes = camera.getParameters().getSupportedPictureSizes();
    if (supportedSizes != null) {
      JSONArray sizes = new JSONArray();
      for (int i=0; i<supportedSizes.size(); i++) {
        Camera.Size size = supportedSizes.get(i);
        int h = size.height;
        int w = size.width;
        JSONObject jsonSize = new JSONObject();
        try {
          jsonSize.put("height", new Integer(h));
          jsonSize.put("width", new Integer(w));
        }
        catch(JSONException e){
          e.printStackTrace();
        }
        sizes.put(jsonSize);
      }
      callbackContext.success(sizes);
      return true;
    }
    callbackContext.error("Camera Parameters access error");
    return true;
  }

  private boolean startCamera(int x, int y, int width, int height, String defaultCamera, Boolean tapToTakePicture, Boolean dragEnabled, final Boolean toBack, String alpha, boolean tapFocus, boolean disableExifHeaderStripping, boolean storeToFile, CallbackContext callbackContext) {
    Log.d(TAG, "start camera action");

    if (fragment != null) {
      callbackContext.error("Camera already started");
      return true;
    }

    final float opacity = Float.parseFloat(alpha);

    fragment = new CameraActivity();
    fragment.setEventListener(this);
    fragment.defaultCamera = defaultCamera;
    fragment.tapToTakePicture = tapToTakePicture;
    fragment.dragEnabled = dragEnabled;
    fragment.tapToFocus = tapFocus;
    fragment.disableExifHeaderStripping = disableExifHeaderStripping;
    fragment.storeToFile = storeToFile;
    fragment.toBack = toBack;

    DisplayMetrics metrics = cordova.getActivity().getResources().getDisplayMetrics();

    // offset
    int computedX = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, x, metrics);
    int computedY = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, y, metrics);

    // size
    int computedWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, width, metrics);
    int computedHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, height, metrics);

    fragment.setRect(computedX, computedY, computedWidth, computedHeight);

    startCameraCallbackContext = callbackContext;

    cordova.getActivity().runOnUiThread(new Runnable() {
      @Override
      public void run() {

        //create or update the layout params for the container view
        FrameLayout containerView = (FrameLayout)cordova.getActivity().findViewById(containerViewId);
        if(containerView == null){
          containerView = new FrameLayout(cordova.getActivity().getApplicationContext());
          containerView.setId(containerViewId);

          FrameLayout.LayoutParams containerLayoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
          cordova.getActivity().addContentView(containerView, containerLayoutParams);
        }

        //display camera below the webview
        if(toBack){
          View view = webView.getView();
          ViewParent rootParent = containerView.getParent();
          ViewParent curParent = view.getParent();

          view.setBackgroundColor(0x00000000);

          // If parents do not match look for.
          if(curParent.getParent() != rootParent) {
            while(curParent != null && curParent.getParent() != rootParent) {
              curParent = curParent.getParent();
            }

            if(curParent != null) {
              ((ViewGroup)curParent).setBackgroundColor(0x00000000);
              ((ViewGroup)curParent).bringToFront();
            } else {
              // Do default...
              curParent = view.getParent();
              webViewParent = curParent;
              ((ViewGroup)view).bringToFront();
            }
          }else{
            // Default
            webViewParent = curParent;
            rootParent.bringChildToFront(((View) webViewParent));
          }

        }else{
          //set camera back to front
          containerView.setAlpha(opacity);
          containerView.bringToFront();
        }

        //add the fragment to the container
        FragmentManager fragmentManager = cordova.getActivity().getFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.add(containerView.getId(), fragment);
        fragmentTransaction.commit();
      }
    });

    return true;
  }

  public void onCameraStarted() {
    Log.d(TAG, "Camera started");

    PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, "Camera started");
    pluginResult.setKeepCallback(false);
    startCameraCallbackContext.sendPluginResult(pluginResult);
  }

  private boolean takeSnapshot(int quality, CallbackContext callbackContext) {
    if(this.hasView(callbackContext) == false){
      return true;
    }

    takeSnapshotCallbackContext = callbackContext;

    fragment.takeSnapshot(quality);
    return true;
  }

  public void onSnapshotTaken(String originalPicture) {
    Log.d(TAG, "returning snapshot");

    JSONArray data = new JSONArray();
    data.put(originalPicture);

    PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, data);
    pluginResult.setKeepCallback(true);
    takeSnapshotCallbackContext.sendPluginResult(pluginResult);
    takeSnapshotCallbackContext = null;
  }

  public void onSnapshotTakenError(String message) {
    Log.d(TAG, "CameraPreview onSnapshotTakenError");
    takeSnapshotCallbackContext.error(message);
    takeSnapshotCallbackContext = null;
  }

  private boolean takePicture(int width, int height, int quality, CallbackContext callbackContext) {
    if(this.hasView(callbackContext) == false){
      return true;
    }
    takePictureCallbackContext = callbackContext;

    fragment.takePicture(width, height, quality);

    return true;
  }

  public void onPictureTaken(String originalPicture) {
    Log.d(TAG, "returning picture");

    JSONArray data = new JSONArray();
    data.put(originalPicture);

    PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, data);
    pluginResult.setKeepCallback(fragment.tapToTakePicture);
    takePictureCallbackContext.sendPluginResult(pluginResult);
  }

  public void onPictureTakenError(String message) {
    Log.d(TAG, "CameraPreview onPictureTakenError");
    takePictureCallbackContext.error(message);
  }

  private boolean startRecordVideo(final String camera, final int width, final int height, final int quality, final boolean withFlash, CallbackContext callbackContext) {
    if(this.hasView(callbackContext) == false){
      return true;
    }
    final String filename = "videoTmp";
    VIDEO_FILE_PATH = cordova.getActivity().getCacheDir().toString() + "/";
    startRecordVideoCallbackContext = callbackContext;
     cordova.getThreadPool().execute(new Runnable() {
      @Override
      public void run() {
        fragment.startRecord(getFilePath(filename), camera, width, height, quality, withFlash);
      }
    });

    return true;
  }

  public void onStartRecordVideo() {
    Log.d(TAG, "onStartRecordVideo started");

    PluginResult pluginResult = new PluginResult(PluginResult.Status.OK);
    pluginResult.setKeepCallback(true);

    startRecordVideoCallbackContext.sendPluginResult(pluginResult);
  }

  public void onStartRecordVideoError(String message) {
    Log.d(TAG, "CameraPreview onStartRecordVideo");

    startRecordVideoCallbackContext.error(message);
  }

  private boolean stopRecordVideo(CallbackContext callbackContext) {
    if(this.hasView(callbackContext) == false){
      return true;
    }

    stopRecordVideoCallbackContext = callbackContext;

    cordova.getThreadPool().execute(new Runnable() {
      @Override
      public void run() {
        fragment.stopRecord();
      }
    });

    return true;
  }

  public void onStopRecordVideo(String file) {
    Log.d(TAG, "onStopRecordVideo success");

    PluginResult result = new PluginResult(PluginResult.Status.OK, file);
    result.setKeepCallback(true);

    stopRecordVideoCallbackContext.sendPluginResult(result);
  }

  public void onStopRecordVideoError(String err) {
    Log.d(TAG, "onStopRecordVideo error");

    stopRecordVideoCallbackContext.error(err);
  }

  private String getFilePath(String filename) {
    String fileName = filename;

    int i = 1;

    while (new File(VIDEO_FILE_PATH + fileName + VIDEO_FILE_EXTENSION).exists()) {
      // Add number suffix if file exists
      fileName = filename + '_' + i;
      i++;
    }

    return VIDEO_FILE_PATH + fileName + VIDEO_FILE_EXTENSION;
  }

  private boolean setColorEffect(String effect, CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    List<String> supportedColors;
    supportedColors = params.getSupportedColorEffects();

    if(supportedColors.contains(effect)){
      params.setColorEffect(effect);
      fragment.setCameraParameters(params);
      callbackContext.success(effect);
    }else{
      callbackContext.error("Color effect not supported" + effect);
      return true;
    }
    return true;
  }

  private boolean getSupportedColorEffects(CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();
    List<String> supportedColors;
    supportedColors = params.getSupportedColorEffects();
    JSONArray jsonColorEffects = new JSONArray();

    if (supportedColors != null) {
      for (int i=0; i<supportedColors.size(); i++) {
          jsonColorEffects.put(new String(supportedColors.get(i)));
      }
    }

    callbackContext.success(jsonColorEffects);

    return true;
  }

  private boolean getExposureModes(CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    if (camera.getParameters().isAutoExposureLockSupported()) {
      JSONArray jsonExposureModes = new JSONArray();
      jsonExposureModes.put(new String("lock"));
      jsonExposureModes.put(new String("continuous"));
      callbackContext.success(jsonExposureModes);
    } else {
      callbackContext.error("Exposure modes not supported");
    }

    return true;
  }

  private boolean getExposureMode(CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    String exposureMode;

    if (camera.getParameters().isAutoExposureLockSupported()) {
      if (camera.getParameters().getAutoExposureLock()) {
        exposureMode = "lock";
      } else {
        exposureMode = "continuous";
      };
      callbackContext.success(exposureMode);
    } else {
      callbackContext.error("Exposure mode not supported");
    }

    return true;
  }

  private boolean setExposureMode(String exposureMode, CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    if (camera.getParameters().isAutoExposureLockSupported()) {
      params.setAutoExposureLock("lock".equals(exposureMode));
      fragment.setCameraParameters(params);
      callbackContext.success();
    } else {
      callbackContext.error("Exposure mode not supported");
    }

    return true;
  }

  private boolean getExposureCompensation(CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    if (camera.getParameters().getMinExposureCompensation() == 0 && camera.getParameters().getMaxExposureCompensation() == 0) {
      callbackContext.error("Exposure corection not supported");
    } else {
      int exposureCompensation = camera.getParameters().getExposureCompensation();
      callbackContext.success(exposureCompensation);
    }

    return true;
  }

  private boolean setExposureCompensation(int exposureCompensation, CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    int minExposureCompensation = camera.getParameters().getMinExposureCompensation();
    int maxExposureCompensation = camera.getParameters().getMaxExposureCompensation();

    if ( minExposureCompensation == 0 && maxExposureCompensation == 0) {
      callbackContext.error("Exposure corection not supported");
    } else {
      if (exposureCompensation < minExposureCompensation) {
        exposureCompensation = minExposureCompensation;
      } else if (exposureCompensation > maxExposureCompensation) {
        exposureCompensation = maxExposureCompensation;
      }
      params.setExposureCompensation(exposureCompensation);
      fragment.setCameraParameters(params);

      callbackContext.success(exposureCompensation);
    }

    return true;
  }

  private boolean getExposureCompensationRange(CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    int minExposureCompensation = camera.getParameters().getMinExposureCompensation();
    int maxExposureCompensation = camera.getParameters().getMaxExposureCompensation();

    if (minExposureCompensation == 0 && maxExposureCompensation == 0) {
      callbackContext.error("Exposure corection not supported");
    } else {
      JSONObject jsonExposureRange = new JSONObject();
      try {
        jsonExposureRange.put("min", new Integer(minExposureCompensation));
        jsonExposureRange.put("max", new Integer(maxExposureCompensation));
      }
      catch(JSONException e){
        e.printStackTrace();
      }
      callbackContext.success(jsonExposureRange);
    }

    return true;
  }

  private boolean getSupportedWhiteBalanceModes(CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    List<String> supportedWhiteBalanceModes;
    supportedWhiteBalanceModes = params.getSupportedWhiteBalance();

    JSONArray jsonWhiteBalanceModes = new JSONArray();
    if (camera.getParameters().isAutoWhiteBalanceLockSupported()) {
      jsonWhiteBalanceModes.put(new String("lock"));
    }
    if (supportedWhiteBalanceModes != null) {
      for (int i=0; i<supportedWhiteBalanceModes.size(); i++) {
        jsonWhiteBalanceModes.put(new String(supportedWhiteBalanceModes.get(i)));
      }
    }

    callbackContext.success(jsonWhiteBalanceModes);
    return true;
  }

  private boolean getWhiteBalanceMode(CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    String whiteBalanceMode;

    if (camera.getParameters().isAutoWhiteBalanceLockSupported()) {
      if (camera.getParameters().getAutoWhiteBalanceLock()) {
        whiteBalanceMode = "lock";
      } else {
        whiteBalanceMode = camera.getParameters().getWhiteBalance();
      };
    } else {
      whiteBalanceMode = camera.getParameters().getWhiteBalance();
    }
    if (whiteBalanceMode != null) {
      callbackContext.success(whiteBalanceMode);
    } else {
      callbackContext.error("White balance mode not supported");
    }

    return true;
  }

  private boolean setWhiteBalanceMode(String whiteBalanceMode, CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    if (whiteBalanceMode.equals("lock")) {
      if (camera.getParameters().isAutoWhiteBalanceLockSupported()) {
        params.setAutoWhiteBalanceLock(true);
        fragment.setCameraParameters(params);
        callbackContext.success();
      } else {
        callbackContext.error("White balance lock not supported");
      }
    } else if (whiteBalanceMode.equals("auto") ||
               whiteBalanceMode.equals("incandescent") ||
               whiteBalanceMode.equals("cloudy-daylight") ||
               whiteBalanceMode.equals("daylight") ||
               whiteBalanceMode.equals("fluorescent") ||
               whiteBalanceMode.equals("shade") ||
               whiteBalanceMode.equals("twilight") ||
               whiteBalanceMode.equals("warm-fluorescent")) {
      params.setWhiteBalance(whiteBalanceMode);
      fragment.setCameraParameters(params);
      callbackContext.success();
    } else {
      callbackContext.error("White balance parameter not supported");
    }

    return true;
  }

  private boolean getMaxZoom(CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    if (camera.getParameters().isZoomSupported()) {
      int maxZoom = camera.getParameters().getMaxZoom();
      callbackContext.success(maxZoom);
    } else {
      callbackContext.error("Zoom not supported");
    }

    return true;
  }

 private boolean getHorizontalFOV(CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    float horizontalViewAngle = params.getHorizontalViewAngle();

    callbackContext.success(String.valueOf(horizontalViewAngle));
    return true;
  }


  private boolean getZoom(CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    if (camera.getParameters().isZoomSupported()) {
      int getZoom = camera.getParameters().getZoom();
      callbackContext.success(getZoom);
    } else {
      callbackContext.error("Zoom not supported");
    }

    return true;
  }

  private boolean setZoom(int zoom, CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    if (camera.getParameters().isZoomSupported()) {
      params.setZoom(zoom);
      fragment.setCameraParameters(params);

      callbackContext.success(zoom);
    } else {
      callbackContext.error("Zoom not supported");
    }

    return true;
  }

  private boolean setPreviewSize(int width, int height, CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    params.setPreviewSize(width, height);
    fragment.setCameraParameters(params);
    camera.startPreview();

    callbackContext.success();
    return true;
  }

  private boolean getSupportedFlashModes(CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();
    List<String> supportedFlashModes;
    supportedFlashModes = params.getSupportedFlashModes();
    JSONArray jsonFlashModes = new JSONArray();

    if (supportedFlashModes != null) {
      for (int i=0; i<supportedFlashModes.size(); i++) {
          jsonFlashModes.put(new String(supportedFlashModes.get(i)));
      }
    }

    callbackContext.success(jsonFlashModes);
    return true;
  }

  private boolean getSupportedFocusModes(CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();
    List<String> supportedFocusModes;
    supportedFocusModes = params.getSupportedFocusModes();

    if (supportedFocusModes != null) {
      JSONArray jsonFocusModes = new JSONArray();
      for (int i=0; i<supportedFocusModes.size(); i++) {
          jsonFocusModes.put(new String(supportedFocusModes.get(i)));
      }

      callbackContext.success(jsonFocusModes);
      return true;
    }

    callbackContext.error("Camera focus modes parameters access error");
    return true;
  }

  private boolean getFocusMode(CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    List<String> supportedFocusModes;
    supportedFocusModes = params.getSupportedFocusModes();

    if (supportedFocusModes != null) {
      String focusMode = params.getFocusMode();
      callbackContext.success(focusMode);
    } else {
      callbackContext.error("FocusMode not supported");
    }

    return true;
  }

  private boolean setFocusMode(String focusMode, CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    List<String> supportedFocusModes;
    List<String> supportedAutoFocusModes = Arrays.asList("auto", "continuous-picture", "continuous-video","macro");
    supportedFocusModes = params.getSupportedFocusModes();
    if (supportedFocusModes.indexOf(focusMode) > -1) {
      params.setFocusMode(focusMode);
      fragment.setCameraParameters(params);
      callbackContext.success(focusMode);
      return true;
    } else {
      callbackContext.error("Focus mode not recognised: " + focusMode);
      return true;
    }
  }

  private boolean getFlashMode(CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    String flashMode = params.getFlashMode();

    if (flashMode != null ) {
      callbackContext.success(flashMode);
    } else {
      callbackContext.error("FlashMode not supported");
    }

    return true;
  }

  private boolean setFlashMode(String flashMode, CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    Camera camera = fragment.getCamera();
    Camera.Parameters params = camera.getParameters();

    List<String> supportedFlashModes;
    supportedFlashModes = camera.getParameters().getSupportedFlashModes();
    if (supportedFlashModes.indexOf(flashMode) > -1) {
      params.setFlashMode(flashMode);
    } else {
      callbackContext.error("Flash mode not recognised: " + flashMode);
      return true;
    }

    fragment.setCameraParameters(params);

    callbackContext.success(flashMode);
    return true;
  }

  private boolean stopCamera(CallbackContext callbackContext) {
    if(webViewParent != null) {
      cordova.getActivity().runOnUiThread(new Runnable() {
        @Override
        public void run() {
          ((ViewGroup)webView.getView()).bringToFront();
          webViewParent = null;
        }
      });
    }

    if(this.hasView(callbackContext) == false){
      return true;
    }

    FragmentManager fragmentManager = cordova.getActivity().getFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
    fragmentTransaction.remove(fragment);
    fragmentTransaction.commit();
    fragment = null;

    callbackContext.success();
    return true;
  }

  private boolean showCamera(CallbackContext callbackContext) {
    if(this.hasView(callbackContext) == false){
      return true;
    }

    FragmentManager fragmentManager = cordova.getActivity().getFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
    fragmentTransaction.show(fragment);
    fragmentTransaction.commit();

    callbackContext.success();
    return true;
  }

  private boolean hideCamera(CallbackContext callbackContext) {
    if(this.hasView(callbackContext) == false){
      return true;
    }

    FragmentManager fragmentManager = cordova.getActivity().getFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
    fragmentTransaction.hide(fragment);
    fragmentTransaction.commit();

    callbackContext.success();
    return true;
  }

  private boolean tapToFocus(final int pointX, final int pointY, CallbackContext callbackContext) {
    if(this.hasView(callbackContext) == false){
      return true;
    }

    setFocusCallbackContext = callbackContext;

    fragment.setFocusArea(pointX, pointY, new Camera.AutoFocusCallback() {
      public void onAutoFocus(boolean success, Camera camera) {
        if (success) {
          onFocusSet(pointX, pointY);
        } else {
          onFocusSetError("fragment.setFocusArea() failed");
        }
      }
    });

    return true;
  }

  public void onFocusSet(final int pointX, final int pointY) {
    Log.d(TAG, "Focus set, returning coordinates");

    JSONObject data = new JSONObject();
    try {
      data.put("x", pointX);
      data.put("y", pointY);
    } catch (JSONException e) {
      Log.d(TAG, "onFocusSet failed to set output payload");
    }

    PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, data);
    pluginResult.setKeepCallback(true);
    setFocusCallbackContext.sendPluginResult(pluginResult);
  }

  public void onFocusSetError(String message) {
    Log.d(TAG, "CameraPreview onFocusSetError");
    setFocusCallbackContext.error(message);
  }

  private boolean switchCamera(CallbackContext callbackContext) {
    if(this.hasView(callbackContext) == false){
      return true;
    }

    fragment.switchCamera();

    callbackContext.success();

    return true;
  }

  public boolean setBackButtonListener(CallbackContext callbackContext) {
    tapBackButtonContext = callbackContext;
    return true;
  }

  public void onBackButton() {
    if(tapBackButtonContext == null) {
      return;
    }

    Log.d(TAG, "Back button tapped, notifying");

    PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, "Back button pressed");
    tapBackButtonContext.sendPluginResult(pluginResult);
  }

  private boolean getCameraCharacteristics(CallbackContext callbackContext) {
    if(this.hasCamera(callbackContext) == false){
      return true;
    }

    JSONObject data = new JSONObject();
    JSONArray cameraCharacteristicsArray = new JSONArray();

    // Get the CameraManager
    CameraManager cManager = (CameraManager) this.cordova.getActivity().getApplicationContext().getSystemService(Context.CAMERA_SERVICE);

    try {
      for (String cameraId : cManager.getCameraIdList()) {
        CameraCharacteristics characteristics = cManager.getCameraCharacteristics(cameraId);

	JSONObject cameraData = new JSONObject();

	// INFO_SUPPORTED_HARDWARE_LEVEL
	Integer supportLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
	cameraData.put("INFO_SUPPORTED_HARDWARE_LEVEL", supportLevel);

	// LENS_FACING
	Integer lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
	cameraData.put("LENS_FACING", lensFacing);

	// SENSOR_INFO_PHYSICAL_SIZE
	SizeF sensorInfoPhysicalSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE);
	cameraData.put("SENSOR_INFO_PHYSICAL_SIZE_WIDTH", new Double(sensorInfoPhysicalSize.getWidth()));
	cameraData.put("SENSOR_INFO_PHYSICAL_SIZE_HEIGHT", new Double(sensorInfoPhysicalSize.getHeight()));

	// SENSOR_INFO_PIXEL_ARRAY_SIZE
	Size sensorInfoPixelSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE);
	cameraData.put("SENSOR_INFO_PIXEL_ARRAY_SIZE_WIDTH", new Integer(sensorInfoPixelSize.getWidth()));
	cameraData.put("SENSOR_INFO_PIXEL_ARRAY_SIZE_HEIGHT", new Integer(sensorInfoPixelSize.getHeight()));

	// LENS_INFO_AVAILABLE_FOCAL_LENGTHS
	float[] focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
	JSONArray focalLengthsArray = new JSONArray();
	for (int focusId=0; focusId<focalLengths.length; focusId++) {
	  JSONObject focalLengthsData = new JSONObject();
	  focalLengthsData.put("FOCAL_LENGTH", new Double(focalLengths[focusId]));
	  focalLengthsArray.put(focalLengthsData);
	}
	cameraData.put("LENS_INFO_AVAILABLE_FOCAL_LENGTHS", focalLengthsArray);

	// add camera data to result list
	cameraCharacteristicsArray.put(cameraData);
      }

      data.put("CAMERA_CHARACTERISTICS", cameraCharacteristicsArray);

    } catch (CameraAccessException e) {
      Log.e(TAG, e.getMessage(), e);
    } catch (JSONException e) {
      Log.d(TAG, "getCameraSensorInfo failed to set output payload");
    }

    callbackContext.success(data);
    return true;
  }

}
