package com.luxand.livenessrecognition;

import Luxand.FSDK;
import Luxand.FSDKCam;
import Luxand.FSDK.HImage;
import Luxand.FSDK.HTracker;
import Luxand.FSDKCam.HCamera;
import Luxand.FSDKCam.TCameras;
import Luxand.FSDK.FSDK_IMAGEMODE;
import Luxand.FSDKCam.FSDK_VideoFormats;

import java.awt.*;
import javax.swing.*;

import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.awt.font.TextLayout;
import java.awt.geom.Rectangle2D;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.concurrent.Semaphore;
import java.awt.font.FontRenderContext;
import java.lang.reflect.InvocationTargetException;

public class LivenessRecognitionPanel extends JPanel implements MouseListener {

    private static final int MAX_FACES = 5;
    private static final String trackerMemoryFile = "tracker70.dat";
    private static final Stroke faceStroke   = new BasicStroke(7);
    private static final Stroke hoverStroke  = new BasicStroke(3);
    private static final Stroke activeStroke = new BasicStroke(4);
    private static final Font textFont = new Font("Arial", Font.PLAIN, 25);
    private static final Font liveFont = new Font("Arial", Font.BOLD, 25);

    private final HImage image = new HImage();
    private final HCamera camera = new HCamera();
    private final Image[] awtImage = new Image[2];
    private final HTracker tracker = new HTracker();
    private final Semaphore semaphore = new Semaphore(1);

    private final long[] facesCount = new long[1];
    private final long[] IDs = new long[MAX_FACES];
    private final ArrayList<Long> missed = new ArrayList<>();
    private final Map<Long, FaceLocator> locators = new HashMap<>();

    private double ratioX = 0;
    private double ratioY = 0;
    private final int[] imageWidth = new int[1];
    private final int[] imageHeight = new int[1];

    public LivenessRecognitionPanel() {
        addMouseListener(this);
    }
    
    public void initialize() {
        if (FSDK.LoadTrackerMemoryFromFile(tracker, trackerMemoryFile) != FSDK.FSDKE_OK)
            Utils.checkErrorCode(() -> FSDK.CreateTracker(tracker), "FSDK_CreateTracker");

        int[] err = new int[1];
        Utils.checkErrorCode(() -> FSDK.SetTrackerMultipleParameters(tracker,
                "RecognizeFaces=true;" +
                        "DetectFacialFeatures=true;" +
                        "InternalResizeWidth=256;" +
                        "HandleArbitraryRotations=false;" +
                        "DetermineFaceRotationAngle=false;" +
                        "FaceDetectionThreshold=5;" +
                        "DetectGender=true;" +
                        "DetectAge=true;" +
                        "DetectExpression=true;" +
                        "DetectAngles=true;",
                err
        ), "FSDK_SetTrackerMultipleParameters");

        Utils.checkErrorCode(FSDKCam::InitializeCapturing, "FSDK_InitializeCapturing");

        final int[] count = new int[1];
        final TCameras cameras = new TCameras();
        Utils.checkErrorCode(() -> FSDKCam.GetCameraList(cameras, count), "FSDK_GetCameraList");
        if (cameras.cameras.length == 0) {
            JOptionPane.showMessageDialog(null, "No camera devices found", "FaceSDK Error", JOptionPane.ERROR_MESSAGE);
            System.exit(-1);
        }

        final String cameraName = cameras.cameras[0];
        final FSDK_VideoFormats videoFormats = new FSDK_VideoFormats();
        Utils.checkErrorCode(() -> FSDKCam.GetVideoFormatList(cameraName, videoFormats, count), "FSDK_GetVideoFormatList");
        Utils.checkErrorCode(() -> FSDKCam.SetVideoFormat(cameraName, videoFormats.formats[0]), "FSDK_SetVideoFormat");
        Utils.checkErrorCode(() -> FSDKCam.OpenVideoCamera(cameraName, camera), "FSDK_OpenCamera");

        new Thread(this::recognition).start();
    }

    private boolean seenID(final long id) {
        for (int i = 0; i < facesCount[0]; ++i)
            if (IDs[i] == id)
                return true;
        return false;
    }

    private void recognition() {
        while (true) {
            Utils.checkErrorCode(() -> FSDKCam.GrabFrame(camera, image), "FSDK_GrabFrame");
            Utils.checkErrorCode(() -> FSDK.MirrorImage(image, true), "FSDK_MirrorImage");
            Utils.checkErrorCode(() -> FSDK.SaveImageToAWTImage(image, awtImage, FSDK_IMAGEMODE.FSDK_IMAGE_COLOR_24BIT), "FSDK_SaveToAWTImage");
            Utils.checkErrorCode(() -> FSDK.FeedFrame(tracker, 0, image, facesCount, IDs), "FSDK_FeedFrame");

            if (imageWidth[0] == 0) {
                Utils.checkErrorCode(() -> FSDK.GetImageWidth(image, imageWidth), "FSDK_GetImageWidth");
                Utils.checkErrorCode(() -> FSDK.GetImageHeight(image, imageHeight), "FSDK_GetImageHeight");
                SwingUtilities.getWindowAncestor(this).setSize(imageWidth[0], imageHeight[0]);
            }

            ratioX = (double)getWidth()  / imageWidth[0];
            ratioY = (double)getHeight() / imageHeight[0];

            Utils.checkErrorCode(() -> FSDK.FreeImage(image), "FSDK_FreeImage");

            semaphore.acquireUninterruptibly();
            awtImage[1] = awtImage[0];

            for (int i = 0; i < facesCount[0]; ++i)
                if (!locators.containsKey(IDs[i]))
                    locators.put(IDs[i], new FaceLocator(IDs[i], tracker));

            missed.clear();
            for (Map.Entry<Long, FaceLocator> entry : locators.entrySet()) {
                if (!seenID(entry.getKey()))
                    missed.add(entry.getKey());
                else
                    entry.getValue().update();
            }

            for (final long id : missed) {
                FaceLocator locator = locators.get(id);

                for (int i = 0; i < facesCount[0]; ++i)
                    if (IDs[i] != id && locator.doesIntersect(locators.get(IDs[i]))) {
                        locators.remove(id);
                        break;
                    }

                if (locators.containsKey(id) && !locator.update())
                    locators.remove(id);
            }

            semaphore.release();

            try {
                SwingUtilities.invokeAndWait(this::repaint);
            } catch (InterruptedException | InvocationTargetException ignored) {}
        }
    }

    private void drawOval(final FaceLocator.FaceRectangle frame, final Graphics2D graphics) {
        graphics.drawOval(
                (int)(frame.x1 * ratioX),
                (int)(frame.y1 * ratioY),
                (int)(frame.w * ratioX),
                (int)(frame.h * ratioY)
        );
    }

    private void drawString(final String value, final FaceLocator.FaceRectangle frame, final Color color, final Font font, final Graphics2D graphics) {
        final FontRenderContext frc = graphics.getFontRenderContext();
        final TextLayout layout = new TextLayout(value, font, frc);
        final Rectangle2D bounds = layout.getBounds();
        
        final int x = (int)((frame.x1 + frame.w / 2) * ratioX - bounds.getWidth() / 2);
        final int y = (int)((frame.y1 + frame.h * 0.1)* ratioY);
        
        graphics.setFont(font);
        
        graphics.setColor(Color.black);
        graphics.drawString(value, x + 2, y + 2);
        
        graphics.setColor(color);
        graphics.drawString(value, x, y);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        semaphore.acquireUninterruptibly();
        g.drawImage(awtImage[1], 0, 0, getWidth(), getHeight(), this);

        Graphics2D graphics = (Graphics2D)g;
        final PointerInfo pointerInfo = MouseInfo.getPointerInfo();
        Point mousePoint = null;
        if (pointerInfo != null) {
            mousePoint = pointerInfo.getLocation();
            SwingUtilities.convertPointFromScreen(mousePoint, this);

            mousePoint.x /= ratioX;
            mousePoint.y /= ratioY;
        }

        for (Map.Entry<Long, FaceLocator> entry : locators.entrySet()) {
            final FaceLocator locator = entry.getValue();
            final FaceLocator.FaceRectangle frame = locator.getFrame();

            graphics.setColor(Color.white);
            graphics.setStroke(faceStroke);
            drawOval(frame, graphics);

            if (mousePoint != null && locator.contains(mousePoint.x, mousePoint.y)) {
                graphics.setColor(Color.blue);
                graphics.setStroke(hoverStroke);
            }
               
            Font font = null;
            Color color = null;
            String message = "";

            if (locator.isActive()) {
                graphics.setColor(color.red);
                graphics.setStroke(activeStroke);
                   
                font = textFont;
                color = Color.LIGHT_GRAY;
                message = locator.getCurrentCommand().toString();
                locator.updateState();
            }

            if (locator.isLive()) {
                graphics.setColor(Color.green);
                graphics.setStroke(activeStroke);

                font = liveFont;
                message = "LIVE!";
                color = Color.green;
            }

            drawOval(frame, graphics);
            if (!message.isEmpty())
                drawString(message, frame, color, font, graphics);

        }
        semaphore.release();
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        final Point point = e.getPoint();
        point.x /= ratioX;
        point.y /= ratioY;

        semaphore.acquireUninterruptibly();
        for (FaceLocator locator : locators.values())
            locator.setActive(locator.contains(point.x, point.y));
        semaphore.release();
    }

    @Override
    public void mousePressed(MouseEvent e) {}

    @Override
    public void mouseReleased(MouseEvent e) {}

    @Override
    public void mouseEntered(MouseEvent e) {}

    @Override
    public void mouseExited(MouseEvent e) {}
}
