package id.co.gtc.erhacam;

import Camera.CameraProperty;
import Camera.LiveCamEvent;
import Camera.ObsbotMeet2Preset;
import Config.CameraConfigEnum;
import ErhaAPI.PhotoResult;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.NotFoundException;
import com.google.zxing.Result;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer;
import javafx.application.Platform;

import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.Slider;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.control.Label;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.AnchorPane;
import lombok.Getter;
import lombok.Setter;

import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.OpenCVFrameGrabber;
import org.bytedeco.opencv.global.opencv_core;
import org.bytedeco.opencv.global.opencv_imgcodecs;
import org.bytedeco.opencv.global.opencv_imgproc;
import org.bytedeco.opencv.opencv_core.*;
import org.opencv.videoio.Videoio;
import org.tinylog.Logger;


import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import static Config.SomeCodes.*;
import static id.co.gtc.erhacam.Detectors.*;
import static org.bytedeco.opencv.global.opencv_core.CV_8UC3;
import static org.bytedeco.opencv.global.opencv_core.mean;
import static org.bytedeco.opencv.global.opencv_imgproc.*;

@SuppressWarnings({"unused"})
public class Cameradetail {
    private static final boolean isCudaAvailable ;
    static{
        isCudaAvailable = opencv_core.getCudaEnabledDeviceCount()>0;
        if (isCudaAvailable){
            System.out.println("CUDA is available");
            opencv_core.printCudaDeviceInfo(0);
        } else {
            System.out.println("CUDA is not available");
        }

    }
    private final AtomicBoolean Capturing = new AtomicBoolean(false);
    private final AtomicBoolean TakingPhoto = new AtomicBoolean(false);
    private final AtomicBoolean IsGrabbingLiveView = new AtomicBoolean(false);
    private OpenCVFrameGrabber mGrabber = null;
    private LiveCamEvent event = null;
    private @Getter @Setter CameraConfigEnum cameraConfigEnum = CameraConfigEnum.CameraConfigCenter;
    private @Getter int LiveFPS = 0;

    /**
     * Get detected QR text from Live View
     */
    private @Getter String qrtext = null;

    @FXML
    private Label cameratitle;

    @FXML
    private ImageView camerastream;

    @FXML
    private AnchorPane streamanchor;

    @FXML
    private Label camerastatus;

    @FXML
    private Slider brightnessSlider;
    @FXML
    private Slider contrastSlider;
    @FXML
    private Slider saturationSlider;
    @FXML
    private Slider hueSlider;
    @FXML
    private Slider gainSlider;
    @FXML
    private Slider exposureSlider;

    @FXML
    private Label face_indicator;

    @FXML
    private Label eye_indicator;

    @FXML
    private Label sharpness_indicator;


    private @Getter final UMat BestMat = new UMat();
    private @Getter final UMat LiveMat = new UMat();
    private @Getter final UMat ReducedMat = new UMat();
    private @Getter final UMat GrayMat = new UMat();

    private @Getter Rect BestMatROI;
    private @Getter Rect ReducedMatROI;
    private @Getter Rect LiveMatROI;

    private boolean IsPortrait = false;

    // putar portrait
    private @Getter Size LiveSize = new Size(360, 640);
    private @Getter Size ReducedSize = new Size(720, 1280);
    private @Getter Size BestSize = new Size(2160, 3840);

    //TODO ini angka dari Erha, cek apakah masih cocok atau tidak
    private @Getter @Setter Size FullCropSize = new Size(1036,1036);

    public int getBestWidth(){
        return BestSize.width();
    }

    public int getBestHeight(){
        return BestSize.height();
    }

    int[] paramjpeg = {opencv_imgcodecs.IMWRITE_JPEG_QUALITY, 100};
    int[] parampng = {opencv_imgcodecs.IMWRITE_PNG_COMPRESSION, 0};


    private void setSliderValue(Slider sld, CameraProperty prop, double value){
        if (sld!=null){
            sld.setMin(prop.Min);
            sld.setMax(prop.Max);
            sld.setValue(value);
        }
    }

    private void resize_streamanchor(){
        if (streamanchor!=null){
            if (streamanchor.getHeight()!=0){
                if (streamanchor.getWidth()!=0){
                    camerastream.setFitHeight(streamanchor.getHeight()-10);
                    //camerastream.setFitWidth(streamanchor.getWidth());
                    camerastream.setPreserveRatio(true);
                }
            }
        }

    }

    @FXML
    public void initialize(){

        streamanchor.heightProperty().addListener(obs -> resize_streamanchor());
        streamanchor.widthProperty().addListener(obs -> resize_streamanchor());

        Platform.runLater(()->{
            setSliderValue(brightnessSlider, ObsbotMeet2Preset.Brightness, config.getBrightness(cameraConfigEnum));
            setSliderValue(contrastSlider, ObsbotMeet2Preset.Contrast, config.getContrast(cameraConfigEnum));
            setSliderValue(saturationSlider, ObsbotMeet2Preset.Saturation, config.getSaturation(cameraConfigEnum));
            setSliderValue(hueSlider, ObsbotMeet2Preset.Hue, config.getHue(cameraConfigEnum));
            setSliderValue(gainSlider, ObsbotMeet2Preset.Gain, config.getGain(cameraConfigEnum));
            setSliderValue(exposureSlider, ObsbotMeet2Preset.ExposureTime, config.getExposure(cameraConfigEnum));

        });



        brightnessSlider.valueProperty().addListener((obs, oldVal, newVal) -> {
            setBrightness(newVal.doubleValue());
            config.setBrightness(cameraConfigEnum, newVal.doubleValue());
            raise_log("Brightness for "+getCameraTitle()+" changed to "+newVal);
        });
        contrastSlider.valueProperty().addListener((obs, oldVal, newVal) -> {
            setContrast(newVal.doubleValue());
            config.setContrast(cameraConfigEnum, newVal.doubleValue());
            raise_log("Contrast for "+getCameraTitle()+" changed to "+newVal);
        });
        saturationSlider.valueProperty().addListener((obs, oldVal, newVal) -> {
            setSaturation(newVal.doubleValue());
            config.setSaturation(cameraConfigEnum, newVal.doubleValue());
            raise_log("Saturation for "+getCameraTitle()+" changed to "+newVal);
        });
        hueSlider.valueProperty().addListener((obs, oldVal, newVal) -> {
            setHue(newVal.doubleValue());
            config.setHue(cameraConfigEnum, newVal.doubleValue());
            raise_log("Hue for "+getCameraTitle()+" changed to "+newVal);
        });
        gainSlider.valueProperty().addListener((obs, oldVal, newVal) -> {
            setGain(newVal.doubleValue());
            config.setGain(cameraConfigEnum, newVal.doubleValue());
            raise_log("Gain for "+getCameraTitle()+" changed to "+newVal);
        });
        exposureSlider.valueProperty().addListener((obs, oldVal, newVal) -> {
            setExposure(newVal.doubleValue());
            config.setExposure(cameraConfigEnum, newVal.doubleValue());
            raise_log("Exposure for "+getCameraTitle()+" changed to "+newVal);
        });

    }

    @FXML
    public void resetClick(){
        brightnessSlider.adjustValue(ObsbotMeet2Preset.Brightness.Default);
        contrastSlider.adjustValue(ObsbotMeet2Preset.Contrast.Default);
        saturationSlider.adjustValue(ObsbotMeet2Preset.Saturation.Default);
        hueSlider.adjustValue(ObsbotMeet2Preset.Hue.Default);
        gainSlider.adjustValue(ObsbotMeet2Preset.Gain.Default);
        exposureSlider.adjustValue(ObsbotMeet2Preset.ExposureTime.Default);

    }



    public boolean isCapturing(){
        return Capturing.get();
    }

    /**
     * Set Camera Title
     * @param title Title of the Camera
     */
    public void setCameraTitle(String title){

        if (ValidString(title)){
            if (cameratitle!=null){
                cameratitle.setText(title);
            }
        }
    }

    public void setSharpness_indicator(double value){
        if (value >= config.getSharpnessThreshold()){
            if (Platform.isFxApplicationThread()){
                sharpness_indicator.setText("OK");
                sharpness_indicator.setStyle("-fx-text-fill: green; -fx-border-color: black");
            } else {
                Platform.runLater(()->{
                    sharpness_indicator.setText("OK");
                    sharpness_indicator.setStyle("-fx-text-fill: green; -fx-border-color: black");
                });
            }
        } else {
            if (Platform.isFxApplicationThread()) {
                sharpness_indicator.setText("BAD");
                sharpness_indicator.setStyle("-fx-text-fill: red; -fx-border-color: black");
            } else {
                Platform.runLater(()->{
                    sharpness_indicator.setText("BAD");
                    sharpness_indicator.setStyle("-fx-text-fill: red; -fx-border-color: black");
                });
            }
        }
    }

    public void setSaturation(double value){
        if (mGrabber!=null){
            mGrabber.setOption(Videoio.CAP_PROP_SATURATION, value);
        }
    }

    public double getSaturation(){
        if (mGrabber!=null){
            return mGrabber.getOption(Videoio.CAP_PROP_SATURATION);
        }
        return 0;
    }

    public void setHue(double value){
        if (mGrabber!=null){
            mGrabber.setOption(Videoio.CAP_PROP_HUE, value);
        }
    }

    public double getHue(){
        if (mGrabber!=null){
            return mGrabber.getOption(Videoio.CAP_PROP_HUE);
        }
        return 0;
    }

    public void setGain(double value){
        if (mGrabber!=null){
            mGrabber.setOption(Videoio.CAP_PROP_GAIN, value);
        }
    }

    public double getGain(){
        if (mGrabber!=null){
            return mGrabber.getOption(Videoio.CAP_PROP_GAIN);
        }
        return 0;
    }

    /**
     * Get Camera Title
     * @return Title of the Camera, or empty string if not set
     */
    public String getCameraTitle(){
        if (cameratitle!=null){
            return cameratitle.getText();
        }
        return "";
    }

    /**
     * Set Camera Status
     * @param status Status of the Camera
     */
    public void setCameraStatus(String status){
        if (ValidString(status)){
            if (camerastatus!=null){
                camerastatus.setText(status);
            }
        }
    }

    /**
     * Get Camera Status
     * @return Status of the Camera, or empty string if not set
     */
    public String getCameraStatus(){
        if (camerastatus!=null){
            return camerastatus.getText();
        }
        return "";
    }

    /**
     * Set Camera Stream
     * @param image Image to be displayed
     */
    public void setCameraStream(Image image){
        if (image!=null){
            if (camerastream!=null){
                camerastream.setImage(image);
            }
        }
    }

    /**
     * Get Camera Stream
     * @return Image of the Camera Stream, or null if not set
     */
    public Image getCameraStream(){
        if (camerastream!=null){
            return camerastream.getImage();
        }
        return null;
    }

    public void setFPS(double value){
        if (mGrabber!=null){
            mGrabber.setOption(Videoio.CAP_PROP_FPS, value);
        }
    }

    public double getFPS(){
        if (mGrabber!=null){
            return mGrabber.getOption(Videoio.CAP_PROP_FPS);
        }
        return 0;
    }

    /**
     * Set Camera Grabber and Target Width and Height
     * @param grabber Camera Grabber
     * @param livewidth Width used on live view
     * @param liveheight Height used on live view
     * @param photowidth Width used on photo capture
     * @param photoheight Height used on photo capture
     * @param reducedwidth Width used on reduced resolution
     * @param reducedheight Height used on reduced resolution
     * @param isPotrait if true, set to portrait mode, otherwise landscape
     */
    public void SetGrabber(OpenCVFrameGrabber grabber, int livewidth, int liveheight, int photowidth, int photoheight, int reducedwidth, int reducedheight, boolean isPotrait){
        if (mGrabber!=null) {
            StopLiveView();
        }

        IsPortrait = isPotrait;

        if (IsPortrait){
            // putar portrait
            LiveSize = new Size(liveheight, livewidth);
            BestSize = new Size(photoheight, photowidth);
            ReducedSize = new Size(reducedheight, reducedwidth);
        } else {
            LiveSize = new Size(livewidth, liveheight);
            BestSize = new Size(photowidth, photoheight);
            ReducedSize = new Size(reducedwidth, reducedheight);
        }



        mGrabber = grabber;

    }

    //Exposure and Focus Tricks :
    // https://stackoverflow.com/questions/53545945/how-to-set-camera-to-auto-exposure-with-opencv-3-4-2
    // https://github.com/opencv/opencv/issues/9738

    /**
     * Set Auto Exposure Mode
     * @param ON if true, set autoexposure on, otherwise off
     */
    public void setAutoExposure(boolean ON){
        if (mGrabber!=null){
            mGrabber.setOption(Videoio.CAP_PROP_AUTO_EXPOSURE, ON?ObsbotMeet2Preset.AutoExposure.On:ObsbotMeet2Preset.AutoExposure.Off);
        }
    }

    /**
     * Get Auto Exposure Mode
     * @return true if autoexposure is on, otherwise off
     */
    public boolean getAutoExposure(){
        if (mGrabber!=null){
            return mGrabber.getOption(Videoio.CAP_PROP_AUTO_EXPOSURE)==ObsbotMeet2Preset.AutoExposure.On;
        }
        return false;
    }

    /**
     * Set Exposure when Auto Exposure is Off
     * @param value exposure value
     */
    public void setExposure(double value){
        if (mGrabber!=null){
            mGrabber.setOption(Videoio.CAP_PROP_EXPOSURE, value);
        }
    }

    /**
     * Get Exposure when Auto Exposure is Off
     * @return exposure value
     */
    public double getExposure(){
        if (mGrabber!=null){
            return mGrabber.getOption(Videoio.CAP_PROP_EXPOSURE);
        }
        return 0;
    }

    /**
     * Set Auto Focus
     * @param ON if true, set autofocus on, otherwise off
     */
    public void setAutoFocus(boolean ON){
        if (mGrabber!=null){
            mGrabber.setOption(Videoio.CAP_PROP_AUTOFOCUS, ON?ObsbotMeet2Preset.AutoFocus.On:ObsbotMeet2Preset.AutoFocus.Off);
        }
    }

    /**
     * Get Auto Focus
     * @return true if autofocus is on, otherwise off
     */
    public boolean getAutoFocus(){
        if (mGrabber!=null){
            return mGrabber.getOption(Videoio.CAP_PROP_AUTOFOCUS)==ObsbotMeet2Preset.AutoFocus.On;
        }
        return false;
    }

    public void setAutoWB(boolean ON){
        if (mGrabber!=null){
            mGrabber.setOption(Videoio.CAP_PROP_AUTO_WB, ON?ObsbotMeet2Preset.AutoWhiteBalance.On:ObsbotMeet2Preset.AutoWhiteBalance.Off);
        }
    }

    public boolean getAutoWB(){
        if (mGrabber!=null){
            return mGrabber.getOption(Videoio.CAP_PROP_AUTO_WB)==ObsbotMeet2Preset.AutoWhiteBalance.On;
        }
        return false;
    }

    /**
     * Set Focus when Auto Focus is Off
     * @param value focus value
     */
    public void setFocus(double value){
        if (mGrabber!=null){
            mGrabber.setOption(Videoio.CAP_PROP_FOCUS, value);
        }
    }

    /**
     * Get Focus when Auto Focus is Off
     * @return focus value
     */
    public double getFocus(){
        if (mGrabber!=null){
            return mGrabber.getOption(Videoio.CAP_PROP_FOCUS);
        }
        return 0;
    }

    public void setBrightness(double value){
        if (mGrabber!=null){
            mGrabber.setOption(Videoio.CAP_PROP_BRIGHTNESS, value);
        }
    }

    public double getBrightness(){
        if (mGrabber!=null){
            return mGrabber.getOption(Videoio.CAP_PROP_BRIGHTNESS);
        }
        return 0;
    }

    public void setContrast(double value){
        if (mGrabber!=null){
            mGrabber.setOption(Videoio.CAP_PROP_CONTRAST, value);
        }
    }

    public double getContrast(){
        if (mGrabber!=null){
            return mGrabber.getOption(Videoio.CAP_PROP_CONTRAST);
        }
        return 0;
    }

    public void setFrameWidth(int width){
        if (mGrabber!=null){
            mGrabber.setOption(Videoio.CAP_PROP_FRAME_WIDTH, width);
        }
    }

    public double getFrameWidth(){
        if (mGrabber!=null){
            return mGrabber.getOption(Videoio.CAP_PROP_FRAME_WIDTH);
        }
        return 0;
    }

    public void setFrameHeight(int height){
        if (mGrabber!=null){
            mGrabber.setOption(Videoio.CAP_PROP_FRAME_HEIGHT, height);
        }
    }

    public double getFrameHeight(){
        if (mGrabber!=null){
            return mGrabber.getOption(Videoio.CAP_PROP_FRAME_HEIGHT);
        }
        return 0;
    }

    public void setSharpness(double value){
        if (mGrabber!=null){
            mGrabber.setOption(Videoio.CAP_PROP_SHARPNESS, value);
        }
    }

    public double getSharpness(){
        if (mGrabber!=null){
            return mGrabber.getOption(Videoio.CAP_PROP_SHARPNESS);
        }
        return 0;
    }

    public void setGamma(double value){
        if (mGrabber!=null){
            mGrabber.setOption(Videoio.CAP_PROP_GAMMA, value);
        }
    }

    public double getGamma(){
        if (mGrabber!=null){
            return mGrabber.getOption(Videoio.CAP_PROP_GAMMA);
        }
        return 0;
    }

    public boolean PutText(String filename, String text, double fontScale, Scalar textColor, int thickness){
        if (ValidString(filename)){
            Mat mat = opencv_imgcodecs.imread(filename);
            if (PutText(mat, text, fontScale, textColor, thickness)){
                return opencv_imgcodecs.imwrite(filename, mat);
            }
        }
        return false;
    }

    public boolean PutText(UMat Mat, String text, double fontScale, Scalar textColor, int thickness){
        if (!Mat.empty()){
            if (text!=null && !text.isEmpty()){
                //String timestamp = prefix+" "+SomeCodes.GetDateTimeString();
                int fontFace = FONT_HERSHEY_SIMPLEX;
                //double fontScale = 4.0;
                //int thickness = 2;
                //Scalar textColor = new Scalar(255, 255, 255, 0);        // white color in BGR format
                int[] baseline = {0};
                Size textSize = getTextSize(text, fontFace, fontScale, thickness, baseline);
                // position of the text in the bottom right corner
                int textX = Mat.cols() - textSize.width() - 10; // 10 pixels from the right
                int textY = Mat.rows() - 10; // 10 pixels from the bottom
                opencv_imgproc.putText(Mat, text, new Point(textX, textY), fontFace, fontScale, textColor, thickness, LINE_8, false);
                return true;
            }

        }
        return false;
    }

    public boolean PutText(Mat Mat, String text, double fontScale, Scalar textColor, int thickness){
        if (!Mat.empty()){
            if (text!=null && !text.isEmpty()){
                //String timestamp = prefix+" "+SomeCodes.GetDateTimeString();
                int fontFace = FONT_HERSHEY_SIMPLEX;
                //double fontScale = 4.0;
                //int thickness = 2;
                //Scalar textColor = new Scalar(255, 255, 255, 0);        // white color in BGR format
                int[] baseline = {0};
                Size textSize = getTextSize(text, fontFace, fontScale, thickness, baseline);
                // position of the text in the bottom right corner
                int textX = Mat.cols() - textSize.width() - 10; // 10 pixels from the right
                int textY = Mat.rows() - 10; // 10 pixels from the bottom
                opencv_imgproc.putText(Mat, text, new Point(textX, textY), fontFace, fontScale, textColor, thickness, LINE_8, false);
                return true;
            }

        }
        return false;
    }

    public String GetFullQualityPhotoPath(String directory, String prefix){
        //if (!ValidDirectory(directory)) directory = currentDirectory;
        //return Path.of(directory, "FullQuality", makeFileName(prefix,".png")).toString();
        return Path.of(config.getFullQualityDirectory(), makeFileName(prefix,".png")).toString();
    }

    public String GetReducedPhotoPath(String directory, String prefix){
        //if (!ValidDirectory(directory)) directory = currentDirectory;
        //return Path.of(directory, "Compressed", makeReducedFileName(prefix,".jpg")).toString();
        return Path.of(config.getCompressedDirectory(), makeFileName(prefix,".jpg")).toString();
    }

    public String GetFullQualityCropPhotoPath(String directory, String prefix){
        //if (!ValidDirectory(directory)) directory = currentDirectory;
        //return Path.of(directory, "FullQualityCrop", makeFileName(prefix,".png")).toString();
        return Path.of(config.getFullQualityCropDirectory(), makeFileName(prefix,".png")).toString();
    }

    public String GetReducedCropPhotoPath(String directory, String prefix){
        //if (!ValidDirectory(directory)) directory = currentDirectory;
        //return Path.of(directory, "CompressedCrop", makeReducedFileName(prefix,".jpg")).toString();
        return Path.of(config.getCompressedCropDirectory(), makeFileName(prefix,".jpg")).toString();
    }

    /**
     * Take Photo from Camera
     * @param directory directory to save the photo, if null, will use default directory
     * @param prefix filename prefix
     * @return filename path of the saved photo, or null if failed
     */
    public PhotoResult TakePhoto(String directory, String prefix) throws InterruptedException {
        PhotoResult result = new PhotoResult(cameratitle.getText());
        if (!ValidDirectory(directory)) directory = currentDirectory;

        if (mGrabber!=null){
            while(IsGrabbingLiveView.get()){
                Thread.sleep(10);
            }
            TakingPhoto.set(true);
            if (!BestMat.empty()){

                // save BestMat at quality 9 PNG
                String filename = GetFullQualityPhotoPath(directory, prefix);

                if (opencv_imgcodecs.imwrite(filename, BestMat, parampng)){
                    result.setFullres(filename);
                } else System.out.println("TakePhoto failed, Unable to Save FullQUality Photo for camera "+cameratitle.getText());

                String xx = CropBestMat(directory, prefix, BestMatROI);
                if (ValidFile(xx)) {
                    result.setFullcrop(xx);
                    result.setBestROI(new Rect(BestMatROI.x(), BestMatROI.y(), BestMatROI.width(), BestMatROI.height()));
                }


                // save ReducedMat at 100% JPEG
                String reducedfilename = GetReducedPhotoPath(directory, prefix);
                opencv_imgproc.resize(BestMat, ReducedMat, ReducedSize);
                if (!opencv_imgcodecs.imwrite(reducedfilename, ReducedMat, paramjpeg)){
                    System.out.println("TakePhoto failed, Unable to Save Reduced Photo for camera "+cameratitle.getText());
                } else result.setCompressedfile(reducedfilename);

                String xy = CropReducedMat(directory, prefix, ReducedMatROI);
                if (ValidFile(xy)){
                    result.setCompressedcrop(xy);
                    result.setReducedROI(new Rect(ReducedMatROI.x(), ReducedMatROI.y(), ReducedMatROI.width(), ReducedMatROI.height()));
                }

            } else raise_log("TakePhoto failed, Live View is Empty");
        } else raise_log("TakePhoto failed, Grabber is null");
        TakingPhoto.set(false);
        return result;
    }



    public String CropBestMat(String directory, String prefix, Rect ROI){
        UMat cloned = new UMat();
        BestMat.copyTo(cloned);
        if (!cloned.empty()) {
            if (ValidROI(ROI)){
                if (ROIInsideUMat(ROI, cloned)){
                    UMat cropped = CropUMat(cloned, ROI);
                    if (cropped != null) {
                        String filename = GetFullQualityCropPhotoPath(directory, prefix);
                        if (opencv_imgcodecs.imwrite(filename, cropped, parampng)) {
                            Logger.info("CropBestMat success, saved as " + filename);
                            return filename;
                        } else Logger.error("CropBestMat failed, Unable to Save BestMat as ",filename);
                    } else Logger.error("CropBestMat failed, Unable to Crop BestMat");
                } else Logger.error("CropBestMat failed, ROI is outside BestMat");
            } else Logger.error("CropBestMat failed, ROI is invalid");
        } else Logger.error("CropBestMat failed, BestMat is empty");
        return null;
    }

    public String CropReducedMat(String directory, String prefix, Rect ROI){
        if (!ReducedMat.empty()){
            if (ValidROI(ROI)){
                if (ROIInsideUMat(ROI,ReducedMat)){
                    UMat cropped = CropUMat(ReducedMat, ROI);
                    if (cropped!=null){
                        String filename = GetReducedCropPhotoPath(directory, prefix);
                        if (opencv_imgcodecs.imwrite(filename, cropped, paramjpeg)){
                            Logger.info("CropReducedMat success, saved as ",filename);
                            return filename;
                        } else Logger.error("CropReducedMat failed, Unable to Save ReducedMat as ",filename);
                    } else Logger.error("CropReducedMat failed, Unable to Crop ReducedMat");
                } else Logger.error("CropReducedMat failed, ROI is outside ReducedMat");
            } else Logger.error("CropReducedMat failed, ROI is invalid");
        } else Logger.error("CropReducedMat failed, ReducedMat is empty");
        return null;
    }




    @SuppressWarnings("SameParameterValue")
    private String makeFileName(String prefix, String extension){
        LocalDateTime ldt = LocalDateTime.now();
        String timetag = ldt.getYear() + "-" + ldt.getMonthValue() + "-" + ldt.getDayOfMonth() + "_" + ldt.getHour() + "-" + ldt.getMinute() + "-" + ldt.getSecond();
        return prefix+" "+timetag+" "+cameratitle.getText() + extension;
    }

    @SuppressWarnings("SameParameterValue")
    private String makeReducedFileName(String prefix, String extension){
        LocalDateTime ldt = LocalDateTime.now();
        String timetag = ldt.getYear() + "-" + ldt.getMonthValue() + "-" + ldt.getDayOfMonth() + "_" + ldt.getHour() + "-" + ldt.getMinute() + "-" + ldt.getSecond();
        return prefix+" "+timetag+" "+cameratitle.getText() + "_reduced" + extension;
    }

    public void StopLiveView(){
        Capturing.set(false);
        if (mGrabber!=null){
            try{
                mGrabber.close();
                System.out.println("Camera "+cameratitle.getText()+" stopped");
                Platform.runLater(()->setCameraStatus("Camera Stopped"));
            } catch (Exception e){
                raise_log("StopLiveView failed, Unable to Stop Camera, Error: " + e.getMessage());
            }
        }

        TakingPhoto.set(false);
        IsGrabbingLiveView.set(false);
    }

//    public Rect GetFace(UMat mat, boolean isfrontal){
//        if (!mat.empty()){
//            Mat originalmat = new Mat();
//            mat.copyTo(originalmat);
//            Mat graymat = new Mat();
//            opencv_imgproc.cvtColor(originalmat,graymat, COLOR_BGR2GRAY);   // convert to grayscale
//            int size = Math.min(graymat.cols(), graymat.rows());
//            int minsize = (int) (size * 0.4);
//            int maxsize = (int) (size * 0.9);
//            System.out.println("GetFace size = "+size+" minsize = "+minsize+" maxsize = "+maxsize);
//            RectVector faces = isfrontal ? Detectors.DetectFrontalFace(graymat, minsize, maxsize) : Detectors.DetectProfileFace(graymat, minsize, maxsize);
//            if (faces.size()>0){
//                Rect result = null;
//                for(Rect xx : faces.get()){
//                    if (result==null){
//                        result = xx;
//                    } else {
//                        if (xx.area()>result.area()){
//                            result = xx;
//                        }
//                    }
//                }
//                return result;
//            }
//        } else raise_log("GetFace failed, Mat is empty");
//        return null;
//    }

    public boolean StartLiveView(LiveCamEvent event, String cameratitle, final boolean use_qr , final boolean use_face) {
        this.event = event;
        if (mGrabber != null) {
            try {

                if (use_qr) raise_log("QR Reader loaded");
                if (use_face) raise_log("Face detector loaded");
                // capture with best resolution
                if (IsPortrait){
                    setFrameHeight(BestSize.width());
                    setFrameWidth(BestSize.height());
                } else {
                    setFrameHeight(BestSize.height());
                    setFrameWidth(BestSize.width());
                }

                LiveFPS = 0;
                mGrabber.start();
                mGrabber.flush();
                System.out.println("Camera "+cameratitle+" started");

                Capturing.set(true);
                if (event!=null) event.onStartCapturing();

                Task<Image> task = new Task<>() {
                    @SuppressWarnings("BusyWait")
                    @Override
                    protected Image call()  {
                        // repeat until capturing is false
                        AtomicInteger fps = new AtomicInteger(0);
                        // eye state = -1 means unknown, 0 means closed, 1 means open
                        int eye_state = -1;
                        boolean waiting_for_second_blink = false;
                        long last_blink = 0;

                        TimerTask fpsTask = new TimerTask() {
                            @Override
                            public void run() {
                                int fpsval = fps.getAndSet(0);
                                if (fpsval!=LiveFPS){
                                    LiveFPS = fpsval;
                                    if (event!=null) event.onIntervalUpdate();
                                }
                            }
                        };





                        Timer timer = new java.util.Timer();
                        timer.scheduleAtFixedRate(fpsTask, 1000, 1000);


                        boolean have_frontal_face;
                        boolean have_left_45_face;
                        int _face_width;
                        int _face_height;
                        boolean have_palm = false;
                        boolean have_fist = false;
                        int no_face_counter = 0;
                        int face_counter = 0;
                        //int open_eye_counter = 0;
                        //int close_eye_counter = 0;

                        while (Capturing.get()) {
                            try {
                                // selama proses pengambilan foto, jangan ambil frame
                                while(TakingPhoto.get() && Capturing.get()){
                                    Thread.sleep(10);
                                }

                                if (!Capturing.get()) return null;
                                IsGrabbingLiveView.set(true);
                                Frame frame;
                                try{
                                    frame = mGrabber.grab();    // grab frame
                                } catch (Exception e){
                                    frame = null;

                                    if (e.getMessage()!=null && !e.getMessage().isEmpty()){
                                        String msg = e.getMessage();
                                        if (msg.contains("start() been called")){
                                            if (Capturing.get()){
                                                System.out.println("Camera "+cameratitle+" has been stopped, restarting");
                                                mGrabber.close();
                                                Wait(100);
                                                mGrabber.start();
                                                mGrabber.flush();
                                            } else {
                                                System.out.println("Camera "+cameratitle+" has been stopped, not restarting");
                                            }
                                        } else System.out.println("Exception on grab frame from camera "+cameratitle+", Message : "+e.getMessage());
                                    }
                                }
                                if (frame==null) continue;
                                Mat mat = matconverter.convert(frame);  // convert to Mat
                                fps.incrementAndGet();

                                UMat originalmat = new UMat();
                                mat.copyTo(originalmat);        // copy to originalmat for using OpenCL
                                if (config.isMirrorCamera()){
                                    // revisi 18/03/2025
                                    UMat flippedmat = new UMat();
                                    opencv_core.flip(originalmat, flippedmat, 0);  // flip vertical
                                    flippedmat.copyTo(originalmat);
                                }
                                if (config.isFlipCamera()){
                                    // revisi 18/03/2025
                                    UMat flippedmat = new UMat();
                                    opencv_core.flip(originalmat, flippedmat, 1);  // flip horizontal
                                    flippedmat.copyTo(originalmat);
                                }

                                // rotate 90 degree counter clockwise karena kamera potrait
                                opencv_core.rotate(originalmat, BestMat, opencv_core.ROTATE_90_COUNTERCLOCKWISE);



                                IsGrabbingLiveView.set(false);

                                if (!BestMat.empty()) {
                                    opencv_imgproc.resize(BestMat, LiveMat, LiveSize);  // resize to LiveSize
                                    opencv_imgproc.cvtColor(LiveMat,GrayMat, COLOR_BGR2GRAY);   // convert to grayscale

                                    if (use_qr){
                                        String qr = DetectQRFromMat(GrayMat);
                                        if (ValidBarCode(qr)){
                                            qrtext = qr;
                                            if (event!=null) event.onDetectedQRCode(qrtext);
                                        }
                                    }
                                    if (use_face){
                                        DetectorResult theface = null;
                                        have_frontal_face = false;
                                        have_left_45_face = false;
                                        _face_width = 0;
                                        _face_height = 0;

                                        List<DetectorResult> frontalfaces = HaveFrontalFace(GrayMat);
                                        if (!frontalfaces.isEmpty()){

                                            for(DetectorResult rect : frontalfaces){
                                                if (rect.haveFace() ){
                                                    rect.FaceRectangle(LiveMat);
                                                    rect.EyesRectangle(LiveMat);
                                                    if (rect.getFaceWidth()>_face_width) _face_width = rect.getFaceWidth();
                                                    if (rect.getFaceHeight()>_face_height) _face_height = rect.getFaceHeight();
                                                    theface = rect;
                                                    have_frontal_face = true;
                                                }
                                            }
                                        } else {
                                            // gak punya frontal face
                                            // coba cek punya profile left face 45 gak
                                            List<DetectorResult> Left45Faces = HaveLeft45Face(GrayMat);
                                            if (!Left45Faces.isEmpty()){
                                                for(DetectorResult rect : Left45Faces){
                                                    if (rect.haveFace()){
                                                        rect.FaceRectangle(LiveMat);
                                                        rect.EyesRectangle(LiveMat);
                                                        if (rect.getFaceWidth()>_face_width) _face_width = rect.getFaceWidth();
                                                        if (rect.getFaceHeight()>_face_height) _face_height = rect.getFaceHeight();
                                                        have_left_45_face = true;

                                                    }
                                                }
                                            }
                                        }

                                        if (have_frontal_face){

                                            if (face_counter<5){
                                                face_counter++;
                                                //System.out.println("Frontal Face Counter = "+face_counter+ " from camera "+cameratitle+" eye count = "+theface.getEyesCount());
                                                continue;
                                            }

                                            no_face_counter = 0;
                                            if (event!=null) event.onFrontalFaceDetector(true, _face_width, _face_height);
                                            LabelVisible(face_indicator,true);

                                            if (theface.getFace()!=null){
                                                LiveMatROI = new Rect(theface.getFace().x(), theface.getFace().y(), theface.getFace().width(), theface.getFace().height());
                                                //System.out.println("Frontal Face Detected from camera "+cameratitle+" "+RectToString(LiveMatROI));
                                            }

                                            if (theface.haveEyes()){
                                                // ada mata (buka mata)
//                                                close_eye_counter=0;
//                                                if (open_eye_counter<1){
//                                                    open_eye_counter++;
//                                                    continue;
//                                                }
                                                //System.out.println("Valid Open Eyes");


                                                if (eye_state!=1){
                                                    // transisi dari tutup mata ke buka mata
                                                    if (eye_state==-1) {
                                                        System.out.println("First Eye Detected from camera "+cameratitle);
                                                        eye_state=1;
                                                        continue;
                                                    }
                                                    System.out.println("Transition from close to open eyes");
                                                    eye_state = 1;
                                                    if (event!=null) event.onEyeDetector(true);

                                                    LabelVisible(eye_indicator,true);

                                                    long now = System.currentTimeMillis();
                                                    if (waiting_for_second_blink){
                                                        long diff = now - last_blink;
                                                        // kalau beda waktu antara blink 1 dan blink 2 kurang dari 2 detik
                                                        if (diff<=2000){
                                                            System.out.println("Double Blink Detected from camera "+cameratitle);
                                                            if (event!=null) event.onBlink((int)diff);
                                                        }
                                                        waiting_for_second_blink = false;
                                                    } else {
                                                        waiting_for_second_blink = true;
                                                        System.out.println("First Blink Detected from camera "+cameratitle);
                                                    }
                                                    last_blink = now;
                                                }
                                            } else {
                                                // ada muka, tidak ada mata
                                                // transisi dari buka mata ke tutup mata
//                                                open_eye_counter=0;
//                                                if (close_eye_counter<1){
//                                                    close_eye_counter++;
//                                                    continue;
//                                                }
                                                //System.out.println("Valid Closed Eyes");
                                                if (eye_state!=0){
                                                    System.out.println("Transition from open to close eyes");
                                                    eye_state = 0;
                                                    if (event!=null) event.onEyeDetector(false);
                                                    LabelVisible(eye_indicator,false);
                                                }
                                            }
                                        } else if (have_left_45_face ){
                                            no_face_counter = 0;
                                            if (event!=null) event.onProfileFaceDetector(true, _face_width, _face_height);
                                            LabelVisible(face_indicator,true);

                                        } else {
                                            // no face detected, but let's not cancel the previous state immediately

                                            if (no_face_counter>90){
                                                // kalau tidak ada face selama 30 frame, reset state
                                                // 30 frame approximately 2 second
                                                eye_state = -1;
                                                last_blink = 0;
                                                waiting_for_second_blink = false;
                                                face_counter = 0;
//                                                if (close_eye_counter!=0 || open_eye_counter!=0){
//                                                    close_eye_counter=0;
//                                                    open_eye_counter=0;
//                                                    System.out.println("Reset Open and Close Eyes");
//                                                }

                                                if (event!=null) {
                                                    event.onFrontalFaceDetector(false, _face_width, _face_height);
                                                    event.onProfileFaceDetector(false, _face_width, _face_height);
                                                    LabelVisible(face_indicator,false);
                                                    LabelVisible(eye_indicator,false);
                                                }
                                            } else no_face_counter++;

                                        }

                                    }

                                    UMat rgbmat = new UMat(LiveMat.size(), CV_8UC3);
                                    cvtColor(LiveMat, rgbmat, COLOR_BGR2RGB);

                                    Mat imgmat = new Mat();
                                    rgbmat.copyTo(imgmat);  // copy back to CPU
                                    // Update Task Value usign matToWritableImage
                                    updateValue(matToWritableImage(imgmat, imgmat.cols(), imgmat.rows()));
                                }
                            } catch (Exception e) {
                                if (ValidString(e.getMessage())){
                                    raise_log("Unable to Grab Frame, Error: " + e.getMessage());
                                }
                                //if (!Capturing.get()) Platform.runLater(this::StopLiveView);
                            }
                        }
                        timer.cancel();
                        return null;
                    }
                };

                // value dari task, yaitu image, akan diupdate ke camerastream
                task.valueProperty().addListener((obs, oldVal, newVal) -> {
                    if (newVal != null) {
                        setCameraStream(newVal);
                    }
                });

                // start task
                new Thread(task).start();

                return true;
            } catch (Exception e) {
                raise_log("StartLiveView failed, Unable to Start Camera, Error: " + e.getMessage());
            }
        } else raise_log("StartLiveView failed, grabber is null");
        return false;
    }



    /**
     * Remap LiveMatROI to BestMatROI and ReducedMatROI Resolution
     * @param scaleX scale factor for width
     * @param scaleY scale factor for height
     */
    public void RemapROI(double scaleX, double scaleY, boolean printdebug){
        BestMatROI = null;
        ReducedMatROI = null;
        if (ValidROI(LiveMatROI)){
            if (ROIInsideUMat(LiveMatROI, LiveMat)){
                if (printdebug) System.out.println("LiveMatROI camera "+cameratitle.getText()+" = "+RectToString(LiveMatROI));

                double scaleXBest = 1.0*BestSize.width()/LiveSize.width();
                double scaleYBest = 1.0*BestSize.height()/LiveSize.height();
                int XBest = (int) (LiveMatROI.x()*scaleXBest);
                int YBest = (int) (LiveMatROI.y()*scaleYBest);
                int WBest = (int) (LiveMatROI.width()*scaleXBest);
                int HBest = (int) (LiveMatROI.height()*scaleYBest);
                int deltaWBest = (int) (BestSize.width() * scaleX);
                int deltaHBest = (int) (BestSize.height() * scaleY);
                XBest = XBest - deltaWBest/2;
                if (XBest<0) XBest = 0;
                YBest = YBest - deltaHBest/2;
                if (YBest<0) YBest = 0;
                WBest = WBest + deltaWBest;
                if (WBest>BestSize.width()) WBest = BestSize.width();
                HBest = HBest + deltaHBest;
                if (HBest>BestSize.height()) HBest = BestSize.height();
                BestMatROI = new Rect(XBest, YBest, WBest, HBest);

                if (printdebug){
                    System.out.println("scaleXBest = "+scaleXBest+" scaleYBest = "+scaleYBest);
                    System.out.println("BestMatROI camera "+cameratitle.getText()+" = "+RectToString(BestMatROI));
                }

                double scaleXReduced = 1.0*ReducedSize.width()/LiveSize.width();
                double scaleYReduced = 1.0*ReducedSize.height()/LiveSize.height();
                int XReduced = (int) (LiveMatROI.x()*scaleXReduced);
                int YReduced = (int) (LiveMatROI.y()*scaleYReduced);
                int WReduced = (int) (LiveMatROI.width()*scaleXReduced);
                int HReduced = (int) (LiveMatROI.height()*scaleYReduced);
                int deltaWReduced = (int) (ReducedSize.width() * scaleX);
                int deltaHReduced = (int) (ReducedSize.height() * scaleY);
                XReduced = XReduced - deltaWReduced/2;
                if (XReduced<0) XReduced = 0;
                YReduced = YReduced - deltaHReduced/2;
                if (YReduced<0) YReduced = 0;
                WReduced = WReduced + deltaWReduced;
                if (WReduced>ReducedSize.width()) WReduced = ReducedSize.width();
                HReduced = HReduced + deltaHReduced;
                if (HReduced>ReducedSize.height()) HReduced = ReducedSize.height();
                ReducedMatROI = new Rect(XReduced, YReduced, WReduced, HReduced);
                if (printdebug){
                    System.out.println("scaleXReduced = "+scaleXReduced+" scaleYReduced = "+scaleYReduced);
                    System.out.println("ReducedMatROI camera "+cameratitle.getText()+" = "+RectToString(ReducedMatROI));
                }


            } //else System.out.println("LiveMatROI is Outside LiveMat for camera "+cameratitle.getText());
        } //else System.out.println("LiveMatROI is invalid for camera "+cameratitle.getText());

    }



    /**
     * Detect QR Code from Mat
     * @param graymat Mat in Gray Scale
     * @return QR Code Text, or null if not detected
     */
    private String DetectQRFromMat(UMat graymat){
        if (qrreader!=null){
            Mat mat = new Mat();
            graymat.copyTo(mat);    // back to CPU, because zxing only accept BufferedImage
            BufferedImage bufferedImage = matToBufferedImage(mat);
            String title = cameratitle.getText();
            BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(bufferedImage)));
            try{
                Result result = qrreader.decode(binaryBitmap);
                if (result!=null){
                    return result.getText();
                }
            } catch (NotFoundException ignored) {
            }
        }
        return null;
    }





    private double getBrightnessFromGrayMat(Mat graymat){
        Scalar mean = mean(graymat);
        return mean.get(0);
    }

    private WritableImage matToWritableImage(Mat mat, int cols, int rows){
        WritableImage writableImage = new WritableImage(cols, rows);
        ByteBuffer buffer = mat.createBuffer();
        PixelFormat<ByteBuffer> pixelFormat = PixelFormat.getByteRgbInstance();
        writableImage.getPixelWriter().setPixels(0, 0, mat.cols(), mat.rows(), pixelFormat, buffer, mat.cols() * 3);
        return writableImage;
    }

    private BufferedImage matToBufferedImage(Mat mat){
        int type = BufferedImage.TYPE_BYTE_GRAY;
        if (mat.channels() > 1) {
            type = BufferedImage.TYPE_3BYTE_BGR;
        }
        BufferedImage image = new BufferedImage(mat.cols(), mat.rows(), type);
        byte[] data = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();

        mat.data().get(data);
        return image;
    }

    private void raise_log(String msg){
        if (event!=null) event.onLog(msg);
    }
}
