package com.almeros.android.multitouch.gesturedetectors;

import android.content.Context;
import android.view.MotionEvent;

/**
 * @author Robert Nordan (robert.nordan@norkart.no)
 *         <p>
 *         Copyright (c) 2013, Norkart AS
 *         <p>
 *         All rights reserved.
 *         <p>
 *         Redistribution and use in source and binary forms, with or without
 *         modification, are permitted provided that the following conditions
 *         are met:
 *         <p>
 *         Redistributions of source code must retain the above copyright
 *         notice, this list of conditions and the following disclaimer.
 *         Redistributions in binary form must reproduce the above copyright
 *         notice, this list of conditions and the following disclaimer in the
 *         documentation and/or other materials provided with the distribution.
 *         <p>
 *         THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *         "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *         LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 *         A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 *         HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 *         INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 *         BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 *         OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 *         AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 *         LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
 *         WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *         POSSIBILITY OF SUCH DAMAGE.
 */
public class ShoveGestureDetector extends TwoFingerGestureDetector {

  /**
   * Listener which must be implemented which is used by ShoveGestureDetector
   * to perform callbacks to any implementing class which is registered to a
   * ShoveGestureDetector via the constructor.
   *
   * @see ShoveGestureDetector.SimpleOnShoveGestureListener
   */
  public interface OnShoveGestureListener {
    public boolean onShove(ShoveGestureDetector detector);

    public boolean onShoveBegin(ShoveGestureDetector detector);

    public void onShoveEnd(ShoveGestureDetector detector);
  }

  /**
   * Helper class which may be extended and where the methods may be
   * implemented. This way it is not necessary to implement all methods of
   * OnShoveGestureListener.
   */
  public static class SimpleOnShoveGestureListener implements
    OnShoveGestureListener {
    public boolean onShove(ShoveGestureDetector detector) {
      return false;
    }

    public boolean onShoveBegin(ShoveGestureDetector detector) {
      return true;
    }

    public void onShoveEnd(ShoveGestureDetector detector) {
      // Do nothing, overridden implementation may be used
    }
  }

  private float prevAverageY;
  private float currAverageY;

  private final OnShoveGestureListener listener;
  private boolean sloppyGesture;

  public ShoveGestureDetector(Context context, OnShoveGestureListener listener) {
    super(context);
    this.listener = listener;
  }

  @Override
  protected void handleStartProgressEvent(int actionCode, MotionEvent event) {
    switch (actionCode) {
      case MotionEvent.ACTION_POINTER_DOWN:
        // At least the second finger is on screen now

        resetState(); // In case we missed an UP/CANCEL event
        prevEvent = MotionEvent.obtain(event);
        timeDelta = 0;

        updateStateByEvent(event);

        // See if we have a sloppy gesture
        sloppyGesture = isSloppyGesture(event);
        if (!sloppyGesture) {
          // No, start gesture now
          gestureInProgress = listener.onShoveBegin(this);
        }
        break;

      case MotionEvent.ACTION_MOVE:
        if (!sloppyGesture) {
          break;
        }

        // See if we still have a sloppy gesture
        sloppyGesture = isSloppyGesture(event);
        if (!sloppyGesture) {
          // No, start normal gesture now
          gestureInProgress = listener.onShoveBegin(this);
        }

        break;

      case MotionEvent.ACTION_POINTER_UP:
        if (!sloppyGesture) {
          break;
        }

        break;
    }
  }

  @Override
  protected void handleInProgressEvent(int actionCode, MotionEvent event) {
    switch (actionCode) {
      case MotionEvent.ACTION_POINTER_UP:
        // Gesture ended but
        updateStateByEvent(event);

        if (!sloppyGesture) {
          listener.onShoveEnd(this);
        }

        resetState();
        break;

      case MotionEvent.ACTION_CANCEL:
        if (!sloppyGesture) {
          listener.onShoveEnd(this);
        }

        resetState();
        break;

      case MotionEvent.ACTION_MOVE:
        updateStateByEvent(event);

        // Only accept the event if our relative pressure is within
        // a certain limit. This can help filter shaky data as a
        // finger is lifted. Also check that shove is meaningful.
        if (currPressure / prevPressure > PRESSURE_THRESHOLD
          && Math.abs(getShovePixelsDelta()) > 0.5f) {
          final boolean updatePrevious = listener.onShove(this);
          if (updatePrevious) {
            prevEvent.recycle();
            prevEvent = MotionEvent.obtain(event);
          }
        }
        break;
    }
  }

  @Override
  protected void resetState() {
    super.resetState();
    sloppyGesture = false;
    prevAverageY = 0.0f;
    currAverageY = 0.0f;
  }

  @Override
  protected void updateStateByEvent(MotionEvent curr) {
    super.updateStateByEvent(curr);

    final MotionEvent prev = prevEvent;
    float py0 = prev.getY(0);
    float py1 = prev.getY(1);
    prevAverageY = (py0 + py1) / 2.0f;

    float cy0 = curr.getY(0);
    float cy1 = curr.getY(1);
    currAverageY = (cy0 + cy1) / 2.0f;
  }

  @Override
  protected boolean isSloppyGesture(MotionEvent event) {
    boolean sloppy = super.isSloppyGesture(event);
    if (sloppy) {
      return true;
    }

    // If it's not traditionally sloppy, we check if the angle between
    // fingers
    // is acceptable.
    double angle = Math.abs(Math.atan2(currFingerDiffY, currFingerDiffX));
    // about 20 degrees, left or right
    return !((0.0f < angle && angle < 0.35f) || 2.79f < angle
      && angle < Math.PI);
  }

  /**
   * Return the distance in pixels from the previous shove event to the
   * current event.
   *
   * @return The current distance in pixels.
   */
  public float getShovePixelsDelta() {
    return currAverageY - prevAverageY;
  }
}
