/*
 * Copyright (C) 2017 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 com.googlecode.android_scripting.facade.bluetooth;

import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHidDevice;
import android.bluetooth.BluetoothHidDeviceAppQosSettings;
import android.bluetooth.BluetoothHidDeviceAppSdpSettings;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.ParcelUuid;

import com.googlecode.android_scripting.Log;
import com.googlecode.android_scripting.facade.EventFacade;
import com.googlecode.android_scripting.facade.FacadeManager;
import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
import com.googlecode.android_scripting.rpc.Rpc;
import com.googlecode.android_scripting.rpc.RpcParameter;

import java.util.List;

public class BluetoothHidDeviceFacade extends RpcReceiver {

    public static final ParcelUuid[] UUIDS = {BluetoothUuid.HID};

    public static final byte ID_KEYBOARD = 1;
    public static final byte ID_MOUSE = 2;

    public static final byte[] HIDD_REPORT_DESC = {
            (byte) 0x05,
            (byte) 0x01, // Usage page (Generic Desktop)
            (byte) 0x09,
            (byte) 0x06, // Usage (Keyboard)
            (byte) 0xA1,
            (byte) 0x01, // Collection (Application)
            (byte) 0x85,
            ID_KEYBOARD, //    Report ID
            (byte) 0x05,
            (byte) 0x07, //       Usage page (Key Codes)
            (byte) 0x19,
            (byte) 0xE0, //       Usage minimum (224)
            (byte) 0x29,
            (byte) 0xE7, //       Usage maximum (231)
            (byte) 0x15,
            (byte) 0x00, //       Logical minimum (0)
            (byte) 0x25,
            (byte) 0x01, //       Logical maximum (1)
            (byte) 0x75,
            (byte) 0x01, //       Report size (1)
            (byte) 0x95,
            (byte) 0x08, //       Report count (8)
            (byte) 0x81,
            (byte) 0x02, //       Input (Data, Variable, Absolute) ; Modifier byte
            (byte) 0x75,
            (byte) 0x08, //       Report size (8)
            (byte) 0x95,
            (byte) 0x01, //       Report count (1)
            (byte) 0x81,
            (byte) 0x01, //       Input (Constant)                 ; Reserved byte
            (byte) 0x75,
            (byte) 0x08, //       Report size (8)
            (byte) 0x95,
            (byte) 0x06, //       Report count (6)
            (byte) 0x15,
            (byte) 0x00, //       Logical Minimum (0)
            (byte) 0x25,
            (byte) 0x65, //       Logical Maximum (101)
            (byte) 0x05,
            (byte) 0x07, //       Usage page (Key Codes)
            (byte) 0x19,
            (byte) 0x00, //       Usage Minimum (0)
            (byte) 0x29,
            (byte) 0x65, //       Usage Maximum (101)
            (byte) 0x81,
            (byte) 0x00, //       Input (Data, Array)              ; Key array (6 keys)
            (byte) 0xC0, // End Collection
            (byte) 0x05,
            (byte) 0x01, // Usage Page (Generic Desktop)
            (byte) 0x09,
            (byte) 0x02, // Usage (Mouse)
            (byte) 0xA1,
            (byte) 0x01, // Collection (Application)
            (byte) 0x85,
            ID_MOUSE, //    Report ID
            (byte) 0x09,
            (byte) 0x01, //    Usage (Pointer)
            (byte) 0xA1,
            (byte) 0x00, //    Collection (Physical)
            (byte) 0x05,
            (byte) 0x09, //       Usage Page (Buttons)
            (byte) 0x19,
            (byte) 0x01, //       Usage minimum (1)
            (byte) 0x29,
            (byte) 0x03, //       Usage maximum (3)
            (byte) 0x15,
            (byte) 0x00, //       Logical minimum (0)
            (byte) 0x25,
            (byte) 0x01, //       Logical maximum (1)
            (byte) 0x75,
            (byte) 0x01, //       Report size (1)
            (byte) 0x95,
            (byte) 0x03, //       Report count (3)
            (byte) 0x81,
            (byte) 0x02, //       Input (Data, Variable, Absolute)
            (byte) 0x75,
            (byte) 0x05, //       Report size (5)
            (byte) 0x95,
            (byte) 0x01, //       Report count (1)
            (byte) 0x81,
            (byte) 0x01, //       Input (constant)                 ; 5 bit padding
            (byte) 0x05,
            (byte) 0x01, //       Usage page (Generic Desktop)
            (byte) 0x09,
            (byte) 0x30, //       Usage (X)
            (byte) 0x09,
            (byte) 0x31, //       Usage (Y)
            (byte) 0x09,
            (byte) 0x38, //       Usage (Wheel)
            (byte) 0x15,
            (byte) 0x81, //       Logical minimum (-127)
            (byte) 0x25,
            (byte) 0x7F, //       Logical maximum (127)
            (byte) 0x75,
            (byte) 0x08, //       Report size (8)
            (byte) 0x95,
            (byte) 0x03, //       Report count (3)
            (byte) 0x81,
            (byte) 0x06, //       Input (Data, Variable, Relative)
            (byte) 0xC0, //    End Collection
            (byte) 0xC0 // End Collection
    };

    // HID mouse movement
    private static final byte[] RIGHT = {0, 1, 0, 0};
    private static final byte[] DOWN = {0, 0, 1, 0};
    private static final byte[] LEFT = {0, -1, 0, 0};
    private static final byte[] UP = {0, 0, -1, 0};

    // Default values.
    private static final int QOS_TOKEN_RATE = 800; // 9 bytes * 1000000 us / 11250 us
    private static final int QOS_TOKEN_BUCKET_SIZE = 9;
    private static final int QOS_PEAK_BANDWIDTH = 0;
    private static final int QOS_LATENCY = 11250;

    private final Service mService;
    private final BluetoothAdapter mBluetoothAdapter;
    private final EventFacade mEventFacade;

    private static boolean sIsHidDeviceReady = false;
    private static BluetoothHidDevice sHidDeviceProfile = null;
    private boolean mKeepMoving = false;

    private final HandlerThread mHandlerThread;
    private final Handler mHandler;

    private BluetoothHidDevice.Callback mCallback = new BluetoothHidDevice.Callback() {
        @Override
        public void onAppStatusChanged(BluetoothDevice pluggedDevice, boolean registered) {
            Log.d("onAppStatusChanged: pluggedDevice=" + pluggedDevice + " registered="
                    + registered);
            Bundle result = new Bundle();
            result.putBoolean("registered", registered);
            mEventFacade.postEvent("onAppStatusChanged", result);
        }

        @Override
        public void onConnectionStateChanged(BluetoothDevice device, int state) {
            Log.d("onConnectionStateChanged: device=" + device + " state=" + state);
            Bundle result = new Bundle();
            result.putInt("state", state);
            mEventFacade.postEvent("onConnectionStateChanged", result);
        }

        @Override
        public void onGetReport(BluetoothDevice device, byte type, byte id, int bufferSize) {
            Log.d("onGetReport: device=" + device + " type=" + type + " id=" + id + " bufferSize="
                    + bufferSize);
            Bundle result = new Bundle();
            result.putByte("type", type);
            result.putByte("id", id);
            result.putInt("bufferSize", bufferSize);
            mEventFacade.postEvent("onGetReport", result);
        }

        @Override
        public void onSetReport(BluetoothDevice device, byte type, byte id, byte[] data) {
            Log.d("onSetReport: device=" + device + " type=" + type + " id=" + id);
            Bundle result = new Bundle();
            result.putByte("type", type);
            result.putByte("id", id);
            result.putByteArray("data", data);
            mEventFacade.postEvent("onSetReport", result);
        }

        @Override
        public void onSetProtocol(BluetoothDevice device, byte protocol) {
            Log.d("onSetProtocol: device=" + device + " protocol=" + protocol);
            Bundle result = new Bundle();
            result.putByte("protocol", protocol);
            mEventFacade.postEvent("onSetProtocol", result);
        }

        @Override
        public void onInterruptData(BluetoothDevice device, byte reportId, byte[] data) {
            Log.d("onInterruptData: device=" + device + " reportId=" + reportId);
            Bundle result = new Bundle();
            result.putByte("registered", reportId);
            result.putByteArray("data", data);
            mEventFacade.postEvent("onInterruptData", result);
        }

        @Override
        public void onVirtualCableUnplug(BluetoothDevice device) {
            Log.d("onVirtualCableUnplug: device=" + device);
            Bundle result = new Bundle();
            mEventFacade.postEvent("onVirtualCableUnplug", result);
        }
    };

    private static BluetoothHidDeviceAppSdpSettings sSdpSettings =
            new BluetoothHidDeviceAppSdpSettings("Mock App", "Mock", "Google",
                    BluetoothHidDevice.SUBCLASS1_COMBO, HIDD_REPORT_DESC);

    private static BluetoothHidDeviceAppQosSettings sQos =
            new BluetoothHidDeviceAppQosSettings(
                    BluetoothHidDeviceAppQosSettings.SERVICE_BEST_EFFORT,
                    QOS_TOKEN_RATE,
                    QOS_TOKEN_BUCKET_SIZE,
                    QOS_PEAK_BANDWIDTH,
                    QOS_LATENCY,
                    BluetoothHidDeviceAppQosSettings.MAX);

    public BluetoothHidDeviceFacade(FacadeManager manager) {
        super(manager);
        mService = manager.getService();
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        mBluetoothAdapter.getProfileProxy(mService, new HidDeviceServiceListener(),
                BluetoothProfile.HID_DEVICE);
        mEventFacade = manager.getReceiver(EventFacade.class);
        mHandlerThread = new HandlerThread("BluetoothHidDeviceFacadeHandler",
                android.os.Process.THREAD_PRIORITY_BACKGROUND);
        mHandlerThread.start();
        mHandler = new Handler(mHandlerThread.getLooper());
        Log.w("Init HID Device Facade");
    }

    class HidDeviceServiceListener implements BluetoothProfile.ServiceListener {

        @Override
        public void onServiceConnected(int profile, BluetoothProfile proxy) {
            Log.d("BluetoothHidDeviceFacade: onServiceConnected");
            sHidDeviceProfile = (BluetoothHidDevice) proxy;
            sIsHidDeviceReady = true;
            if (proxy == null) {
                Log.e("proxy is still null");
            }
        }

        @Override
        public void onServiceDisconnected(int profile) {
            sIsHidDeviceReady = false;
        }
    }

    public Boolean hidDeviceConnect(BluetoothDevice device) {
        return sHidDeviceProfile != null && sHidDeviceProfile.connect(device);
    }

    public Boolean hidDeviceDisconnect(BluetoothDevice device) {
        return sHidDeviceProfile != null && sHidDeviceProfile.disconnect(device);
    }

    /**
     * Check whether the HID Device profile service is ready to use.
     * @return true if HID Device profile is ready to use; otherwise false
     */
    @Rpc(description = "Is HID Device profile ready.")
    public Boolean bluetoothHidDeviceIsReady() {
        Log.d("isReady");
        return sHidDeviceProfile != null && sIsHidDeviceReady;
    }

    /**
     * Connect to a Bluetooth HID input host.
     * @param device name or MAC address or the HID input host
     * @return true if successfully connected to the HID host; otherwise false
     * @throws Exception error from Bluetooth HidDevService
     */
    @Rpc(description = "Connect to an HID host.")
    public Boolean bluetoothHidDeviceConnect(
            @RpcParameter(name = "device",
                    description = "Name or MAC address of a bluetooth device.")
                    String device)
            throws Exception {
        if (sHidDeviceProfile == null) {
            return false;
        }
        BluetoothDevice mDevice =
                BluetoothFacade.getDevice(BluetoothFacade.DiscoveredDevices, device);
        Log.d("Connecting to device " + mDevice.getAlias());
        return hidDeviceConnect(mDevice);
    }

    /**
     * Disconnect a Bluetooth HID input host.
     * @param device name or MAC address or the HID input host
     * @return true if successfully disconnected the HID host; otherwise false
     * @throws Exception error from Bluetooth HidDevService
     */
    @Rpc(description = "Disconnect an HID host.")
    public Boolean bluetoothHidDeviceDisconnect(
            @RpcParameter(name = "device",
                    description = "Name or MAC address of a device.")
                    String device)
            throws Exception {
        if (sHidDeviceProfile == null) {
            return false;
        }
        Log.d("Connected devices: " + sHidDeviceProfile.getConnectedDevices());
        BluetoothDevice mDevice = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(),
                device);
        return hidDeviceDisconnect(mDevice);
    }

    /**
     * Get all the devices connected through HID Device Service.
     * @return a list of all the devices connected through HID Device Service,
     * or null if the HID device profile is not ready.
     */
    @Rpc(description = "Get all the devices connected through HID Device Service.")
    public List<BluetoothDevice> bluetoothHidDeviceGetConnectedDevices() {
        if (sHidDeviceProfile == null) {
            return null;
        }
        return sHidDeviceProfile.getConnectedDevices();
    }

    /**
     * Get the connection status of the specified device
     * @param deviceID name or MAC address or the HID input host
     * @return the status of the device
     */
    @Rpc(description = "Get the connection status of a device.")
    public Integer bluetoothHidDeviceGetConnectionStatus(
            @RpcParameter(name = "deviceID",
                    description = "Name or MAC address of a bluetooth device.")
                    String deviceID) {
        if (sHidDeviceProfile == null) {
            return BluetoothProfile.STATE_DISCONNECTED;
        }
        List<BluetoothDevice> deviceList = sHidDeviceProfile.getConnectedDevices();
        BluetoothDevice device;
        try {
            device = BluetoothFacade.getDevice(deviceList, deviceID);
        } catch (Exception e) {
            return BluetoothProfile.STATE_DISCONNECTED;
        }
        return sHidDeviceProfile.getConnectionState(device);
    }

    /**
     * Register app for the HID Device service using default settings. This adds a SDP record.
     * @return true if successfully registered the app; otherwise false
     * @throws Exception error from Bluetooth HidDevService
     */
    @Rpc(description = "Register app for the HID Device service using default settings.")
    public Boolean bluetoothHidDeviceRegisterApp() throws Exception {
        return sHidDeviceProfile != null
                && sHidDeviceProfile.registerApp(
                        sSdpSettings, null, sQos, command -> command.run(), mCallback);
    }

    /**
     * Unregister app for the HID Device service.
     *
     * @return true if successfully unregistered the app; otherwise false
     * @throws Exception error from Bluetooth HidDevService
     */
    @Rpc(description = "Unregister app.")
    public Boolean bluetoothHidDeviceUnregisterApp() throws Exception {
        return sHidDeviceProfile != null && sHidDeviceProfile.unregisterApp();
    }

    /**
     * Send a data report to a connected HID host using interrupt channel.
     * @param deviceID name or MAC address or the HID input host
     * @param id report Id, as defined in descriptor. Can be 0 in case Report Id are not defined in
     * descriptor.
     * @param report report data
     * @return true if successfully sent the report; otherwise false
     * @throws Exception error from Bluetooth HidDevService
     */
    @Rpc(description = "Send report to a connected HID host using interrupt channel.")
    public Boolean bluetoothHidDeviceSendReport(
            @RpcParameter(name = "deviceID",
                    description = "Name or MAC address of a bluetooth device.")
                    String deviceID,
            @RpcParameter(name = "descriptor",
                    description = "Descriptor of the report")
                    Integer id,
            @RpcParameter(name = "report")
                    String report) throws Exception {
        if (sHidDeviceProfile == null) {
            return false;
        }

        BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(),
                deviceID);
        byte[] reportByteArray = report.getBytes();
        return sHidDeviceProfile.sendReport(device, id, reportByteArray);
    }

    /**
     * Send a bytes array data report to a connected HID host using interrupt channel.
     * @param deviceID name or MAC address or the HID input host
     * @param id report Id, as defined in descriptor. Can be 0 in case Report Id are not defined in
     * descriptor.
     * @param report byte array to be sent into HID device
     * @return true if successfully sent the report; otherwise false
     * @throws Exception error from Bluetooth HidDevService
     */
    @Rpc(description = "Send bytes array report to a connected HID host using interrupt channel.")
    public Boolean bluetoothHidDeviceSendBytesArrayReport(
            @RpcParameter(name = "deviceID",
                    description = "Name or MAC address of a bluetooth device.")
                    String deviceID,
            @RpcParameter(name = "descriptor",
                    description = "Descriptor of the report")
                    Integer id,
            @RpcParameter(name = "report")
                    byte[] report) throws Exception {
        if (sHidDeviceProfile == null) {
            return false;
        }

        BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(),
                deviceID);
        return sHidDeviceProfile.sendReport(device, id, report);
    }

    /**
     * Send a report to the connected HID host as reply for GET_REPORT request from the HID host.
     * @param deviceID name or MAC address or the HID input host
     * @param type type of the report, as in request
     * @param id id of the report, as in request
     * @param report report data
     * @return true if successfully sent the reply report; otherwise false
     * @throws Exception error from Bluetooth HidDevService
     */
    @Rpc(description = "Send reply report to a connected HID..")
    public Boolean bluetoothHidDeviceReplyReport(
            @RpcParameter(name = "deviceID",
                    description = "Name or MAC address of a bluetooth device.")
                    String deviceID,
            @RpcParameter(name = "type",
                    description = "Type as in the report.")
                    Integer type,
            @RpcParameter(name = "id",
                    description = "id as in the report.")
                    Integer id,
            @RpcParameter(name = "report")
                    String report) throws Exception {
        if (sHidDeviceProfile == null) {
            return false;
        }

        BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(),
                deviceID);
        byte[] reportByteArray = report.getBytes();
        return sHidDeviceProfile.replyReport(
                device, (byte) (int) type, (byte) (int) id, reportByteArray);
    }

    /**
     * Send a bytes array report to the connected HID host as reply for GET_REPORT request
     * from the HID host.
     * @param deviceID name or MAC address or the HID input host
     * @param type type of the report, as in request
     * @param id id of the report, as in request
     * @param report byte array to be sent into HID device
     * @return true if successfully sent the reply report; otherwise false
     * @throws Exception error from Bluetooth HidDevService
     */
    @Rpc(description = "Send reply bytes array report to a connected HID..")
    public Boolean bluetoothHidDeviceReplyBytesArrayReport(
            @RpcParameter(name = "deviceID",
                    description = "Name or MAC address of a bluetooth device.")
                    String deviceID,
            @RpcParameter(name = "type",
                    description = "Type as in the report.")
                    Integer type,
            @RpcParameter(name = "id",
                    description = "id as in the report.")
                    Integer id,
            @RpcParameter(name = "report")
                    byte[] report) throws Exception {
        if (sHidDeviceProfile == null) {
            return false;
        }

        BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(),
                deviceID);
        return sHidDeviceProfile.replyReport(
                device, (byte) (int) type, (byte) (int) id, report);
    }

    /**
     * Send error handshake message as reply for invalid SET_REPORT request from the HID host.
     * @param deviceID name or MAC address or the HID input host
     * @param error error byte
     * @return true if successfully sent the error handshake message; otherwise false
     * @throws Exception error from Bluetooth HidDevService
     */
    @Rpc(description = "Send error handshake message to a connected HID host.")
    public Boolean bluetoothHidDeviceReportError(
            @RpcParameter(name = "deviceID",
                    description = "Name or MAC address of a bluetooth device.")
                    String deviceID,
            @RpcParameter(name = "error",
                    description = "Error byte")
                    Integer error) throws Exception {
        if (sHidDeviceProfile == null) {
            return false;
        }

        BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(),
                deviceID);
        return sHidDeviceProfile.reportError(device, (byte) (int) error);
    }

   /**
     * Start to send HID mouse input to HID host continuously for given duration.
     * @param deviceID name or MAC address for the HID input host
     * @param duration time in millisecond to send HID report continuously
     * @return true if successfully sent the error handshake message; otherwise false
     */
    @Rpc(description = "Start to send HID report continuously")
    public Boolean bluetoothHidDeviceMoveRepeatedly(
            @RpcParameter(name = "deviceID",
                    description = "Name or MAC address of a bluetooth device.")
                    String deviceID,
            @RpcParameter(name = "duration",
                    description = "duration")
                    Integer duration,
            @RpcParameter(name = "interval",
                    description = "interval")
                    Integer interval) throws Exception {
        if (sHidDeviceProfile == null || mKeepMoving) {
            return false;
        }
        BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(),
                deviceID);
        mHandler.post(new Runnable() {
            final long mStopTime = System.currentTimeMillis() + duration;
            private void sendAndWait(byte[] report) {
                if (!mKeepMoving) {
                    return;
                }
                sHidDeviceProfile.sendReport(device, ID_MOUSE, report);
                long endTime = System.currentTimeMillis() + interval;
                while (mKeepMoving && endTime > System.currentTimeMillis()) {
                    //Busy waiting
                    if (mStopTime < System.currentTimeMillis()) {
                        mKeepMoving = false;
                        return;
                    }
                }
            }
            public void run() {
                mKeepMoving = true;
                while (mKeepMoving && mStopTime > System.currentTimeMillis()) {
                    sendAndWait(RIGHT);
                    sendAndWait(DOWN);
                    sendAndWait(LEFT);
                    sendAndWait(UP);
                }
                mKeepMoving = false;
            }
        });
        return true;
    }

    /**
     * Stop sending HID report to HID host
     */
    @Rpc(description = "Stop sending HID report")
    public void bluetoothHidDeviceStopMoving() {
        mKeepMoving = false;
    }

    @Override
    public void shutdown() {
        Log.w("Quit handler thread");
        mHandlerThread.quit();
    }

}
