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

package com.pedro.encoder.input.video;

import android.content.Context;
import android.content.res.Configuration;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.media.CamcorderProfile;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;

import com.pedro.encoder.Frame;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * Created by pedro on 20/01/17.
 *
 * This class need use same resolution, fps and imageFormat that VideoEncoder
 * Tested with YV12 and NV21.
 * <p>
 * Advantage = you can control fps of the stream.
 * Disadvantages = you cant use all resolutions, only resolution that your camera support.
 * <p>
 * If you want use all resolutions. You can use libYuv for resize images in OnPreviewFrame:
 * https://chromium.googlesource.com/libyuv/libyuv/
 */

public class Camera1ApiManager implements Camera.PreviewCallback, Camera.FaceDetectionListener {

  private String TAG = "Camera1ApiManager";
  private Camera camera = null;
  private SurfaceView surfaceView;
  private TextureView textureView;
  private SurfaceTexture surfaceTexture;
  private GetCameraData getCameraData;
  private boolean running = false;
  private boolean lanternEnable = false;
  private boolean videoStabilizationEnable = false;
  private boolean autoFocusEnabled = false;
  private int cameraSelect;
  private CameraHelper.Facing facing = CameraHelper.Facing.BACK;
  private boolean isPortrait = false;
  private Context context;

  //default parameters for camera
  private int width = 640;
  private int height = 480;
  private int fps = 30;
  private int rotation = 0;
  private int imageFormat = ImageFormat.NV21;
  private byte[] yuvBuffer;
  private List<Camera.Size> previewSizeBack;
  private List<Camera.Size> previewSizeFront;
  private float distance;
  private CameraCallbacks cameraCallbacks;
  private final int focusAreaSize = 100;

  //Face detector
  public interface FaceDetectorCallback {
    void onGetFaces(Camera.Face[] faces, Rect scaleSensor, int sensorOrientation);
  }

  private final int sensorOrientation = 0;
  //Value obtained from Camera.Face documentation api about bounds
  private final Rect faceSensorScale = new Rect(-1000, -1000, 1000, 1000);
  private FaceDetectorCallback faceDetectorCallback;

  public Camera1ApiManager(SurfaceView surfaceView, GetCameraData getCameraData) {
    this.surfaceView = surfaceView;
    this.getCameraData = getCameraData;
    this.context = surfaceView.getContext();
    init();
  }

  public Camera1ApiManager(TextureView textureView, GetCameraData getCameraData) {
    this.textureView = textureView;
    this.getCameraData = getCameraData;
    this.context = textureView.getContext();
    init();
  }

  public Camera1ApiManager(SurfaceTexture surfaceTexture, Context context) {
    this.surfaceTexture = surfaceTexture;
    this.context = context;
    init();
  }

  private void init() {
    cameraSelect = selectCameraBack();
    previewSizeBack = getPreviewSize();
    cameraSelect = selectCameraFront();
    previewSizeFront = getPreviewSize();
  }

  public void setRotation(int rotation) {
    this.rotation = rotation;
  }

  public void setSurfaceTexture(SurfaceTexture surfaceTexture) {
    this.surfaceTexture = surfaceTexture;
  }

  public int getWidth() {
    return width;
  }

  public int getHeight() {
    return height;
  }

  public void setCameraFacing(CameraHelper.Facing cameraFacing) {
    facing = cameraFacing;
  }

  public void setCameraSelect(int cameraFacing) {
    cameraSelect = cameraFacing;
  }

  public void start(CameraHelper.Facing cameraFacing, int width, int height, int fps) {
    int facing = cameraFacing == CameraHelper.Facing.BACK ? Camera.CameraInfo.CAMERA_FACING_BACK
        : Camera.CameraInfo.CAMERA_FACING_FRONT;
    this.width = width;
    this.height = height;
    this.fps = fps;
    cameraSelect =
        facing == Camera.CameraInfo.CAMERA_FACING_BACK ? selectCameraBack() : selectCameraFront();
    start();
  }

  public void start(int facing, int width, int height, int fps) {
    this.width = width;
    this.height = height;
    this.fps = fps;
    cameraSelect = facing;
    selectCamera(facing);
    start();
  }

  public void start(int width, int height, int fps) {
    start(cameraSelect, width, height, fps);
  }

  private void start() {
    if (!checkCanOpen()) {
      throw new CameraOpenException("This camera resolution cant be opened");
    }
    yuvBuffer = new byte[width * height * 3 / 2];
    try {
      camera = Camera.open(cameraSelect);
      Camera.CameraInfo info = new Camera.CameraInfo();
      Camera.getCameraInfo(cameraSelect, info);
      facing = info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT ? CameraHelper.Facing.FRONT : CameraHelper.Facing.BACK;
      isPortrait = context.getResources().getConfiguration().orientation
          == Configuration.ORIENTATION_PORTRAIT;
      Camera.Parameters parameters = camera.getParameters();
      parameters.setPreviewSize(width, height);
      parameters.setPreviewFormat(imageFormat);
      int[] range = adaptFpsRange(fps, parameters.getSupportedPreviewFpsRange());
      Log.i(TAG, "fps: " + range[0] + " - " + range[1]);
      parameters.setPreviewFpsRange(range[0], range[1]);

      List<String> supportedFocusModes = parameters.getSupportedFocusModes();
      if (supportedFocusModes != null && !supportedFocusModes.isEmpty()) {
        if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
          parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
          autoFocusEnabled = true;
        } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
          parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
          autoFocusEnabled = true;
        } else {
          parameters.setFocusMode(supportedFocusModes.get(0));
          autoFocusEnabled = false;
        }
      }
      camera.setParameters(parameters);
      camera.setDisplayOrientation(rotation);
      if (surfaceView != null) {
        camera.setPreviewDisplay(surfaceView.getHolder());
        camera.addCallbackBuffer(yuvBuffer);
        camera.setPreviewCallbackWithBuffer(this);
      } else if (textureView != null) {
        camera.setPreviewTexture(textureView.getSurfaceTexture());
        camera.addCallbackBuffer(yuvBuffer);
        camera.setPreviewCallbackWithBuffer(this);
      } else {
        camera.setPreviewTexture(surfaceTexture);
      }
      camera.startPreview();
      running = true;
      if (cameraCallbacks != null) {
        cameraCallbacks.onCameraOpened();
        cameraCallbacks.onCameraChanged(facing);
      }
      Log.i(TAG, width + "X" + height);
    } catch (IOException e) {
      if (cameraCallbacks != null) cameraCallbacks.onCameraError(e.getMessage());
      Log.e(TAG, "Error", e);
    }
  }

  public void setPreviewOrientation(final int orientation) {
    this.rotation = orientation;
    if (camera != null && running) {
      camera.stopPreview();
      camera.setDisplayOrientation(orientation);
      camera.startPreview();
    }
  }

  public void setZoom(int level) {
    try {
      if (camera != null && running && camera.getParameters() != null && camera.getParameters()
          .isZoomSupported()) {
        android.hardware.Camera.Parameters params = camera.getParameters();
        int maxZoom = params.getMaxZoom();
        if (level > maxZoom) level = maxZoom;
        else if (level < getMinZoom()) level = getMinZoom();
        params.setZoom(level);
        camera.setParameters(params);
      }
    } catch (Exception e) {
      Log.e(TAG, "Error", e);
    }
  }

  public int getZoom() {
    try {
      if (camera != null && running && camera.getParameters() != null && camera.getParameters()
          .isZoomSupported()) {
        android.hardware.Camera.Parameters params = camera.getParameters();
        return params.getZoom();
      } else {
        return getMinZoom();
      }
    } catch (Exception e) {
      Log.e(TAG, "Error", e);
      return getMinZoom();
    }
  }

  public int getMaxZoom() {
    try {
      if (camera != null && running && camera.getParameters() != null && camera.getParameters()
          .isZoomSupported()) {
        android.hardware.Camera.Parameters params = camera.getParameters();
        return params.getMaxZoom();
      } else {
        return getMinZoom();
      }
    } catch (Exception e) {
      Log.e(TAG, "Error", e);
      return getMinZoom();
    }
  }

  public int getMinZoom() { return 0; }

  public void setZoom(MotionEvent event) {
    try {
      if (camera != null && running && camera.getParameters() != null && camera.getParameters()
          .isZoomSupported()) {
        android.hardware.Camera.Parameters params = camera.getParameters();
        int maxZoom = params.getMaxZoom();
        int zoom = params.getZoom();
        float newDist = CameraHelper.getFingerSpacing(event);

        if (newDist > distance) {
          if (zoom < maxZoom) {
            zoom++;
          }
        } else if (newDist < distance) {
          if (zoom > 0) {
            zoom--;
          }
        }

        distance = newDist;
        params.setZoom(zoom);
        camera.setParameters(params);
      }
    } catch (Exception e) {
      Log.e(TAG, "Error", e);
    }
  }

  public void setExposure(int value) {
    if (camera != null && camera.getParameters() != null) {
      android.hardware.Camera.Parameters params = camera.getParameters();
      if (value > params.getMaxExposureCompensation()) value = params.getMaxExposureCompensation();
      else if (value < params.getMinExposureCompensation()) value = params.getMinExposureCompensation();
      params.setExposureCompensation(value);
      camera.setParameters(params);
    }
  }

  public int getExposure() {
    if (camera != null && camera.getParameters() != null) {
      android.hardware.Camera.Parameters params = camera.getParameters();
      return params.getExposureCompensation();
    }
    return 0;
  }

  public int getMaxExposure() {
    if (camera != null && camera.getParameters() != null) {
      android.hardware.Camera.Parameters params = camera.getParameters();
      return params.getMaxExposureCompensation();
    }
    return 0;
  }

  public int getMinExposure() {
    if (camera != null && camera.getParameters() != null) {
      android.hardware.Camera.Parameters params = camera.getParameters();
      return params.getMinExposureCompensation();
    }
    return 0;
  }

  private int selectCameraBack() {
    return selectCamera(Camera.CameraInfo.CAMERA_FACING_BACK);
  }

  private int selectCameraFront() {
    return selectCamera(Camera.CameraInfo.CAMERA_FACING_FRONT);
  }

  private int selectCamera(int facing) {
    int number = Camera.getNumberOfCameras();
    for (int i = 0; i < number; i++) {
      Camera.CameraInfo info = new Camera.CameraInfo();
      Camera.getCameraInfo(i, info);
      if (info.facing == facing) return i;
    }
    return 0;
  }

  public void stop() {
    if (camera != null) {
      camera.stopPreview();
      camera.setPreviewCallback(null);
      camera.setPreviewCallbackWithBuffer(null);
      camera.release();
      camera = null;
    }
    running = false;
  }

  public boolean isRunning() {
    return running;
  }

  private int[] adaptFpsRange(int expectedFps, List<int[]> fpsRanges) {
    expectedFps *= 1000;
    int[] closestRange = fpsRanges.get(0);
    int measure = Math.abs(closestRange[0] - expectedFps) + Math.abs(closestRange[1] - expectedFps);
    for (int[] range : fpsRanges) {
      if (range[0] <= expectedFps && range[1] >= expectedFps) {
        int curMeasure = Math.abs(((range[0] + range[1]) / 2) - expectedFps);
        if (curMeasure < measure) {
          closestRange = range;
          measure = curMeasure;
        } else if (curMeasure == measure) {
          if (Math.abs(range[0] - expectedFps) < Math.abs(closestRange[1] - expectedFps)) {
            closestRange = range;
            measure = curMeasure;
          }
        }
      }
    }
    return closestRange;
  }

  @Override
  public void onPreviewFrame(byte[] data, Camera camera) {
    getCameraData.inputYUVData(new Frame(data, rotation, facing == CameraHelper.Facing.FRONT && isPortrait, imageFormat));
    camera.addCallbackBuffer(yuvBuffer);
  }

  public Camera.Size getCameraSize(int width, int height) {
    if (camera != null) {
      return camera.new Size(width, height);
    } else {
      camera = Camera.open(cameraSelect);
      Camera.Size size = camera.new Size(width, height);
      camera.release();
      camera = null;
      return size;
    }
  }

  /**
   * See: https://developer.android.com/reference/android/graphics/ImageFormat.html to know name of
   * constant values
   * Example: 842094169 -> YV12, 17 -> NV21
   */
  public List<Integer> getCameraPreviewImageFormatSupported() {
    List<Integer> formats;
    if (camera != null) {
      formats = camera.getParameters().getSupportedPreviewFormats();
      for (Integer i : formats) {
        Log.i(TAG, "camera format supported: " + i);
      }
    } else {
      camera = Camera.open(cameraSelect);
      formats = camera.getParameters().getSupportedPreviewFormats();
      camera.release();
      camera = null;
    }
    return formats;
  }

  private List<Camera.Size> getPreviewSize() {
    List<Camera.Size> previewSizes;
    Camera.Size maxSize;
    if (camera != null) {
      maxSize = getMaxEncoderSizeSupported();
      previewSizes = camera.getParameters().getSupportedPreviewSizes();
    } else {
      camera = Camera.open(cameraSelect);
      maxSize = getMaxEncoderSizeSupported();
      previewSizes = camera.getParameters().getSupportedPreviewSizes();
      camera.release();
      camera = null;
    }
    //discard preview more high than device can record
    Iterator<Camera.Size> iterator = previewSizes.iterator();
    while (iterator.hasNext()) {
      Camera.Size size = iterator.next();
      if (size.width > maxSize.width || size.height > maxSize.height) {
        Log.i(TAG, size.width + "X" + size.height + ", not supported for encoder");
        iterator.remove();
      }
    }
    return previewSizes;
  }

  public List<Camera.Size> getPreviewSizeBack() {
    return previewSizeBack;
  }

  public List<Camera.Size> getPreviewSizeFront() {
    return previewSizeFront;
  }

  /**
   * @return max size that device can record.
   */
  private Camera.Size getMaxEncoderSizeSupported() {
    if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_2160P)) {
      return camera.new Size(3840, 2160);
    } else if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_1080P)) {
      return camera.new Size(1920, 1080);
    } else if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_720P)) {
      return camera.new Size(1280, 720);
    } else {
      return camera.new Size(640, 480);
    }
  }

  public CameraHelper.Facing getCameraFacing() {
    return facing;
  }

  public void switchCamera() throws CameraOpenException {
    if (camera != null) {
      int oldCamera = cameraSelect;
      int number = Camera.getNumberOfCameras();
      for (int i = 0; i < number; i++) {
        if (cameraSelect != i) {
          cameraSelect = i;
          if (!checkCanOpen()) {
            cameraSelect = oldCamera;
            throw new CameraOpenException("This camera resolution cant be opened");
          }
          stop();
          start();
          return;
        }
      }
    }
  }

  public void switchCamera(int cameraId) throws CameraOpenException {
    if (camera != null) {
      int oldCamera = cameraSelect;
      cameraSelect = cameraId;
      if (!checkCanOpen()) {
        cameraSelect = oldCamera;
        throw new CameraOpenException("This camera resolution cant be opened");
      }
      stop();
      start();
      return;
    }
  }

  private boolean checkCanOpen() {
    List<Camera.Size> previews;
    if (cameraSelect == selectCameraBack()) {
      previews = previewSizeBack;
    } else {
      previews = previewSizeFront;
    }
    for (Camera.Size size : previews) {
      if (size.width == width && size.height == height) {
        return true;
      }
    }
    return false;
  }

  public boolean isLanternEnabled() {
    return lanternEnable;
  }

  /**
   * @required: <uses-permission android:name="android.permission.FLASHLIGHT"/>
   */
  public void enableLantern() throws Exception {
    if (camera != null) {
      Camera.Parameters parameters = camera.getParameters();
      List<String> supportedFlashModes = parameters.getSupportedFlashModes();
      if (supportedFlashModes != null && !supportedFlashModes.isEmpty()) {
        if (supportedFlashModes.contains(Camera.Parameters.FLASH_MODE_TORCH)) {
          parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
          camera.setParameters(parameters);
          lanternEnable = true;
        } else {
          Log.e(TAG, "Lantern unsupported");
          throw new Exception("Lantern unsupported");
        }
      }
    }
  }

  public List<int[]> getSupportedFps() {
    List<int[]> supportedFps;
    if (camera != null) {
      supportedFps = camera.getParameters().getSupportedPreviewFpsRange();
    } else {
      camera = Camera.open(cameraSelect);
      supportedFps = camera.getParameters().getSupportedPreviewFpsRange();
      camera.release();
      camera = null;
    }
    for (int[] range : supportedFps) {
      range[0] /= 1000;
      range[1] /= 1000;
    }
    return supportedFps;
  }

  /**
   * @required: <uses-permission android:name="android.permission.FLASHLIGHT"/>
   */
  public void disableLantern() {
    if (camera != null) {
      Camera.Parameters parameters = camera.getParameters();
      parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
      camera.setParameters(parameters);
      lanternEnable = false;
    }
  }
  
  private Camera.AutoFocusCallback autoFocusTakePictureCallback = new Camera.AutoFocusCallback() {
    @Override
    public void onAutoFocus(boolean success, Camera camera) {
      if (success) {
        Log.i(TAG, "tapToFocus success");
      } else {
        Log.e(TAG, "tapToFocus failed");
      }
    }
  };

  public void tapToFocus(View view, MotionEvent event) {
    if (camera != null && camera.getParameters() != null) {
      Camera.Parameters parameters = camera.getParameters();
      if (parameters.getMaxNumMeteringAreas() > 0) {
        Rect rect = calculateFocusArea(event.getX(), event.getY(), view.getWidth(), view.getHeight());
        parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
        List<Camera.Area> meteringAreas = new ArrayList<>();
        meteringAreas.add(new Camera.Area(rect, 800));
        parameters.setFocusAreas(meteringAreas);
        try {
          camera.setParameters(parameters);
        }catch (Exception e) {
          Log.i(TAG, "tapToFocus error: " + e.getMessage());
        }
      }
      camera.autoFocus(autoFocusTakePictureCallback);
    }
  }

  public void enableAutoFocus() {
    if (camera != null) {
      Camera.Parameters parameters = camera.getParameters();
      List<String> supportedFocusModes = parameters.getSupportedFocusModes();
      if (supportedFocusModes != null && !supportedFocusModes.isEmpty()) {
        if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
          parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
          autoFocusEnabled = true;
        } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
          parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
          autoFocusEnabled = true;
        } else {
          autoFocusEnabled = false;
          parameters.setFocusMode(supportedFocusModes.get(0));
        }
      }
      camera.setParameters(parameters);
    }
  }

  public void disableAutoFocus() {
    if (camera != null) {
      Camera.Parameters parameters = camera.getParameters();
      List<String> supportedFocusModes = parameters.getSupportedFocusModes();
      if (supportedFocusModes != null && !supportedFocusModes.isEmpty()) {
        if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) {
          parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY);
        } else {
          parameters.setFocusMode(supportedFocusModes.get(0));
        }
      }
      autoFocusEnabled = false;
      camera.setParameters(parameters);
    }
  }

  public boolean isAutoFocusEnabled() {
    return autoFocusEnabled;
  }

  public void enableRecordingHint() {
    if (camera != null) {
      Camera.Parameters parameters = camera.getParameters();
      parameters.setRecordingHint(true);
      camera.setParameters(parameters);
    }
  }

  public void disableRecordingHint() {
    if (camera != null) {
      Camera.Parameters parameters = camera.getParameters();
      parameters.setRecordingHint(false);
      camera.setParameters(parameters);
    }
  }

  public boolean enableFaceDetection(FaceDetectorCallback faceDetectorCallback) {
    try {
      if (camera != null) {
        this.faceDetectorCallback = faceDetectorCallback;
        camera.setFaceDetectionListener(this);
        camera.startFaceDetection();
        return true;
      } else {
        Log.e(TAG, "face detection called with camera stopped");
        return false;
      }
    } catch (IllegalArgumentException e) {
      Log.e(TAG, "face detection unsupported");
      return false;
    }
  }

  public void disableFaceDetection() {
    if (camera != null) {
      faceDetectorCallback = null;
      camera.stopFaceDetection();
      camera.setFaceDetectionListener(null);
    }
  }

  public boolean enableVideoStabilization() {
    if (camera != null) {
      Camera.Parameters parameters = camera.getParameters();
      if (parameters.isVideoStabilizationSupported()) {
        parameters.setVideoStabilization(true);
        videoStabilizationEnable = true;
      }
    }
    return videoStabilizationEnable;
  }

  public void disableVideoStabilization() {
    if (camera != null) {
      Camera.Parameters parameters = camera.getParameters();
      if (parameters.isVideoStabilizationSupported()) {
        parameters.setVideoStabilization(false);
        videoStabilizationEnable = false;
      }
    }
  }

  public boolean isVideoStabilizationEnabled() {
    return videoStabilizationEnable;
  }

  public void setCameraCallbacks(CameraCallbacks cameraCallbacks) {
    this.cameraCallbacks = cameraCallbacks;
  }

  public boolean isFaceDetectionEnabled() {
    return faceDetectorCallback != null;
  }

  @Override
  public void onFaceDetection(Camera.Face[] faces, Camera camera) {
    if (faceDetectorCallback != null) faceDetectorCallback.onGetFaces(faces, faceSensorScale, sensorOrientation);
  }

  private Rect calculateFocusArea(float x, float y, float previewWidth, float previewHeight) {
    int left = clamp((int) (x / previewWidth * 2000f - 1000f), focusAreaSize);
    int top = clamp((int) (y / previewHeight * 2000f - 1000f), focusAreaSize);
    return new Rect(left, top, left + focusAreaSize, top + focusAreaSize);
  }

  private int clamp(int touchCoordinateInCameraReper, int focusAreaSize) {
    int result;
    if (Math.abs(touchCoordinateInCameraReper) + focusAreaSize / 2 > 1000){
      if (touchCoordinateInCameraReper > 0){
        result = 1000 - focusAreaSize / 2;
      } else {
        result = -1000 + focusAreaSize / 2;
      }
    } else{
      result = touchCoordinateInCameraReper - focusAreaSize / 2;
    }
    return result;
  }
}
