// Copyright 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser.test.util;

import android.app.Instrumentation;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

import org.chromium.base.ThreadUtils;

import java.util.concurrent.ExecutionException;

/**
 * Collection of utilities for generating touch events.
 * Based on android.test.TouchUtils, but slightly more flexible (allows to
 * specify coordinates for longClick, splits drag operation in three stages, etc).
 */
public class TestTouchUtils {
    /**
     * Returns the absolute location in screen coordinates from location relative
     * to view.
     * @param v The view the coordinates are relative to.
     * @param x Relative x location.
     * @param y Relative y location.
     * @return the absolute x and y location in an array.
     */
    public static int[] getAbsoluteLocationFromRelative(View v, int x, int y) {
        int location[] = new int[2];
        v.getLocationOnScreen(location);
        location[0] += x;
        location[1] += y;
        return location;
    }

    private static void sendAction(Instrumentation instrumentation, int action, long downTime,
            float x, float y) {
        long eventTime = SystemClock.uptimeMillis();
        MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0);
        instrumentation.sendPointerSync(event);
        instrumentation.waitForIdleSync();
    }

    /**
     * Sends (synchronously) a single click to an absolute screen coordinates.
     *
     * @param instrumentation Instrumentation object used by the test.
     * @param x Screen absolute x location.
     * @param y Screen absolute y location.
     */
    public static void singleClick(Instrumentation instrumentation, float x, float y) {
        long downTime = SystemClock.uptimeMillis();
        sendAction(instrumentation, MotionEvent.ACTION_DOWN, downTime, x, y);
        sendAction(instrumentation, MotionEvent.ACTION_UP, downTime, x, y);
    }

    /**
     * Sends (synchronously) a single click to the View at the specified coordinates.
     *
     * @param instrumentation Instrumentation object used by the test.
     * @param v The view the coordinates are relative to.
     * @param x Relative x location to the view.
     * @param y Relative y location to the view.
     */
    public static void singleClickView(Instrumentation instrumentation, View v, int x, int y) {
        int location[] = getAbsoluteLocationFromRelative(v, x, y);
        int absoluteX = location[0];
        int absoluteY = location[1];
        singleClick(instrumentation, absoluteX, absoluteY);
    }

    /**
     * Sends (synchronously) a single click to the center of the View.
     *
     * @param instrumentation Instrumentation object used by the test.
     * @param v The view the coordinates are relative to.
     */
    public static void singleClickView(Instrumentation instrumentation, View v) {
        int x = v.getWidth() / 2;
        int y = v.getHeight() / 2;
        singleClickView(instrumentation, v, x, y);
    }

    /**
     * Sleeps for at least the length of the double tap timeout.
     *
     * @param instrumentation Instrumentation object used by the test.
     */
    public static void sleepForDoubleTapTimeout(Instrumentation instrumentation) {
        SystemClock.sleep((long) (ViewConfiguration.getDoubleTapTimeout() * 1.5));
    }

    /**
     * Sends (synchronously) a long click to the View at the specified coordinates.
     *
     * @param instrumentation Instrumentation object used by the test.
     * @param v The view the coordinates are relative to.
     * @param x Relative x location to the view.
     * @param y Relative y location to the view.
     */
    public static void longClickView(Instrumentation instrumentation, View v, int x, int y) {
        int location[] = getAbsoluteLocationFromRelative(v, x, y);
        int absoluteX = location[0];
        int absoluteY = location[1];

        long downTime = SystemClock.uptimeMillis();
        sendAction(instrumentation, MotionEvent.ACTION_DOWN, downTime, absoluteX, absoluteY);
        SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout() * 1.5));
        sendAction(instrumentation, MotionEvent.ACTION_UP, downTime, absoluteX, absoluteY);
    }

    /**
     * Sends (synchronously) a long click to the View at its center.
     *
     * @param instrumentation Instrumentation object used by the test.
     * @param v The view to long click.
     */
    public static void longClickView(Instrumentation instrumentation, View v) {
        int x = v.getWidth() / 2;
        int y = v.getHeight() / 2;
        longClickView(instrumentation, v, x, y);
    }

    /**
     * Starts (synchronously) a drag motion. Normally followed by dragTo() and dragEnd().
     *
     * @param instrumentation Instrumentation object used by the test.
     * @param x The x location.
     * @param y The y location.
     * @return The downTime of the triggered event.
     */
    public static long dragStart(Instrumentation instrumentation, float x, float y) {
        long downTime = SystemClock.uptimeMillis();
        sendAction(instrumentation, MotionEvent.ACTION_DOWN, downTime, x, y);
        return downTime;
    }

    /**
     * Drags / moves (synchronously) to the specified coordinates. Normally preceded by
     * dragStart() and followed by dragEnd()
     *
     * @param instrumentation Instrumentation object used by the test.
     * @param fromX The relative x-coordinate of the start point of the drag.
     * @param toX The relative x-coordinate of the end point of the drag.
     * @param fromY The relative y-coordinate of the start point of the drag.
     * @param toY The relative y-coordinate of the end point of the drag.
     * @param stepCount The total number of motion events that should be generated during the drag.
     * @param downTime The initial time of the drag, in ms.
     */
    public static void dragTo(Instrumentation instrumentation, float fromX, float toX, float fromY,
            float toY, int stepCount, long downTime) {
        float x = fromX;
        float y = fromY;
        float yStep = (toY - fromY) / stepCount;
        float xStep = (toX - fromX) / stepCount;
        for (int i = 0; i < stepCount; ++i) {
            y += yStep;
            x += xStep;
            sendAction(instrumentation, MotionEvent.ACTION_MOVE, downTime, x, y);
        }
    }

    /**
     * Finishes (synchronously) a drag / move at the specified coordinate.
     * Normally preceded by dragStart() and dragTo().
     *
     * @param instrumentation Instrumentation object used by the test.
     * @param x The x location.
     * @param y The y location.
     * @param downTime The initial time of the drag, in ms.
     */
    public static void dragEnd(Instrumentation instrumentation, float x, float y, long downTime) {
        sendAction(instrumentation, MotionEvent.ACTION_UP, downTime, x, y);
    }

    /**
     * Performs a drag between the given coordinates, specified relative to the given view.
     * This method makes calls to dragStart, dragTo and dragEnd.
     *
     * @param instrumentation Instrumentation object used by the test.
     * @param view The view the coordinates are relative to.
     * @param fromX The relative x-coordinate of the start point of the drag.
     * @param toX The relative x-coordinate of the end point of the drag.
     * @param fromY The relative y-coordinate of the start point of the drag.
     * @param toY The relative y-coordinate of the end point of the drag.
     * @param stepCount The total number of motion events that should be generated during the drag.
     */
    public static void dragCompleteView(Instrumentation instrumentation, View view,
            int fromX, int toX, int fromY, int toY, int stepCount) {
        int fromLocation[] = getAbsoluteLocationFromRelative(view, fromX, fromY);
        int toLocation[] = getAbsoluteLocationFromRelative(view, toX, toY);
        long downTime = dragStart(instrumentation, fromLocation[0], fromLocation[1]);
        dragTo(instrumentation, fromLocation[0], toLocation[0], fromLocation[1], toLocation[1],
                stepCount, downTime);
        dragEnd(instrumentation, toLocation[0], toLocation[1], downTime);
    }

    /**
     * Calls performClick on a View on the main UI thread.
     *
     * @param instrumentation Instrumentation object used by the test.
     * @param v The view to call performClick on.
     */
    public static void performClickOnMainSync(Instrumentation instrumentation, final View v) {
        instrumentation.runOnMainSync(() -> v.performClick());
    }

    /**
     * Calls performLongClick on a View on the main UI thread.
     *
     * @param instrumentation Instrumentation object used by the test.
     * @param v The view to call performLongClick on.
     */
    public static void performLongClickOnMainSync(Instrumentation instrumentation, final View v)
            throws ExecutionException {
        ThreadUtils.runOnUiThreadBlocking(() -> v.performLongClick());
    }
}
