//================================================================================================================================
//
// Copyright (c) 2015-2022 VisionStar Information Technology (Shanghai) Co., Ltd. All Rights Reserved.
// EasyAR is the registered trademark or trademark of VisionStar Information Technology (Shanghai) Co., Ltd in China
// and other countries for the augmented reality technology developed by VisionStar Information Technology (Shanghai) Co., Ltd.
//
//================================================================================================================================

package cn.easyar.samples.helloarcustomcamera;

import android.content.Context;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.PreviewCallback;
import android.os.SystemClock;
import android.util.Log;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import cn.easyar.CameraDeviceFocusMode;
import cn.easyar.CameraDeviceType;
import cn.easyar.CameraParameters;
import cn.easyar.FunctorOfVoidFromInputFrame;
import cn.easyar.InputFrame;
import cn.easyar.PixelFormat;
import cn.easyar.Vec2I;

@SuppressWarnings("deprecation")
class CameraDeviceCamera implements CameraDevice, PreviewCallback {
    private static final String TAG = "EasyAR";

    private final FunctorOfVoidFromInputFrame mInputFrameHandler;
    private CameraParameters mCameraParameters;
    private int mBufferCapacity = 8;
    private AtomicInteger mBufferSize = new AtomicInteger(8);
    private int mCameraIndex = -1;
    private int mCameraDeviceType = CameraDeviceType.Unknown;
    private int mCameraOrientation = 0;

    private List<Camera.Size> mSupportedPreviewSizes;
    private List<int[]> mSupportedPreviewFpsRanges;
    private int mWidth = 1280;
    private int mHeight = 960;
    private Vec2I mSizeToSet = new Vec2I(mWidth, mHeight);
    private int mCurrentFpsRange = -1;
    private Boolean mFlashTorchMode = null;
    private Integer mFocusMode = null;

    private boolean mStarted = false;

    private volatile Camera mCameraDevice; //we check mCameraDevice == null in different threads, which requires 'volatile' to gain Load-Acquire/Store-Release semantics on ARM processors, which may trigger 'getParameters failed' in getSizeHeight from CameraDevice::onPause()
    private SurfaceTexture mSurfaceTexture;

    public CameraDeviceCamera(Context context, FunctorOfVoidFromInputFrame inputFrameHandler) {
        this.mInputFrameHandler = inputFrameHandler;
        this.mCameraParameters = CameraParameters.createWithDefaultIntrinsics(new Vec2I(640, 480), CameraDeviceType.Unknown, 0);
    }

    @Override
    public int getBufferCapacity() {
        return mBufferCapacity;
    }

    @Override
    public void setBufferCapacity(int capacity) {
        mBufferCapacity = capacity;
        if (ready()) {
            for (int k = mBufferSize.get(); k < capacity; k += 1) {
                mCameraDevice.addCallbackBuffer(new byte[mWidth * mHeight * 3 / 2]);
            }
            mBufferSize.set(mBufferCapacity);
        }
    }

    @Override
    public boolean openWithIndex(int index) {
        try {
            close();
            mSurfaceTexture = new SurfaceTexture(0);
            mCameraDevice = Camera.open(index);
            if (mCameraDevice != null) {
                mCameraIndex = index;
                CameraInfo cameraInfo = new CameraInfo();
                Camera.getCameraInfo(mCameraIndex, cameraInfo);
                mCameraDeviceType = CameraDeviceType.Unknown;
                if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {
                    mCameraDeviceType = CameraDeviceType.Back;
                } else if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) {
                    mCameraDeviceType = CameraDeviceType.Front;
                }
                mCameraOrientation = cameraInfo.orientation;

                if (mCameraParameters != null) {
                    mCameraParameters.dispose();
                }
                mCameraParameters = CameraParameters.createWithDefaultIntrinsics(new Vec2I(mWidth, mHeight), mCameraDeviceType, mCameraOrientation);

                Camera.Parameters params = mCameraDevice.getParameters();
                params.setPreviewFormat(ImageFormat.NV21); //always supported
                mSupportedPreviewSizes = params.getSupportedPreviewSizes();
                mSupportedPreviewFpsRanges = params.getSupportedPreviewFpsRange();

                if ((mCurrentFpsRange < 0) || (mCurrentFpsRange >= mSupportedPreviewFpsRanges.size())) {
                    mCurrentFpsRange = mSupportedPreviewFpsRanges.size() - 1;
                    int i = 0;
                    for (int[] range : mSupportedPreviewFpsRanges) {
                        if ((range[0] == 30000) && (range[1] == 30000)) {
                            mCurrentFpsRange = i;
                        }
                        i += 1;
                    }
                    params.setPreviewFpsRange(mSupportedPreviewFpsRanges.get(mCurrentFpsRange)[0], mSupportedPreviewFpsRanges.get(mCurrentFpsRange)[1]);
                }

                Camera.Size size = getOptimalPreviewSize(mSizeToSet.data[0], mSizeToSet.data[1]);
                params.setPreviewSize(size.width, size.height);
                mCameraDevice.setParameters(params);
                params = mCameraDevice.getParameters();
                size = params.getPreviewSize();
                mWidth = size.width;
                mHeight = size.height;
                if (mCameraParameters != null) {
                    mCameraParameters.dispose();
                }
                mCameraParameters = CameraParameters.createWithDefaultIntrinsics(new Vec2I(mWidth, mHeight), mCameraDeviceType, mCameraOrientation);

                mCameraDevice.setParameters(params);

                setFrameRateRange(mCurrentFpsRange);
                return true;
            }
        } catch (Exception e) {
            Log.e(TAG, "Camera1: \n" + Log.getStackTraceString(e));
            if (mCameraDevice != null) {
                try {
                    mCameraDevice.release();
                } catch (Exception ex) {
                }
                mCameraDevice = null;
            }
            return false;
        }
        return false;
    }

    @Override
    public boolean openWithSpecificType(int type) {
        CameraInfo cameraInfo = new CameraInfo();
        int cameraCount = Camera.getNumberOfCameras();
        for (int k = 0; k < cameraCount; k += 1) {
            Camera.getCameraInfo(k, cameraInfo);
            if ((type == CameraDeviceType.Back) && (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK)) {
                return openWithIndex(k);
            }
            if ((type == CameraDeviceType.Front) && (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT)) {
                return openWithIndex(k);
            }
            if (type == CameraDeviceType.Unknown) {
                return openWithIndex(k);
            }
        }
        return false;
    }

    @Override
    public void close() {
        stop();
        if (mCameraParameters != null) {
            mCameraParameters.dispose();
            mCameraParameters = null;
        }
        if (mSurfaceTexture != null) {
            mSurfaceTexture.release();
            mSurfaceTexture = null;
        }
    }

    @Override
    public boolean start() {
        if (!ready()) {
            if (!openWithIndex(mCameraIndex)) {
                return false;
            }
        }

        try {
            mCameraDevice.setPreviewTexture(mSurfaceTexture);
            mCameraDevice.setPreviewCallbackWithBuffer(this);
            for (int i = 0; i < mBufferCapacity; i += 1) {
                mCameraDevice.addCallbackBuffer(new byte[mWidth * mHeight * 3 / 2]);
            }
            mBufferSize.set(mBufferCapacity);
            mCameraDevice.startPreview();
        } catch (Exception e) {
            Log.e(TAG, "Camera1: \n" + Log.getStackTraceString(e));
            return false;
        }
        if (mFlashTorchMode != null) {
            setFlashTorchMode(mFlashTorchMode);
        }
        if (mFocusMode != null) {
            setFocusMode(mFocusMode);
        }
        mStarted = true;
        return true;
    }

    @Override
    public void stop() {
        if (!ready())
            return;

        mStarted = false;
        try {
            mCameraDevice.stopPreview();
        } catch (Exception ex) {
        }
        try {
            mCameraDevice.setPreviewCallback(null);
        } catch (Exception ex) {
        }
        try {
            mCameraDevice.release();
        } catch (Exception ex) {
        }
        mCameraDevice = null;
    }

    @Override
    public int getIndex() {
        return mCameraIndex;
    }

    @Override
    public int getType() {
        return mCameraDeviceType;
    }

    @Override
    public int getCameraOrientation() {
        return mCameraOrientation;
    }

    @Override
    public CameraParameters getCameraParameters() {
        return new CameraParameters(mCameraParameters.size(), mCameraParameters.focalLength(), mCameraParameters.principalPoint(), mCameraParameters.cameraDeviceType(), mCameraParameters.cameraOrientation());
    }

    @Override
    public void setCameraParameters(CameraParameters cameraParameters) {
        if (cameraParameters == null) { return; }
        if (mCameraParameters != null) {
            mCameraParameters.dispose();
        }
        mCameraParameters = new CameraParameters(cameraParameters.size(), cameraParameters.focalLength(), cameraParameters.principalPoint(), cameraParameters.cameraDeviceType(), cameraParameters.cameraOrientation());
    }

    @Override
    public int getSizeWidth(){
        return mWidth;
    }

    @Override
    public int getSizeHeight(){
        return mHeight;
    }

    @Override
    public int getNumSupportedSize(){
        if (mSupportedPreviewSizes == null) { return 0; }
        return mSupportedPreviewSizes.size();
    }

    @Override
    public int getSupportedSizeWidth(int index){
        if (mSupportedPreviewSizes == null) { return 0; }
        if ((index < 0) || (index >= mSupportedPreviewSizes.size())) { return 0; }
        Camera.Size size = mSupportedPreviewSizes.get(index);
        return size.width;
    }

    @Override
    public int getSupportedSizeHeight(int index){
        if (mSupportedPreviewSizes == null) { return 0; }
        if ((index < 0) || (index >= mSupportedPreviewSizes.size())) { return 0; }
        Camera.Size size = mSupportedPreviewSizes.get(index);
        return size.height;
    }

    @Override
    public boolean setSize(int width, int height) {
        mSizeToSet = new Vec2I(width, height);
        if (!ready()) { return false; }

        try {
            boolean started = mStarted;
            if (started) {
                close();
                start();
            }
            return true;
        } catch (Exception e) {
            Log.e(TAG, "Camera1: \n" + Log.getStackTraceString(e));
            return false;
        }
    }

    @Override
    public int getNumSupportedFrameRateRange() {
        if (mSupportedPreviewFpsRanges == null) { return 0; }
        return mSupportedPreviewFpsRanges.size();
    }

    @Override
    public float getSupportedFrameRateRangeLower(int index) {
        if (mSupportedPreviewFpsRanges == null) { return 0; }
        if ((index < 0) || (index >= mSupportedPreviewFpsRanges.size())) { return 0; }
        return mSupportedPreviewFpsRanges.get(index)[0] / 1000.0f;
    }

    @Override
    public float getSupportedFrameRateRangeUpper(int index) {
        if (mSupportedPreviewFpsRanges == null) { return 0; }
        if ((index < 0) || (index >= mSupportedPreviewFpsRanges.size())) { return 0; }
        return mSupportedPreviewFpsRanges.get(index)[1] / 1000.0f;
    }

    @Override
    public int getFrameRateRange() {
        if (mSupportedPreviewFpsRanges == null) { return -1; }
        return mCurrentFpsRange;
    }

    @Override
    public boolean setFrameRateRange(int index) {
        if (mSupportedPreviewFpsRanges == null) { return false; }
        if ((index < 0) || (index >= mSupportedPreviewFpsRanges.size())) { return false; }
        mCurrentFpsRange = index;

        try {
            Camera.Parameters params = mCameraDevice.getParameters();
            params.setPreviewFpsRange(mSupportedPreviewFpsRanges.get(mCurrentFpsRange)[0], mSupportedPreviewFpsRanges.get(mCurrentFpsRange)[1]);
            mCameraDevice.setParameters(params);
            return true;
        } catch (Exception e) {
            Log.e(TAG, "Camera1: \n" + Log.getStackTraceString(e));
            return false;
        }
    }

    @Override
    public boolean setFlashTorchMode(boolean on) {
        if (!ready()) {
            mFlashTorchMode = on;
            return false;
        }

        try {
            Camera.Parameters params = mCameraDevice.getParameters();
            String mode = null;
            if (on) {
                mode = Camera.Parameters.FLASH_MODE_TORCH;
            } else {
                mode = Camera.Parameters.FLASH_MODE_OFF;
            }
            List<String> supported = mCameraDevice.getParameters().getSupportedFlashModes();
            if ((supported != null) && (supported.contains(mode))) {
                params.setFlashMode(mode);
                mCameraDevice.setParameters(params);
                mFlashTorchMode = on;
                return true;
            }

            return false;
        } catch (Exception e) {
            Log.e(TAG, "Camera1: \n" + Log.getStackTraceString(e));
            return false;
        }
    }

    @Override
    public boolean setFocusMode(int focusMode) {
        if (!ready()) {
            mFocusMode = focusMode;
            return false;
        }

        try {
            Camera.Parameters params = mCameraDevice.getParameters();
            List<String> modes = params.getSupportedFocusModes();
            switch (focusMode){
                case CameraDeviceFocusMode.Normal:
                    if (modes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
                        params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
                    } else {
                        return false;
                    }
                    break;
                case CameraDeviceFocusMode.Continousauto:
                    if (modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
                        params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
                    } else if (modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
                        params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
                    } else {
                        return false;
                    }
                    break;
                case CameraDeviceFocusMode.Infinity:
                    if (modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) {
                        params.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY);
                    } else {
                        return false;
                    }
                    break;
                case CameraDeviceFocusMode.Macro:
                    if (modes.contains(Camera.Parameters.FOCUS_MODE_MACRO)) {
                        params.setFocusMode(Camera.Parameters.FOCUS_MODE_MACRO);
                    } else {
                        return false;
                    }
                    break;
                case CameraDeviceFocusMode.Medium:
                    return false;
                default:
                    return false;
            }
            mFocusMode = focusMode;
            mCameraDevice.setParameters(params);
            return true;
        } catch (Exception e) {
            Log.e(TAG, "Camera1: \n" + Log.getStackTraceString(e));
            return false;
        }
    }

    @Override
    public boolean autoFocus() {
        try {
            mCameraDevice.autoFocus(new Camera.AutoFocusCallback() {
                public void onAutoFocus(boolean success, Camera camera) {
                    if (mFocusMode != null) {
                        setFocusMode(mFocusMode);
                    }
                }
            });
        } catch (java.lang.RuntimeException e) {
            e.printStackTrace();
            Log.e(TAG,"Camera1: autoFocus must call after startPreview and in appropriate focus mode");
            return false;
        } catch (Exception e) {
            Log.e(TAG, "Camera1: \n" + Log.getStackTraceString(e));
            return false;
        }
        return true;
    }

    public boolean ready() {
        return mCameraDevice != null;
    }

    private float getRatioError(float x, float x0) {
        float a = (x / Math.max(x0, 1) - 1);
        float b = (x0 / Math.max(x, 1) - 1);
        return a * a + b * b;
    }

    private Camera.Size getOptimalPreviewSize(int width, int height) {
        Camera.Size s = null;
        float minError = Float.MAX_VALUE;
        for (Camera.Size size : mSupportedPreviewSizes) {
            float error = getRatioError(width, size.width) + getRatioError(height, size.height);

            //Log.d("EasyAR", "Camera1: getOptimalPreviewSize: " + width + " " + height + " " + size.width + " " + size.height + " " + error);

            if (error < minError) {
                minError = error;
                s = size;
            }
        }
        return s;
    }

    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        if (data == null) return;

        cn.easyar.Buffer b = null;
        cn.easyar.Image i = null;
        InputFrame inputFrame = null;
        CameraParameters camParams = null;
        try {
            b = cn.easyar.Buffer.wrapByteArray(data);
            i = new cn.easyar.Image(b, PixelFormat.YUV_NV21, mWidth, mHeight);
            double double_timestamp = SystemClock.elapsedRealtimeNanos() * 1e-9;
            camParams = mCameraParameters.getResized(new Vec2I(mWidth, mHeight));
            inputFrame = InputFrame.createWithImageAndCameraParametersAndTemporal(i, camParams, double_timestamp);
            mInputFrameHandler.invoke(inputFrame);
        } finally {
            if (camParams != null) { camParams.dispose(); }
            if (inputFrame != null) { inputFrame.dispose(); }
            if (i != null) { i.dispose(); }
            if (b != null) { b.dispose(); }
        }

        if (mBufferSize.get() <= mBufferCapacity) {
            camera.addCallbackBuffer(data);
        } else {
            mBufferSize.getAndDecrement();
        }
    }
}
