/*
 * Copyright (C) 2013 DroidDriver committers
 *
 * 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.droiddriver.actions;

import android.graphics.Rect;
import android.os.SystemClock;
import android.view.ViewConfiguration;

import com.google.android.droiddriver.UiElement;
import com.google.android.droiddriver.exceptions.ActionException;
import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
import com.google.android.droiddriver.util.Events;
import com.google.android.droiddriver.util.Strings;
import com.google.android.droiddriver.util.Strings.ToStringHelper;

/**
 * An action that swipes the touch screen.
 */
public class SwipeAction extends EventAction implements ScrollAction {
  // Milliseconds between synthesized ACTION_MOVE events.
  // Note: ACTION_MOVE_INTERVAL is the minimum interval between injected events;
  // the actual interval typically is longer.
  private static final int ACTION_MOVE_INTERVAL = 5;
  /**
   * The magic number from UiAutomator. This value is empirical. If it actually
   * results in a fling, you can change it with {@link #setScrollSteps}.
   */
  private static int scrollSteps = 55;
  private static int flingSteps = 3;

  /** Returns the {@link #scrollSteps} used in {@link #toScroll}. */
  public static int getScrollSteps() {
    return scrollSteps;
  }

  /** Sets the {@link #scrollSteps} used in {@link #toScroll}. */
  public static void setScrollSteps(int scrollSteps) {
    SwipeAction.scrollSteps = scrollSteps;
  }

  /** Returns the {@link #flingSteps} used in {@link #toFling}. */
  public static int getFlingSteps() {
    return flingSteps;
  }

  /** Sets the {@link #flingSteps} used in {@link #toFling}. */
  public static void setFlingSteps(int flingSteps) {
    SwipeAction.flingSteps = flingSteps;
  }

  /**
   * Gets {@link SwipeAction} instances for scrolling.
   * <p>
   * Note: This may result in flinging instead of scrolling, depending on the
   * size of the target UiElement and the SDK version of the device. If it does
   * not behave as expected, you can change steps with {@link #setScrollSteps}.
   * </p>
   *
   * @param direction specifies where the view port will move, instead of the
   *        finger.
   * @see ViewConfiguration#getScaledMinimumFlingVelocity
   */
  public static SwipeAction toScroll(PhysicalDirection direction) {
    return new SwipeAction(direction, scrollSteps);
  }

  /**
   * Gets {@link SwipeAction} instances for flinging.
   * <p>
   * Note: This may not actually fling, depending on the size of the target
   * UiElement and the SDK version of the device. If it does not behave as
   * expected, you can change steps with {@link #setFlingSteps}.
   * </p>
   *
   * @param direction specifies where the view port will move, instead of the
   *        finger.
   * @see ViewConfiguration#getScaledMinimumFlingVelocity
   */
  public static SwipeAction toFling(PhysicalDirection direction) {
    return new SwipeAction(direction, flingSteps);
  }

  private final PhysicalDirection direction;
  private final boolean drag;
  private final int steps;
  private final float topMarginRatio;
  private final float leftMarginRatio;
  private final float bottomMarginRatio;
  private final float rightMarginRatio;

  /**
   * Defaults timeoutMillis to 1000 and no drag.
   */
  public SwipeAction(PhysicalDirection direction, int steps) {
    this(direction, steps, false, 1000L);
  }

  /**
   * Defaults all margin ratios to 0.1F.
   */
  public SwipeAction(PhysicalDirection direction, int steps, boolean drag, long timeoutMillis) {
    this(direction, steps, drag, timeoutMillis, 0.1F, 0.1F, 0.1F, 0.1F);
  }

  /**
   * @param direction the scroll direction specifying where the view port will
   *        move, instead of the finger.
   * @param steps minimum 2; (steps-1) is the number of {@code ACTION_MOVE} that
   *        will be injected between {@code ACTION_DOWN} and {@code ACTION_UP}.
   * @param drag whether this is a drag
   * @param timeoutMillis
   * @param topMarginRatio margin ratio from top
   * @param leftMarginRatio margin ratio from left
   * @param bottomMarginRatio margin ratio from bottom
   * @param rightMarginRatio margin ratio from right
   */
  public SwipeAction(PhysicalDirection direction, int steps, boolean drag, long timeoutMillis,
      float topMarginRatio, float leftMarginRatio, float bottomMarginRatio, float rightMarginRatio) {
    super(timeoutMillis);
    this.direction = direction;
    this.steps = Math.max(2, steps);
    this.drag = drag;
    this.topMarginRatio = topMarginRatio;
    this.bottomMarginRatio = bottomMarginRatio;
    this.leftMarginRatio = leftMarginRatio;
    this.rightMarginRatio = rightMarginRatio;
  }

  @Override
  public boolean perform(InputInjector injector, UiElement element) {
    Rect elementRect = element.getVisibleBounds();

    int topMargin = (int) (elementRect.height() * topMarginRatio);
    int bottomMargin = (int) (elementRect.height() * bottomMarginRatio);
    int leftMargin = (int) (elementRect.width() * leftMarginRatio);
    int rightMargin = (int) (elementRect.width() * rightMarginRatio);
    int adjustedbottom = elementRect.bottom - bottomMargin;
    int adjustedTop = elementRect.top + topMargin;
    int adjustedLeft = elementRect.left + leftMargin;
    int adjustedRight = elementRect.right - rightMargin;
    int startX;
    int startY;
    int endX;
    int endY;

    switch (direction) {
      case DOWN:
        startX = elementRect.centerX();
        startY = adjustedbottom;
        endX = elementRect.centerX();
        endY = adjustedTop;
        break;
      case UP:
        startX = elementRect.centerX();
        startY = adjustedTop;
        endX = elementRect.centerX();
        endY = adjustedbottom;
        break;
      case LEFT:
        startX = adjustedLeft;
        startY = elementRect.centerY();
        endX = adjustedRight;
        endY = elementRect.centerY();
        break;
      case RIGHT:
        startX = adjustedRight;
        startY = elementRect.centerY();
        endX = adjustedLeft;
        endY = elementRect.centerY();
        break;
      default:
        throw new ActionException("Unknown scroll direction: " + direction);
    }

    double xStep = ((double) (endX - startX)) / steps;
    double yStep = ((double) (endY - startY)) / steps;

    // First touch starts exactly at the point requested
    long downTime = Events.touchDown(injector, startX, startY);
    SystemClock.sleep(ACTION_MOVE_INTERVAL);
    if (drag) {
      SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout() * 1.5f));
    }
    for (int i = 1; i < steps; i++) {
      Events.touchMove(injector, downTime, startX + (int) (xStep * i), startY + (int) (yStep * i));
      SystemClock.sleep(ACTION_MOVE_INTERVAL);
    }
    if (drag) {
      // Hold final position for a little bit to simulate drag.
      SystemClock.sleep(100);
    }
    Events.touchUp(injector, downTime, endX, endY);
    return true;
  }

  @Override
  public String toString() {
    ToStringHelper toStringHelper = Strings.toStringHelper(this);
    toStringHelper.addValue(direction);
    toStringHelper.add("steps", steps);
    if (drag) {
      toStringHelper.addValue("drag");
    }
    return toStringHelper.toString();
  }
}
