/*
 * Copyright (C) 2021 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.hardware.input;

import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.view.InputEvent;
import android.view.MotionEvent;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * An event describing a touchscreen interaction originating from a remote device.
 *
 * The pointer id, tool type, action, and location are required; pressure and main axis size are
 * optional.
 *
 * Note: A VirtualTouchEvent with ACTION_CANCEL can only be created with TOOL_TYPE_PALM (and vice
 * versa). Events are injected into the uinput kernel module, which has no concept of cancelling
 * an action. The only way to state the intention that a pointer should not be handled as a pointer
 * is to change its tool type to TOOL_TYPE_PALM.
 *
 * @hide
 */
@SystemApi
public final class VirtualTouchEvent implements Parcelable {

    /** @hide */
    public static final int TOOL_TYPE_UNKNOWN = MotionEvent.TOOL_TYPE_UNKNOWN;
    /** Tool type indicating that the user's finger is the origin of the event. */
    public static final int TOOL_TYPE_FINGER = MotionEvent.TOOL_TYPE_FINGER;
    /**
     * Tool type indicating that a user's palm (or other input mechanism to be rejected) is the
     * origin of the event.
     */
    public static final int TOOL_TYPE_PALM = MotionEvent.TOOL_TYPE_PALM;
    /** @hide */
    @IntDef(prefix = { "TOOL_TYPE_" }, value = {
            TOOL_TYPE_UNKNOWN,
            TOOL_TYPE_FINGER,
            TOOL_TYPE_PALM,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ToolType {}

    /** @hide */
    public static final int ACTION_UNKNOWN = -1;
    /** Action indicating the tool has been pressed down to the touchscreen. */
    public static final int ACTION_DOWN = MotionEvent.ACTION_DOWN;
    /** Action indicating the tool has been lifted from the touchscreen. */
    public static final int ACTION_UP = MotionEvent.ACTION_UP;
    /** Action indicating the tool has been moved along the face of the touchscreen. */
    public static final int ACTION_MOVE = MotionEvent.ACTION_MOVE;
    /** Action indicating the tool cancelled the current movement. */
    public static final int ACTION_CANCEL = MotionEvent.ACTION_CANCEL;
    /** @hide */
    @IntDef(prefix = { "ACTION_" }, value = {
            ACTION_UNKNOWN,
            ACTION_DOWN,
            ACTION_UP,
            ACTION_MOVE,
            ACTION_CANCEL,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Action {}

    // The maximum number of pointers that can be touching the screen at once. (See MAX_POINTERS
    // in frameworks/native/include/input/Input.h)
    private static final int MAX_POINTERS = 16;

    private final int mPointerId;
    private final @ToolType int mToolType;
    private final @Action int mAction;
    private final float mX;
    private final float mY;
    private final float mPressure;
    private final float mMajorAxisSize;
    private final long mEventTimeNanos;

    private VirtualTouchEvent(int pointerId, @ToolType int toolType, @Action int action,
            float x, float y, float pressure, float majorAxisSize, long eventTimeNanos) {
        mPointerId = pointerId;
        mToolType = toolType;
        mAction = action;
        mX = x;
        mY = y;
        mPressure = pressure;
        mMajorAxisSize = majorAxisSize;
        mEventTimeNanos = eventTimeNanos;
    }

    private VirtualTouchEvent(@NonNull Parcel parcel) {
        mPointerId = parcel.readInt();
        mToolType = parcel.readInt();
        mAction = parcel.readInt();
        mX = parcel.readFloat();
        mY = parcel.readFloat();
        mPressure = parcel.readFloat();
        mMajorAxisSize = parcel.readFloat();
        mEventTimeNanos = parcel.readLong();
    }

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeInt(mPointerId);
        dest.writeInt(mToolType);
        dest.writeInt(mAction);
        dest.writeFloat(mX);
        dest.writeFloat(mY);
        dest.writeFloat(mPressure);
        dest.writeFloat(mMajorAxisSize);
        dest.writeLong(mEventTimeNanos);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public String toString() {
        return "VirtualTouchEvent("
                + " pointerId=" + mPointerId
                + " toolType=" + MotionEvent.toolTypeToString(mToolType)
                + " action=" + MotionEvent.actionToString(mAction)
                + " x=" + mX
                + " y=" + mY
                + " pressure=" + mPressure
                + " majorAxisSize=" + mMajorAxisSize
                + " eventTime(ns)=" + mEventTimeNanos;
    }

    /**
     * Returns the pointer id associated with this event.
     */
    public int getPointerId() {
        return mPointerId;
    }

    /**
     * Returns the tool type associated with this event.
     */
    public @ToolType int getToolType() {
        return mToolType;
    }

    /**
     * Returns the action associated with this event.
     */
    public @Action int getAction() {
        return mAction;
    }

    /**
     * Returns the x-axis location associated with this event.
     */
    public float getX() {
        return mX;
    }

    /**
     * Returns the y-axis location associated with this event.
     */
    public float getY() {
        return mY;
    }

    /**
     * Returns the pressure associated with this event. Returns {@link Float#NaN} if omitted.
     */
    public float getPressure() {
        return mPressure;
    }

    /**
     * Returns the major axis size associated with this event. Returns {@link Float#NaN} if omitted.
     */
    public float getMajorAxisSize() {
        return mMajorAxisSize;
    }

    /**
     * Returns the time this event occurred, in the {@link SystemClock#uptimeMillis()} time base but
     * with nanosecond (instead of millisecond) precision.
     *
     * @see InputEvent#getEventTime()
     */
    public long getEventTimeNanos() {
        return mEventTimeNanos;
    }

    /**
     * Builder for {@link VirtualTouchEvent}.
     */
    public static final class Builder {

        private @ToolType int mToolType = TOOL_TYPE_UNKNOWN;
        private int mPointerId = MotionEvent.INVALID_POINTER_ID;
        private @Action int mAction = ACTION_UNKNOWN;
        private float mX = Float.NaN;
        private float mY = Float.NaN;
        private float mPressure = Float.NaN;
        private float mMajorAxisSize = Float.NaN;
        private long mEventTimeNanos = 0L;

        /**
         * Creates a {@link VirtualTouchEvent} object with the current builder configuration.
         *
         * @throws IllegalArgumentException if one of the required arguments is missing or if
         * ACTION_CANCEL is not set in combination with TOOL_TYPE_PALM. See
         * {@link VirtualTouchEvent} for a detailed explanation.
         */
        public @NonNull VirtualTouchEvent build() {
            if (mToolType == TOOL_TYPE_UNKNOWN || mPointerId == MotionEvent.INVALID_POINTER_ID
                    || mAction == ACTION_UNKNOWN || Float.isNaN(mX) || Float.isNaN(mY)) {
                throw new IllegalArgumentException(
                        "Cannot build virtual touch event with unset required fields");
            }
            if ((mToolType == TOOL_TYPE_PALM && mAction != ACTION_CANCEL)
                    || (mAction == ACTION_CANCEL && mToolType != TOOL_TYPE_PALM)) {
                throw new IllegalArgumentException(
                        "ACTION_CANCEL and TOOL_TYPE_PALM must always appear together");
            }
            return new VirtualTouchEvent(mPointerId, mToolType, mAction, mX, mY, mPressure,
                    mMajorAxisSize, mEventTimeNanos);
        }

        /**
         * Sets the pointer id of the event.
         *
         * <p>A Valid pointer id need to be in the range of 0 to 15.
         *
         * @return this builder, to allow for chaining of calls
         */
        public @NonNull Builder setPointerId(
                @IntRange(from = 0, to = MAX_POINTERS - 1) int pointerId) {
            if (pointerId < 0 || pointerId > 15) {
                throw new IllegalArgumentException(
                        "The pointer id must be in the range 0 - " + (MAX_POINTERS - 1)
                                + "inclusive, but was: " + pointerId);
            }
            mPointerId = pointerId;
            return this;
        }

        /**
         * Sets the tool type of the event.
         *
         * @return this builder, to allow for chaining of calls
         */
        public @NonNull Builder setToolType(@ToolType int toolType) {
            if (toolType != TOOL_TYPE_FINGER && toolType != TOOL_TYPE_PALM) {
                throw new IllegalArgumentException("Unsupported touch event tool type");
            }
            mToolType = toolType;
            return this;
        }

        /**
         * Sets the action of the event.
         *
         * @return this builder, to allow for chaining of calls
         */
        public @NonNull Builder setAction(@Action int action) {
            if (action != ACTION_DOWN && action != ACTION_UP && action != ACTION_MOVE
                    && action != ACTION_CANCEL) {
                throw new IllegalArgumentException(
                        "Unsupported touch event action type: " + action);
            }
            mAction = action;
            return this;
        }

        /**
         * Sets the x-axis location of the event.
         *
         * @return this builder, to allow for chaining of calls
         */
        public @NonNull Builder setX(float absX) {
            mX = absX;
            return this;
        }

        /**
         * Sets the y-axis location of the event.
         *
         * @return this builder, to allow for chaining of calls
         */
        public @NonNull Builder setY(float absY) {
            mY = absY;
            return this;
        }

        /**
         * Sets the pressure of the event. This field is optional and can be omitted.
         *
         * @param pressure The pressure of the touch.
         *                 Note: The VirtualTouchscreen, consuming VirtualTouchEvents, is
         *                 configured with a pressure axis range from 0.0 to 255.0. Only the
         *                 lower end of the range is enforced. You can pass values larger than
         *                 255.0. With physical input devices this could happen if the
         *                 calibration is off. Values larger than 255.0 will not be trimmed and
         *                 passed on as is.
         *
         * @throws IllegalArgumentException if the pressure is smaller than 0.
         *
         * @return this builder, to allow for chaining of calls
         */
        public @NonNull Builder setPressure(@FloatRange(from = 0f) float pressure) {
            if (pressure < 0f) {
                throw new IllegalArgumentException("Touch event pressure cannot be negative");
            }
            mPressure = pressure;
            return this;
        }

        /**
         * Sets the major axis size of the event. This field is optional and can be omitted.
         *
         * @return this builder, to allow for chaining of calls
         */
        public @NonNull Builder setMajorAxisSize(@FloatRange(from = 0f) float majorAxisSize) {
            if (majorAxisSize < 0f) {
                throw new IllegalArgumentException(
                        "Touch event major axis size cannot be negative");
            }
            mMajorAxisSize = majorAxisSize;
            return this;
        }

        /**
         * Sets the time (in nanoseconds) when this specific event was generated. This may be
         * obtained from {@link SystemClock#uptimeMillis()} (with nanosecond precision instead of
         * millisecond), but can be different depending on the use case.
         * This field is optional and can be omitted.
         *
         * @return this builder, to allow for chaining of calls
         * @see InputEvent#getEventTime()
         */
        public @NonNull Builder setEventTimeNanos(long eventTimeNanos) {
            if (eventTimeNanos < 0L) {
                throw new IllegalArgumentException("Event time cannot be negative");
            }
            mEventTimeNanos = eventTimeNanos;
            return this;
        }
    }

    public static final @NonNull Parcelable.Creator<VirtualTouchEvent> CREATOR =
            new Parcelable.Creator<VirtualTouchEvent>() {
        public VirtualTouchEvent createFromParcel(Parcel source) {
            return new VirtualTouchEvent(source);
        }
        public VirtualTouchEvent[] newArray(int size) {
            return new VirtualTouchEvent[size];
        }
    };
}
