package com.seatel.mobilehall.ui.profile.activity;

import android.Manifest;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.YuvImage;
import android.media.Image;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.AspectRatio;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentManager;

import com.dinuscxj.progressbar.CircleProgressBar;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.mlkit.vision.common.InputImage;
import com.google.mlkit.vision.face.Face;
import com.google.mlkit.vision.face.FaceDetection;
import com.google.mlkit.vision.face.FaceDetector;
import com.google.mlkit.vision.face.FaceDetectorOptions;
import com.seatel.mobilehall.R;
import com.seatel.mobilehall.util.Constant;
import com.seatel.mobilehall.util.RoundedPreviewView;

import org.tensorflow.lite.Interpreter;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.ReadOnlyBufferException;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import at.nineyards.anyline.models.AnylineImage;

public class ProfileFaceScanActivity extends AppCompatActivity {
    private static final String TAG = "ProfileFaceScanActivity";
    private static final int PERMISSION_CODE = 1001;
    private static final String CAMERA_PERMISSION = Manifest.permission.CAMERA;
    private RoundedPreviewView previewView;
    private CameraSelector cameraSelector;
    private ProcessCameraProvider cameraProvider;
    private int lensFacing = CameraSelector.LENS_FACING_FRONT;
    private Preview previewUseCase;
    private ImageAnalysis analysisUseCase;
    //   private GraphicOverlay graphicOverlay;
    private ImageView previewImg;
    private ImageView showImage;

    private CircleProgressBar customProgress;


    private Interpreter tfLite;
    private boolean flipX = false;
    private boolean start = true;
    private float[][] embeddings;

    private static final float IMAGE_MEAN = 128.0f;
    private static final float IMAGE_STD = 128.0f;
    private static final int INPUT_SIZE = 112;
    private static final int OUTPUT_SIZE = 192;

    private static final float EYE_CLOSED_THRESHOLD = 0.1f;
    private static final float EYE_OPEN_THRESHOLD = 0.90f; // Adjust as needed

    private static final float OPEN_THRESHOLD = 0.50f;
    private static final float CLOSE_THRESHOLD = 0.2f;

    private boolean isLoading = false;

    private int state = 0;
    private boolean wereEyesClosed = false;

    public static void launchForResult(Context context) {
        Intent intent = new Intent(context, ProfileFaceScanActivity.class);
        if (context instanceof AppCompatActivity) {
            ((AppCompatActivity) context).startActivityForResult(intent, Constant.SCAN_FACE);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_face_scan);
        previewView = findViewById(R.id.previewView);
        if (previewView == null) {
            Log.e(TAG, "PreviewView initialization failed");
            return;
        }
        customProgress = findViewById(R.id.custom_progress);
        showImage = findViewById(R.id.showImageScan);
        loadModel();


    }


    @Override
    protected void onResume() {
        super.onResume();
        startCamera();
    }

    private void stopCamera() {
        if (cameraProvider != null) {
            cameraProvider.unbindAll();
            cameraProvider = null;
            previewUseCase = null;
            analysisUseCase = null;
        }
    }

    /**
     * Permissions Handler
     */
    private void getPermissions() {
        ActivityCompat.requestPermissions(this, new String[]{CAMERA_PERMISSION}, PERMISSION_CODE);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, int[] grantResults) {
        for (int r : grantResults) {
            if (r == PackageManager.PERMISSION_DENIED) {
                Toast.makeText(this, "Permission Denied", Toast.LENGTH_SHORT).show();
                return;
            }
        }

        if (requestCode == PERMISSION_CODE) {
            setupCamera();
        }

        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    /**
     * Setup camera & use cases
     */
    private void startCamera() {
        if (ContextCompat.checkSelfPermission(this, CAMERA_PERMISSION) == PackageManager.PERMISSION_GRANTED) {
            setupCamera();
        } else {
            getPermissions();
        }
    }

    private void setupCamera() {
        final ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this);

        cameraSelector = new CameraSelector.Builder().requireLensFacing(lensFacing).build();

        cameraProviderFuture.addListener(() -> {
            try {
                cameraProvider = cameraProviderFuture.get();
                bindAllCameraUseCases();
            } catch (ExecutionException | InterruptedException e) {
                Log.e(TAG, "cameraProviderFuture.addListener Error", e);
            }
        }, ContextCompat.getMainExecutor(this));
    }

    private void bindAllCameraUseCases() {
        if (cameraProvider != null) {
            cameraProvider.unbindAll();
            bindPreviewUseCase();
            bindAnalysisUseCase();
        }
    }

    private void bindPreviewUseCase() {
        if (cameraProvider == null) {
            return;
        }

        if (previewUseCase != null) {
            cameraProvider.unbind(previewUseCase);
        }

        Preview.Builder builder = new Preview.Builder();
        builder.setTargetAspectRatio(AspectRatio.RATIO_4_3);
        builder.setTargetRotation(getRotation());

        previewUseCase = builder.build();
        //  previewUseCase.setSurfaceProvider();

        PreviewView internalPreviewView = previewView.getPreviewView();
        previewUseCase.setSurfaceProvider(internalPreviewView.getSurfaceProvider());


        try {
            cameraProvider.bindToLifecycle(this, cameraSelector, previewUseCase);
        } catch (Exception e) {
            Log.e(TAG, "Error when bind preview", e);
        }
    }

    private void bindAnalysisUseCase() {
        if (cameraProvider == null) {
            return;
        }

        if (analysisUseCase != null) {
            cameraProvider.unbind(analysisUseCase);
        }

        Executor cameraExecutor = Executors.newSingleThreadExecutor();

        ImageAnalysis.Builder builder = new ImageAnalysis.Builder();
        builder.setTargetAspectRatio(AspectRatio.RATIO_4_3);
        builder.setTargetRotation(getRotation());

        analysisUseCase = builder.build();
        analysisUseCase.setAnalyzer(cameraExecutor, this::analyze);

        try {
            cameraProvider.bindToLifecycle(this, cameraSelector, analysisUseCase);
        } catch (Exception e) {
            Log.e(TAG, "Error when bind analysis", e);
        }
    }

    protected int getRotation() {
        if (previewView != null && previewView.getDisplay() != null) {
            return previewView.getDisplay().getRotation();
        } else {
            Log.e(TAG, "PreviewView or Display is null");
            // Return a default rotation or handle the case appropriately
            return 0; // Default rotation, could also use a specific value based on your requirements
        }
    }

    private void switchCamera() {
        if (lensFacing == CameraSelector.LENS_FACING_BACK) {
            lensFacing = CameraSelector.LENS_FACING_FRONT;
            flipX = true;
        } else {
            lensFacing = CameraSelector.LENS_FACING_BACK;
            flipX = false;
        }

        if (cameraProvider != null) cameraProvider.unbindAll();
        startCamera();
    }

    /**
     * Face detection processor
     */
    @SuppressLint("UnsafeOptInUsageError")
    private void analyze(@NonNull ImageProxy image) {
        if (image.getImage() == null) return;

        InputImage inputImage = InputImage.fromMediaImage(image.getImage(), image.getImageInfo().getRotationDegrees());

        FaceDetectorOptions options = new FaceDetectorOptions.Builder().setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE).setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL).setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL).setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL).build();
        FaceDetector faceDetector = FaceDetection.getClient(options);

        faceDetector.process(inputImage).addOnSuccessListener(faces -> onSuccessListener(faces, inputImage)).addOnFailureListener(e -> Log.e(TAG, "Barcode process failure", e)).addOnCompleteListener(task -> image.close());
    }

    private void onSuccessListener(List<Face> faces, InputImage inputImage) {
        if (faces.size() > 0) {
            Face face = faces.get(0);
            Float leftEyeOpenProb = face.getLeftEyeOpenProbability();
            Float rightEyeOpenProb = face.getRightEyeOpenProbability();
            //   Log.d(TAG, "onSuccessListener: " + leftEyeOpenProb + " " + rightEyeOpenProb);

            //  detectBlink(leftEyeOpenProb, rightEyeOpenProb, inputImage, face);

            if (leftEyeOpenProb != null && rightEyeOpenProb != null) {
                boolean isLeftEyeOpen = leftEyeOpenProb > EYE_OPEN_THRESHOLD;
                boolean isRightEyeOpen = rightEyeOpenProb > EYE_OPEN_THRESHOLD;

                boolean areEyesOpen = isLeftEyeOpen && isRightEyeOpen;

                // Detect transition from closed to open eyes
                if (!isLoading) {
                    float value = Math.min(leftEyeOpenProb, rightEyeOpenProb);
                    Log.d(TAG, "value::>>> " + value);

                    switch (state) {
                        case 0:
                            if (value >= OPEN_THRESHOLD) {
                                // Both eyes are initially open
                                Log.i(TAG, "Eyes open");
                                state = 1;
                            }
                            break;

                        case 1:
                            if (value < CLOSE_THRESHOLD) {
                                Log.i(TAG, "Eyes Close");
                                // Both eyes become closed
                                state = 2;
                            }
                            break;

                        case 2:
                            if (value >= OPEN_THRESHOLD) {
                                // Both eyes are open again
                                Log.i(TAG, "Eyes open again, capturing image!");
                                if (wereEyesClosed && areEyesOpen) {
                                    try {
                                        if (inputImage != null && inputImage.getMediaImage() != null) {
                                            Image mediaImage = inputImage.getMediaImage();
                                            if (inputImage != null) {
                                                Bitmap bitmap = mediaImgToBmp(mediaImage, inputImage.getRotationDegrees(), face.getBoundingBox());
                                                showImage.setImageBitmap(bitmap);
                                                simulateProgress(bitmap);
                                            } else {
                                                Log.e(TAG, "Image is already closed");
                                            }
                                            mediaImage.close(); // Ensure the image is closed after processing
                                        } else {
                                            Log.e(TAG, "Input image or media image is null");
                                        }

                                    } catch (Exception e) {
                                        Log.e(TAG, "Error processing image", e);
                                    }
                                }

                                state = 0;
                            }
                            break;
                    }

                }


                // Update the eye state
                wereEyesClosed = !areEyesOpen;
            } else {
                Log.d(TAG, "Eye open probabilities not available");
            }
        }
    }


    private Bitmap inputImageToBitmap(InputImage inputImage) {
        try {
            // Assuming inputImage is from the `ImageProxy` which has a method `getBitmap()`
            Bitmap bitmap = inputImage.getBitmapInternal();
            return bitmap;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private void simulateProgress(Bitmap bitmap) {
        isLoading = true;
        // Create a ValueAnimator that runs from 0 to 100 over 4000 milliseconds.
        final ValueAnimator animator = ValueAnimator.ofInt(0, 100);

        // Update the custom progress bar as the animator progresses.
        animator.addUpdateListener(animation -> {
            int progress = (int) animation.getAnimatedValue();
            customProgress.setProgress(progress);
        });

        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);

                if (!isFinishing() && !isDestroyed()) {

                    customProgress.postDelayed(() -> {
                        //  animator.cancel();
                        cameraProvider.unbind(analysisUseCase);
                        stopCamera();
                        AnylineImage newImage = new AnylineImage(bitmap);
                        String path = setupImagePath(newImage);
                        Intent intent = new Intent();
                        intent.putExtra("faceImage", path);
                        setResult(AppCompatActivity.RESULT_OK, intent);
                        onBackPressed();
                    }, 400L);
                }

            }
        });

        // Set the duration for the animation.
        animator.setDuration(2000L);
        // Start the animation.
        animator.start();
    }

    @Override
    public void onBackPressed() {
        if (isFinishing() || isDestroyed()) {
            // Activity is in an invalid state for fragment transactions
            return;
        }

        FragmentManager fragmentManager = getSupportFragmentManager();
        if (fragmentManager != null && fragmentManager.getBackStackEntryCount() > 0) {
            fragmentManager.popBackStackImmediate();
        } else {
            super.onBackPressed();
        }
    }

    protected String setupImagePath(AnylineImage image) {
        String imagePath = "";
        long time = System.currentTimeMillis();
        try {
            File directory;
            if (getExternalFilesDir(null) != null) {
                directory = new File(getExternalFilesDir(null), "results");
            } else if (this.getFilesDir() != null) {
                directory = new File(this.getFilesDir(), "results");
            } else {
                directory = new File(getFilesDir(), "results");
            }
            imagePath = new File(directory, "profile_scan" + time + ".jpg").getAbsolutePath();
            // Create the directory if it does not exist
            if (!directory.exists()) {
                directory.mkdirs();
            }
            // Save the image
            image.save(new File(imagePath), 100);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return imagePath;
    }


    /**
     * Recognize Processor
     */


    public String recognizeImage(final Bitmap bitmap) {
        // set image to preview
        previewImg.setImageBitmap(bitmap);

        //Create ByteBuffer to store normalized image

        ByteBuffer imgData = ByteBuffer.allocateDirect(INPUT_SIZE * INPUT_SIZE * 3 * 4);

        imgData.order(ByteOrder.nativeOrder());

        int[] intValues = new int[INPUT_SIZE * INPUT_SIZE];

        //get pixel values from Bitmap to normalize
        bitmap.getPixels(intValues, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());

        imgData.rewind();

        for (int i = 0; i < INPUT_SIZE; ++i) {
            for (int j = 0; j < INPUT_SIZE; ++j) {
                int pixelValue = intValues[i * INPUT_SIZE + j];
                imgData.putFloat((((pixelValue >> 16) & 0xFF) - IMAGE_MEAN) / IMAGE_STD);
                imgData.putFloat((((pixelValue >> 8) & 0xFF) - IMAGE_MEAN) / IMAGE_STD);
                imgData.putFloat(((pixelValue & 0xFF) - IMAGE_MEAN) / IMAGE_STD);
            }
        }
        //imgData is input to our model
        Object[] inputArray = {imgData};

        Map<Integer, Object> outputMap = new HashMap<>();


        embeddings = new float[1][OUTPUT_SIZE]; //output of model will be stored in this variable

        outputMap.put(0, embeddings);

//        tfLite.runForMultipleInputsOutputs(inputArray, outputMap); //Run model


        float distance;

        return null;
    }

    /**
     * Bitmap Converter
     */

    private Bitmap mediaImgToBmp(Image image, int rotation, Rect boundingBox) {
        Bitmap frame_bmp = toBitmap(image);
        return rotateBitmap(frame_bmp, rotation, flipX);
    }

    /*private Bitmap mediaImgToBmp(Image image, int rotation, Rect boundingBox) {
        //Convert media image to Bitmap
        Bitmap frame_bmp = toBitmap(image);

        //Adjust orientation of Face
        Bitmap frame_bmp1 = rotateBitmap(frame_bmp, rotation, flipX);

        //Crop out bounding box from whole Bitmap(image)
      *//*  float padding = 0.0f;
        RectF adjustedBoundingBox = new RectF(boundingBox.left - padding, boundingBox.top - padding, boundingBox.right + padding, boundingBox.bottom + padding);
        Bitmap cropped_face = getCropBitmapByCPU(frame_bmp1, adjustedBoundingBox);*//*

        //Resize bitmap to 112,112
        return rotateBitmap(frame_bmp, rotation, flipX);
    }*/

    private Bitmap getResizedBitmap(Bitmap bm) {
        int width = bm.getWidth();
        int height = bm.getHeight();
        float scaleWidth = ((float) 112) / width;
        float scaleHeight = ((float) 112) / height;
        // CREATE A MATRIX FOR THE MANIPULATION
        Matrix matrix = new Matrix();
        // RESIZE THE BIT MAP
        matrix.postScale(scaleWidth, scaleHeight);

        // "RECREATE" THE NEW BITMAP
        Bitmap resizedBitmap = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, false);
        bm.recycle();
        return resizedBitmap;
    }

    private static Bitmap getCropBitmapByCPU(Bitmap source, RectF cropRectF) {
        Bitmap resultBitmap = Bitmap.createBitmap((int) cropRectF.width(), (int) cropRectF.height(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(resultBitmap);

        // draw background
        Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
        paint.setColor(Color.WHITE);
        canvas.drawRect(//from  w w  w. ja v  a  2s. c  om
                new RectF(0, 0, cropRectF.width(), cropRectF.height()), paint);

        Matrix matrix = new Matrix();
        matrix.postTranslate(-cropRectF.left, -cropRectF.top);

        canvas.drawBitmap(source, matrix, paint);

        if (source != null && !source.isRecycled()) {
            source.recycle();
        }

        return resultBitmap;
    }

    private static Bitmap rotateBitmap(Bitmap bitmap, int rotationDegrees, boolean flipX) {
        Matrix matrix = new Matrix();

        // Rotate the image back to straight.
        matrix.postRotate(rotationDegrees);

        // Mirror the image along the X or Y axis.
        matrix.postScale(flipX ? -1.0f : 1.0f, 1.0f);
        Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);

        // Recycle the old bitmap if it has changed.
        if (rotatedBitmap != bitmap) {
            bitmap.recycle();
        }
        return rotatedBitmap;
    }

    private static byte[] YUV_420_888toNV21(Image image) {

        int width = image.getWidth();
        int height = image.getHeight();
        int ySize = width * height;
        int uvSize = width * height / 4;

        byte[] nv21 = new byte[ySize + uvSize * 2];

        ByteBuffer yBuffer = image.getPlanes()[0].getBuffer(); // Y
        ByteBuffer uBuffer = image.getPlanes()[1].getBuffer(); // U
        ByteBuffer vBuffer = image.getPlanes()[2].getBuffer(); // V

        int rowStride = image.getPlanes()[0].getRowStride();
        assert (image.getPlanes()[0].getPixelStride() == 1);

        int pos = 0;

        if (rowStride == width) { // likely
            yBuffer.get(nv21, 0, ySize);
            pos += ySize;
        } else {
            long yBufferPos = -rowStride; // not an actual position
            for (; pos < ySize; pos += width) {
                yBufferPos += rowStride;
                yBuffer.position((int) yBufferPos);
                yBuffer.get(nv21, pos, width);
            }
        }

        rowStride = image.getPlanes()[2].getRowStride();
        int pixelStride = image.getPlanes()[2].getPixelStride();

        assert (rowStride == image.getPlanes()[1].getRowStride());
        assert (pixelStride == image.getPlanes()[1].getPixelStride());

        if (pixelStride == 2 && rowStride == width && uBuffer.get(0) == vBuffer.get(1)) {
            // maybe V an U planes overlap as per NV21, which means vBuffer[1] is alias of uBuffer[0]
            byte savePixel = vBuffer.get(1);
            try {
                vBuffer.put(1, (byte) ~savePixel);
                if (uBuffer.get(0) == (byte) ~savePixel) {
                    vBuffer.put(1, savePixel);
                    vBuffer.position(0);
                    uBuffer.position(0);
                    vBuffer.get(nv21, ySize, 1);
                    uBuffer.get(nv21, ySize + 1, uBuffer.remaining());

                    return nv21; // shortcut
                }
            } catch (ReadOnlyBufferException ex) {
                // unfortunately, we cannot check if vBuffer and uBuffer overlap
            }

            // unfortunately, the check failed. We must save U and V pixel by pixel
            vBuffer.put(1, savePixel);
        }

        // other optimizations could check if (pixelStride == 1) or (pixelStride == 2),
        // but performance gain would be less significant

        for (int row = 0; row < height / 2; row++) {
            for (int col = 0; col < width / 2; col++) {
                int vuPos = col * pixelStride + row * rowStride;
                nv21[pos++] = vBuffer.get(vuPos);
                nv21[pos++] = uBuffer.get(vuPos);
            }
        }

        return nv21;
    }

    private Bitmap toBitmap(Image image) {

        byte[] nv21 = YUV_420_888toNV21(image);


        YuvImage yuvImage = new YuvImage(nv21, ImageFormat.NV21, image.getWidth(), image.getHeight(), null);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        yuvImage.compressToJpeg(new Rect(0, 0, yuvImage.getWidth(), yuvImage.getHeight()), 100, out);

        byte[] imageBytes = out.toByteArray();

        return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
    }

    /**
     * Model loader
     */
    @SuppressWarnings("deprecation")
    private void loadModel() {
        try {
            //model name
            String modelFile = "mobile_face_net.tflite";
            tfLite = new Interpreter(loadModelFile(ProfileFaceScanActivity.this, modelFile));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private MappedByteBuffer loadModelFile(Activity activity, String MODEL_FILE) throws IOException {
        AssetFileDescriptor fileDescriptor = activity.getAssets().openFd(MODEL_FILE);
        FileInputStream inputStream = new FileInputStream(fileDescriptor.getFileDescriptor());
        FileChannel fileChannel = inputStream.getChannel();
        long startOffset = fileDescriptor.getStartOffset();
        long declaredLength = fileDescriptor.getDeclaredLength();
        return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength);
    }
}