/*
 * This file provided by Facebook is for non-commercial testing and evaluation
 * purposes only.  Facebook reserves all rights not expressly granted.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package com.facebook.samples.gestures;

import android.annotation.TargetApi;
import android.os.Build;
import android.view.MotionEvent;

/**
 * Component that detects and tracks multiple pointers based on touch events.
 * <p>
 * Each time a pointer gets pressed or released, the current gesture (if any) will end, and a new
 * one will be started (if there are still pressed pointers left). It is guaranteed that the number
 * of pointers within the single gesture will remain the same during the whole gesture.
 */
public class MultiPointerGestureDetector {

  /** The listener for receiving notifications when gestures occur. */
  public interface Listener {
    /** Responds to the beginning of a gesture. */
    public void onGestureBegin(MultiPointerGestureDetector detector);

    /** Responds to the update of a gesture in progress. */
    public void onGestureUpdate(MultiPointerGestureDetector detector);

    /** Responds to the end of a gesture. */
    public void onGestureEnd(MultiPointerGestureDetector detector);
  }

  private static final int MAX_POINTERS = 2;

  private boolean mGestureInProgress;
  private int mCount;
  private final int mId[] = new int[MAX_POINTERS];
  private final float mStartX[] = new float[MAX_POINTERS];
  private final float mStartY[] = new float[MAX_POINTERS];
  private final float mCurrentX[] = new float[MAX_POINTERS];
  private final float mCurrentY[] = new float[MAX_POINTERS];

  private Listener mListener = null;

  public MultiPointerGestureDetector() {
    reset();
  }

  /** Factory method that creates a new instance of MultiPointerGestureDetector */
  public static MultiPointerGestureDetector newInstance() {
    return new MultiPointerGestureDetector();
  }

  /**
   * Sets the listener.
   * @param listener listener to set
   */
  public void setListener(Listener listener) {
    mListener = listener;
  }

  /**
   * Resets the component to the initial state.
   */
  @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
  public void reset() {
    mGestureInProgress = false;
    mCount = 0;
    for (int i = 0; i < MAX_POINTERS; i++) {
      mId[i] = Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH ?
              -1 : MotionEvent.INVALID_POINTER_ID;
    }
  }

  /**
   * This method can be overridden in order to perform threshold check or something similar.
   * @return whether or not to start a new gesture
   */
  protected boolean shouldStartGesture() {
    return true;
  }

  private void startGesture() {
    if (!mGestureInProgress) {
      mGestureInProgress = true;
      if (mListener != null) {
        mListener.onGestureBegin(this);
      }
    }
  }

  private void stopGesture() {
    if (mGestureInProgress) {
      mGestureInProgress = false;
      if (mListener != null) {
        mListener.onGestureEnd(this);
      }
    }
  }

  /**
   * Gets the index of the i-th pressed pointer.
   * Normally, the index will be equal to i, except in the case when the pointer is released.
   * @return index of the specified pointer or -1 if not found (i.e. not enough pointers are down)
   */
  private int getPressedPointerIndex(MotionEvent event, int i) {
    final int count = event.getPointerCount();
    final int action = event.getActionMasked();
    final int index = event.getActionIndex();
    if (action == MotionEvent.ACTION_UP ||
        action == MotionEvent.ACTION_POINTER_UP) {
      if (i >= index) {
        i++;
      }
    }
    return (i < count) ? i : -1;
  }

  /**
   * Handles the given motion event.
   * @param event event to handle
   * @return whether or not the event was handled
   */
  public boolean onTouchEvent(final MotionEvent event) {
    switch (event.getActionMasked()) {
      case MotionEvent.ACTION_MOVE: {
        // update pointers
        for (int i = 0; i < MAX_POINTERS; i++) {
          int index = event.findPointerIndex(mId[i]);
          if (index != -1) {
            mCurrentX[i] = event.getX(index);
            mCurrentY[i] = event.getY(index);
          }
        }
        // start a new gesture if not already started
        if (!mGestureInProgress && shouldStartGesture()) {
          startGesture();
        }
        // notify listener
        if (mGestureInProgress && mListener != null) {
          mListener.onGestureUpdate(this);
        }
        break;
      }

      case MotionEvent.ACTION_DOWN:
      case MotionEvent.ACTION_POINTER_DOWN:
      case MotionEvent.ACTION_POINTER_UP:
      case MotionEvent.ACTION_UP: {
        // we'll restart the current gesture (if any) whenever the number of pointers changes
        // NOTE: we only restart existing gestures here, new gestures are started in ACTION_MOVE
        boolean wasGestureInProgress = mGestureInProgress;
        stopGesture();
        reset();
        // update pointers
        for (int i = 0; i < MAX_POINTERS; i++) {
          int index = getPressedPointerIndex(event, i);
          if (index == -1) {
            break;
          }
          mId[i] = event.getPointerId(index);
          mCurrentX[i] = mStartX[i] = event.getX(index);
          mCurrentY[i] = mStartY[i] = event.getY(index);
          mCount++;
        }
        // restart the gesture (if any) if there are still pointers left
        if (wasGestureInProgress && mCount > 0) {
          startGesture();
        }
        break;
      }

      case MotionEvent.ACTION_CANCEL: {
        stopGesture();
        reset();
        break;
      }
    }
    return true;
  }

  /** Restarts the current gesture */
  public void restartGesture() {
    if (!mGestureInProgress) {
      return;
    }
    stopGesture();
    for (int i = 0; i < MAX_POINTERS; i++) {
      mStartX[i] = mCurrentX[i];
      mStartY[i] = mCurrentY[i];
    }
    startGesture();
  }

  /** Gets whether gesture is in progress or not */
  public boolean isGestureInProgress() {
    return mGestureInProgress;
  }

  /** Gets the number of pointers in the current gesture */
  public int getCount() {
    return mCount;
  }

  /**
   * Gets the start X coordinates for the all pointers
   * Mutable array is exposed for performance reasons and is not to be modified by the callers.
   */
  public float[] getStartX() {
    return mStartX;
  }

  /**
   * Gets the start Y coordinates for the all pointers
   * Mutable array is exposed for performance reasons and is not to be modified by the callers.
   */
  public float[] getStartY() {
    return mStartY;
  }

  /**
   * Gets the current X coordinates for the all pointers
   * Mutable array is exposed for performance reasons and is not to be modified by the callers.
   */
  public float[] getCurrentX() {
    return mCurrentX;
  }

  /**
   * Gets the current Y coordinates for the all pointers
   * Mutable array is exposed for performance reasons and is not to be modified by the callers.
   */
  public float[] getCurrentY() {
    return mCurrentY;
  }
}