package com.visagetechnologies.androidcameraplugin;

import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.app.AlertDialog;
import android.view.Surface;
import android.widget.TextView;
import android.graphics.Matrix;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.Size;
import android.os.Bundle;
import android.util.TypedValue;
import android.util.Log;
import android.view.Display;
import android.view.Gravity;
import android.view.WindowManager;
import android.widget.Toast;

import java.io.IOException;
import java.util.List;
import java.lang.*;
import java.lang.String;
import java.nio.ByteBuffer;

import com.unity3d.player.Camera2Wrapper;
import com.unity3d.player.UnityPlayer;
import com.visagetechnologies.bsc.FrameProvider;
import com.visagetechnologies.CamIL.Plane;
import com.visagetechnologies.CamIL.IFormat;
import com.visagetechnologies.CamIL.EFormat;
import com.visagetechnologies.makeupsdk.MakeupEngine;

public class CameraActivity {
    public final String TAG = "JavaCamTrackerView";

    public FrameProvider frameProvider;

    SurfaceTexture tex;

    int imageWidth = 0;
    int imageHeight = 0;

    Camera cam;
    boolean isCamOpen = false;
    int cameraFacing = CameraInfo.CAMERA_FACING_FRONT;

    int screenOrientation;

    public CameraActivity(Context context, FrameProvider frameProvider) {
        this.frameProvider = frameProvider;

        CameraInfo camInfo = new CameraInfo();

        Display display = ((WindowManager) context.getSystemService(context.WINDOW_SERVICE)).getDefaultDisplay();
        this.screenOrientation = display.getRotation();
    }

    /**
     * Calculates supported frame dimensions that are closest (by eucledian distance) to desired width and height
     *
     * @param parameters camera parameters
     * @param width      desired frame width
     * @param height     desired frame height
     */
    private void setPreviewSize(Camera.Parameters parameters, int width, int height) {
        int idx = 0;
        double dist1 = Integer.MAX_VALUE;
        double dist2 = Integer.MAX_VALUE;

        List<Size> sizes = parameters.getSupportedPreviewSizes();

        for (int i = 0; i < sizes.size(); i++) {
            dist2 = Math.abs(
                    Math.sqrt(Math.pow((sizes.get(i).width - width), 2) +
                            Math.pow((sizes.get(i).height - height), 2))
            );

            if (dist2 < dist1) {
                idx = i;
                dist1 = dist2;
            }
        }

        parameters.setPreviewSize(
                sizes.get(idx).width,
                sizes.get(idx).height
        );
    }

    /**
     * @return camera device ID
     */
    private int getCameraId() {
        int numberOfCameras = Camera.getNumberOfCameras();

        for (int cameraIndex = 0; cameraIndex < numberOfCameras; cameraIndex++) {
            CameraInfo info = new CameraInfo();
            Camera.getCameraInfo(cameraIndex, info);

            if (info.facing == this.cameraFacing)
                return cameraIndex;
            else if (info.facing == this.cameraFacing)
                return cameraIndex;
        }

        return -1;
    }

    /**
     * Start grabbing frames from camera
     */
    public void GrabFromCamera() {

        int camId = getCameraId();
        Log.d(TAG, "camId: " + camId);

        try {
            cam = Camera.open(camId);
        } catch (Exception e) {
            Log.e(TAG, "Unable to open camera");
            return;
        }

        imageWidth = imageWidth <= 0 ? 640 : imageWidth;
        imageHeight = imageHeight <= 0 ? 480 : imageHeight;

        Camera.Parameters parameters = cam.getParameters();
        setPreviewSize(parameters, imageWidth, imageHeight);
        parameters.setPreviewFormat(ImageFormat.NV21);
        cam.setParameters(parameters);

        final Size previewSize = cam.getParameters().getPreviewSize();
        imageWidth = previewSize.width;
        imageHeight = previewSize.height;

        Log.i(TAG, "Current preview size is " + imageWidth + ", " + imageHeight);

        double bytesPerPixel = (ImageFormat.getBitsPerPixel(cam.getParameters().getPreviewFormat()) / 8.0);
        int dataBufferSize = (int) (imageWidth * imageHeight * bytesPerPixel);

        cam.addCallbackBuffer(new byte[dataBufferSize]);

        try {
            tex = new SurfaceTexture(0);
            Log.i(TAG, "Setting preview tex");
            cam.setPreviewTexture(tex);
        } catch (IOException e) {
            e.printStackTrace();
        }

        cam.setPreviewCallbackWithBuffer(new PreviewCallback() {
            public void onPreviewFrame(byte[] data, Camera camera) {

                IFormat format = com.visagetechnologies.BeautySightCore.GetFormat(EFormat.RGB);

                byte[] data_rotated = rotateNV21(data, imageWidth, imageHeight, 270, cameraFacing == CameraInfo.CAMERA_FACING_FRONT);
                byte[] rgb = new byte[imageWidth * imageHeight * 3];
                decodeNV21(rgb, data_rotated, imageHeight, imageWidth);

                com.visagetechnologies.CamIL.Image frame = frameProvider.ReuseInputFrame();
                com.visagetechnologies.CamIL.Plane plane = frame.GetPlane(0);

                // If buffered image does not fit the new frame
                // we have to replace it with new one.
                // Also, it is reasonable to assume if it's the same format
                // we only need to match dimensions of the first plane
                boolean isReusingBuffer =
                    frame.GetFormat().Equals(format) &&
                    plane.GetWidth() == imageWidth &&
                    plane.GetHeight() == imageHeight;

                if (!isReusingBuffer) {
                    frame = new com.visagetechnologies.CamIL.Image(imageHeight, imageWidth, EFormat.RGB);
                    plane = frame.GetPlane(0);
                }

                int size = plane.GetSize();
                ByteBuffer nativeBuffer = plane.GetBuffer();
                nativeBuffer.put(rgb, 0, size);
                plane.delete();

                int id = frameProvider.AddFrame(frame); // queue new frame

                if(!isReusingBuffer) {
                    frame.delete();
                }

                camera.addCallbackBuffer(data); // reuse android camera buffer
            }
        });

        Log.d(TAG, "Starting preview");
        cam.startPreview();
        isCamOpen = true;
    }

    /**
     * Stops camera.
     */
    public void closeCamera() {
        if (cam != null) {
            cam.stopPreview();
            cam.release();
            cam = null;
        }
    }

    public void onPause() {
        Log.d(TAG, "On Pause");
        closeCamera();
    }

    public void onResume() {
        Log.d(TAG, "On Resume: " + isCamOpen);
        if (isCamOpen)
            GrabFromCamera();
    }

    public void onDestroy() {
        Log.d(TAG, "On Destroy");
        closeCamera();
    }

    void decodeNV21(byte[] rgb, byte[] yuv420sp, int width, int height) {
        final int frameSize = width * height;

        for (int j = 0, yp = 0; j < height; j++) {
            int uvp = frameSize + (j >> 1) * width, u = 0, v = 0;
            for (int i = 0; i < width; i++, yp++) {
                int y = (0xff & ((int) yuv420sp[yp])) - 16;
                if (y < 0) y = 0;

                if ((i & 1) == 0) {
                    v = (0xff & yuv420sp[uvp++]) - 128;
                    u = (0xff & yuv420sp[uvp++]) - 128;
                }

                int y1192 = 1192 * y;
                int r = (y1192 + 1634 * v);
                int g = (y1192 - 833 * v - 400 * u);
                int b = (y1192 + 2066 * u);

                if (r < 0) r = 0;
                else if (r > 262143) r = 262143;

                if (g < 0) g = 0;
                else if (g > 262143) g = 262143;

                if (b < 0) b = 0;
                else if (b > 262143) b = 262143;

                rgb[yp * 3] = (byte) ((r >> 10) & 0xff);
                rgb[yp * 3 + 1] = (byte) ((g >> 10) & 0xff);
                rgb[yp * 3 + 2] = (byte) ((b >> 10) & 0xff);

                // int rgbInt = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff);
                // rgb[yp * 3] = (byte) ((rgbInt >> 16) & 0xff);
                // rgb[yp * 3 + 1] = (byte) ((rgbInt >> 8) & 0xff);
                // rgb[yp * 3 + 2] = (byte) (rgbInt & 0xff);
            }
        }
    }

    public static byte[] rotateNV21(byte[] yuv, int width, int height, int rotation, boolean flip) {
        if (rotation == 0) return yuv;
        if (rotation % 90 != 0 || rotation < 0 || rotation > 270) {
            throw new IllegalArgumentException("0 <= rotation < 360, rotation % 90 == 0");
        }

        final byte[] output = new byte[yuv.length];
        final int frameSize = width * height;
        final boolean swap = rotation % 180 != 0;
        final boolean xflip = flip ^ rotation % 270 != 0;
        final boolean yflip = rotation >= 180;

        for (int j = 0; j < height; j++) {
            for (int i = 0; i < width; i++) {
                final int yIn = j * width + i;
                final int uIn = frameSize + (j >> 1) * width + (i & ~1);
                final int vIn = uIn + 1;

                final int wOut = swap ? height : width;
                final int hOut = swap ? width : height;
                final int iSwapped = swap ? j : i;
                final int jSwapped = swap ? i : j;
                final int iOut = xflip ? wOut - iSwapped - 1 : iSwapped;
                final int jOut = yflip ? hOut - jSwapped - 1 : jSwapped;

                final int yOut = jOut * wOut + iOut;
                final int uOut = frameSize + (jOut >> 1) * wOut + (iOut & ~1);
                final int vOut = uOut + 1;

                output[yOut] = (byte) (0xff & yuv[yIn]);
                output[uOut] = (byte) (0xff & yuv[uIn]);
                output[vOut] = (byte) (0xff & yuv[vIn]);
            }
        }
        return output;
    }
}
