/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * 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 android.server.wm;

import static android.server.wm.StateLogger.log;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import android.app.Instrumentation;
import android.content.Context;
import android.graphics.Rect;
import android.os.SystemClock;
import android.util.Log;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

public class TouchHelper {

    private static final String TAG = "TouchHelper";

    public final Context mContext;
    public final Instrumentation mInstrumentation;
    public final WindowManagerStateHelper mWmState;

    public TouchHelper(Instrumentation instrumentation, WindowManagerStateHelper wmState) {
        mInstrumentation = instrumentation;
        mContext = mInstrumentation.getContext();
        mWmState = wmState;
    }

    /**
     * Insert an input event (ACTION_DOWN -> ACTION_CANCEL) to ensures the display to be focused
     * without triggering potential clicked to impact the test environment.
     * (e.g: Keyguard credential activated unexpectedly.)
     *
     * @param displayId the display ID to gain focused by inject swipe action
     */
    public void touchAndCancelOnDisplayCenterSync(int displayId) {
        WindowManagerState.DisplayContent dc = mWmState.getDisplay(displayId);
        if (dc == null) {
            // never get wm state before?
            mWmState.computeState();
            dc = mWmState.getDisplay(displayId);
        }
        if (dc == null) {
            log("Cannot tap on display: " + displayId);
            return;
        }
        final Rect bounds = dc.getDisplayRect();
        final int x = bounds.left + bounds.width() / 2;
        final int y = bounds.top + bounds.height() / 2;
        final long downTime = SystemClock.uptimeMillis();
        injectMotion(downTime, downTime, MotionEvent.ACTION_DOWN, x, y, displayId, true /* sync */);

        final long eventTime = SystemClock.uptimeMillis();
        final int touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
        final int tapX = x + Math.round(touchSlop / 2.0f);
        final int tapY = y + Math.round(touchSlop / 2.0f);
        injectMotion(downTime, eventTime, MotionEvent.ACTION_CANCEL, tapX, tapY, displayId,
                true /* sync */);
    }

    public void tapOnDisplaySync(int x, int y, int displayId) {
        tapOnDisplay(x, y, displayId, true /* sync*/);
    }

    public void tapOnDisplay(int x, int y, int displayId, boolean sync) {
        tapOnDisplay(x, y, displayId, sync, /* waitAnimations */ true);
    }

    public void tapOnDisplay(int x, int y, int displayId, boolean sync, boolean waitAnimations) {
        final long downTime = SystemClock.uptimeMillis();
        injectMotion(downTime, downTime, MotionEvent.ACTION_DOWN, x, y, displayId, sync,
                waitAnimations);

        final long upTime = SystemClock.uptimeMillis();
        injectMotion(downTime, upTime, MotionEvent.ACTION_UP, x, y, displayId, sync,
                waitAnimations);

        if (waitAnimations) {
            mWmState.waitForWithAmState(state -> state.getFocusedDisplayId() == displayId,
                    "top focused displayId: " + displayId);
        }
        // This is needed after a tap in multi-display to ensure that the display focus has really
        // changed, if needed. The call to syncInputTransaction will wait until focus change has
        // propagated from WMS to native input before returning.
        mInstrumentation.getUiAutomation().syncInputTransactions(waitAnimations);
    }

    public void tapOnCenter(Rect bounds, int displayId) {
        tapOnCenter(bounds, displayId, true  /* waitAnimation */);
    }

    public void tapOnCenter(Rect bounds, int displayId, boolean waitAnimation) {
        final int tapX = bounds.left + bounds.width() / 2;
        final int tapY = bounds.top + bounds.height() / 2;
        tapOnDisplay(tapX, tapY, displayId, true /* sync */, waitAnimation);
    }

    public void tapOnViewCenter(View view) {
        tapOnViewCenter(view, true /* waitAnimations */);
    }

    public void tapOnViewCenter(View view, boolean waitAnimations) {
        final int[] topleft = new int[2];
        view.getLocationOnScreen(topleft);
        int x = topleft[0] + view.getWidth() / 2;
        int y = topleft[1] + view.getHeight() / 2;
        tapOnDisplay(x, y, view.getDisplay().getDisplayId(), true /* sync */, waitAnimations);
    }

    public void tapOnTaskCenter(WindowManagerState.Task task) {
        tapOnCenter(task.getBounds(), task.mDisplayId);
    }

    public void tapOnDisplayCenter(int displayId) {
        final Rect bounds = mWmState.getDisplay(displayId).getDisplayRect();
        tapOnDisplaySync(bounds.centerX(), bounds.centerY(), displayId);
    }

    public static void injectMotion(long downTime, long eventTime, int action,
            int x, int y, int displayId, boolean sync) {
        injectMotion(downTime, eventTime, action, x, y, displayId, sync,
                true /* waitForAnimations */);
    }

    public static void injectMotion(long downTime, long eventTime, int action,
            int x, int y, int displayId, boolean sync, boolean waitAnimations) {
        Log.d(
                TAG,
                "injectMotion="
                        + MotionEvent.actionToString(action)
                        + " at ["
                        + x
                        + ","
                        + y
                        + "]"
                        + " on displayId="
                        + displayId);
        final MotionEvent event = MotionEvent.obtain(downTime, eventTime, action,
                x, y, 0 /* metaState */);
        event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
        event.setDisplayId(displayId);
        getInstrumentation().getUiAutomation().injectInputEvent(event, sync, waitAnimations);
    }

    public static void injectKey(int keyCode, boolean longPress, boolean sync) {
        final long downTime = injectKeyActionDown(keyCode, longPress, sync);
        injectKeyActionUp(keyCode, downTime, /* cancelled = */ false, sync);
    }

    public static long injectKeyActionDown(int keyCode, boolean longPress, boolean sync) {
        final long downTime = SystemClock.uptimeMillis();
        int repeatCount = 0;
        final KeyEvent downEvent =
                new KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, repeatCount);
        getInstrumentation().getUiAutomation().injectInputEvent(downEvent, sync);
        if (longPress) {
            repeatCount += 1;
            final KeyEvent repeatEvent = new KeyEvent(downTime, SystemClock.uptimeMillis(),
                    KeyEvent.ACTION_DOWN, keyCode, repeatCount);
            getInstrumentation().getUiAutomation().injectInputEvent(repeatEvent, sync);
        }
        return downTime;
    }

    public static void injectKeyActionUp(int keyCode, long downTime, boolean cancelled,
            boolean sync) {
        final int flags;
        if (cancelled) {
            flags = KeyEvent.FLAG_CANCELED;
        } else {
            flags = 0;
        }
        final KeyEvent upEvent = new KeyEvent(downTime, SystemClock.uptimeMillis(),
                KeyEvent.ACTION_UP, keyCode, /* repeatCount = */ 0, /* metaState= */ 0,
                KeyCharacterMap.VIRTUAL_KEYBOARD, /* scancode= */ 0, flags);
        getInstrumentation().getUiAutomation().injectInputEvent(upEvent, sync);
    }

    public void tapOnTaskCenterAsync(WindowManagerState.Task task) {
        final Rect bounds = task.getBounds();
        final int x = bounds.left + bounds.width() / 2;
        final int y = bounds.top + bounds.height() / 2;
        tapOnDisplay(x, y, task.mDisplayId, false /* sync*/);
    }
}
