/*
 * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
 */
package com.sun.javafx.tk.quantum;

import com.sun.glass.events.KeyEvent;
import com.sun.glass.events.TouchEvent;
import java.util.HashMap;
import java.util.Map;
import javafx.event.EventType;
import javafx.scene.input.SwipeEvent;

public class SwipeGestureRecognizer implements GestureRecognizer {
    private static final boolean VERBOSE = false;

    // Swipes must be longer than that
    private static final double DISTANCE_THRESHOLD = 10; // pixel

    // Traveling this distance against the swipe direction at its end cancels it
    private static final double BACKWARD_DISTANCE_THRASHOLD = 5; // pixel

    // Trajectory tracking period
    static final long TIME_STEP = 100000000; // nanosec

    private SwipeRecognitionState state = SwipeRecognitionState.IDLE;
    MultiTouchTracker tracker = new MultiTouchTracker();
    private ViewScene scene;

    SwipeGestureRecognizer(final ViewScene scene) {
        this.scene = scene;
    }

    @Override
    public void notifyBeginTouchEvent(long time, int modifiers, boolean isDirect,
            int touchEventCount) {
        tracker.params(modifiers, isDirect);
    }

    @Override
    public void notifyNextTouchEvent(long time, int type, long touchId,
                                     int x, int y, int xAbs, int yAbs) {
        switch(type) {
            case TouchEvent.TOUCH_PRESSED:
                tracker.pressed(touchId, time, x, y, xAbs, yAbs);
                break;
            case TouchEvent.TOUCH_STILL:
                // fall through
            case TouchEvent.TOUCH_MOVED:
                tracker.progress(touchId, time, xAbs, yAbs);
                break;
            case TouchEvent.TOUCH_RELEASED:
                tracker.released(touchId, time, x, y, xAbs, yAbs);
                break;
            default:
                throw new RuntimeException("Error in swipe gesture recognition: "
                        + "unknown touch state: " + state);
        }
    }

    @Override
    public void notifyEndTouchEvent(long time) {
        // nothing to do
    }

    private EventType<SwipeEvent> calcSwipeType(TouchPointTracker tracker) {

        final double distanceX = tracker.getDistanceX();
        final double distanceY = tracker.getDistanceY();
        final double absDistanceX = Math.abs(distanceX);
        final double absDistanceY = Math.abs(distanceY);
        
        final boolean horizontal = absDistanceX > absDistanceY;

        final double primaryDistance = horizontal ? distanceX : distanceY;
        final double absPrimaryDistance = horizontal ? absDistanceX : absDistanceY;
        final double absSecondaryDistance = horizontal ? absDistanceY : absDistanceX;
        final double absPrimaryLength = horizontal ?
                tracker.lengthX : tracker.lengthY;
        final double maxSecondaryDeviation = horizontal ?
                tracker.maxDeviationY : tracker.maxDeviationX;
        final double lastPrimaryMovement = horizontal ?
                tracker.lastXMovement : tracker.lastYMovement;

        if (absPrimaryDistance <= DISTANCE_THRESHOLD) {
            // too small movement
            return null;
        }

        if (absSecondaryDistance > absPrimaryDistance * 0.839 /* tan(2Pi/9) */) {
            // too diagonal - in range of 10 degrees
            return null;
        }

        if (maxSecondaryDeviation > absPrimaryLength / (tracker.getDuration() / 100)) {
            // too imprecise for the performed speed (the slower movement, 
            // the higher precision requred)
            return null;
        }

        if (absPrimaryLength > absPrimaryDistance * 1.5) {
            // too much back and forth
            return null;
        }

        if (Math.signum(primaryDistance) != Math.signum(lastPrimaryMovement) &&
                Math.abs(lastPrimaryMovement) > BACKWARD_DISTANCE_THRASHOLD) {
            // gesture finished in the oposite direction
            return null;
        }

        if (horizontal) {
            return tracker.getDistanceX() < 0
                    ? SwipeEvent.SWIPE_LEFT : SwipeEvent.SWIPE_RIGHT;
        } else {
            return tracker.getDistanceY() < 0
                    ? SwipeEvent.SWIPE_UP : SwipeEvent.SWIPE_DOWN;
        }
    }

    private void handleSwipeType(EventType<SwipeEvent> swipeType,
            CenterComputer cc, int touchCount, int modifiers, boolean isDirect) {

        if (swipeType == null) {
            return;
        }

        if (VERBOSE) {
            System.err.println("handleSwipeType swipeType=" + swipeType);
        }

        scene.sceneListener.swipeEvent(swipeType, touchCount,
                cc.getX(),
                cc.getY(),
                cc.getAbsX(),
                cc.getAbsY(),
                (modifiers & KeyEvent.MODIFIER_SHIFT) != 0,
                (modifiers & KeyEvent.MODIFIER_CONTROL) != 0,
                (modifiers & KeyEvent.MODIFIER_ALT) != 0,
                (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0,
                isDirect);
    }

    private class CenterComputer {
        double totalAbsX = 0, totalAbsY = 0;
        double totalX = 0, totalY = 0;
        int count = 0;

        public void add(double x, double y, double xAbs, double yAbs) {
            totalAbsX += xAbs;
            totalAbsY += yAbs;
            totalX += x;
            totalY += y;

            count++;
        }

        public double getX() {
            return count == 0 ? 0 : totalX / count;
        }

        public double getY() {
            return count == 0 ? 0 : totalY / count;
        }

        public double getAbsX() {
            return count == 0 ? 0 : totalAbsX / count;
        }

        public double getAbsY() {
            return count == 0 ? 0 : totalAbsY / count;
        }

        public void reset() {
            totalX = 0;
            totalY = 0;
            totalAbsX = 0;
            totalAbsY = 0;
            count = 0;
        }
    }

    private class MultiTouchTracker {
        SwipeRecognitionState state = SwipeRecognitionState.IDLE;
        Map<Long, TouchPointTracker> trackers =
                new HashMap<Long, TouchPointTracker>();
        CenterComputer cc = new CenterComputer();
        int modifiers;
        boolean direct;
        private int touchCount;
        private int currentTouchCount;
        private EventType<SwipeEvent> type;

        public void params(int modifiers, boolean direct) {
            this.modifiers = modifiers;
            this.direct = direct;
        }

        public void pressed(long id, long nanos, int x, int y, int xAbs, int yAbs) {
            currentTouchCount++;
            switch (state) {
                case IDLE:
                    currentTouchCount = 1;
                    state = SwipeRecognitionState.ADDING;
                    // fallthrough
                case ADDING:
                    TouchPointTracker tracker = new TouchPointTracker();
                    tracker.start(nanos, x, y, xAbs, yAbs);
                    trackers.put(id, tracker);
                    break;
                case REMOVING:
                    // we don't allow for swipes with varying touch count
                    state = SwipeRecognitionState.FAILURE;
                    break;
            }
        }

        public void released(long id, long nanos, int x, int y, int xAbs, int yAbs) {
            if (state != SwipeRecognitionState.FAILURE) {
                TouchPointTracker tracker = trackers.get(id);

                if (tracker == null) {
                    // we don't know this ID, something went completely wrong
                    state = SwipeRecognitionState.FAILURE;
                    throw new RuntimeException("Error in swipe gesture "
                            + "recognition: released unknown touch point");
                }

                tracker.end(nanos, x, y, xAbs, yAbs);
                cc.add(tracker.beginX, tracker.beginY,
                        tracker.beginAbsX, tracker.beginAbsY);
                cc.add(tracker.endX, tracker.endY,
                        tracker.endAbsX, tracker.endAbsY);

                final EventType<SwipeEvent> swipeType = calcSwipeType(tracker);

                switch (state) {
                    case IDLE:
                        reset();
                        throw new RuntimeException("Error in swipe gesture "
                                + "recognition: released touch point outside "
                                + "of gesture");
                    case ADDING:
                        state = SwipeRecognitionState.REMOVING;
                        touchCount = currentTouchCount;
                        type = swipeType;
                        break;
                    case REMOVING:
                        if (type != swipeType) {
                            // each finger does something else
                            state = SwipeRecognitionState.FAILURE;
                        }
                }
                trackers.remove(id);
            }

            currentTouchCount--;

            if (currentTouchCount == 0) {
                if (state == SwipeRecognitionState.REMOVING) {
                    handleSwipeType(type, cc, touchCount, modifiers, direct);
                }

                state = SwipeRecognitionState.IDLE;
                reset();
            }
        }

        public void progress(long id, long nanos, int x, int y) {

            if (state == SwipeRecognitionState.FAILURE) {
                return;
            }

            TouchPointTracker tracker = trackers.get(id);

            if (tracker == null) {
                // we don't know this ID, something went completely wrong
                state = SwipeRecognitionState.FAILURE;
                throw new RuntimeException("Error in swipe gesture "
                        + "recognition: reported unknown touch point");
            }

            tracker.progress(nanos, x, y);
        }

        void reset() {
            trackers.clear();
            cc.reset();
            state = SwipeRecognitionState.IDLE;
        }
    }

    private class TouchPointTracker {
        long time, beginTime, endTime;
        double beginX, beginY, endX, endY;
        double beginAbsX, beginAbsY, endAbsX, endAbsY;
        double lengthX, lengthY;
        double maxDeviationX, maxDeviationY;
        double lastXMovement, lastYMovement;
        double lastX, lastY;

        public void start(long nanos, double x, double y, double absX, double absY) {
            beginX = x;
            beginY = y;
            beginAbsX = absX;
            beginAbsY = absY;
            lastX = absX;
            lastY = absY;
            beginTime = nanos / 1000000;
        }

        public void end(long nanos, double x, double y, double absX, double absY) {
            progress(nanos, x, y);
            endX = x;
            endY = y;
            endAbsX = absX;
            endAbsY = absY;
            endTime = nanos / 1000000;
        }

        public void progress(long nanos, double x, double y) {
            if (nanos > time + TIME_STEP) {
                final double deltaX = x - lastX;
                final double deltaY = y - lastY;

                time = nanos;
                lengthX += Math.abs(deltaX);
                lengthY += Math.abs(deltaY);
                lastX = x;
                lastY = y;

                final double devX = Math.abs(x - beginAbsX);
                if (devX > maxDeviationX) { maxDeviationX = devX; }

                final double devY = Math.abs(y - beginAbsY);
                if (devY > maxDeviationY) { maxDeviationY = devY; }

                if (Math.signum(deltaX) == Math.signum(lastXMovement)) {
                    lastXMovement += deltaX;
                } else {
                    lastXMovement = deltaX;
                }

                if (Math.signum(deltaY) == Math.signum(lastYMovement)) {
                    lastYMovement += deltaY;
                } else {
                    lastYMovement = deltaY;
                }
            }
        }

        public double getDistanceX() {
            return endX - beginX;
        }

        public double getDistanceY() {
            return endY - beginY;
        }

        public long getDuration() {
            return endTime - beginTime;
        }
    }

    private enum SwipeRecognitionState {
        IDLE,
        ADDING,
        REMOVING,
        FAILURE
    }
}
