/*******************************************************************************
 * Copyright 2011 See AUTHORS file.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package com.badlogic.gdx.input;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.utils.TimeUtils;
import com.badlogic.gdx.utils.Timer;
import com.badlogic.gdx.utils.Timer.Task;

/** {@link InputProcessor} implementation that detects gestures (tap, long press, fling, pan, zoom, pinch) and hands them to a
 * {@link GestureListener}.
 * @author mzechner */
public class GestureDetector extends InputAdapter {
	final GestureListener listener;
	private float tapSquareSize;
	private long tapCountInterval;
	private float longPressSeconds;
	private long maxFlingDelay;

	private boolean inTapSquare;
	private int tapCount;
	private long lastTapTime;
	private float lastTapX, lastTapY;
	private int lastTapButton, lastTapPointer;
	boolean longPressFired;
	private boolean pinching;
	private boolean panning;

	private final VelocityTracker tracker = new VelocityTracker();
	private float tapSquareCenterX, tapSquareCenterY;
	private long gestureStartTime;
	Vector2 pointer1 = new Vector2();
	private final Vector2 pointer2 = new Vector2();
	private final Vector2 initialPointer1 = new Vector2();
	private final Vector2 initialPointer2 = new Vector2();

	private final Task longPressTask = new Task() {
		@Override
		public void run () {
			if (!longPressFired) longPressFired = listener.longPress(pointer1.x, pointer1.y);
		}
	};

	/** Creates a new GestureDetector with default values: halfTapSquareSize=20, tapCountInterval=0.4f, longPressDuration=1.1f,
	 * maxFlingDelay=0.15f. */
	public GestureDetector (GestureListener listener) {
		this(20, 0.4f, 1.1f, 0.15f, listener);
	}

	/** @param halfTapSquareSize half width in pixels of the square around an initial touch event, see
	 *           {@link GestureListener#tap(float, float, int, int)}.
	 * @param tapCountInterval time in seconds that must pass for two touch down/up sequences to be detected as consecutive taps.
	 * @param longPressDuration time in seconds that must pass for the detector to fire a
	 *           {@link GestureListener#longPress(float, float)} event.
	 * @param maxFlingDelay time in seconds the finger must have been dragged for a fling event to be fired, see
	 *           {@link GestureListener#fling(float, float, int)}
	 * @param listener May be null if the listener will be set later. */
	public GestureDetector (float halfTapSquareSize, float tapCountInterval, float longPressDuration, float maxFlingDelay,
		GestureListener listener) {
		this.tapSquareSize = halfTapSquareSize;
		this.tapCountInterval = (long)(tapCountInterval * 1000000000l);
		this.longPressSeconds = longPressDuration;
		this.maxFlingDelay = (long)(maxFlingDelay * 1000000000l);
		this.listener = listener;
	}

	@Override
	public boolean touchDown (int x, int y, int pointer, int button) {
		return touchDown((float)x, (float)y, pointer, button);
	}

	public boolean touchDown (float x, float y, int pointer, int button) {
		if (pointer > 1) return false;

		if (pointer == 0) {
			pointer1.set(x, y);
			gestureStartTime = Gdx.input.getCurrentEventTime();
			tracker.start(x, y, gestureStartTime);
			if (Gdx.input.isTouched(1)) {
				// Start pinch.
				inTapSquare = false;
				pinching = true;
				initialPointer1.set(pointer1);
				initialPointer2.set(pointer2);
				longPressTask.cancel();
			} else {
				// Normal touch down.
				inTapSquare = true;
				pinching = false;
				longPressFired = false;
				tapSquareCenterX = x;
				tapSquareCenterY = y;
				if (!longPressTask.isScheduled()) Timer.schedule(longPressTask, longPressSeconds);
			}
		} else {
			// Start pinch.
			pointer2.set(x, y);
			inTapSquare = false;
			pinching = true;
			initialPointer1.set(pointer1);
			initialPointer2.set(pointer2);
			longPressTask.cancel();
		}
		return listener.touchDown(x, y, pointer, button);
	}

	@Override
	public boolean touchDragged (int x, int y, int pointer) {
		return touchDragged((float)x, (float)y, pointer);
	}

	public boolean touchDragged (float x, float y, int pointer) {
		if (pointer > 1) return false;
		if (longPressFired) return false;

		if (pointer == 0)
			pointer1.set(x, y);
		else
			pointer2.set(x, y);

		// handle pinch zoom
		if (pinching) {
			if (listener != null) {
				boolean result = listener.pinch(initialPointer1, initialPointer2, pointer1, pointer2);
				return listener.zoom(initialPointer1.dst(initialPointer2), pointer1.dst(pointer2)) || result;
			}
			return false;
		}

		// update tracker
		tracker.update(x, y, Gdx.input.getCurrentEventTime());

		// check if we are still tapping.
		if (inTapSquare && !isWithinTapSquare(x, y, tapSquareCenterX, tapSquareCenterY)) {
			longPressTask.cancel();
			inTapSquare = false;
		}

		// if we have left the tap square, we are panning
		if (!inTapSquare) {
			panning = true;
			return listener.pan(x, y, tracker.deltaX, tracker.deltaY);
		}

		return false;
	}

	@Override
	public boolean touchUp (int x, int y, int pointer, int button) {
		return touchUp((float)x, (float)y, pointer, button);
	}

	public boolean touchUp (float x, float y, int pointer, int button) {
		if (pointer > 1) return false;

		// check if we are still tapping.
		if (inTapSquare && !isWithinTapSquare(x, y, tapSquareCenterX, tapSquareCenterY)) inTapSquare = false;

		boolean wasPanning = panning;
		panning = false;

		longPressTask.cancel();
		if (longPressFired) return false;

		if (inTapSquare) {
			// handle taps
			if (lastTapButton != button || lastTapPointer != pointer || TimeUtils.nanoTime() - lastTapTime > tapCountInterval
				|| !isWithinTapSquare(x, y, lastTapX, lastTapY)) tapCount = 0;
			tapCount++;
			lastTapTime = TimeUtils.nanoTime();
			lastTapX = x;
			lastTapY = y;
			lastTapButton = button;
			lastTapPointer = pointer;
			gestureStartTime = 0;
			return listener.tap(x, y, tapCount, button);
		}

		if (pinching) {
			// handle pinch end
			pinching = false;
			panning = true;
			// we are in pan mode again, reset velocity tracker
			if (pointer == 0) {
				// first pointer has lifted off, set up panning to use the second pointer...
				tracker.start(pointer2.x, pointer2.y, Gdx.input.getCurrentEventTime());
			} else {
				// second pointer has lifted off, set up panning to use the first pointer...
				tracker.start(pointer1.x, pointer1.y, Gdx.input.getCurrentEventTime());
			}
			return false;
		}

		// handle no longer panning
		boolean handled = false;
		if (wasPanning && !panning) handled = listener.panStop(x, y, pointer, button);

		// handle fling
		gestureStartTime = 0;
		long time = Gdx.input.getCurrentEventTime();
		if (time - tracker.lastTime < maxFlingDelay) {
			tracker.update(x, y, time);
			handled = listener.fling(tracker.getVelocityX(), tracker.getVelocityY(), button) || handled;
		}
		return handled;
	}

	/** No further gesture events will be triggered for the current touch, if any. */
	public void cancel () {
		longPressTask.cancel();
		longPressFired = true;
	}

	/** @return whether the user touched the screen long enough to trigger a long press event. */
	public boolean isLongPressed () {
		return isLongPressed(longPressSeconds);
	}

	/** @param duration
	 * @return whether the user touched the screen for as much or more than the given duration. */
	public boolean isLongPressed (float duration) {
		if (gestureStartTime == 0) return false;
		return TimeUtils.nanoTime() - gestureStartTime > (long)(duration * 1000000000l);
	}

	public boolean isPanning () {
		return panning;
	}

	public void reset () {
		gestureStartTime = 0;
		panning = false;
		inTapSquare = false;
	}

	private boolean isWithinTapSquare (float x, float y, float centerX, float centerY) {
		return Math.abs(x - centerX) < tapSquareSize && Math.abs(y - centerY) < tapSquareSize;
	}

	/** The tap square will not longer be used for the current touch. */
	public void invalidateTapSquare () {
		inTapSquare = false;
	}

	public void setTapSquareSize (int tapSquareSize) {
		this.tapSquareSize = tapSquareSize;
	}

	/** @param tapCountInterval time in seconds that must pass for two touch down/up sequences to be detected as consecutive taps. */
	public void setTapCountInterval (float tapCountInterval) {
		this.tapCountInterval = (long)(tapCountInterval * 1000000000l);
	}

	public void setLongPressSeconds (float longPressSeconds) {
		this.longPressSeconds = longPressSeconds;
	}

	public void setMaxFlingDelay (long maxFlingDelay) {
		this.maxFlingDelay = maxFlingDelay;
	}

	/** Register an instance of this class with a {@link GestureDetector} to receive gestures such as taps, long presses, flings,
	 * panning or pinch zooming. Each method returns a boolean indicating if the event should be handed to the next listener (false
	 * to hand it to the next listener, true otherwise).
	 * @author mzechner */
	public static interface GestureListener {
		/** @see InputProcessor#touchDown(int, int, int, int) */
		public boolean touchDown (float x, float y, int pointer, int button);

		/** Called when a tap occured. A tap happens if a touch went down on the screen and was lifted again without moving outside
		 * of the tap square. The tap square is a rectangular area around the initial touch position as specified on construction
		 * time of the {@link GestureDetector}.
		 * @param count the number of taps. */
		public boolean tap (float x, float y, int count, int button);

		public boolean longPress (float x, float y);

		/** Called when the user dragged a finger over the screen and lifted it. Reports the last known velocity of the finger in
		 * pixels per second.
		 * @param velocityX velocity on x in seconds
		 * @param velocityY velocity on y in seconds */
		public boolean fling (float velocityX, float velocityY, int button);

		/** Called when the user drags a finger over the screen.
		 * @param deltaX the difference in pixels to the last drag event on x.
		 * @param deltaY the difference in pixels to the last drag event on y. */
		public boolean pan (float x, float y, float deltaX, float deltaY);

		/** Called when no longer panning. */
		public boolean panStop (float x, float y, int pointer, int button);

		/** Called when the user performs a pinch zoom gesture. The original distance is the distance in pixels when the gesture
		 * started.
		 * @param initialDistance distance between fingers when the gesture started.
		 * @param distance current distance between fingers. */
		public boolean zoom (float initialDistance, float distance);

		/** Called when a user performs a pinch zoom gesture. Reports the initial positions of the two involved fingers and their
		 * current positions.
		 * @param initialPointer1
		 * @param initialPointer2
		 * @param pointer1
		 * @param pointer2 */
		public boolean pinch (Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2);
	}

	/** Derrive from this if you only want to implement a subset of {@link GestureListener}.
	 * @author mzechner */
	public static class GestureAdapter implements GestureListener {
		@Override
		public boolean touchDown (float x, float y, int pointer, int button) {
			return false;
		}

		@Override
		public boolean tap (float x, float y, int count, int button) {
			return false;
		}

		@Override
		public boolean longPress (float x, float y) {
			return false;
		}

		@Override
		public boolean fling (float velocityX, float velocityY, int button) {
			return false;
		}

		@Override
		public boolean pan (float x, float y, float deltaX, float deltaY) {
			return false;
		}

		@Override
		public boolean panStop (float x, float y, int pointer, int button) {
			return false;
		}

		@Override
		public boolean zoom (float initialDistance, float distance) {
			return false;
		}

		@Override
		public boolean pinch (Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2) {
			return false;
		}
	}

	static class VelocityTracker {
		int sampleSize = 10;
		float lastX, lastY;
		float deltaX, deltaY;
		long lastTime;
		int numSamples;
		float[] meanX = new float[sampleSize];
		float[] meanY = new float[sampleSize];
		long[] meanTime = new long[sampleSize];

		public void start (float x, float y, long timeStamp) {
			lastX = x;
			lastY = y;
			deltaX = 0;
			deltaY = 0;
			numSamples = 0;
			for (int i = 0; i < sampleSize; i++) {
				meanX[i] = 0;
				meanY[i] = 0;
				meanTime[i] = 0;
			}
			lastTime = timeStamp;
		}

		public void update (float x, float y, long timeStamp) {
			long currTime = timeStamp;
			deltaX = x - lastX;
			deltaY = y - lastY;
			lastX = x;
			lastY = y;
			long deltaTime = currTime - lastTime;
			lastTime = currTime;
			int index = numSamples % sampleSize;
			meanX[index] = deltaX;
			meanY[index] = deltaY;
			meanTime[index] = deltaTime;
			numSamples++;
		}

		public float getVelocityX () {
			float meanX = getAverage(this.meanX, numSamples);
			float meanTime = getAverage(this.meanTime, numSamples) / 1000000000.0f;
			if (meanTime == 0) return 0;
			return meanX / meanTime;
		}

		public float getVelocityY () {
			float meanY = getAverage(this.meanY, numSamples);
			float meanTime = getAverage(this.meanTime, numSamples) / 1000000000.0f;
			if (meanTime == 0) return 0;
			return meanY / meanTime;
		}

		private float getAverage (float[] values, int numSamples) {
			numSamples = Math.min(sampleSize, numSamples);
			float sum = 0;
			for (int i = 0; i < numSamples; i++) {
				sum += values[i];
			}
			return sum / numSamples;
		}

		private long getAverage (long[] values, int numSamples) {
			numSamples = Math.min(sampleSize, numSamples);
			long sum = 0;
			for (int i = 0; i < numSamples; i++) {
				sum += values[i];
			}
			if (numSamples == 0) return 0;
			return sum / numSamples;
		}

		private float getSum (float[] values, int numSamples) {
			numSamples = Math.min(sampleSize, numSamples);
			float sum = 0;
			for (int i = 0; i < numSamples; i++) {
				sum += values[i];
			}
			if (numSamples == 0) return 0;
			return sum;
		}
	}
}
