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

package com.vectras.vm.x11.input;

import static android.view.KeyEvent.*;
import static android.view.MotionEvent.*;
import static androidx.core.math.MathUtils.clamp;
import static com.vectras.vm.x11.input.InputStub.*;
import static java.nio.charset.StandardCharsets.UTF_8;

import android.graphics.PointF;
import android.view.KeyEvent;
import android.view.MotionEvent;

import com.vectras.vm.x11.X11Activity;

import java.util.List;
import java.util.TreeSet;

/**
 * A set of functions to send users' activities, which are represented by Android classes, to
 * remote host machine. This class uses a {@link InputStub} to do the real injections.
 */
public final class InputEventSender {
    private static final int XI_TouchBegin = 18;
    private static final int XI_TouchUpdate = 19;
    private static final int XI_TouchEnd = 20;

    private final InputStub mInjector;

    public boolean tapToMove = false;
    public boolean preferScancodes = false;
    public boolean pointerCapture = false;
    public boolean scaleTouchpad = false;
    public float capturedPointerSpeedFactor = 100;
    public boolean dexMetaKeyCapture = false;
    public boolean pauseKeyInterceptingWithEsc = false;

    /** Set of pressed keys for which we've sent TextEvent. */
    private final TreeSet<Integer> mPressedTextKeys;

    public InputEventSender(InputStub injector) {
        if (injector == null)
            throw new NullPointerException();
        mInjector = injector;
        mPressedTextKeys = new TreeSet<>();
    }

    private static final List<Integer> buttons = List.of(BUTTON_UNDEFINED, BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT);
    public void sendMouseEvent(PointF pos, int button, boolean down, boolean relative) {
        if (!buttons.contains(button))
            return;
        mInjector.sendMouseEvent(pos != null ? (int) pos.x : 0, pos != null ? (int) pos.y : 0, button, down, relative);
    }

    public void sendMouseDown(int button, boolean relative) {
        if (!buttons.contains(button)) 
            return;
        mInjector.sendMouseEvent(0, 0, button, true, relative);
    }

    public void sendMouseUp(int button, boolean relative) {
        if (!buttons.contains(button))
            return;
        mInjector.sendMouseEvent(0, 0, button, false, relative);
    }

    public void sendMouseClick(int button, boolean relative) {
        if (!buttons.contains(button))
            return;
        mInjector.sendMouseEvent(0, 0, button, true, relative);
        mInjector.sendMouseEvent(0, 0, button, false, relative);
    }

    public void sendCursorMove(float x, float y, boolean relative) {
        mInjector.sendMouseEvent(x, y, BUTTON_UNDEFINED, false, relative);
    }

    public void sendMouseWheelEvent(float distanceX, float distanceY) {
        mInjector.sendMouseWheelEvent(distanceX, distanceY);
    }

    final boolean[] pointers = new boolean[10];
    /**
     * Extracts the touch point data from a MotionEvent, converts each point into a marshallable
     * object and passes the set of points to the JNI layer to be transmitted to the remote host.
     *
     * @param event The event to send to the remote host for injection.  NOTE: This object must be
     *              updated to represent the remote machine's coordinate system before calling this
     *              function.
     */
    public void sendTouchEvent(MotionEvent event, RenderData renderData) {
        int action = event.getActionMasked();

        if (action == ACTION_MOVE || action == ACTION_HOVER_MOVE || action == ACTION_HOVER_ENTER || action == ACTION_HOVER_EXIT) {
            // In order to process all of the events associated with an ACTION_MOVE event, we need
            // to walk the list of historical events in order and add each event to our list, then
            // retrieve the current move event data.
            int pointerCount = event.getPointerCount();

            for (int p = 0; p < pointerCount; p++)
                pointers[event.getPointerId(p)] = false;

            for (int p = 0; p < pointerCount; p++) {
                int x = clamp((int) (event.getX(p) * renderData.scale.x), 0, renderData.screenWidth);
                int y = clamp((int) (event.getY(p) * renderData.scale.y), 0, renderData.screenHeight);
                pointers[event.getPointerId(p)] = true;
                mInjector.sendTouchEvent(XI_TouchUpdate, event.getPointerId(p), x, y);
            }

            // Sometimes Android does not send ACTION_POINTER_UP/ACTION_UP so some pointers are "stuck" in pressed state.
            for (int p = 0; p < 10; p++) {
                if (!pointers[p])
                    mInjector.sendTouchEvent(XI_TouchEnd, p, 0, 0);
            }
        } else {
            // For all other events, we only want to grab the current/active pointer.  The event
            // contains a list of every active pointer but passing all of of these to the host can
            // cause confusion on the remote OS side and result in broken touch gestures.
            int activePointerIndex = event.getActionIndex();
            int id = event.getPointerId(activePointerIndex);
            int x =  clamp((int) (event.getX(activePointerIndex) * renderData.scale.x), 0, renderData.screenWidth);
            int y =  clamp((int) (event.getY(activePointerIndex) * renderData.scale.y), 0, renderData.screenHeight);
            int a = (action == MotionEvent.ACTION_DOWN || action == ACTION_POINTER_DOWN) ? XI_TouchBegin : XI_TouchEnd;
            if (a == XI_TouchEnd)
                mInjector.sendTouchEvent(XI_TouchUpdate, id, x, y);
            mInjector.sendTouchEvent(a, id, x, y);
        }
    }

    /**
     * Converts the {@link KeyEvent} into low-level events and sends them to the host as either
     * key-events or text-events. This contains some logic for handling some special keys, and
     * avoids sending a key-up event for a key that was previously injected as a text-event.
     */
    public boolean sendKeyEvent(KeyEvent e) {
        int keyCode = e.getKeyCode();
        boolean pressed = e.getAction() == KeyEvent.ACTION_DOWN;

        // Events received from software keyboards generate TextEvent in two
        // cases:
        //   1. This is an ACTION_MULTIPLE event.
        //   2. Ctrl, Alt and Meta are not pressed.
        // This ensures that on-screen keyboard always injects input that
        // correspond to what user sees on the screen, while physical keyboard
        // acts as if it is connected to the remote host.
        if (e.getAction() == ACTION_MULTIPLE) {
            if (e.getCharacters() != null)
                mInjector.sendTextEvent(e.getCharacters().getBytes(UTF_8));
            else if (e.getUnicodeChar() != 0)
                mInjector.sendTextEvent(String.valueOf((char)e.getUnicodeChar()).getBytes(UTF_8));
            return true;
        }

        boolean no_modifiers = (!e.isAltPressed() && !e.isCtrlPressed() && !e.isMetaPressed())
                || ((e.getMetaState() & META_ALT_RIGHT_ON) != 0 && (e.getCharacters() != null || e.getUnicodeChar() != 0)); // For layouts with AltGr
        // For Enter getUnicodeChar() returns 10 (line feed), but we still
        // want to send it as KeyEvent.
        char unicode = keyCode != KEYCODE_ENTER ? (char) e.getUnicodeChar() : 0;
        int scancode = (preferScancodes || !no_modifiers) ? e.getScanCode(): 0;

        if (!preferScancodes) {
            if (pressed && unicode != 0 && no_modifiers) {
                mPressedTextKeys.add(keyCode);
                if ((e.getMetaState() & META_ALT_RIGHT_ON) != 0)
                    mInjector.sendKeyEvent(0, KEYCODE_ALT_RIGHT, false); // For layouts with AltGr

                mInjector.sendTextEvent(String.valueOf(unicode).getBytes(UTF_8));

                if ((e.getMetaState() & META_ALT_RIGHT_ON) != 0)
                    mInjector.sendKeyEvent(0, KEYCODE_ALT_RIGHT, true); // For layouts with AltGr
                return true;
            }

            if (!pressed && mPressedTextKeys.contains(keyCode)) {
                mPressedTextKeys.remove(keyCode);
                return true;
            }
        }

        // KEYCODE_AT, KEYCODE_POUND, KEYCODE_STAR and KEYCODE_PLUS are
        // deprecated, but they still need to be here for older devices and
        // third-party keyboards that may still generate these events. See
        // https://source.android.com/devices/input/keyboard-devices.html#legacy-unsupported-keys
        char[][] chars = {
                { KEYCODE_AT, '@', KEYCODE_2 },
                { KEYCODE_POUND, '#', KEYCODE_3 },
                { KEYCODE_STAR, '*', KEYCODE_8 },
                { KEYCODE_PLUS, '+', KEYCODE_EQUALS }
        };

        for (char[] i: chars) {
            if (e.getKeyCode() != i[0])
                continue;

            if ((e.getCharacters() != null && String.valueOf(i[1]).contentEquals(e.getCharacters()))
                    || e.getUnicodeChar() == i[1]) {
                mInjector.sendKeyEvent(0, KEYCODE_SHIFT_LEFT, pressed);
                mInjector.sendKeyEvent(0, i[2], pressed);
                return true;
            }
        }

        // Ignoring Android's autorepeat.
        if (e.getRepeatCount() > 0)
            return true;

        if (keyCode == KEYCODE_ESCAPE && !pressed && e.hasNoModifiers())
            X11Activity.setCapturingEnabled(false);

        // We try to send all other key codes to the host directly.
        return mInjector.sendKeyEvent(scancode, keyCode, pressed);
    }
}
