/*
Copyright 2011 NativeDriver committers
Copyright 2011 Google Inc.

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.google.android.testing.nativedriver.server;

import javax.annotation.Nullable;

import org.openqa.selenium.Point;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.interactions.internal.Coordinates;
import org.openqa.selenium.support.ui.Clock;

import android.app.Instrumentation;
import android.view.MotionEvent;
import android.view.ViewConfiguration;

import com.google.android.testing.nativedriver.common.Touch;
import com.google.common.base.Preconditions;

/**
 * {@code Touch} interface implementation.
 *
 * @author Dezheng Xu
 */
public class AndroidNativeTouch implements Touch {
  // Use as the last argument for creating MotionEvent instance
  protected static final int DEFAULT_META_STATE = 0;

   // The duration in milliseconds before a press turns into a long press,
   // "x1.5" to ensure.
  protected static final long DURATION_OF_LONG_PRESS
      = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f);

   // The duration in milliseconds between the first tap's up event and second
   // tap's down event for an interaction to be considered a double-tap.
   // "/1.5" to ensure.
  protected static final long DURATION_BETWEEN_DOUBLE_TAP
      = (long) (ViewConfiguration.getDoubleTapTimeout() / 1.5f);

  private static final long UNDEFINED_TIME = Long.MIN_VALUE;

  private final Instrumentation instrumentation;
  private final Clock clock;
  private Coordinates currentActiveCoordinates;

  // We are only accessing downTime in synchronized blocks
  private long downTime = UNDEFINED_TIME;

  public AndroidNativeTouch(Clock clock, Instrumentation instrumentation) {
    this.clock = clock;
    this.instrumentation = instrumentation;
  }

  public static AndroidNativeTouch withDefaults(
      Instrumentation instrumentation) {
    Clock clock = new AndroidSystemClock();
    return new AndroidNativeTouch(clock, instrumentation);
  }

  @Override
  public synchronized void tap(@Nullable Coordinates where) {
    if (!isTouchStateReleased()) {
      throw new IllegalStateException(
          "Attempt to tap when touch state is already down");
    }
    updateActiveCoordinates(where);
    Point point = currentActiveCoordinates.getLocationOnScreen();
    tap(point.getX(), point.getY());
  }

  @Override
  public synchronized void doubleTap(@Nullable Coordinates where) {
    if (!isTouchStateReleased()) {
      throw new IllegalStateException(
          "Attempt to double tap when touch state is already down");
    }
    updateActiveCoordinates(where);
    Point point = currentActiveCoordinates.getLocationOnScreen();
    doubleTap(point.getX(), point.getY());
  }

  @Override
  public synchronized void touchDown(@Nullable Coordinates where) {
    if (!isTouchStateReleased()) {
      throw new IllegalStateException(
          "Attempt to touch down when touch state is already down");
    }
    updateActiveCoordinates(where);
    Point point = currentActiveCoordinates.getLocationOnScreen();
    touchDown(point.getX(), point.getY());
  }

  @Override
  public synchronized void touchUp(@Nullable Coordinates where) {
    if (isTouchStateReleased()) {
      throw new IllegalStateException(
          "Attempt to release touch when touch is already released");
    }
    updateActiveCoordinates(where);
    Point point = currentActiveCoordinates.getLocationOnScreen();
    touchUp(point.getX(), point.getY());
  }

  @Override
  public synchronized void touchMove(Coordinates where) {
    Preconditions.checkNotNull(where);
    updateActiveCoordinates(where);
    if (!isTouchStateReleased()) {
      Point point = where.getLocationOnScreen();
      touchMove(point.getX(), point.getY());
    }
  }

  @Override
  public synchronized void touchMove(
      Coordinates where, long xOffset, long yOffset) {
    // Even Mouse.mouseMove(Coordinates where, long xOffset, long yOffset) is
    // not supported yet in current WebDriver implementation.
    throw new UnsupportedOperationException(
        "Moving to arbitrary (x, y) coordinates not supported.");
  }

  @Override
  public synchronized void longClick(Coordinates where) {
    if (!isTouchStateReleased()) {
      throw new IllegalStateException(
          "Attempt to longclick when touch state is already down");
    }
    updateActiveCoordinates(where);
    Point point = currentActiveCoordinates.getLocationOnScreen();
    longClick(point.getX(), point.getY());
  }

  protected void touchDown(int x, int y) {
    downTime = clock.now();
    MotionEvent motionEvent = MotionEvent.obtain(downTime, clock.now(),
        MotionEvent.ACTION_DOWN, x, y, DEFAULT_META_STATE);
    sendMotionEvent(motionEvent);
  }

  protected void touchUp(int x, int y) {
    MotionEvent motionEvent = MotionEvent.obtain(downTime, clock.now(),
        MotionEvent.ACTION_UP, x, y, DEFAULT_META_STATE);
    setTouchStateReleased();
    sendMotionEvent(motionEvent);
  }

  protected void touchMove(int x, int y) {
    MotionEvent motionEvent = MotionEvent.obtain(downTime, clock.now(),
        MotionEvent.ACTION_MOVE, x, y, DEFAULT_META_STATE);
    sendMotionEvent(motionEvent);
  }

  protected void tap(int x, int y) {
    touchDown(x, y);
    int scaledTouchSlopAdjustment = getScaledTouchSlopAdjustment();
    touchMove(x + scaledTouchSlopAdjustment, y + scaledTouchSlopAdjustment);
    touchUp(x, y);
  }

  protected void drag(int x1, int y1, int x2, int y2) {
    touchDown(x1, y1);
    //int scaledTouchSlopAdjustment = getScaledTouchSlopAdjustment();
    touchMove(x2 , y2);
    touchUp(x2, y2);
  }

  protected void longClick(int x, int y) {
    touchDown(x, y);
    int scaledTouchSlopAdjustment = getScaledTouchSlopAdjustment();
    touchMove(x + scaledTouchSlopAdjustment, y + scaledTouchSlopAdjustment);
    sleep(DURATION_OF_LONG_PRESS);
    touchUp(x, y);
  }

  protected void doubleTap(int x, int y) {
    tap(x, y);
    sleep(DURATION_BETWEEN_DOUBLE_TAP);
    tap(x, y);
  }

  protected void sleep(long millis) {
    try {
      Thread.sleep(millis);
    } catch (InterruptedException exception) {
      Thread.currentThread().interrupt();
      throw new WebDriverException(exception);
    }
  }

  protected void sendMotionEvent(MotionEvent motionEvent) {
    instrumentation.waitForIdleSync();
    try {
      instrumentation.sendPointerSync(motionEvent);
    } catch (Exception ignored) {
    }
  }

  protected boolean isTouchStateReleased() {
    return downTime == UNDEFINED_TIME;
  }

  protected int getScaledTouchSlopAdjustment() {
    int touchSlop = ViewConfiguration.get(instrumentation.getContext())
        .getScaledTouchSlop();
    return touchSlop / 2;
  }

  private void updateActiveCoordinates(Coordinates coordinates) {
    if (coordinates != null) {
      currentActiveCoordinates = coordinates;
    } else if (currentActiveCoordinates == null) {
      throw new IllegalStateException(
          "No current active coordinates and given coordinates is null.");
    }
  }

  private void setTouchStateReleased() {
    downTime = UNDEFINED_TIME;
  }

  //TODO(dxu): think about how to deal with ACTION_CANCEL
}

@@ -5746,32 +5746,102 @@
 Down(x, y);%0A    
+// HACK: OS 2.3.x instrumentation.waitForIdleSync(); not return%0A    //
 int scaledTouchS
@@ -5884,32 +5884,34 @@
 justment();%0A    
+//
 touchMove(x + sc
@@ -6254,24 +6254,26 @@
 (x, y);%0A    
+//
 int scaledTo
@@ -6320,32 +6320,34 @@
 justment();%0A    
+//
 touchMove(x + sc

Comment out tap and longClick touchMove