/*
 *  Copyright 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

package tee3.webrtc;

import android.content.Context;
import android.content.res.Configuration;
import android.hardware.Camera;
import android.os.Handler;
import android.os.SystemClock;
import android.view.Surface;
import android.view.WindowManager;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.TimeUnit;

@SuppressWarnings("deprecation")
public
class Camera1Session implements CameraSession {
  private static final String TAG = "Camera1Session";
  private static final int NUMBER_OF_CAPTURE_BUFFERS = 3;

  private static final Histogram camera1StartTimeMsHistogram =
      Histogram.createCounts("WebRTC.Android.Camera1.StartTimeMs", 1, 10000, 50);
  private static final Histogram camera1StopTimeMsHistogram =
      Histogram.createCounts("WebRTC.Android.Camera1.StopTimeMs", 1, 10000, 50);
  private static final Histogram camera1ResolutionHistogram = Histogram.createEnumeration(
      "WebRTC.Android.Camera1.Resolution", CameraEnumerationAndroid.COMMON_RESOLUTIONS.size());

  private static int remoteOrientation = Integer.MAX_VALUE;
  private static int maxZoom;
  private static int orgZoom = -1;

  private static enum SessionState { RUNNING, STOPPED }

  private final Handler cameraThreadHandler;
  private final Events events;
  private final boolean captureToTexture;
  private final Context applicationContext;
  private final SurfaceTextureHelper surfaceTextureHelper;
  private final int cameraId;
  private final Camera camera;
  private final Camera.CameraInfo info;
  private final CameraEnumerationAndroid.CaptureFormat captureFormat;
  // Used only for stats. Only used on the camera thread.
  private final long constructionTimeNs; // Construction time of this class.

  private SessionState state;
  private boolean firstFrameReported = false;
  private boolean isWorking = false;
  private boolean isFlashTurnOn;
  private static Camera1Session INSTANCE;

  // TODO(titovartem) make correct fix during webrtc:9175
  @SuppressWarnings("ByteBufferBackingArray")
  public static void create(final CreateSessionCallback callback, final Events events,
                            final boolean captureToTexture, final Context applicationContext,
                            final SurfaceTextureHelper surfaceTextureHelper, final int cameraId, final int width,
                            final int height, final int framerate) {
    final long constructionTimeNs = System.nanoTime();
    Logging.d(TAG, "Open camera " + cameraId);
    events.onCameraOpening();

    final Camera camera;
    try {
      camera = Camera.open(cameraId);
    } catch (RuntimeException e) {
      callback.onFailure(FailureType.ERROR, e.getMessage());
      return;
    }
    if (camera == null) {
      callback.onFailure(FailureType.ERROR,
          "android.hardware.Camera.open returned null for camera id = " + cameraId);
      return;
    }

    try {
      camera.setPreviewTexture(surfaceTextureHelper.getSurfaceTexture());
    } catch (IOException | RuntimeException e) {
      camera.release();
      callback.onFailure(FailureType.ERROR, e.getMessage());
      return;
    }

    final Camera.CameraInfo info = new Camera.CameraInfo();
    Camera.getCameraInfo(cameraId, info);

    if (remoteOrientation != Integer.MAX_VALUE) {
      info.orientation = remoteOrientation;
    }

    final CameraEnumerationAndroid.CaptureFormat captureFormat;
    try {
      final Camera.Parameters parameters = camera.getParameters();
      maxZoom = parameters.getMaxZoom();
      orgZoom = parameters.getZoom();
      captureFormat = findClosestCaptureFormat(parameters, width, height, framerate);
      final Size pictureSize = findClosestPictureSize(parameters, width, height);
      updateCameraParameters(camera, parameters, captureFormat, pictureSize, captureToTexture);
    } catch (RuntimeException e) {
      camera.release();
      callback.onFailure(FailureType.ERROR, e.getMessage());
      return;
    }

    if (!captureToTexture) {
      final int frameSize = captureFormat.frameSize();
      for (int i = 0; i < NUMBER_OF_CAPTURE_BUFFERS; ++i) {
        final ByteBuffer buffer = ByteBuffer.allocateDirect(frameSize);
        camera.addCallbackBuffer(buffer.array());
      }
    }
    // Calculate orientation manually and send it as CVO insted.
    camera.setDisplayOrientation(0 /* degrees */);

    callback.onDone(new Camera1Session(events, captureToTexture, applicationContext,
        surfaceTextureHelper, cameraId, camera, info, captureFormat, constructionTimeNs));
  }

  private static void updateCameraParameters(Camera camera,
                                             Camera.Parameters parameters, CameraEnumerationAndroid.CaptureFormat captureFormat, Size pictureSize,
                                             boolean captureToTexture) {

    final List<String> focusModes = parameters.getSupportedFocusModes();

    parameters.setPreviewFpsRange(captureFormat.framerate.min, captureFormat.framerate.max);
    parameters.setPreviewSize(captureFormat.width, captureFormat.height);
    parameters.setPictureSize(pictureSize.width, pictureSize.height);
    if (!captureToTexture) {
      parameters.setPreviewFormat(captureFormat.imageFormat);
    }

    if (parameters.isVideoStabilizationSupported()) {
      parameters.setVideoStabilization(true);
    }
    if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
      parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
    }
    camera.setParameters(parameters);
  }

  private static CameraEnumerationAndroid.CaptureFormat findClosestCaptureFormat(
      Camera.Parameters parameters, int width, int height, int framerate) {
    // Find closest supported format for |width| x |height| @ |framerate|.
    final List<CameraEnumerationAndroid.CaptureFormat.FramerateRange> supportedFramerates =
        Camera1Enumerator.convertFramerates(parameters.getSupportedPreviewFpsRange());
    Logging.d(TAG, "Available fps ranges: " + supportedFramerates);

    final CameraEnumerationAndroid.CaptureFormat.FramerateRange fpsRange =
        CameraEnumerationAndroid.getClosestSupportedFramerateRange(supportedFramerates, framerate);

    final Size previewSize = CameraEnumerationAndroid.getClosestSupportedSize(
        Camera1Enumerator.convertSizes(parameters.getSupportedPreviewSizes()), width, height);
    CameraEnumerationAndroid.reportCameraResolution(camera1ResolutionHistogram, previewSize);

    return new CameraEnumerationAndroid.CaptureFormat(previewSize.width, previewSize.height, fpsRange);
  }

  private static Size findClosestPictureSize(
      Camera.Parameters parameters, int width, int height) {
    return CameraEnumerationAndroid.getClosestSupportedSize(
        Camera1Enumerator.convertSizes(parameters.getSupportedPictureSizes()), width, height);
  }

  private Camera1Session(Events events, boolean captureToTexture, Context applicationContext,
                         SurfaceTextureHelper surfaceTextureHelper, int cameraId, Camera camera,
                         Camera.CameraInfo info, CameraEnumerationAndroid.CaptureFormat captureFormat,
                         long constructionTimeNs) {
    Logging.d(TAG, "Create new camera1 session on camera " + cameraId);
    INSTANCE = this;
    this.cameraThreadHandler = new Handler();
    this.events = events;
    this.captureToTexture = captureToTexture;
    this.applicationContext = applicationContext;
    this.surfaceTextureHelper = surfaceTextureHelper;
    this.cameraId = cameraId;
    this.camera = camera;
    this.info = info;
    this.captureFormat = captureFormat;
    this.constructionTimeNs = constructionTimeNs;

    startCapturing();
  }

  @Override
  public void stop() {
    Logging.d(TAG, "Stop camera1 session on camera " + cameraId);
    checkIsOnCameraThread();
    if (state != SessionState.STOPPED) {
      final long stopStartTime = System.nanoTime();
      stopInternal();
      final int stopTimeMs = (int) TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - stopStartTime);
      camera1StopTimeMsHistogram.addSample(stopTimeMs);
    }
  }

  private void startCapturing() {
    Logging.d(TAG, "Start capturing");
    checkIsOnCameraThread();
    isWorking = true;
    state = SessionState.RUNNING;
    camera.setErrorCallback(new Camera.ErrorCallback() {
      @Override
      public void onError(int error, Camera camera) {
        String errorMessage;
        if (error == Camera.CAMERA_ERROR_SERVER_DIED) {
          errorMessage = "Camera server died!";
        } else {
          errorMessage = "Camera error: " + error;  // error 2：CAMERA_ERROR_EVICTED , camera is disconnect, other people is using!
        }
        Logging.e(TAG, errorMessage);
        stopInternal();
        if (error == Camera.CAMERA_ERROR_EVICTED) {
          events.onCameraDisconnected(Camera1Session.this);
        } else {
          events.onCameraError(Camera1Session.this, errorMessage);
        }
      }
    });

    if (captureToTexture) {
      listenForTextureFrames();
    } else {
      listenForBytebufferFrames();
    }
    try {
      camera.startPreview();
    } catch (RuntimeException e) {
      stopInternal();
      events.onCameraError(this, e.getMessage());
    }
  }

  private void stopInternal() {
    isWorking = false;
    Logging.d(TAG, "Stop internal");
    checkIsOnCameraThread();
    if (state == SessionState.STOPPED) {
      Logging.d(TAG, "Camera is already stopped");
      return;
    }

    if (isFlashTurnOn) {
      turnOffFlash();
    }
    state = SessionState.STOPPED;
    surfaceTextureHelper.stopListening();
    // Note: stopPreview or other driver code might deadlock. Deadlock in
    // android.hardware.Camera._stopPreview(Native Method) has been observed on
    // Nexus 5 (hammerhead), OS version LMY48I.
    camera.stopPreview();
    camera.release();
    events.onCameraClosed(this);
    Logging.d(TAG, "Stop done");
  }

  private void listenForTextureFrames() {
    surfaceTextureHelper.startListening(new SurfaceTextureHelper.OnTextureFrameAvailableListener() {
      @Override
      public void onTextureFrameAvailable(
          int oesTextureId, float[] transformMatrix, long timestampNs) {
        checkIsOnCameraThread();

        if (state != SessionState.RUNNING) {
          Logging.d(TAG, "Texture frame captured but camera is no longer running.");
          surfaceTextureHelper.returnTextureFrame();
          return;
        }

        if (!firstFrameReported) {
          final int startTimeMs =
              (int) TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - constructionTimeNs);
          camera1StartTimeMsHistogram.addSample(startTimeMs);
          firstFrameReported = true;
        }

        int rotation = getFrameOrientation();
        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
          // Undo the mirror that the OS "helps" us with.
          // http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
          transformMatrix = RendererCommon.multiplyMatrices(
              transformMatrix, RendererCommon.horizontalFlipMatrix());
        }
        final VideoFrame.Buffer buffer =
            surfaceTextureHelper.createTextureBuffer(captureFormat.width, captureFormat.height,
                RendererCommon.convertMatrixToAndroidGraphicsMatrix(transformMatrix));
        final VideoFrame frame = new VideoFrame(buffer, rotation, timestampNs);
        events.onFrameCaptured(Camera1Session.this, frame);
        frame.release();
      }
    });
  }

  private void listenForBytebufferFrames() {
    camera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {
      @Override
      public void onPreviewFrame(final byte[] data, Camera callbackCamera) {
        checkIsOnCameraThread();

        if (callbackCamera != camera) {
          Logging.e(TAG, "Callback from a different camera. This should never happen.");
          return;
        }

        if (state != SessionState.RUNNING) {
          Logging.d(TAG, "Bytebuffer frame captured but camera is no longer running.");
          return;
        }

        final long captureTimeNs = TimeUnit.MILLISECONDS.toNanos(SystemClock.elapsedRealtime());

        if (!firstFrameReported) {
          final int startTimeMs =
              (int) TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - constructionTimeNs);
          camera1StartTimeMsHistogram.addSample(startTimeMs);
          firstFrameReported = true;
        }

        VideoFrame.Buffer frameBuffer = new NV21Buffer(
            data, captureFormat.width, captureFormat.height, () -> cameraThreadHandler.post(() -> {
              if (state == SessionState.RUNNING) {
                camera.addCallbackBuffer(data);
              }
            }));
        final VideoFrame frame = new VideoFrame(frameBuffer, getFrameOrientation(), captureTimeNs);
        events.onFrameCaptured(Camera1Session.this, frame);
        frame.release();
      }
    });
  }

  private int getDeviceOrientation() {
    int orientation = 0;

    WindowManager wm = (WindowManager) applicationContext.getSystemService(Context.WINDOW_SERVICE);
    switch (wm.getDefaultDisplay().getRotation()) {
      case Surface.ROTATION_90:
        orientation = 90;
        break;
      case Surface.ROTATION_180:
        orientation = 180;
        break;
      case Surface.ROTATION_270:
        orientation = 270;
        break;
      case Surface.ROTATION_0:
      default:
        orientation = 0;
        break;
    }
    return orientation;
  }

  private int getFrameOrientation() {
    int rotation, orientation = applicationContext.getResources().getConfiguration().orientation;
    if (TextureViewRenderer.activity != null) {
      int requestedOrientation = TextureViewRenderer.activity.getRequestedOrientation();
      orientation = requestedOrientation == -1 ? Configuration.ORIENTATION_UNDEFINED : applicationContext.getResources().getConfiguration().orientation;
    }

    if (orientation == Configuration.ORIENTATION_UNDEFINED || (info.orientation != 90 && info.orientation != 270)) {
      rotation = getDeviceOrientation();
    } else {
      if (orientation == Configuration.ORIENTATION_PORTRAIT) {
        rotation = 0;
      } else {
        rotation = 90;
      }
    }

    if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
      rotation = 360 - rotation;
    }
    return (info.orientation + rotation) % 360;
  }

  private void checkIsOnCameraThread() {
    if (Thread.currentThread() != cameraThreadHandler.getLooper().getThread()) {
      throw new IllegalStateException("Wrong thread");
    }
  }

  public static Camera1Session getInstance() {
    return INSTANCE;
  }

  public boolean isSupportZoom() {
    if (camera == null) {
      return false;
    }
    return camera.getParameters().isZoomSupported();
  }

  public int setCameraZoom(int zoom) {
    if (zoom < 0 || zoom > 100) {
      return -1;
    } else if (camera == null) {
      return -2;
    } else if (!isSupportZoom()) {
      return -3;
    }

    try {
      zoom = zoom * maxZoom / 100;
      Camera.Parameters parameters = camera.getParameters();
      parameters.setZoom(zoom);
      camera.setParameters(parameters);
      return 0;
    } catch (Exception e) {
      e.printStackTrace();
    }
    return -4;
  }

  public static int getMaxZoom() {
    return maxZoom;
  }

  public static int getOrgZoom() {
    return orgZoom;
  }

  public int turnOnFlash() {
    return setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
  }

  public int turnOffFlash() {
    return setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
  }

  private int setFlashMode(String flashMode) {
    if (camera == null) {
      return -2;
    }
    if (!isWorking) {
      return -3;
    }
    Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
    Camera.getCameraInfo(cameraId, cameraInfo);
    if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
      Camera.Parameters parameters = camera.getParameters();
      parameters.setFlashMode(flashMode);
      camera.setParameters(parameters);
      isFlashTurnOn = flashMode == Camera.Parameters.FLASH_MODE_TORCH;
      return 0;
    } else {
      return -1;
    }
  }

  public static void setCameraOrientation(int orientation) {
    remoteOrientation = orientation;
  }
}
