package com.stencyl.gestures;

import openfl.ui.MultitouchInputMode;
import openfl.display.Sprite;
import openfl.display.DisplayObjectContainer;

import haxe.Timer;
import openfl.Lib;
import openfl.display.InteractiveObject;
import openfl.events.Event;
import openfl.events.MouseEvent;
import openfl.events.TouchEvent;
import openfl.geom.Point;
import openfl.geom.Rectangle;
import openfl.ui.Multitouch;

import com.stencyl.utils.motion.*;

#if haxe3
private typedef Hash<T> = Map<String, T>;
#end

#if cpp
typedef TouchEventType = TouchEvent;
typedef MouseEventType = MouseEvent;
#else
typedef TouchEventType = Dynamic;
typedef MouseEventType = Dynamic;
#end

class RoxGestureAgent {

    public static inline var TOUCH_POINT = 1;
    public static inline var GESTURE = 2;
    public static inline var GESTURE_CAPTURE = 3;
    public static inline var PAN_X = 1;
    public static inline var PAN_Y = 2;
    public static inline var PAN_XY = 3;

    public static var multitouchSupported(get, null): Bool;

    public var mode(default, null): Int;
    public var longPressDelay = 1.0;
    public var swipeTimeout = 0.1; // can be zero, means no timeout

    private static inline var READY = 0;
    private static inline var BEGIN = 1;
    private static inline var TWO_FINGER_MOVE = 2;
    private static inline var MOVE = 4;
    private static var R2D = 180 / Math.PI;

//    private static inline var DOUBLE_TAP_DELAY = 0.3;
    private static inline var SWIPE_SCROLL_TIME = 2.0;
    private static inline var SWIPE_SAMPLE_TIME = 0.2;
    private static inline var VELOCITY_RATIO = 0.25;
    private static var touchEvents = [
        TouchEvent.TOUCH_BEGIN, TouchEvent.TOUCH_END, TouchEvent.TOUCH_MOVE, TouchEvent.TOUCH_OVER,
        TouchEvent.TOUCH_OUT, TouchEvent.TOUCH_ROLL_OVER, TouchEvent.TOUCH_ROLL_OUT, TouchEvent.TOUCH_TAP ];
    private static var mouseEvents = [
        MouseEvent.MOUSE_DOWN, MouseEvent.MOUSE_UP, MouseEvent.MOUSE_MOVE, MouseEvent.MOUSE_OVER,
        MouseEvent.MOUSE_OUT, MouseEvent.ROLL_OVER, MouseEvent.ROLL_OUT, MouseEvent.CLICK ];
    private static var geTouchEvents = [
        TouchEvent.TOUCH_BEGIN, TouchEvent.TOUCH_END, TouchEvent.TOUCH_MOVE, TouchEvent.TOUCH_OUT ];
    private static var geMouseEvents = [
        MouseEvent.MOUSE_DOWN, MouseEvent.MOUSE_UP, MouseEvent.MOUSE_MOVE, MouseEvent.MOUSE_OUT ];

    private static var initialized = false;
    private static var moveToleSqr = 1.0;

    private var owner: InteractiveObject;
    private var touch0: TouchPoint;
    private var touch1: TouchPoint;
    private var touchList: List<TouchPoint>;
    private var listenEvents: Array<String>;
    private var handler: Dynamic -> Void;
    private var longPressTimer: TweenTimer;
    private var tweener: TweenFloat2;
    private var overlay: Sprite; // used in capture mode, to capture events outside the owner
    /**
     * READY -> begin:BEGIN -> end:tap
     * READY -> begin:BEGIN -> move:MOVE [-> MOVE]: pan
     * READY -> begin:BEGIN -> begin:TWO_FINGER_MOVE -> move:TWO_FINGER_MOVE:pinch & rotation
     * READY -> begin:BEGIN -> move:MOVE -> end:swipe
     **/
    private var state: Int;

    public function new(inOwner: InteractiveObject, ?inMode: Null<Int> = GESTURE) {
        init();
        owner = inOwner;
        mode = inMode;
        var isTouch =
            #if desktop false;
            #else Multitouch.supportsTouchEvents;
            #end
        owner.mouseEnabled = true;
        listenEvents = mode == TOUCH_POINT ? (isTouch ? touchEvents : mouseEvents) : (isTouch ? geTouchEvents : geMouseEvents);
        handler = mode == TOUCH_POINT ? (isTouch ? convertTouch : convertMouse) : (isTouch ? onTouch : onMouse);
        for (type in listenEvents) owner.addEventListener(type, handler);
        if (mode == GESTURE_CAPTURE) Lib.current.stage.addEventListener(Event.RESIZE, function(e) { overlay = null; } );
        touchList = new List<TouchPoint>();
        setReady();
    }

    private inline static function init() {
        if (!initialized) {
            initialized = true;
            var stage = Lib.current.stage;
            moveToleSqr = new Point(stage.stageWidth, stage.stageHeight).length * 0.008;
            moveToleSqr *= moveToleSqr;
            if (Multitouch.supportsTouchEvents) Multitouch.inputMode = MultitouchInputMode.TOUCH_POINT;
        }
    }

    public function detach() {
        stopTween();
        setReady();
        for (type in listenEvents) owner.removeEventListener(type, handler);
        owner = null;
        overlay = null;
    }

    public inline function getHandler(?flags: Int = PAN_XY) : Dynamic -> Void {
        return #if haxe3 handleEvent.bind(flags) #else callback(handleEvent, flags) #end;
    }

    public inline function startTweenXY(target: InteractiveObject, interval: Float, x: Float, y: Float) {
        tweener = new TweenFloat2();
        tweener.tween(target.x, x, target.y, y, Easing.linear, Std.int(interval*1000)).doOnUpdate(function() {
            target.x = tweener.value1;
            target.y = tweener.value2;
        });
    }

    public inline function stopTween() {
        if (tweener != null) {
            TweenManager.cancel(tweener);
            tweener = null;
        }
    }

    public static inline function localOffset(obj: InteractiveObject, globalOffset: Point) : Point {
        var pt = obj.parent.localToGlobal(new Point(obj.x, obj.y));
        pt.offset(globalOffset.x, globalOffset.y);
        pt = obj.parent.globalToLocal(pt);
        pt.offset(-obj.x, -obj.y);
        return pt;
    }

    private function handleEvent(flags: Int, e: RoxGestureEvent) {
//        trace(">>>t=" + e.target.name+",o="+owner.name+",e="+e);
        var sp: InteractiveObject = cast(e.target);
        if (sp != owner) return;
        switch (e.type) {
            case RoxGestureEvent.GESTURE_PAN:
                var pt = localOffset(sp, cast(e.extra));
                if (flags & PAN_X != 0) sp.x += pt.x;
                if (flags & PAN_Y != 0) sp.y += pt.y;
            case RoxGestureEvent.GESTURE_SWIPE:
                var pt = localOffset(sp, new Point(e.extra.x * SWIPE_SCROLL_TIME, e.extra.y * SWIPE_SCROLL_TIME));
                startTweenXY(sp, SWIPE_SCROLL_TIME, sp.x + pt.x, sp.y + pt.y);
            case RoxGestureEvent.GESTURE_PINCH:
                var scale: Float = e.extra;
                var spt = sp.parent.localToGlobal(new Point(sp.x, sp.y));
                var dx = spt.x - e.stageX, dy = spt.y - e.stageY;
                var angle = Math.atan2(dy, dx);
                var nowlen = new Point(dx, dy).length;
                var newlen = nowlen * scale;
                var newpos = Point.polar(newlen, angle);
                newpos.offset(e.stageX, e.stageY);
                newpos = sp.parent.globalToLocal(newpos);
                sp.scaleX *= scale;
                sp.scaleY *= scale;
                sp.x = newpos.x;
                sp.y = newpos.y;
            case RoxGestureEvent.GESTURE_ROTATION:
                var angle: Float = e.extra;
                var spt = sp.parent.localToGlobal(new Point(sp.x, sp.y));
                var dx = spt.x - e.stageX, dy = spt.y - e.stageY;
                var nowang = Math.atan2(dy, dx);
                var length = new Point(dx, dy).length;
                var newang = nowang + angle;
                var newpos = Point.polar(length, newang);
                newpos.offset(e.stageX, e.stageY);
                newpos = sp.parent.globalToLocal(newpos);
                sp.rotation += R2D * angle;
                sp.x = newpos.x;
                sp.y = newpos.y;
        }
    }

    private inline static function get_multitouchSupported() : Bool {
//        trace("multitouchSupported: isTouch=" + Multitouch.supportsTouchEvents + ",maxPoints=" + Multitouch.maxTouchPoints);
        return Multitouch.supportsTouchEvents && Multitouch.maxTouchPoints > 1;
    }

    private inline function convertTouch(e: TouchEventType) {
        owner.dispatchEvent(new RoxGestureEvent(typeMap.get(e.type), e.bubbles, e.cancelable,
                e.localX, e.localY, e.stageX, e.stageY, e.touchPointID, this));
    }

    private inline function convertMouse(e: MouseEventType) {
        var t: String = e.type;
        if (t == MouseEvent.MOUSE_DOWN || t == MouseEvent.MOUSE_UP || t == MouseEvent.CLICK || e.buttonDown) {
            owner.dispatchEvent(new RoxGestureEvent(typeMap.get(t), e.bubbles, e.cancelable,
                    e.localX, e.localY, e.stageX, e.stageY, 0, this));
        }
    }

    private inline function onTouch(e: TouchEventType) {
        var id: Int = e.touchPointID;
//        trace("onTouch:e=" + e +",touchId="+id);
        var prim = touch0 == null || touch0.tid == id;
        if (prim || (touch1 != null && touch1.tid == id) || (touch0 != null && touch1 == null && touch0.tid != id)) {
//            if (handleTouch(typeMap.get(e.type), e, prim, id)) e.rox_stopPropagation();
			#if cpp
			handleTouch(typeMap.get(e.type), touchToMouse(e), prim, id);
			#else
            handleTouch(typeMap.get(e.type), e, prim, id);
			#end
        }
//        if (id <= 1) {
//            if (handleTouch(typeMap.get(e.type), e, id == 0)) e.rox_stopPropagation();
//        }
    }
	
	#if cpp
	private inline function touchToMouse(e:TouchEventType):MouseEventType
	{
		var eType = switch(e.type)
		{
			case TouchEvent.TOUCH_BEGIN: MouseEvent.MOUSE_DOWN;
			case TouchEvent.TOUCH_END: MouseEvent.MOUSE_UP;
			case TouchEvent.TOUCH_MOVE: MouseEvent.MOUSE_MOVE;
			case TouchEvent.TOUCH_OUT: MouseEvent.MOUSE_OUT;
			case TouchEvent.TOUCH_OVER: MouseEvent.MOUSE_OVER;
			case TouchEvent.TOUCH_ROLL_OUT: MouseEvent.ROLL_OUT;
			case TouchEvent.TOUCH_ROLL_OVER: MouseEvent.ROLL_OVER;
			case TouchEvent.TOUCH_TAP: MouseEvent.CLICK;
			case _: MouseEvent.MOUSE_MOVE;
		}

        var me = new MouseEventType(
			eType, e.bubbles, e.cancelable, e.localX, e.localY,
			e.relatedObject, e.ctrlKey, e.altKey, e.shiftKey,
			false, 0, e.commandKey, 0);

        me.stageX = e.stageX;
        me.stageY = e.stageY;

        return me;
	}
	#end

    private inline function onMouse(e: MouseEventType) {
//        trace("onMouse:e=" + e);
        var t: String = e.type;
        if (t == MouseEvent.MOUSE_DOWN || t == MouseEvent.MOUSE_UP || e.buttonDown) {
//            if (handleTouch(typeMap.get(t), e, true, 0)) e.rox_stopPropagation();
            handleTouch(typeMap.get(t), e, true, 0);
        }
    }

    private function handleTouch(type: String, e: MouseEventType, prim: Bool, touchId: Int) : Bool {
//        trace("type=" + type + ",e=" + e);
        var pt = new TouchPoint(owner, e, touchId);
        var tp = prim ? touch0 : touch1;
        if (type == RoxGestureEvent.TOUCH_MOVE && tp != null && pt.distSqr(tp) < moveToleSqr) return false; // NO MOVE -> skip
        var handled = true;
        switch (state) {
        case READY:
            if (prim && type == RoxGestureEvent.TOUCH_BEGIN) {
                state = BEGIN;
                touch0 = pt;
                longPressTimer = cast TweenManager.timer(Std.int(longPressDelay*1000)).doOnComplete(sendLongPress.bind(pt));
                stopTween();
                if (mode == GESTURE_CAPTURE) {
                    var stage = Lib.current.stage;
                    if (overlay == null) {
                        overlay = new Sprite();
                        overlay.name = "overlay";
                        overlay.graphics.beginFill(0xFFFFFF, 1 / 255);
                        overlay.graphics.drawRect(0, 0, stage.stageWidth, stage.stageHeight);
                    }
                    stage.addChild(overlay);
                    for (type in listenEvents) overlay.addEventListener(type, handler);
                }
            } else {
                handled = false;
            }
        case BEGIN:
            if (prim && type == RoxGestureEvent.TOUCH_END) {
                owner.dispatchEvent(new RoxGestureEvent(RoxGestureEvent.GESTURE_TAP, pt.lx, pt.ly, pt.sx, pt.sy, this));
                setReady();
            } else if (prim && type == RoxGestureEvent.TOUCH_MOVE) {
                var pan = new Point(pt.sx - touch0.sx, pt.sy - touch0.sy);
                owner.dispatchEvent(new RoxGestureEvent(RoxGestureEvent.GESTURE_PAN, pt.lx, pt.ly, pt.sx, pt.sy, this, pan));
                setMove(pt);
            } else if (!prim && type == RoxGestureEvent.TOUCH_BEGIN) {
                setTwoFingerMove(pt);
            } else {
                handled = false;
            }
        case MOVE:
            if (prim && type == RoxGestureEvent.TOUCH_MOVE) {
                var pan = new Point(pt.sx - touch0.sx, pt.sy - touch0.sy);
                owner.dispatchEvent(new RoxGestureEvent(RoxGestureEvent.GESTURE_PAN, pt.lx, pt.ly, pt.sx, pt.sy, this, pan));
                setMove(pt);
            } else if (prim && (type == RoxGestureEvent.TOUCH_END ||
                    (mode != GESTURE_CAPTURE && type == RoxGestureEvent.TOUCH_OUT && e.target == owner))) {
                if (swipeTimeout <= 0 || pt.time - touch0.time < swipeTimeout) {
                    var beginpt = touchList.pop(), endpt: TouchPoint = null;
                    for (i in touchList) {
                        if (beginpt.time - i.time > SWIPE_SAMPLE_TIME) break;
                        endpt = i;
                    }
                    if (endpt != null) {
                        var dx: Float, dy: Float;
                        var angle = Math.atan2(dy = beginpt.sy - endpt.sy, dx = beginpt.sx - endpt.sx);
                        var velocity = Point.polar(new Point(dx, dy).length / (beginpt.time - endpt.time) * VELOCITY_RATIO, angle);
                        owner.dispatchEvent(new RoxGestureEvent(RoxGestureEvent.GESTURE_SWIPE, pt.lx, pt.ly, pt.sx, pt.sy, this, velocity));
                    }
                }
                setReady();
            } else if (!prim && type == RoxGestureEvent.TOUCH_BEGIN) {
                owner.dispatchEvent(new RoxGestureEvent(RoxGestureEvent.GESTURE_BEGIN, pt.lx, pt.ly, pt.sx, pt.sy, this));
                setTwoFingerMove(pt);
            } else {
                handled = false;
            }
        case TWO_FINGER_MOVE:
            if (type == RoxGestureEvent.TOUCH_END) {
                owner.dispatchEvent(new RoxGestureEvent(RoxGestureEvent.GESTURE_END, pt.lx, pt.ly, pt.sx, pt.sy, this));
                setReady();
            } else if (type == RoxGestureEvent.TOUCH_MOVE) {
                var pt1 = prim ? touch1 : touch0, pt2 = prim ? touch0 : touch1;
                var scale = Point.distance(pt.spt, pt1.spt) / Point.distance(pt2.spt, pt1.spt);
                var angle = Math.atan2(pt.sy - pt1.sy, pt.sx - pt1.sx) - Math.atan2(pt2.sy - pt1.sy, pt2.sx - pt1.sx); // in radius
                var mid = Point.interpolate(pt1.lpt, pt2.lpt, 0.5);
                var gmid = owner.localToGlobal(mid);
                if (scale != 1)
                    owner.dispatchEvent(new RoxGestureEvent(RoxGestureEvent.GESTURE_PINCH, mid.x, mid.y, gmid.x, gmid.y, this, scale));
                if (angle != 0)
                    owner.dispatchEvent(new RoxGestureEvent(RoxGestureEvent.GESTURE_ROTATION, mid.x, mid.y, gmid.x, gmid.y, this, angle));
                if (prim) { touch0 = pt; } else { touch1 = pt; }
            } else {
                handled = false;
            }
        }
        return handled;
    }

    private inline function sendLongPress(pt: TouchPoint) {
        owner.dispatchEvent(new RoxGestureEvent(RoxGestureEvent.GESTURE_LONG_PRESS, pt.lx, pt.ly, pt.sx, pt.sy, this));
        setReady();
    }

    private function setReady() {
        state = READY;
        touch0 = touch1 = null;
        touchList.clear();
        cancelLongPress();
        if (overlay != null && mode == GESTURE_CAPTURE) {
            for (type in listenEvents) overlay.removeEventListener(type, handler);
            Lib.current.stage.removeChild(overlay);
        }
    }

    private inline function setMove(pt: TouchPoint) {
        state = MOVE;
        if (touch0 != null) touchList.push(touch0);
        touch0 = pt;
        cancelLongPress();
    }

    private inline function setTwoFingerMove(pt: TouchPoint) {
        state = TWO_FINGER_MOVE;
        touch1 = pt;
        cancelLongPress();
    }

    private inline function cancelLongPress() {
        if (longPressTimer != null) {
            TweenManager.cancel(longPressTimer);
            longPressTimer = null;
        }
    }

    private static var MAP: Array<String> = [
        MouseEvent.MOUSE_DOWN, RoxGestureEvent.TOUCH_BEGIN,
        MouseEvent.MOUSE_UP, RoxGestureEvent.TOUCH_END,
        MouseEvent.MOUSE_MOVE, RoxGestureEvent.TOUCH_MOVE,
        MouseEvent.MOUSE_OVER, RoxGestureEvent.TOUCH_OVER,
        MouseEvent.MOUSE_OUT, RoxGestureEvent.TOUCH_OUT,
        MouseEvent.ROLL_OVER, RoxGestureEvent.TOUCH_ROLL_OVER,
        MouseEvent.ROLL_OUT, RoxGestureEvent.TOUCH_ROLL_OUT,
        MouseEvent.CLICK, RoxGestureEvent.TOUCH_TAP,
        TouchEvent.TOUCH_BEGIN, RoxGestureEvent.TOUCH_BEGIN,
        TouchEvent.TOUCH_END, RoxGestureEvent.TOUCH_END,
        TouchEvent.TOUCH_MOVE, RoxGestureEvent.TOUCH_MOVE,
        TouchEvent.TOUCH_OVER, RoxGestureEvent.TOUCH_OVER,
        TouchEvent.TOUCH_OUT, RoxGestureEvent.TOUCH_OUT,
        TouchEvent.TOUCH_ROLL_OVER, RoxGestureEvent.TOUCH_ROLL_OVER,
        TouchEvent.TOUCH_ROLL_OUT, RoxGestureEvent.TOUCH_ROLL_OUT,
        TouchEvent.TOUCH_TAP, RoxGestureEvent.TOUCH_TAP ];

    private static var typeMap: Hash<String> = initTypeMap();

    private inline static function initTypeMap() {
        var map = new Hash<String>();
        for (i in 0...(MAP.length >> 1)) {
            map.set(MAP[i << 1], MAP[(i << 1) + 1]);
        }
        return map;
    }

}

private class TouchPoint {
    public var tid: Int;
    public var lx: Float;
    public var ly: Float;
    public var sx: Float;
    public var sy: Float;
    public var lpt: Point;
    public var spt: Point;
    public var time: Float;
    public function new(src: InteractiveObject, e: MouseEventType, touchId: Int) {
        tid = touchId;
        sx = e.stageX;
        sy = e.stageY;
        spt = new Point(sx, sy);
        lpt = src.globalToLocal(spt);
        lx = lpt.x;
        ly = lpt.y;
        time = Timer.stamp();
    }

    public inline function distSqr(pt: TouchPoint) : Float {
        var dx = pt.sx - this.sx, dy = pt.sy - this.sy;
        return dx * dx + dy * dy;
    }
}