/**
 * Created by Fabrice Armisen (farmisen@gmail.com) on 1/4/16.
 */

package com.lwansbrough.RCTCamera;

import android.graphics.drawable.GradientDrawable;
import android.hardware.Camera;
import android.media.CamcorderProfile;
import android.util.Log;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.lang.Math;

public class RCTCamera {
    private static RCTCamera ourInstance;
    private final HashMap<Integer, CameraInfoWrapper> _cameraInfos;
    private final HashMap<Integer, Integer> _cameraTypeToIndex;
    private final Map<Number, Camera> _cameras;
    private static final Resolution RESOLUTION_480P = new Resolution(853, 480); // 480p shoots for a 16:9 HD aspect ratio, but can otherwise fall back/down to any other supported camera sizes, such as 800x480 or 720x480, if (any) present. See getSupportedPictureSizes/getSupportedVideoSizes below.
    private static final Resolution RESOLUTION_720P = new Resolution(1280, 720);
    private static final Resolution RESOLUTION_1080P = new Resolution(1920, 1080);
    private boolean _barcodeScannerEnabled = false;
    private List<String> _barCodeTypes = null;
    private int _orientation = -1;
    private int _actualDeviceOrientation = 0;
    private int _adjustedDeviceOrientation = 0;

    public static RCTCamera getInstance() {
        return ourInstance;
    }
    public static void createInstance(int deviceOrientation) {
        ourInstance = new RCTCamera(deviceOrientation);
    }


    public synchronized Camera acquireCameraInstance(int type) {
        if (null == _cameras.get(type) && null != _cameraTypeToIndex.get(type)) {
            try {
                Camera camera = Camera.open(_cameraTypeToIndex.get(type));
                _cameras.put(type, camera);
                adjustPreviewLayout(type);
            } catch (Exception e) {
                Log.e("RCTCamera", "acquireCameraInstance failed", e);
            }
        }
        return _cameras.get(type);
    }

    public void releaseCameraInstance(int type) {
        // Release seems async and creates race conditions. Remove from map first before releasing.
        Camera releasingCamera = _cameras.get(type);
        if (null != releasingCamera) {
            _cameras.remove(type);
            releasingCamera.release();
        }
    }

    public int getPreviewWidth(int type) {
        CameraInfoWrapper cameraInfo = _cameraInfos.get(type);
        if (null == cameraInfo) {
            return 0;
        }
        return cameraInfo.previewWidth;
    }

    public int getPreviewHeight(int type) {
        CameraInfoWrapper cameraInfo = _cameraInfos.get(type);
        if (null == cameraInfo) {
            return 0;
        }
        return cameraInfo.previewHeight;
    }

    public int getPreviewVisibleHeight(int type) {
        CameraInfoWrapper cameraInfo = _cameraInfos.get(type);
        if (null == cameraInfo) {
            return 0;
        }
        return cameraInfo.previewVisibleHeight;
    }

    public int getPreviewVisibleWidth(int type) {
        CameraInfoWrapper cameraInfo = _cameraInfos.get(type);
        if (null == cameraInfo) {
            return 0;
        }
        return cameraInfo.previewVisibleWidth;
    }

    public Camera.Size getBestSize(List<Camera.Size> supportedSizes, int maxWidth, int maxHeight) {
        Camera.Size bestSize = null;
        for (Camera.Size size : supportedSizes) {
            if (size.width > maxWidth || size.height > maxHeight) {
                continue;
            }

            if (bestSize == null) {
                bestSize = size;
                continue;
            }

            int resultArea = bestSize.width * bestSize.height;
            int newArea = size.width * size.height;

            if (newArea > resultArea) {
                bestSize = size;
            }
        }

        return bestSize;
    }

    private Camera.Size getSmallestSize(List<Camera.Size> supportedSizes) {
        Camera.Size smallestSize = null;
        for (Camera.Size size : supportedSizes) {
            if (smallestSize == null) {
                smallestSize = size;
                continue;
            }

            int resultArea = smallestSize.width * smallestSize.height;
            int newArea = size.width * size.height;

            if (newArea < resultArea) {
                smallestSize = size;
            }
        }

        return smallestSize;
    }

    private Camera.Size getClosestSize(List<Camera.Size> supportedSizes, int matchWidth, int matchHeight) {
      Camera.Size closestSize = null;
      for (Camera.Size size : supportedSizes) {
          if (closestSize == null) {
              closestSize = size;
              continue;
          }

          double currentDelta = Math.sqrt(Math.pow(closestSize.width - matchWidth,2) + Math.pow(closestSize.height - matchHeight,2));
          double newDelta = Math.sqrt(Math.pow(size.width - matchWidth,2) + Math.pow(size.height - matchHeight,2));

          if (newDelta < currentDelta) {
              closestSize = size;
          }
      }
      return closestSize;
    }

    protected List<Camera.Size> getSupportedVideoSizes(Camera camera) {
        Camera.Parameters params = camera.getParameters();
        // defer to preview instead of params.getSupportedVideoSizes() http://bit.ly/1rxOsq0
        // but prefer SupportedVideoSizes!
        List<Camera.Size> sizes = params.getSupportedVideoSizes();
        if (sizes != null) {
            return sizes;
        }

        // Video sizes may be null, which indicates that all the supported
        // preview sizes are supported for video recording.
        return params.getSupportedPreviewSizes();
    }

    public int getOrientation() {
        return _orientation;
    }

    public void setOrientation(int orientation) {
        if (_orientation == orientation) {
            return;
        }
        _orientation = orientation;
        adjustPreviewLayout(RCTCameraModule.RCT_CAMERA_TYPE_FRONT);
        adjustPreviewLayout(RCTCameraModule.RCT_CAMERA_TYPE_BACK);
    }

    public boolean isBarcodeScannerEnabled() {
      return _barcodeScannerEnabled;
    }

    public void setBarcodeScannerEnabled(boolean barcodeScannerEnabled) {
        _barcodeScannerEnabled = barcodeScannerEnabled;
    }

    public List<String> getBarCodeTypes() {
        return _barCodeTypes;
    }

    public void setBarCodeTypes(List<String> barCodeTypes) {
        _barCodeTypes = barCodeTypes;
    }

    public int getActualDeviceOrientation() {
        return _actualDeviceOrientation;
    }

    public void setAdjustedDeviceOrientation(int orientation) {
        _adjustedDeviceOrientation = orientation;
    }

    public int getAdjustedDeviceOrientation() {
        return _adjustedDeviceOrientation;
    }

    public void setActualDeviceOrientation(int actualDeviceOrientation) {
        _actualDeviceOrientation = actualDeviceOrientation;
        adjustPreviewLayout(RCTCameraModule.RCT_CAMERA_TYPE_FRONT);
        adjustPreviewLayout(RCTCameraModule.RCT_CAMERA_TYPE_BACK);
    }

    public void setCaptureMode(final int cameraType, final int captureMode) {
        Camera camera = _cameras.get(cameraType);
        if (camera == null) {
            return;
        }

        // Set (video) recording hint based on camera type. For video recording, setting
        // this hint can help reduce the time it takes to start recording.
        Camera.Parameters parameters = camera.getParameters();
        parameters.setRecordingHint(captureMode == RCTCameraModule.RCT_CAMERA_CAPTURE_MODE_VIDEO);
        try{
          camera.setParameters(parameters);
        }
        catch(RuntimeException e ) {
          Log.e("RCTCamera", "setParameters failed", e);
        }
    }

    public void setCaptureQuality(int cameraType, String captureQuality) {
        Camera camera = this.acquireCameraInstance(cameraType);
        if (camera == null) {
            return;
        }

        Camera.Parameters parameters = camera.getParameters();
        Camera.Size pictureSize = null;
        List<Camera.Size> supportedSizes = parameters.getSupportedPictureSizes();
        switch (captureQuality) {
            case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_LOW:
                pictureSize = getSmallestSize(supportedSizes);
                break;
            case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_MEDIUM:
                pictureSize = supportedSizes.get(supportedSizes.size() / 2);
                break;
            case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_HIGH:
                pictureSize = getBestSize(parameters.getSupportedPictureSizes(), Integer.MAX_VALUE, Integer.MAX_VALUE);
                break;
            case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_PREVIEW:
                Camera.Size optimalPreviewSize = getBestSize(parameters.getSupportedPreviewSizes(), Integer.MAX_VALUE, Integer.MAX_VALUE);
                pictureSize = getClosestSize(parameters.getSupportedPictureSizes(), optimalPreviewSize.width, optimalPreviewSize.height);
                break;
            case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_480P:
                pictureSize = getBestSize(supportedSizes, RESOLUTION_480P.width, RESOLUTION_480P.height);
                break;
            case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_720P:
                pictureSize = getBestSize(supportedSizes, RESOLUTION_720P.width, RESOLUTION_720P.height);
                break;
            case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_1080P:
                pictureSize = getBestSize(supportedSizes, RESOLUTION_1080P.width, RESOLUTION_1080P.height);
                break;
        }

        if (pictureSize != null) {
            parameters.setPictureSize(pictureSize.width, pictureSize.height);
            try{
            camera.setParameters(parameters);
            }
            catch(RuntimeException e ) {
              Log.e("RCTCamera", "setParameters failed", e);
            }
        }
    }

    public CamcorderProfile setCaptureVideoQuality(int cameraType, String captureQuality) {
        Camera camera = this.acquireCameraInstance(cameraType);
        if (camera == null) {
            return null;
        }

        Camera.Size videoSize = null;
        List<Camera.Size> supportedSizes = getSupportedVideoSizes(camera);
        CamcorderProfile cm = null;
        switch (captureQuality) {
            case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_LOW:
                videoSize = getSmallestSize(supportedSizes);
                cm = CamcorderProfile.get(_cameraTypeToIndex.get(cameraType), CamcorderProfile.QUALITY_480P);
                break;
            case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_MEDIUM:
                videoSize = supportedSizes.get(supportedSizes.size() / 2);
                cm = CamcorderProfile.get(_cameraTypeToIndex.get(cameraType), CamcorderProfile.QUALITY_720P);
                break;
            case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_HIGH:
                videoSize = getBestSize(supportedSizes, Integer.MAX_VALUE, Integer.MAX_VALUE);
                cm = CamcorderProfile.get(_cameraTypeToIndex.get(cameraType), CamcorderProfile.QUALITY_HIGH);
                break;
            case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_480P:
                videoSize = getBestSize(supportedSizes, RESOLUTION_480P.width, RESOLUTION_480P.height);
                cm = CamcorderProfile.get(_cameraTypeToIndex.get(cameraType), CamcorderProfile.QUALITY_480P);
                break;
            case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_720P:
                videoSize = getBestSize(supportedSizes, RESOLUTION_720P.width, RESOLUTION_720P.height);
                cm = CamcorderProfile.get(_cameraTypeToIndex.get(cameraType), CamcorderProfile.QUALITY_720P);
                break;
            case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_1080P:
                videoSize = getBestSize(supportedSizes, RESOLUTION_1080P.width, RESOLUTION_1080P.height);
                cm = CamcorderProfile.get(_cameraTypeToIndex.get(cameraType), CamcorderProfile.QUALITY_1080P);
                break;
        }

        if (cm == null){
            return null;
        }

        if (videoSize != null) {
            cm.videoFrameHeight = videoSize.height;
            cm.videoFrameWidth = videoSize.width;
        }

        return cm;
    }

    public void setTorchMode(int cameraType, int torchMode) {
        Camera camera = this.acquireCameraInstance(cameraType);
        if (null == camera) {
            return;
        }

        Camera.Parameters parameters = camera.getParameters();
        String value = parameters.getFlashMode();
        switch (torchMode) {
            case RCTCameraModule.RCT_CAMERA_TORCH_MODE_ON:
                value = Camera.Parameters.FLASH_MODE_TORCH;
                break;
            case RCTCameraModule.RCT_CAMERA_TORCH_MODE_OFF:
                value = Camera.Parameters.FLASH_MODE_OFF;
                break;
        }

        List<String> flashModes = parameters.getSupportedFlashModes();
        if (flashModes != null && flashModes.contains(value)) {
            parameters.setFlashMode(value);
            try{
              camera.setParameters(parameters);
            }
          catch(RuntimeException e ) {
            Log.e("RCTCamera", "setParameters failed", e);
          }
        }
    }

    public void setFlashMode(int cameraType, int flashMode) {
        Camera camera = this.acquireCameraInstance(cameraType);
        if (null == camera) {
            return;
        }

        Camera.Parameters parameters = camera.getParameters();
        String value = parameters.getFlashMode();
        switch (flashMode) {
            case RCTCameraModule.RCT_CAMERA_FLASH_MODE_AUTO:
                value = Camera.Parameters.FLASH_MODE_AUTO;
                break;
            case RCTCameraModule.RCT_CAMERA_FLASH_MODE_ON:
                value = Camera.Parameters.FLASH_MODE_ON;
                break;
            case RCTCameraModule.RCT_CAMERA_FLASH_MODE_OFF:
                value = Camera.Parameters.FLASH_MODE_OFF;
                break;
        }
        List<String> flashModes = parameters.getSupportedFlashModes();
        if (flashModes != null && flashModes.contains(value)) {
            parameters.setFlashMode(value);
            try{
            camera.setParameters(parameters);
            }
            catch(RuntimeException e ) {
              Log.e("RCTCamera", "setParameters failed", e);
            }
        }
    }

    public void setZoom(int cameraType, int zoom) {
        Camera camera = this.acquireCameraInstance(cameraType);
        if (null == camera) {
            return;
        }

        Camera.Parameters parameters = camera.getParameters();
        int maxZoom = parameters.getMaxZoom();
        if (parameters.isZoomSupported()) {
            if (zoom >=0 && zoom < maxZoom) {
                parameters.setZoom(zoom);
                try{
                  camera.setParameters(parameters);
                }
                catch(RuntimeException e ) {
                  Log.e("RCTCamera", "setParameters failed", e);
                }
            }
        }
    }

    public void adjustCameraRotationToDeviceOrientation(int type, int deviceOrientation) {
        Camera camera = _cameras.get(type);
        if (null == camera) {
            return;
        }

        CameraInfoWrapper cameraInfo = _cameraInfos.get(type);
        int rotation;
        int orientation = cameraInfo.info.orientation;
        if (cameraInfo.info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            rotation = (orientation + deviceOrientation * 90) % 360;
        } else {
            rotation = (orientation - deviceOrientation * 90 + 360) % 360;
        }
        cameraInfo.rotation = rotation;
        Camera.Parameters parameters = camera.getParameters();
        parameters.setRotation(cameraInfo.rotation);

        try {
            camera.setParameters(parameters);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void adjustPreviewLayout(int type) {
        Camera camera = _cameras.get(type);
        if (null == camera) {
            return;
        }

        CameraInfoWrapper cameraInfo = _cameraInfos.get(type);
        int displayRotation;
        int rotation;
        int orientation = cameraInfo.info.orientation;
        if (cameraInfo.info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            rotation = (orientation + _actualDeviceOrientation * 90) % 360;
            displayRotation = (720 - orientation - _actualDeviceOrientation * 90) % 360;
        } else {
            rotation = (orientation - _actualDeviceOrientation * 90 + 360) % 360;
            displayRotation = rotation;
        }
        cameraInfo.rotation = rotation;
        // TODO: take in account the _orientation prop

        setAdjustedDeviceOrientation(rotation);
        camera.setDisplayOrientation(displayRotation);

        Camera.Parameters parameters = camera.getParameters();
        parameters.setRotation(cameraInfo.rotation);

        // set preview size
        // defaults to highest resolution available
        Camera.Size optimalPreviewSize = getBestSize(parameters.getSupportedPreviewSizes(), Integer.MAX_VALUE, Integer.MAX_VALUE);
        int width = optimalPreviewSize.width;
        int height = optimalPreviewSize.height;

        parameters.setPreviewSize(width, height);
        try {
            camera.setParameters(parameters);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (cameraInfo.rotation == 0 || cameraInfo.rotation == 180) {
            cameraInfo.previewWidth = width;
            cameraInfo.previewHeight = height;
        } else {
            cameraInfo.previewWidth = height;
            cameraInfo.previewHeight = width;
        }
    }

    public void setPreviewVisibleSize(int type, int width, int height) {
        CameraInfoWrapper cameraInfo = _cameraInfos.get(type);
        if (cameraInfo == null) {
            return;
        }

        cameraInfo.previewVisibleWidth = width;
        cameraInfo.previewVisibleHeight = height;
    }

    private RCTCamera(int deviceOrientation) {
        _cameras = new HashMap<>();
        _cameraInfos = new HashMap<>();
        _cameraTypeToIndex = new HashMap<>();

        _actualDeviceOrientation = deviceOrientation;

        // map camera types to camera indexes and collect cameras properties
        for (int i = 0; i < Camera.getNumberOfCameras(); i++) {
            Camera.CameraInfo info = new Camera.CameraInfo();
            Camera.getCameraInfo(i, info);
            if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT && _cameraInfos.get(RCTCameraModule.RCT_CAMERA_TYPE_FRONT) == null) {
                _cameraInfos.put(RCTCameraModule.RCT_CAMERA_TYPE_FRONT, new CameraInfoWrapper(info));
                _cameraTypeToIndex.put(RCTCameraModule.RCT_CAMERA_TYPE_FRONT, i);
                acquireCameraInstance(RCTCameraModule.RCT_CAMERA_TYPE_FRONT);
                releaseCameraInstance(RCTCameraModule.RCT_CAMERA_TYPE_FRONT);
            } else if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK && _cameraInfos.get(RCTCameraModule.RCT_CAMERA_TYPE_BACK) == null) {
                _cameraInfos.put(RCTCameraModule.RCT_CAMERA_TYPE_BACK, new CameraInfoWrapper(info));
                _cameraTypeToIndex.put(RCTCameraModule.RCT_CAMERA_TYPE_BACK, i);
                acquireCameraInstance(RCTCameraModule.RCT_CAMERA_TYPE_BACK);
                releaseCameraInstance(RCTCameraModule.RCT_CAMERA_TYPE_BACK);
            }
        }
    }

    private class CameraInfoWrapper {
        public final Camera.CameraInfo info;
        public int rotation = 0;
        public int previewWidth = -1;
        public int previewHeight = -1;
        public int previewVisibleWidth = -1;
        public int previewVisibleHeight = -1;

        public CameraInfoWrapper(Camera.CameraInfo info) {
            this.info = info;
        }
    }

    private static class Resolution {
        public int width;
        public int height;

        public Resolution(final int width, final int height) {
            this.width = width;
            this.height = height;
        }
    }
}
