package com.luxand.livenessrecognition;

import java.util.*;
import Luxand.FSDK;
import Luxand.FSDK.*;

public class FaceLocator {

    public static class FaceRectangle {

        public double x1, y1, x2, y2, w, h;

        public void set(final double x, final double y, final double w, final double h) {
            this.w = w;
            this.h = h;

            x1 = x;
            y1 = y;
            x2 = x1 + w;
            y2 = y1 + h;
        }

    }

    public enum Command {
        TURN_LEFT("Turn left"),
        TURN_RIGHT("Turn right"),
        LOOK_STRAIGHT("Look straight"),
        LOOK_UP("Look up"),
        LOOK_DOWN("Look down"),
        SMILE("Make a smile");

        private final String description;

        Command(String value) {
            description = value;
        }

        public String toString() {
            return description;
        }

    }

    public static class Point {

        public double x = 0;
        public double y = 0;

    }

    private static class LowPassFilter {

        private static final double a = 0.35;
        private Double y = null;

        public double pass(double x) {
            y = a * x + (1 - a) * (y == null ? x : y);
            return y;
        }

    }

    private static final double HOR_CONF_LEVEL_LOW = 2;
    private static final double HOR_CONF_LEVEL_HIGH = 17;

    private static final double UP_CONF_LEVEL_LOW = 2;
    private static final double UP_CONF_LEVEL_HIGH = 5;
    private static final double DOWN_CONF_LEVEL_LOW = -2;
    private static final double DOWN_CONF_LEVEL_HIGH = -5;

    private static final double SMILE_CONF_LEVEL_LOW = 0.3;
    private static final double SMILE_CONF_LEVEL_HIGH = 0.6;

    private static final int[] LEFT_EYE = new int[] {
            FSDK.FSDKP_LEFT_EYE,
            FSDK.FSDKP_LEFT_EYE_INNER_CORNER,
            FSDK.FSDKP_LEFT_EYE_OUTER_CORNER,
            FSDK.FSDKP_LEFT_EYE_LOWER_LINE1,
            FSDK.FSDKP_LEFT_EYE_LOWER_LINE2,
            FSDK.FSDKP_LEFT_EYE_LOWER_LINE3,
            FSDK.FSDKP_LEFT_EYE_UPPER_LINE1,
            FSDK.FSDKP_LEFT_EYE_UPPER_LINE2,
            FSDK.FSDKP_LEFT_EYE_UPPER_LINE3,
            FSDK.FSDKP_LEFT_EYE_RIGHT_IRIS_CORNER,
            FSDK.FSDKP_LEFT_EYE_LEFT_IRIS_CORNER
    };

    private static final int[] RIGHT_EYE = new int[] {
            FSDK.FSDKP_RIGHT_EYE,
            FSDK.FSDKP_RIGHT_EYE_INNER_CORNER,
            FSDK.FSDKP_RIGHT_EYE_OUTER_CORNER,
            FSDK.FSDKP_RIGHT_EYE_LOWER_LINE1,
            FSDK.FSDKP_RIGHT_EYE_LOWER_LINE2,
            FSDK.FSDKP_RIGHT_EYE_LOWER_LINE3,
            FSDK.FSDKP_RIGHT_EYE_UPPER_LINE1,
            FSDK.FSDKP_RIGHT_EYE_UPPER_LINE2,
            FSDK.FSDKP_RIGHT_EYE_UPPER_LINE3,
            FSDK.FSDKP_RIGHT_EYE_RIGHT_IRIS_CORNER,
            FSDK.FSDKP_RIGHT_EYE_LEFT_IRIS_CORNER
    };

    private static final Set<Command> LEFT_RIGHT = new HashSet<Command>() {{ add(Command.TURN_LEFT); add(Command.TURN_RIGHT); }};
    private static final Set<Command> UP_DOWN = new HashSet<Command>() {{ add(Command.LOOK_DOWN); add(Command.LOOK_UP); }};

    private static final Random random = new Random();

    private final long faceID;
    private int commandID;
    private int countdown;

    private final FaceRectangle frame = new FaceRectangle();

    private boolean live = false;
    private boolean active = false;

    private final Point center = new Point();

    private final LowPassFilter lpf = new LowPassFilter();

    private final HTracker tracker;

    private List<Command> commands;
    private final Set<Command> state = new HashSet<Command>() {{ add(Command.LOOK_STRAIGHT); }};

    private final FSDK_Features.ByReference features = new FSDK_Features.ByReference();

    public FaceLocator(final long faceID, final HTracker tracker) {
        this.faceID = faceID;
        this.tracker = tracker;
    }

    public FSDK_Features getFeatures() {
        return features;
    }

    public Command getCurrentCommand() {
        return commands.get(commandID);
    }

    public FaceRectangle getFrame() {
        return frame;
    }

    public boolean doesIntersect(final FaceLocator other) {
        return !(
                frame.x1 >= other.frame.x2 ||
                frame.x2 <  other.frame.x1 ||
                frame.y1 >= other.frame.y2 ||
                frame.y2 <  other.frame.y1
        );
    }

    public boolean contains(double x, double y) {
        x -= center.x;
        y -= center.y;

        return Math.pow(2 * x / frame.w, 2) + Math.pow(2 * y / frame.h, 2) <= 1;
    }

    public boolean isActive() {
        return active && !live;
    }

    public boolean isLive() {
        return live;
    }

    public void setActive(boolean active) {
        this.active = active;
        if (!active)
            return;

        live = false;
        commandID = 0;

        commands = new ArrayList<Command>() {{ add(Command.LOOK_STRAIGHT); }};

        while (commands.size() < 6) {
            Command cmd = Command.values()[random.nextInt(6)];
            if (commands.get(commands.size() - 1) != cmd)
                commands.add(cmd);
        }

        if (!commands.contains(Command.SMILE))
            commands.set(random.nextInt(commands.size() - 1), Command.SMILE);
    }

    private boolean approveCommand(final Command command) {
        if (!state.contains(command))
            return false;

        if (command == Command.SMILE)
            return true;

        return !state.contains(Command.SMILE);
    }

    private Map<String, String> getKeyValuePairs(String value) {
        String[] pairs = value.split(";");
        return new HashMap<String, String>() {{
            for (String pair : pairs) {
                String[] keyValue = pair.split("=");
                put(keyValue[0].toLowerCase(), keyValue[1]);
            }
        }};
    }

    private double getValue(final Map<String, String> values, String key) {
        if (!values.containsKey(key))
            return 0;

        return Double.parseDouble(values.get(key));
    }

    public void updateState() {
        final String[] result = new String[] { "" };
        if (FSDK.GetTrackerFacialAttribute(tracker, 0, faceID, "Expression", result, 256) != FSDK.FSDKE_OK)
            return;

        final double smile = getValue(getKeyValuePairs(result[0]), "smile");

        if (FSDK.GetTrackerFacialAttribute(tracker, 0, faceID, "Angles", result, 256) != FSDK.FSDKE_OK)
            return;

        final Map<String, String> keyValuePairs = getKeyValuePairs(result[0]);
        final double pan = getValue(keyValuePairs, "pan");
        final double tilt = getValue(keyValuePairs, "tilt");

        Command st = null;

        if (Math.abs(pan) < HOR_CONF_LEVEL_LOW)
            st = Command.LOOK_STRAIGHT;
        else if (Math.abs(pan) > HOR_CONF_LEVEL_HIGH)
            st = pan > 0 ? Command.TURN_LEFT : Command.TURN_RIGHT;

        if (st != null) {
            state.removeAll(LEFT_RIGHT);
            state.add(st);
        }

        st = null;

        if (tilt > UP_CONF_LEVEL_HIGH)
            st = Command.LOOK_UP;
        else if (tilt < DOWN_CONF_LEVEL_HIGH)
            st = Command.LOOK_DOWN;
        else if (tilt > DOWN_CONF_LEVEL_LOW && tilt < UP_CONF_LEVEL_LOW)
            st = Command.LOOK_STRAIGHT;

        if (st != null) {
            state.removeAll(UP_DOWN);
            state.add(st);
        }

        if (state.contains(Command.LOOK_UP)   || state.contains(Command.LOOK_DOWN) ||
                state.contains(Command.TURN_LEFT) || state.contains(Command.TURN_RIGHT))
            state.remove(Command.LOOK_STRAIGHT);

        if (smile < SMILE_CONF_LEVEL_LOW)
            state.remove(Command.SMILE);
        else if (smile > SMILE_CONF_LEVEL_HIGH)
            state.add(Command.SMILE);

        if (active && !live)
            if (approveCommand(commands.get(commandID))) {
                ++commandID;
                if (commandID >= commands.size())
                    live = true;
            }

        countdown = 35;
    }

    private static TPoint dotCenter(TPoint[] points) {
        TPoint result = new TPoint() {{
            x = 0;
            y = 0;
        }};

        for (TPoint point : points) {
            result.x += point.x;
            result.y += point.y;
        }

        result.x /= points.length;
        result.y /= points.length;

        return result;
    }

    private static TPoint[] getPoints(FSDK_Features features, int[] indices) {
        TPoint[] result = new TPoint[indices.length];

        for (int i = 0; i < result.length; ++i)
            result[i] = features.features[indices[i]];

        return result;
    }

    public boolean update() {
        if (FSDK.GetTrackerFacialFeatures(tracker, 0, faceID, features) != FSDK.FSDKE_OK) {
            countdown -= 1;

            if (countdown <= 8)
                frame.set(frame.x1 + frame.w * 0.025, frame.y1 + frame.h * 0.025, frame.w * 0.95, frame.h * 0.95);

            return countdown > 0;
        }

        final TPoint left  = dotCenter(getPoints(features, LEFT_EYE));
        final TPoint right = dotCenter(getPoints(features, RIGHT_EYE));

        final double w = lpf.pass((right.x - left.x) * 2.8);
        final double h = w * 1.4;

        center.x = (right.x + left.x) / 2.;
        center.y = (right.y + left.y) / 2.;

        frame.set(center.x - w / 2, center.y - h / 2, w, h);
        return true;
    }

}
