//
// Triple Play - utilities for use in PlayN-based games
// Copyright (c) 2011-2013, Three Rings Design, Inc. - All rights reserved.
// http://github.com/threerings/tripleplay/blob/master/LICENSE

package tripleplay.gesture;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import playn.core.Touch;
import playn.core.Touch.Event;

import pythagoras.f.IRectangle;

import react.Value;
import react.ValueView;
import tripleplay.util.Timer;
import tripleplay.util.Timer.Handle;

/**
 * Resolves user touch input in terms of a set of {@link Gesture}s that are registered with this
 * director. May either be used as a standalone listener on a layer, or along with a set of other
 * regions in a given layer via GestureRegionDirector.
 *
 * Will only consider touches that start within the defined bounds, but if it is given touch events
 * that end outside of the bounds, but started inside the bounds it will react to them.
 */
public class GestureDirector
    implements Touch.LayerListener
{
    public final IRectangle bounds;

    public GestureDirector (IRectangle bounds, Timer timer) {
        this.bounds = bounds;
        _timer = timer;
    }

    /**
     * Adds a gesture to the set considered during each user interaction. Returns this director for
     * chaining.
     */
    public GestureDirector add (Gesture<?> gesture) {
        _gestures.add(gesture);
        return this;
    }

    /**
     * Removes the given gesture from the set considered during each user interaction. Returns false
     * if that gesture was not found.
     */
    public boolean remove (Gesture<?> gesture) {
        return _gestures.remove(gesture);
    }

    public boolean touchInBounds (Event touch) {
        return bounds.contains(touch.localX(), touch.localY());
    }

    public boolean trackingTouch (Event touch) {
        return _currentTouches.containsKey(touch.id());
    }

    public ValueView<Gesture<?>> greedyGesture () {
        return _greedy;
    }

    @Override public void onTouchStart (Event touch) {
        if (!touchInBounds(touch)) return;

        if (_currentTouches.isEmpty()) {
            // new user interaction!
            for (Gesture<?> gesture : _gestures) gesture.start();
            _greedy.update(null);
        }
        _currentTouches.put(touch.id(), touch);
        evaluateGestures(new GestureNode(GestureNode.Type.START, touch));
    }

    @Override public void onTouchMove (Event touch) {
        if (!trackingTouch(touch)) return;
        _currentTouches.put(touch.id(), touch);
        evaluateGestures(new GestureNode(GestureNode.Type.MOVE, touch));
    }

    @Override public void onTouchEnd (Event touch) {
        if (!trackingTouch(touch)) return;
        _currentTouches.remove(touch.id());
        evaluateGestures(new GestureNode(GestureNode.Type.END, touch));
    }

    @Override public void onTouchCancel (Event touch) {
        if (!trackingTouch(touch)) return;
        _currentTouches.remove(touch.id());
        evaluateGestures(new GestureNode(GestureNode.Type.CANCEL, touch));
    }

    protected void onTouchPause (Event touch) {
        if (!trackingTouch(touch)) {
            Log.log.warning("Bad state: received pause dispatch for an event we're not tracking",
                "event", touch);
            return;
        }
        // no need to update _currentTouches, it already has this touch registered
        evaluateGestures(new GestureNode(GestureNode.Type.PAUSE, touch));
    }

    protected void evaluateGestures (final GestureNode node) {
        // dispatch a pause event on touches that haven't moved for PAUSE_DELAY.
        Handle handle = _currentMoves.remove(node.touch.id());
        if (handle != null) handle.cancel();
        if (node.type == GestureNode.Type.MOVE || node.type == GestureNode.Type.START) {
            handle = _timer.after(PAUSE_DELAY, new Runnable() {
                @Override public void run () { onTouchPause(node.touch); }
            });
            _currentMoves.put(node.touch.id(), handle);
        }

        Gesture<?> currentGreedy = _greedy.get();
        if (currentGreedy != null) {
            currentGreedy.evaluate(node);
            return;
        }

        List<Gesture<?>> greedy = new ArrayList<Gesture<?>>();
        List<Gesture<?>> complete = new ArrayList<Gesture<?>>();
        for (Gesture<?> gesture : _gestures) {
            if (gesture.state() == Gesture.State.UNQUALIFIED) continue;

            gesture.evaluate(node);
            if (gesture.state() == Gesture.State.GREEDY) greedy.add(gesture);
            else if (gesture.state() == Gesture.State.COMPLETE) complete.add(gesture);
        }

        int greedyAndComplete = greedy.size() + complete.size();
        if (greedyAndComplete > 1) {
            Log.log.warning(
                "More than one gesture transitioned to GREEDY or COMPLETE on a single node",
                "node", node, "greedy", greedy, "complete", complete);
            // soldier on: the first greedy gesture will have priority
        }
        _greedy.update(currentGreedy = (greedy.isEmpty() ? null : greedy.get(0)));
        if (greedyAndComplete > 0) {
            // put all but the potential greedy gesture into UNQUALIFIED for the remainder of this
            // interaction.
            for (Gesture<?> gesture : _gestures) if (currentGreedy != gesture) gesture.cancel();
        }
    }

    protected static final int PAUSE_DELAY = 500; // in ms.

    protected Timer _timer;
    protected Map<Integer, Event> _currentTouches = new HashMap<Integer, Event>();
    protected Map<Integer, Handle> _currentMoves = new HashMap<Integer, Handle>();
    protected Set<Gesture<?>> _gestures = new HashSet<Gesture<?>>();
    protected Value<Gesture<?>> _greedy = Value.create(null);
}

@@ -2053,32 +2053,595 @@
 _greedy;%0A    %7D%0A%0A
+    /**%0A     * Returns the number of milliseconds to wait after the last touch event to transition the%0A     * current gestures to PAUSE.%0A     */%0A    public int pauseDelay () %7B%0A        return _pauseDelay;%0A    %7D%0A%0A    /**%0A     * Sets the number of milliseconds to wait after the last touch even to transition the%0A     * current guestures to PAUSE. The default is 500 (half a second).%0A     *%0A     * @return this GestureDirector for call chaining.%0A     */%0A    public GestureDirector setPauseDelay (int value) %7B%0A        _pauseDelay = value;%0A        return this;%0A    %7D%0A%0A
     @Override pu
@@ -2671,32 +2671,32 @@
 (Event touch) %7B%0A
-
         if (!tou
@@ -4494,27 +4494,27 @@
 r.after(
-PAUSE_DELAY
+_pauseDelay
 , new Ru
@@ -6043,69 +6043,8 @@
  %7D%0A%0A
-    protected static final int PAUSE_DELAY = 500; // in ms.%0A%0A
     
@@ -6298,24 +6298,24 @@
 ture%3C?%3E%3E();%0A
-
     protecte
@@ -6363,11 +6363,78 @@
 e(null);
+%0A%0A    protected int _pauseDelay = 500; // in ms, .5 second default.
 %0A%7D%0A

Allow callers to modify the pause delay.