package com.wonderkiln.camerakit;

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.PackageManager;
import android.content.res.TypedArray;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.AttributeSet;
import android.view.Display;
import android.view.Surface;
import android.view.WindowManager;

import com.camerakit.R;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.vision.text.TextRecognizer;
import com.wonderkiln.camerakit.api16.Camera1;
import com.wonderkiln.camerakit.base.CameraImpl;
import com.wonderkiln.camerakit.base.CameraViewLayout;
import com.wonderkiln.camerakit.base.PreviewImpl;
import com.wonderkiln.camerakit.base.SurfaceViewPreview;
import com.wonderkiln.camerakit.events.CameraKitEventCallback;
import com.wonderkiln.camerakit.events.CameraKitEventListener;
import com.wonderkiln.camerakit.events.CameraKitImage;
import com.wonderkiln.camerakit.events.CameraKitVideo;
import com.wonderkiln.camerakit.events.EventDispatcher;
import com.wonderkiln.camerakit.types.CaptureMethod;
import com.wonderkiln.camerakit.types.Facing;
import com.wonderkiln.camerakit.types.Flash;
import com.wonderkiln.camerakit.types.Focus;
import com.wonderkiln.camerakit.types.Permissions;
import com.wonderkiln.camerakit.types.VideoQuality;
import com.wonderkiln.camerakit.utils.AspectRatio;
import com.wonderkiln.camerakit.utils.DisplayOrientationDetector;
import com.wonderkiln.camerakit.utils.PostProcessor;
import com.wonderkiln.camerakit.utils.Size;
import com.wonderkiln.camerakit.vision.CameraKitTextDetect;
import com.wonderkiln.camerakit.vision.GooglePlayServicesUnavailableException;
import com.wonderkiln.camerakit.vision.TextProcessor;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.hardware.display.DisplayManagerCompat;
import androidx.core.view.ViewCompat;

import static com.wonderkiln.camerakit.CameraKit.Constants.FACING_BACK;
import static com.wonderkiln.camerakit.CameraKit.Constants.FACING_FRONT;
import static com.wonderkiln.camerakit.CameraKit.Constants.FLASH_AUTO;
import static com.wonderkiln.camerakit.CameraKit.Constants.FLASH_OFF;
import static com.wonderkiln.camerakit.CameraKit.Constants.FLASH_ON;
import static com.wonderkiln.camerakit.CameraKit.Constants.FLASH_TORCH;
import static com.wonderkiln.camerakit.CameraKit.Constants.PERMISSIONS_LAZY;
import static com.wonderkiln.camerakit.CameraKit.Constants.PERMISSIONS_PICTURE;
import static com.wonderkiln.camerakit.CameraKit.Constants.PERMISSIONS_STRICT;

public class CameraView extends CameraViewLayout {

    private static Handler sWorkerHandler;

    static {
        // Initialize a single worker thread. This can be static since only a single camera
        // reference can exist at a time.
        HandlerThread workerThread = new HandlerThread("CameraViewWorker");
        workerThread.setDaemon(true);
        workerThread.start();
        sWorkerHandler = new Handler(workerThread.getLooper());
    }

    @Facing
    private int mFacing;

    @Flash
    private int mFlash;

    @Focus
    private int mFocus;

    @CaptureMethod
    private int mMethod;

    private boolean mPinchToZoom;

    private float mZoom;

    @Permissions
    private int mPermissions;

    @VideoQuality
    private int     mVideoQuality;
    private int     mJpegQuality;
    private int     mVideoBitRate;
    private int     mDeviceOrientation;
    private boolean mLockVideoAspectRatio;
    private boolean mCropOutput;
    private boolean mDoubleTapToToggleFacing;

    private boolean mAdjustViewBounds;

    private DisplayOrientationDetector mDisplayOrientationDetector;
    private CameraImpl                 mCameraImpl;

    private PreviewImpl mPreviewImpl;

    private boolean mIsStarted;

    private EventDispatcher mEventDispatcher;

    private FocusMarkerLayout focusMarkerLayout;

    public CameraView(@NonNull Context context) {
        this(context, null);
    }

    public CameraView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CameraView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        if (attrs != null) {
            TypedArray a = context.getTheme().obtainStyledAttributes(
                    attrs,
                    R.styleable.CameraView,
                    0, 0);

            try {
                mFacing = a.getInteger(R.styleable.CameraView_ckFacing, CameraKit.Defaults.DEFAULT_FACING);
                mFlash = a.getInteger(R.styleable.CameraView_ckFlash, CameraKit.Defaults.DEFAULT_FLASH);
                mFocus = a.getInteger(R.styleable.CameraView_ckFocus, CameraKit.Defaults.DEFAULT_FOCUS);
                mMethod = a.getInteger(R.styleable.CameraView_ckMethod, CameraKit.Defaults.DEFAULT_METHOD);
                mPinchToZoom = a.getBoolean(R.styleable.CameraView_ckPinchToZoom, CameraKit.Defaults.DEFAULT_PINCH_TO_ZOOM);
                mZoom = a.getFloat(R.styleable.CameraView_ckZoom, CameraKit.Defaults.DEFAULT_ZOOM);
                mPermissions = a.getInteger(R.styleable.CameraView_ckPermissions, CameraKit.Defaults.DEFAULT_PERMISSIONS);
                mVideoQuality = a.getInteger(R.styleable.CameraView_ckVideoQuality, CameraKit.Defaults.DEFAULT_VIDEO_QUALITY);
                mJpegQuality = a.getInteger(R.styleable.CameraView_ckJpegQuality, CameraKit.Defaults.DEFAULT_JPEG_QUALITY);
                mCropOutput = a.getBoolean(R.styleable.CameraView_ckCropOutput, CameraKit.Defaults.DEFAULT_CROP_OUTPUT);
                mVideoBitRate = a.getInteger(R.styleable.CameraView_ckVideoBitRate, CameraKit.Defaults.DEFAULT_VIDEO_BIT_RATE);
                mDeviceOrientation = a.getInteger(R.styleable.CameraView_deviceOrientation, CameraKit.Defaults.DEFAULT_DEVICE_ORIENTATION);
                mDoubleTapToToggleFacing = a.getBoolean(R.styleable.CameraView_ckDoubleTapToToggleFacing, CameraKit.Defaults.DEFAULT_DOUBLE_TAP_TO_TOGGLE_FACING);
                mLockVideoAspectRatio = a.getBoolean(R.styleable.CameraView_ckLockVideoAspectRatio, true);
                mAdjustViewBounds = a.getBoolean(R.styleable.CameraView_android_adjustViewBounds, CameraKit.Defaults.DEFAULT_ADJUST_VIEW_BOUNDS);
            } finally {
                a.recycle();
            }
        }

        mEventDispatcher = new EventDispatcher();

        mPreviewImpl = new SurfaceViewPreview(context, this);
        mCameraImpl = new Camera1(mEventDispatcher, mPreviewImpl);

        mIsStarted = false;

        // Handle situations where there's only 1 camera & it's front facing OR it's a chromebook in laptop mode
        WindowManager windowService = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        boolean isChromebookInLaptopMode = (context.getPackageManager().hasSystemFeature("org.chromium.arc.device_management") && windowService.getDefaultDisplay().getRotation() == Surface.ROTATION_0);
        if (mCameraImpl.frontCameraOnly() || isChromebookInLaptopMode) {
            mFacing = FACING_FRONT;
        }
        setDeviceOrientation(mDeviceOrientation);
        setFacing(mFacing);
        setFlash(mFlash);
        setFocus(mFocus);
        setMethod(mMethod);
        setPinchToZoom(mPinchToZoom);
        setZoom(mZoom);
        setPermissions(mPermissions);
        setVideoQuality(mVideoQuality);
        setVideoBitRate(mVideoBitRate);
        setLockVideoAspectRatio(mLockVideoAspectRatio);
        if (!isInEditMode()) {
            mDisplayOrientationDetector = new DisplayOrientationDetector(context) {
                @Override
                public void onDisplayOrDeviceOrientationChanged(int displayOrientation, int deviceOrientation) {
                    mCameraImpl.setDisplayAndDeviceOrientation(displayOrientation, deviceOrientation);
                    mPreviewImpl.setDisplayOrientation(displayOrientation);
                }
            };

            focusMarkerLayout = new FocusMarkerLayout(getContext());
            addView(focusMarkerLayout);
        }
    }

    private void setDeviceOrientation(int mDeviceOrientation) {
        if (mDeviceOrientation == 0) {
            mCameraImpl.setDeviceOrientation(90);
        } else {
            mCameraImpl.setDeviceOrientation(180);
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (!isInEditMode()) {
            mDisplayOrientationDetector.enable(
                    ViewCompat.isAttachedToWindow(this)
                            ? DisplayManagerCompat.getInstance(getContext().getApplicationContext())
                            .getDisplay(Display.DEFAULT_DISPLAY)
                            : null
            );
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        if (!isInEditMode()) {
            mDisplayOrientationDetector.disable();
        }
        super.onDetachedFromWindow();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mAdjustViewBounds) {
            Size previewSize = getPreviewSize();
            if (previewSize != null) {
                if (getLayoutParams().width == LayoutParams.WRAP_CONTENT) {
                    int height = MeasureSpec.getSize(heightMeasureSpec);
                    float ratio = (float) height / (float) previewSize.getHeight();
                    int width = (int) (previewSize.getWidth() * ratio);
                    super.onMeasure(
                            MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                            heightMeasureSpec
                    );
                    return;
                } else if (getLayoutParams().height == LayoutParams.WRAP_CONTENT) {
                    int width = MeasureSpec.getSize(widthMeasureSpec);
                    float ratio = (float) width / (float) previewSize.getWidth();
                    int height = (int) (previewSize.getHeight() * ratio);
                    super.onMeasure(
                            widthMeasureSpec,
                            MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
                    );
                    return;
                }
            } else {
                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                return;
            }
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    public boolean isStarted() {
        return mIsStarted;
    }

    public void addController(CameraKitController controller) {

    }

    public void start() {
        if (mIsStarted || !isEnabled()) {
            // Already started, do nothing.
            return;
        }
        mIsStarted = true;
        int cameraCheck = ContextCompat.checkSelfPermission(getContext(), Manifest.permission.CAMERA);
        int audioCheck = ContextCompat.checkSelfPermission(getContext(), Manifest.permission.RECORD_AUDIO);

        switch (mPermissions) {
            case PERMISSIONS_STRICT:
                if (cameraCheck != PackageManager.PERMISSION_GRANTED || audioCheck != PackageManager.PERMISSION_GRANTED) {
                    requestPermissions(true, true);
                    return;
                }
                break;

            case PERMISSIONS_LAZY:
                if (cameraCheck != PackageManager.PERMISSION_GRANTED) {
                    requestPermissions(true, true);
                    return;
                }
                break;

            case PERMISSIONS_PICTURE:
                if (cameraCheck != PackageManager.PERMISSION_GRANTED) {
                    requestPermissions(true, false);
                    return;
                }
                break;
        }
        mCameraImpl.start();
    }

    public void stop() {
        if (!mIsStarted) {
            // Already stopped, do nothing.
            return;
        }
        mIsStarted = false;
        mCameraImpl.stop();
    }


    @Override
    protected CameraImpl getCameraImpl() {
        return mCameraImpl;
    }

    @Override
    protected PreviewImpl getPreviewImpl() {
        return mPreviewImpl;
    }

    @Override
    protected void onZoom(float modifier, boolean start) {
        if (mPinchToZoom) {
            mCameraImpl.modifyZoom((modifier - 1) * 0.8f + 1);
        }
    }

    @Override
    protected void onTapToFocus(float x, float y) {
        if (mFocus == CameraKit.Constants.FOCUS_TAP || mFocus == CameraKit.Constants.FOCUS_TAP_WITH_MARKER) {
            focusMarkerLayout.focus(x, y);

            float px = x - getPreviewImpl().getX();
            float py = y - getPreviewImpl().getY();
            mCameraImpl.setFocusArea(px / (float) getPreviewImpl().getWidth(), py / (float) getPreviewImpl().getHeight());
        }
    }

    @Override
    protected void onToggleFacing() {
        if (mDoubleTapToToggleFacing) {
            toggleFacing();
        }
    }

    @Nullable
    public CameraProperties getCameraProperties() {
        return mCameraImpl.getCameraProperties();
    }

    @Facing
    public int getFacing() {
        return mFacing;
    }

    public boolean isFacingFront() {
        return mFacing == CameraKit.Constants.FACING_FRONT;
    }

    public boolean isFacingBack() {
        return mFacing == CameraKit.Constants.FACING_BACK;
    }

    public void setFacing(@Facing final int facing) {
        this.mFacing = facing;
        sWorkerHandler.post(new Runnable() {
            @Override
            public void run() {
                mCameraImpl.setFacing(facing);
            }
        });
    }

    public void setFlash(@Flash int flash) {
        this.mFlash = flash;
        mCameraImpl.setFlash(flash);
    }

    @Flash
    public int getFlash() {
        return mFlash;
    }

    public void setFocus(@Focus int focus) {
        this.mFocus = focus;
        if (this.mFocus == CameraKit.Constants.FOCUS_TAP_WITH_MARKER) {
            mCameraImpl.setFocus(CameraKit.Constants.FOCUS_TAP);
            return;
        }

        mCameraImpl.setFocus(mFocus);
    }

    public void setMethod(@CaptureMethod int method) {
        this.mMethod = method;
        mCameraImpl.setMethod(mMethod);
    }

    public void setPinchToZoom(boolean zoom) {
        this.mPinchToZoom = zoom;
    }

    public void setZoom(float zoom) {
        this.mZoom = zoom;
        mCameraImpl.setZoom(zoom);
    }

    public void setPermissions(@Permissions int permissions) {
        this.mPermissions = permissions;
    }

    public void setVideoQuality(@VideoQuality int videoQuality) {
        this.mVideoQuality = videoQuality;
        mCameraImpl.setVideoQuality(mVideoQuality);
    }

    public void setVideoBitRate(int videoBirRate) {
        this.mVideoBitRate = videoBirRate;
        mCameraImpl.setVideoBitRate(mVideoBitRate);
    }

    public void setLockVideoAspectRatio(boolean lockVideoAspectRatio) {
        this.mLockVideoAspectRatio = lockVideoAspectRatio;
        mCameraImpl.setLockVideoAspectRatio(lockVideoAspectRatio);
    }

    public void setJpegQuality(int jpegQuality) {
        this.mJpegQuality = jpegQuality;
    }

    public void setCropOutput(boolean cropOutput) {
        this.mCropOutput = cropOutput;
    }

    @Facing
    public int toggleFacing() {
        switch (mFacing) {
            case FACING_BACK:
                setFacing(FACING_FRONT);
                break;

            case FACING_FRONT:
                setFacing(FACING_BACK);
                break;
        }

        return mFacing;
    }

    @Flash
    public int toggleFlash() {
        switch (mFlash) {
            case FLASH_OFF:
                setFlash(FLASH_ON);
                break;

            case FLASH_ON:
                setFlash(FLASH_AUTO);
                break;

            case FLASH_AUTO:
            case FLASH_TORCH:
                setFlash(FLASH_OFF);
                break;
        }

        return mFlash;
    }

    public void captureImage() {
        captureImage(null);
    }

    public boolean setTextDetectionListener(final CameraKitEventCallback<CameraKitTextDetect> callback) throws GooglePlayServicesUnavailableException {
        TextRecognizer textRecognizer = new TextRecognizer.Builder(getContext()).build();
        textRecognizer.setProcessor(new TextProcessor(mEventDispatcher, callback));
        int code = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(getContext().getApplicationContext());
        if (code != ConnectionResult.SUCCESS) {
            throw new GooglePlayServicesUnavailableException();
        }

        if (textRecognizer.isOperational()) {
            mCameraImpl.setTextDetector(textRecognizer);
            return true;
        } else {
            return false;
        }
    }

    public void captureImage(final CameraKitEventCallback<CameraKitImage> callback) {
        mCameraImpl.captureImage(new CameraImpl.ImageCapturedCallback() {
            @Override
            public void imageCaptured(byte[] jpeg) {
                PostProcessor postProcessor = new PostProcessor(jpeg);
                postProcessor.setJpegQuality(mJpegQuality);
                postProcessor.setFacing(mFacing);
                if (mCropOutput)
                    postProcessor.setCropOutput(AspectRatio.of(getWidth(), getHeight()));

                CameraKitImage image = new CameraKitImage(postProcessor.getJpeg());
                if (callback != null)
                    callback.callback(image);
                mEventDispatcher.dispatch(image);
            }
        });
    }

    public void captureVideo() {
        captureVideo(null, null);
    }

    public void captureVideo(File videoFile) {
        captureVideo(videoFile, null);
    }

    public void captureVideo(CameraKitEventCallback<CameraKitVideo> callback) {
        captureVideo(null, callback);
    }

    public void captureVideo(File videoFile, CameraKitEventCallback<CameraKitVideo> callback) {
        captureVideo(videoFile, 0, callback);
    }

    public void captureVideo(File videoFile, int maxDuration, final CameraKitEventCallback<CameraKitVideo> callback) {
        mCameraImpl.captureVideo(videoFile, maxDuration, new CameraImpl.VideoCapturedCallback() {
            @Override
            public void videoCaptured(File file) {
                CameraKitVideo video = new CameraKitVideo(file);
                if (callback != null)
                    callback.callback(video);
                mEventDispatcher.dispatch(video);
            }
        });
    }

    public void stopVideo() {
        mCameraImpl.stopVideo();
    }

    public Size getPreviewSize() {
        return mCameraImpl != null ? mCameraImpl.getPreviewResolution() : null;
    }

    public Size getCaptureSize() {
        return mCameraImpl != null ? mCameraImpl.getCaptureResolution() : null;
    }

    private void requestPermissions(boolean requestCamera, boolean requestAudio) {
        Activity activity = null;
        Context context = getContext();
        while (context instanceof ContextWrapper) {
            if (context instanceof Activity) {
                activity = (Activity) context;
            }
            context = ((ContextWrapper) context).getBaseContext();
        }

        List<String> permissions = new ArrayList<>();
        if (requestCamera)
            permissions.add(Manifest.permission.CAMERA);
        if (requestAudio)
            permissions.add(Manifest.permission.RECORD_AUDIO);

        if (activity != null) {
            ActivityCompat.requestPermissions(
                    activity,
                    permissions.toArray(new String[permissions.size()]),
                    CameraKit.Constants.PERMISSION_REQUEST_CAMERA);
        }
    }

    public void addCameraKitListener(CameraKitEventListener CameraKitEventListener) {
        mEventDispatcher.addListener(CameraKitEventListener);
    }

    public void bindCameraKitListener(Object object) {
        mEventDispatcher.addBinding(object);
    }

}