package org.imaginer.scol;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.camera2.Camera2Config;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraFilter;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.MeteringPoint;
import androidx.camera.core.SurfaceOrientedMeteringPointFactory;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Point;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.media.Image;
import android.util.Log;
import android.util.Size;
import android.view.Surface;
import android.content.res.Configuration;

import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

@SuppressLint("UnsafeExperimentalUsageError")

public class CameraBackend extends AppCompatActivity
{
  private final Semaphore mutex = new Semaphore(1);
  private ProcessCameraProvider mCameraProvider;
  private CameraCharacteristics mCharacteristics = null;
  private Camera mCamera = null;
  private ImageAnalysis mImageAnalysis = null;
  private final ExecutorService cameraExecutor = Executors.newSingleThreadExecutor();
  private String mCameraId;
  //private byte[] mFrameRGB;
  private byte[] mBuffer;
  private byte[] mSendBuffer;
  private Size mPreviewSize = new Size(640, 480);
  private int mRequestedWidth = 640;
  private int mRequestedHeight = 480;
  private int mIndex = 0;
  private boolean mUpdated = false;
  private boolean mActive = false;
  private boolean mTorch = false;
  private boolean mConnecting = false;
  private boolean mClosing = false;
  private NativeWrap mActivity;

  public CameraBackend()
  {
    mCamera = null;
  }

  public static class CameraIdFilter implements CameraFilter
  {
    private int index = 0;

    public CameraIdFilter(int id)
    {
      index = id-2;
    }

    @Override public List<CameraInfo> filter(@NonNull List<CameraInfo> cameraInfos)
    {
      List<CameraInfo> resultCameras = new ArrayList();
      int i = 0;
      for (CameraInfo camera : cameraInfos)
      {
        /*
        if (index > 2)
        {
          @SuppressLint({"RestrictedApi", "UnsafeOptInUsageError"}) Integer lensFacing = Camera2CameraInfo.extractCameraCharacteristics(camera).get(CameraCharacteristics.LENS_FACING);
          if (lensFacing != null && lensFacing == CameraMetadata.LENS_FACING_EXTERNAL)
            resultCameras.add(camera);
        }
        else
        */
        if (i == index)
        {
          resultCameras.add(camera);
          break;
        }
        i++;
      }
      return resultCameras;
    }
  }

  public void openCamera(int width, int height, int id) throws InterruptedException
  {
    mRequestedWidth = width;
    mRequestedHeight = height;
    mIndex = id;
    if (id == 0)
      mIndex = CameraSelector.LENS_FACING_BACK;
    else if (id == 1)
      mIndex = CameraSelector.LENS_FACING_FRONT;

    if (!mActivity.CheckCameraPermission())
      return;

    Context context = mActivity.getApplicationContext();

    try
    {
      ProcessCameraProvider.configureInstance(Camera2Config.defaultConfig());
    }
    catch (Exception e)
    {
      //already done
    }

    final ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(context);

    int select = mIndex;

    mConnecting = true;

    cameraProviderFuture.addListener(new Runnable()
    {
      @SuppressLint("RestrictedApi")
      @Override
      public void run()
      {
        try
        {
          mCameraProvider = cameraProviderFuture.get();
          mImageAnalysis = new ImageAnalysis.Builder().setTargetResolution(new Size(mRequestedWidth, mRequestedHeight))
                  .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).build();
          mImageAnalysis.setAnalyzer(cameraExecutor, new CameraGetBuffer());

          CameraSelector cameraSelector = null;
          if (select == CameraSelector.LENS_FACING_BACK || select == CameraSelector.LENS_FACING_FRONT)
            cameraSelector = new CameraSelector.Builder().requireLensFacing(select).build();
          else
            cameraSelector = new CameraSelector.Builder().addCameraFilter(new CameraIdFilter(select)).build();

          if (mCamera != null)
            mCameraProvider.unbindAll();

          mActive = false;
          mCamera = null;
          mCharacteristics = null;
          mUpdated = false;

          if (!mCameraProvider.hasCamera(cameraSelector))
          {
            Log.e("ScolApp", "[openCamera] No compatible camera found");
            mActive = false;
            mCamera = null;
            mConnecting = false;
            mUpdated = false;
            return;
          }

          mCamera = mCameraProvider.bindToLifecycle((LifecycleOwner)mActivity, cameraSelector, mImageAnalysis);
          mCameraId = Objects.requireNonNull(mImageAnalysis.getCamera()).getCameraInfoInternal().getCameraId();

          CameraManager manager = (CameraManager) mActivity.getSystemService(Context.CAMERA_SERVICE);
          mCharacteristics = manager.getCameraCharacteristics(mCameraId);

          if (mCamera.getCameraInfo().hasFlashUnit())
            mCamera.getCameraControl().enableTorch(mTorch);

          mPreviewSize = mImageAnalysis.getAttachedSurfaceResolution();
          mActive = true;
        }
        catch(Exception e)
        {
          Log.e("ScolApp", "[openCamera] Use case binding failed", e);
          mActive = false;
          mCamera = null;
          mUpdated = false;
        }
        finally
        {
          mConnecting = false;
        }
      }
    }, ContextCompat.getMainExecutor(context));

    try
    {
      cameraProviderFuture.get(5000, TimeUnit.MILLISECONDS);
      while (mConnecting)
      {
        Thread.sleep(5);
      }
    }
    catch(Exception e)
    {
      Log.e("ScolApp", "[openCamera] Timeout", e);
    }
  }

  private class CameraGetBuffer implements ImageAnalysis.Analyzer
  {
    private byte[] mLastBuffer = null;

    @Override
    public void analyze(@NonNull ImageProxy image)
    {
      Image frame = image.getImage();
      if (frame != null)
        mLastBuffer = FormatConverter.YUV420toNV21(image.getImage(), mLastBuffer);

      image.close();

      /* Copy bitmap buffer */
      runOnUiThread(new Runnable()
      {
        @Override
        public void run()
        {
          try
          {
            mutex.acquire();
            mBuffer = mLastBuffer.clone();
            mUpdated = true;
          }
          catch(Exception e)
          {
            mUpdated = false;
          }
          finally
          {
            mutex.release();
          }
        }
      });
    }
  }

  // This method has to be called from Activity's onCreate method.
  public void setup(NativeWrap activity)
  {
    if (activity == null)
      Log.d("ScolApp", "activity == null");

    Log.i("ScolApp", "setup create view ...");

    mActivity = activity;
  }

  public void torchOn()
  {
    mTorch = true;

    if ((mCamera != null) && (mCamera.getCameraInfo().hasFlashUnit()))
      mCamera.getCameraControl().enableTorch(mTorch);
  }

  public void torchOff()
  {
    mTorch = false;

    if ((mCamera != null) && (mCamera.getCameraInfo().hasFlashUnit()))
      mCamera.getCameraControl().enableTorch(mTorch);
  }

  public void closeCamera()
  {
    if ((mCamera != null) && !mClosing)
    {
      mClosing = true;
      Context context = mActivity.getApplicationContext();
      final ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(context);
      cameraProviderFuture.addListener(new Runnable()
      {
        @Override
        public void run()
        {
          try
          {
            mCameraProvider.unbindAll();
          }
          catch(Exception e)
          {
            Log.e("ScolApp", "[closeCamera] Exception", e);
          }
          finally
          {
            mCameraProvider = null;
            mCamera = null;
            mBuffer = null;
            mSendBuffer = null;
            mActive = false;
            mClosing = false;
            mUpdated = false;
          }
        }
      }, ContextCompat.getMainExecutor(mActivity.getApplicationContext()));

      try
      {
        cameraProviderFuture.get(5000, TimeUnit.MILLISECONDS);
        while (mClosing)
        {
          Thread.sleep(5);
        }
      }
      catch(Exception e)
      {
        Log.e("ScolApp", "[closeCamera] Timeout", e);
      }

      Log.d("ScolApp", "[closeCamera] camera closed");
    }
  }

  public byte[] grabFrame()
  {
    if (mCamera == null || !mUpdated)
      return null;

    try
    {
      mutex.acquire();
      mSendBuffer = mBuffer.clone();
      mUpdated = false;
    }
    catch(Exception e)
    {
      mSendBuffer = null;
      mUpdated = false;
    }
    finally
    {
      mutex.release();
    }

    //logMemoryInfo();
    return mSendBuffer;
  }

  public void doTouchFocus(final int x, final int y, final int w, final int h)
  {
    if (mActive && (mCamera != null))
    {
      try
      {
        SurfaceOrientedMeteringPointFactory factory = new SurfaceOrientedMeteringPointFactory((float)w, (float)h);
        MeteringPoint point = factory.createPoint((float)x, (float)y);
        FocusMeteringAction action = new FocusMeteringAction.Builder(point).disableAutoCancel().build();
        mCamera.getCameraControl().startFocusAndMetering(action);
      }
      catch (Exception e)
      {
        e.printStackTrace();
      }
    }
  }

  public boolean isCameraOpened() { return mCamera != null; }

  public boolean isCameraFacing()
  {
    if (mCharacteristics == null)
      return false;

    return (mCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT);
  }

  public Point getPreviewSize()
  {
    return new Point(mPreviewSize.getWidth(), mPreviewSize.getHeight());
  }

  public int getDeviceOrientation()
  {
    if (mCamera == null)
      return 0;

    //device mode
    Configuration config = mActivity.getResources().getConfiguration();

    //hardware rotation
    int camOrientation = mCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);

    int degrees = 0;
    //Orientation
    int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
    switch (rotation)
    {
      case Surface.ROTATION_0: degrees = 0; break;
      case Surface.ROTATION_90: degrees = 90; break;
      case Surface.ROTATION_180: degrees = 180; break;
      case Surface.ROTATION_270: degrees = 270; break;
    }

    //if ((config.orientation == Configuration.ORIENTATION_LANDSCAPE) && ((infos.orientation % 90) == 0) || (config.orientation == Configuration.ORIENTATION_PORTRAIT) && ((infos.orientation % 90) != 0))
    //  degrees = (degrees + 90) % 360;

    int result;
    if (mCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT)
    {
      result = (camOrientation + degrees) % 360;
      result = (360 - result) % 360;  // compensate the mirror
    }
    else // back-facing
    {
      result = (camOrientation - degrees + 360) % 360;
    }

    /*
    if (config.orientation == Configuration.ORIENTATION_PORTRAIT)
      Log.d("ScolApp", "Camera mode: portrait");
    else
      Log.d("ScolApp", "Camera mode: landscape");

    if ((infos.orientation % 90) != 0)
      Log.d("ScolApp", "Camera orientation: 90");
    else
      Log.d("ScolApp", "Camera orientation: 0");
    */
    //Log.d("ScolApp", "Camera rotation: " + Integer.toString(result));

    return result;
  }

}
