/*
 * Copyright (C) 2010 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.usb;

import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
import android.util.Log;

import com.android.internal.util.Preconditions;

import dalvik.system.CloseGuard;

import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.util.Objects;

/**
 * A class representing USB request packet.
 * This can be used for both reading and writing data to or from a
 * {@link android.hardware.usb.UsbDeviceConnection}.
 * UsbRequests can be used to transfer data on bulk and interrupt endpoints.
 * Requests on bulk endpoints can be sent synchronously via {@link UsbDeviceConnection#bulkTransfer}
 * or asynchronously via {@link #queue} and {@link UsbDeviceConnection#requestWait}.
 * Requests on interrupt endpoints are only send and received asynchronously.
 *
 * <p>Requests on endpoint zero are not supported by this class;
 * use {@link UsbDeviceConnection#controlTransfer} for endpoint zero requests instead.
 */
public class UsbRequest {

    private static final String TAG = "UsbRequest";

    // From drivers/usb/core/devio.c
    static final int MAX_USBFS_BUFFER_SIZE = 16384;

    // used by the JNI code
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private long mNativeContext;

    private UsbEndpoint mEndpoint;

    /** The buffer that is currently being read / written */
    @UnsupportedAppUsage
    private ByteBuffer mBuffer;

    /** The amount of data to read / write when using {@link #queue} */
    @UnsupportedAppUsage
    private int mLength;

    // for client use
    private Object mClientData;

    // Prevent the connection from being finalized
    private UsbDeviceConnection mConnection;

    /**
     * Whether this buffer was {@link #queue(ByteBuffer) queued using the new behavior} or
     * {@link #queue(ByteBuffer, int) queued using the deprecated behavior}.
     */
    private boolean mIsUsingNewQueue;

    /** Temporary buffer than might be used while buffer is enqueued */
    private ByteBuffer mTempBuffer;

    private final CloseGuard mCloseGuard = CloseGuard.get();

    /**
     * Lock for queue, enqueue and dequeue, so a queue operation can be finished by a dequeue
     * operation on a different thread.
     */
    private final Object mLock = new Object();

    public UsbRequest() {
    }

    /**
     * Initializes the request so it can read or write data on the given endpoint.
     * Whether the request allows reading or writing depends on the direction of the endpoint.
     *
     * @param endpoint the endpoint to be used for this request.
     * @return true if the request was successfully opened.
     */
    public boolean initialize(UsbDeviceConnection connection, UsbEndpoint endpoint) {
        mEndpoint = endpoint;
        mConnection = Objects.requireNonNull(connection, "connection");

        boolean wasInitialized = native_init(connection, endpoint.getAddress(),
                endpoint.getAttributes(), endpoint.getMaxPacketSize(), endpoint.getInterval());

        if (wasInitialized) {
            mCloseGuard.open("UsbRequest.close");
        }

        return wasInitialized;
    }

    /**
     * Releases all resources related to this request.
     */
    public void close() {
        synchronized (mLock) {
            if (mNativeContext != 0) {
                mEndpoint = null;
                mConnection = null;
                native_close();
                mCloseGuard.close();
            }
        }
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            if (mCloseGuard != null) {
                mCloseGuard.warnIfOpen();
            }

            close();
        } finally {
            super.finalize();
        }
    }

    /**
     * Returns the endpoint for the request, or null if the request is not opened.
     *
     * @return the request's endpoint
     */
    public UsbEndpoint getEndpoint() {
        return mEndpoint;
    }

    /**
     * Returns the client data for the request.
     * This can be used in conjunction with {@link #setClientData}
     * to associate another object with this request, which can be useful for
     * maintaining state between calls to {@link #queue} and
     * {@link android.hardware.usb.UsbDeviceConnection#requestWait}
     *
     * @return the client data for the request
     */
    public Object getClientData() {
        return mClientData;
    }

    /**
     * Sets the client data for the request.
     * This can be used in conjunction with {@link #getClientData}
     * to associate another object with this request, which can be useful for
     * maintaining state between calls to {@link #queue} and
     * {@link android.hardware.usb.UsbDeviceConnection#requestWait}
     *
     * @param data the client data for the request
     */
    public void setClientData(Object data) {
        mClientData = data;
    }

    /**
     * Queues the request to send or receive data on its endpoint.
     * <p>For OUT endpoints, the given buffer data will be sent on the endpoint. For IN endpoints,
     * the endpoint will attempt to read the given number of bytes into the specified buffer. If the
     * queueing operation is successful, return true. The result will be returned via
     * {@link UsbDeviceConnection#requestWait}</p>
     *
     * @param buffer the buffer containing the bytes to write, or location to store the results of a
     *               read. Position and array offset will be ignored and assumed to be 0. Limit and
     *               capacity will be ignored. Once the request
     *               {@link UsbDeviceConnection#requestWait() is processed} the position will be set
     *               to the number of bytes read/written.
     * @param length number of bytes to read or write. Before {@value Build.VERSION_CODES#P}, a
     *               value larger than 16384 bytes would be truncated down to 16384. In API
     *               {@value Build.VERSION_CODES#P} and after, any value of length is valid.
     *
     * @return true if the queueing operation succeeded
     *
     * @deprecated Use {@link #queue(ByteBuffer)} instead.
     */
    @Deprecated
    public boolean queue(ByteBuffer buffer, int length) {
        UsbDeviceConnection connection = mConnection;
        if (connection == null) {
            // The expected exception by CTS Verifier - USB Device test
            throw new NullPointerException("invalid connection");
        }

        // Calling into the underlying UsbDeviceConnection to synchronize on its lock, to prevent
        // the connection being closed while queueing.
        return connection.queueRequest(this, buffer, length);
    }

    /**
     * This is meant to be called from UsbDeviceConnection after synchronizing using the lock over
     * there, to prevent the connection being closed while queueing.
     */
    /* package */ boolean queueIfConnectionOpen(ByteBuffer buffer, int length) {
        UsbDeviceConnection connection = mConnection;
        if (connection == null || !connection.isOpen()) {
            // The expected exception by CTS Verifier - USB Device test
            throw new NullPointerException("invalid connection");
        }

        boolean out = (mEndpoint.getDirection() == UsbConstants.USB_DIR_OUT);
        boolean result;

        if (connection.getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P
                && length > MAX_USBFS_BUFFER_SIZE) {
            length = MAX_USBFS_BUFFER_SIZE;
        }

        synchronized (mLock) {
            // save our buffer for when the request has completed
            mBuffer = buffer;
            mLength = length;

            // Note: On a buffer slice we lost the capacity information about the underlying buffer,
            // hence we cannot check if the access would be a data leak/memory corruption.

            if (buffer.isDirect()) {
                result = native_queue_direct(buffer, length, out);
            } else if (buffer.hasArray()) {
                result = native_queue_array(buffer.array(), length, out);
            } else {
                throw new IllegalArgumentException("buffer is not direct and has no array");
            }
            if (!result) {
                mBuffer = null;
                mLength = 0;
            }
        }

        return result;
    }

    /**
     * Queues the request to send or receive data on its endpoint.
     *
     * <p>For OUT endpoints, the remaining bytes of the buffer will be sent on the endpoint. For IN
     * endpoints, the endpoint will attempt to fill the remaining bytes of the buffer. If the
     * queueing operation is successful, return true. The result will be returned via
     * {@link UsbDeviceConnection#requestWait}</p>
     *
     * @param buffer the buffer containing the bytes to send, or the buffer to fill. The state
     *               of the buffer is undefined until the request is returned by
     *               {@link UsbDeviceConnection#requestWait}. If the request failed the buffer
     *               will be unchanged; if the request succeeded the position of the buffer is
     *               incremented by the number of bytes sent/received. Before
     *               {@value Build.VERSION_CODES#P}, a buffer of length larger than 16384 bytes
     *               would throw IllegalArgumentException. In API {@value Build.VERSION_CODES#P}
     *               and after, any size buffer is valid.
     *
     * @return true if the queueing operation succeeded
     */
    public boolean queue(@Nullable ByteBuffer buffer) {
        UsbDeviceConnection connection = mConnection;
        if (connection == null) {
            // The expected exception by CTS Verifier - USB Device test
            throw new IllegalStateException("invalid connection");
        }

        // Calling into the underlying UsbDeviceConnection to synchronize on its lock, to prevent
        // the connection being closed while queueing.
        return connection.queueRequest(this, buffer);
    }

    /**
     * This is meant to be called from UsbDeviceConnection after synchronizing using the lock over
     * there, to prevent the connection being closed while queueing.
     */
    /* package */ boolean queueIfConnectionOpen(@Nullable ByteBuffer buffer) {
        UsbDeviceConnection connection = mConnection;
        if (connection == null || !connection.isOpen()) {
            // The expected exception by CTS Verifier - USB Device test
            throw new IllegalStateException("invalid connection");
        }

        // Request need to be initialized
        Preconditions.checkState(mNativeContext != 0, "request is not initialized");

        // Request can not be currently queued
        Preconditions.checkState(!mIsUsingNewQueue, "this request is currently queued");

        boolean isSend = (mEndpoint.getDirection() == UsbConstants.USB_DIR_OUT);
        boolean wasQueued;

        synchronized (mLock) {
            mBuffer = buffer;

            if (buffer == null) {
                // Null buffers enqueue empty USB requests which is supported
                mIsUsingNewQueue = true;
                wasQueued = native_queue(null, 0, 0);
            } else {
                if (connection.getContext().getApplicationInfo().targetSdkVersion
                        < Build.VERSION_CODES.P) {
                    // Can only send/receive MAX_USBFS_BUFFER_SIZE bytes at once
                    Preconditions.checkArgumentInRange(buffer.remaining(), 0, MAX_USBFS_BUFFER_SIZE,
                            "number of remaining bytes");
                }

                // Can not receive into read-only buffers.
                Preconditions.checkArgument(!(buffer.isReadOnly() && !isSend), "buffer can not be "
                        + "read-only when receiving data");

                if (!buffer.isDirect()) {
                    mTempBuffer = ByteBuffer.allocateDirect(mBuffer.remaining());

                    if (isSend) {
                        // Copy buffer into temporary buffer
                        mBuffer.mark();
                        mTempBuffer.put(mBuffer);
                        mTempBuffer.flip();
                        mBuffer.reset();
                    }

                    // Send/Receive into the temp buffer instead
                    buffer = mTempBuffer;
                }

                mIsUsingNewQueue = true;
                wasQueued = native_queue(buffer, buffer.position(), buffer.remaining());
            }
        }

        if (!wasQueued) {
            mIsUsingNewQueue = false;
            mTempBuffer = null;
            mBuffer = null;
        }

        return wasQueued;
    }

    /* package */ void dequeue(boolean useBufferOverflowInsteadOfIllegalArg) {
        boolean isSend = (mEndpoint.getDirection() == UsbConstants.USB_DIR_OUT);
        int bytesTransferred;

        synchronized (mLock) {
            if (mIsUsingNewQueue) {
                bytesTransferred = native_dequeue_direct();
                mIsUsingNewQueue = false;

                if (mBuffer == null) {
                    // Nothing to do
                } else if (mTempBuffer == null) {
                    mBuffer.position(mBuffer.position() + bytesTransferred);
                } else {
                    mTempBuffer.limit(bytesTransferred);

                    // The user might have modified mBuffer which might make put/position fail.
                    // Changing the buffer while a request is in flight is not supported. Still,
                    // make sure to free mTempBuffer correctly.
                    try {
                        if (isSend) {
                            mBuffer.position(mBuffer.position() + bytesTransferred);
                        } else {
                            // Copy temp buffer back into original buffer
                            mBuffer.put(mTempBuffer);
                        }
                    } finally {
                        mTempBuffer = null;
                    }
                }
            } else {
                if (mBuffer.isDirect()) {
                    bytesTransferred = native_dequeue_direct();
                } else {
                    bytesTransferred = native_dequeue_array(mBuffer.array(), mLength, isSend);
                }
                if (bytesTransferred >= 0) {
                    int bytesToStore = Math.min(bytesTransferred, mLength);
                    try {
                        mBuffer.position(bytesToStore);
                    } catch (IllegalArgumentException e) {
                        if (useBufferOverflowInsteadOfIllegalArg) {
                            Log.e(TAG, "Buffer " + mBuffer + " does not have enough space to read "
                                    + bytesToStore + " bytes", e);
                            throw new BufferOverflowException();
                        } else {
                            throw e;
                        }
                    }
                }
            }

            mBuffer = null;
            mLength = 0;
        }
    }

    /**
     * Cancels a pending queue operation.
     *
     * @return true if cancelling succeeded
     */
    public boolean cancel() {
        UsbDeviceConnection connection = mConnection;
        if (connection == null) {
            return false;
        }

        return connection.cancelRequest(this);
    }

    /**
     * Cancels a pending queue operation (for use when the UsbDeviceConnection associated
     * with this request is synchronized). This ensures we don't have a race where the
     * device is closed and then the request is canceled which would lead to a
     * use-after-free because the cancel operation uses the device connection
     * information freed in the when UsbDeviceConnection is closed.<br/>
     *
     * This method assumes the connected is not closed while this method is executed.
     *
     * @return true if cancelling succeeded.
     */
    /* package */ boolean cancelIfOpen() {
        UsbDeviceConnection connection = mConnection;
        if (mNativeContext == 0 || (connection != null && !connection.isOpen())) {
            Log.w(TAG,
                    "Detected attempt to cancel a request on a connection which isn't open");
            return false;
        }
        return native_cancel();
    }

    private native boolean native_init(UsbDeviceConnection connection, int ep_address,
            int ep_attributes, int ep_max_packet_size, int ep_interval);
    private native void native_close();
    private native boolean native_queue(ByteBuffer buffer, int offset, int length);
    private native boolean native_queue_array(byte[] buffer, int length, boolean out);
    private native int native_dequeue_array(byte[] buffer, int length, boolean out);
    private native boolean native_queue_direct(ByteBuffer buffer, int length, boolean out);
    private native int native_dequeue_direct();
    private native boolean native_cancel();
}
