/*
 * 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.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Bundle;

import com.googlecode.android_scripting.Log;
import com.googlecode.android_scripting.MainThread;
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.lang.reflect.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;

public class GattServerFacade extends RpcReceiver {
    private final EventFacade mEventFacade;
    private BluetoothAdapter mBluetoothAdapter;
    private BluetoothManager mBluetoothManager;
    private final Service mService;
    private final Context mContext;
    private final HashMap<Integer, BluetoothGattCharacteristic> mCharacteristicList;
    private final HashMap<Integer, BluetoothGattDescriptor> mDescriptorList;
    private final HashMap<Integer, BluetoothGattServer> mBluetoothGattServerList;
    private final HashMap<Integer, BtGattServerCallback> mBluetoothGattServerCallbackList;
    private final HashMap<Integer, BluetoothGattService> mGattServiceList;
    private final HashMap<Integer, List<BluetoothGattService>> mBluetoothGattDiscoveredServicesList;
    private final HashMap<Integer, List<BluetoothDevice>> mGattServerDiscoveredDevicesList;
    private static int sCharacteristicCount;
    private static int sDescriptorCount;
    private static int sGattServerCallbackCount;
    private static int sGattServerCount;
    private static int sGattServiceCount;

    public GattServerFacade(FacadeManager manager) {
        super(manager);
        mService = manager.getService();
        mContext = mService.getApplicationContext();
        mBluetoothAdapter = MainThread.run(mService, new Callable<BluetoothAdapter>() {
            @Override
            public BluetoothAdapter call() throws Exception {
                return BluetoothAdapter.getDefaultAdapter();
            }
        });
        mBluetoothManager = (BluetoothManager) mContext.getSystemService(Service.BLUETOOTH_SERVICE);
        mEventFacade = manager.getReceiver(EventFacade.class);
        mCharacteristicList = new HashMap<Integer, BluetoothGattCharacteristic>();
        mDescriptorList = new HashMap<Integer, BluetoothGattDescriptor>();
        mBluetoothGattServerList = new HashMap<Integer, BluetoothGattServer>();
        mBluetoothGattServerCallbackList = new HashMap<Integer, BtGattServerCallback>();
        mGattServiceList = new HashMap<Integer, BluetoothGattService>();
        mBluetoothGattDiscoveredServicesList = new HashMap<Integer, List<BluetoothGattService>>();
        mGattServerDiscoveredDevicesList = new HashMap<Integer, List<BluetoothDevice>>();
    }

    /**
     * Open a new Gatt server.
     *
     * @param index the bluetooth gatt server callback to open on
     * @return the index of the newly opened gatt server
     * @throws Exception
     */
    @Rpc(description = "Open new gatt server")
    public int gattServerOpenGattServer(@RpcParameter(name = "index") Integer index)
            throws Exception {
        if (mBluetoothGattServerCallbackList.get(index) != null) {
            BluetoothGattServer mGattServer =
                    mBluetoothManager.openGattServer(mContext, mBluetoothGattServerCallbackList.get(
                        index));
            sGattServerCount += 1;
            int in = sGattServerCount;
            mBluetoothGattServerList.put(in, mGattServer);
            return in;
        } else {
            throw new Exception("Invalid index input:" + Integer.toString(index));
        }
    }

    /**
     * Add a service to a bluetooth gatt server
     *
     * @param index the bluetooth gatt server to add a service to
     * @param serviceIndex the service to add to the bluetooth gatt server
     * @throws Exception
     */
    @Rpc(description = "Add service to bluetooth gatt server")
    public void gattServerAddService(@RpcParameter(name = "index") Integer index,
            @RpcParameter(name = "serviceIndex") Integer serviceIndex) throws Exception {
        if (mBluetoothGattServerList.get(index) != null) {
            if (mGattServiceList.get(serviceIndex) != null) {
                mBluetoothGattServerList.get(index).addService(mGattServiceList.get(serviceIndex));
            } else {
                throw new Exception("Invalid serviceIndex input:" + Integer.toString(serviceIndex));
            }
        } else {
            throw new Exception("Invalid index input:" + Integer.toString(index));
        }
    }

    /**
     * Add a service to a bluetooth gatt server
     *
     * @param index the bluetooth gatt server to add a service to
     * @throws Exception
     */
    @Rpc(description = "Clear services from bluetooth gatt server")
    public void gattServerClearServices(
            @RpcParameter(name = "index") Integer index) throws Exception {
        if (mBluetoothGattServerList.get(index) != null) {
            mBluetoothGattServerList.get(index).clearServices();
        } else {
            throw new Exception("Invalid index input:" + Integer.toString(index));
        }
    }

    /**
     * Get connected devices of the gatt server
     *
     * @param gattServerIndex the gatt server index
     * @throws Exception
     */
    @Rpc(description = "Return a list of connected gatt devices.")
    public List<BluetoothDevice> gattServerGetConnectedDevices(
            @RpcParameter(name = "gattServerIndex") Integer gattServerIndex) throws Exception {
        if (mBluetoothGattServerList.get(gattServerIndex) == null) {
            throw new Exception("Invalid gattServerIndex: " + Integer.toString(gattServerIndex));
        }
        List<BluetoothDevice> connectedDevices =
                mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER);
        mGattServerDiscoveredDevicesList.put(gattServerIndex, connectedDevices);
        return connectedDevices;
    }

    /**
     * Get connected devices of the gatt server
     *
     * @param gattServerIndex the gatt server index
     * @param bluetoothDeviceIndex the remotely connected bluetooth device
     * @param requestId the ID of the request that was received with the callback
     * @param status the status of the request to be sent to the remote devices
     * @param offset value offset for partial read/write response
     * @param value the value of the attribute that was read/written
     * @throws Exception
     */
    @Rpc(description = "Send a response after a write.")
    public void gattServerSendResponse(
            @RpcParameter(name = "gattServerIndex") Integer gattServerIndex,
            @RpcParameter(name = "bluetoothDeviceIndex") Integer bluetoothDeviceIndex,
            @RpcParameter(name = "requestId") Integer requestId,
            @RpcParameter(name = "status") Integer status,
            @RpcParameter(name = "offset") Integer offset,
            @RpcParameter(name = "value") byte[] value) throws Exception {

        BluetoothGattServer gattServer = mBluetoothGattServerList.get(gattServerIndex);
        if (gattServer == null) {
            throw new Exception("Invalid gattServerIndex: " + Integer.toString(gattServerIndex));
        }
        List<BluetoothDevice> connectedDevices =
                mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER);
        BluetoothDevice bluetoothDevice = connectedDevices.get(bluetoothDeviceIndex);
        gattServer.sendResponse(bluetoothDevice, requestId, status, offset, value);
    }

    /**
     * Notify that characteristic was changed
     *
     * @deprecated Use {@link #gattServerNotifyCharacteristicChangedByInstanceId(
     *  gattServerIndex, bluetoothDeviceIndex, instanceId, confirm)} instead.
     * @param gattServerIndex the gatt server index
     * @param bluetoothDeviceIndex the remotely connected bluetooth device
     * @param characteristicIndex characteristic index
     * @param confirm shall we expect confirmation
     * @throws Exception
     */
    @Deprecated
    @Rpc(description = "Notify that characteristic was changed.")
    public void gattServerNotifyCharacteristicChanged(
            @RpcParameter(name = "gattServerIndex") Integer gattServerIndex,
            @RpcParameter(name = "bluetoothDeviceIndex") Integer bluetoothDeviceIndex,
            @RpcParameter(name = "characteristicIndex") Integer characteristicIndex,
            @RpcParameter(name = "confirm") Boolean confirm) throws Exception {

        BluetoothGattServer gattServer = mBluetoothGattServerList.get(gattServerIndex);
        if (gattServer == null) {
            throw new Exception("Invalid gattServerIndex: " + Integer.toString(gattServerIndex));
        }
        List<BluetoothDevice> connectedDevices =
                mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER);
        if (connectedDevices == null) {
            throw new Exception(
                    "Connected device list empty for gattServerIndex:" + Integer.toString(
                        gattServerIndex));
        }
        BluetoothDevice bluetoothDevice = connectedDevices.get(bluetoothDeviceIndex);
        if (bluetoothDevice == null) {
            throw new Exception(
                    "Invalid bluetoothDeviceIndex: " + Integer.toString(bluetoothDeviceIndex));
        }

        BluetoothGattCharacteristic bluetoothCharacteristic = mCharacteristicList.get(
                characteristicIndex);
        if (bluetoothCharacteristic == null) {
            throw new Exception(
                    "Invalid characteristicIndex: " + Integer.toString(characteristicIndex));
        }

        gattServer.notifyCharacteristicChanged(bluetoothDevice, bluetoothCharacteristic, confirm);
    }

    /**
     * Notify that characteristic was changed
     *
     * @param gattServerIndex the gatt server index
     * @param bluetoothDeviceIndex the remotely connected bluetooth device
     * @param characteristicIndex characteristic index
     * @param confirm shall we expect confirmation
     * @throws Exception
     */
    @Rpc(description = "Notify that characteristic was changed by Instance Id.")
    public void gattServerNotifyCharacteristicChangedByInstanceId(
            @RpcParameter(name = "gattServerIndex") Integer gattServerIndex,
            @RpcParameter(name = "bluetoothDeviceIndex") Integer bluetoothDeviceIndex,
            @RpcParameter(name = "instanceId") Integer instanceId,
            @RpcParameter(name = "confirm") Boolean confirm) throws Exception {

        BluetoothGattServer gattServer = mBluetoothGattServerList.get(gattServerIndex);
        if (gattServer == null) {
            throw new Exception("Invalid gattServerIndex: " + Integer.toString(gattServerIndex));
        }
        List<BluetoothDevice> connectedDevices =
                mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER);
        if (connectedDevices == null) {
            throw new Exception(
                    "Connected device list empty for gattServerIndex:" + Integer.toString(
                        gattServerIndex));
        }
        BluetoothDevice bluetoothDevice = connectedDevices.get(bluetoothDeviceIndex);
        if (bluetoothDevice == null) {
            throw new Exception(
                    "Invalid bluetoothDeviceIndex: " + Integer.toString(bluetoothDeviceIndex));
        }

        for (BluetoothGattCharacteristic mGattChar : mCharacteristicList.values()) {
            if (mGattChar.getInstanceId() == instanceId) {
                        Log.i("Found Characteristic to get value. instanceId: "
                        + Integer.toString(instanceId)
                        + " UUID: " + mGattChar.getUuid().toString());
                gattServer.notifyCharacteristicChanged(bluetoothDevice, mGattChar, confirm);
            }
        }
    }

    /**
     * Create a new bluetooth gatt service
     *
     * @param uuid the UUID that characterises the service
     * @param serviceType the service type
     * @return The index of the new bluetooth gatt service
     */
    @Rpc(description = "Create new bluetooth gatt service")
    public int gattServerCreateService(@RpcParameter(name = "uuid") String uuid,
            @RpcParameter(name = "serviceType") Integer serviceType) {
        sGattServiceCount += 1;
        int index = sGattServiceCount;
        mGattServiceList.put(index, new BluetoothGattService(UUID.fromString(uuid), serviceType));
        return index;
    }

    /**
     * Add a characteristic to a bluetooth gatt service
     *
     * @param index the bluetooth gatt service index
     * @param serviceUuid the service Uuid to get
     * @param characteristicIndex the character index to use
     * @throws Exception
     */
    @Rpc(description = "Add a characteristic to a bluetooth gatt service")
    public void gattServiceAddCharacteristic(
            @RpcParameter(name = "index") Integer index,
            @RpcParameter(name = "serviceUuid") String serviceUuid,
            @RpcParameter(name = "characteristicIndex") Integer characteristicIndex)
            throws Exception {
        if (mBluetoothGattServerList.get(index) != null
                && mBluetoothGattServerList.get(index).getService(
                    UUID.fromString(serviceUuid)) != null
                && mCharacteristicList.get(characteristicIndex) != null) {
            mBluetoothGattServerList.get(index).getService(UUID.fromString(serviceUuid))
                    .addCharacteristic(mCharacteristicList.get(characteristicIndex));
        } else {
            if (mBluetoothGattServerList.get(index) == null) {
                throw new Exception("Invalid index input:" + index);
            } else if (mCharacteristicList.get(characteristicIndex) == null) {
                throw new Exception("Invalid characteristicIndex input:" + characteristicIndex);
            } else {
                throw new Exception("Invalid serviceUuid input:" + serviceUuid);
            }
        }
    }

    /**
     * Add a characteristic to a bluetooth gatt service
     *
     * @param index the bluetooth gatt service to add a characteristic to
     * @param characteristicIndex the characteristic to add
     * @throws Exception
     */
    @Rpc(description = "Add a characteristic to a bluetooth gatt service")
    public void gattServerAddCharacteristicToService(@RpcParameter(name = "index") Integer index,
            @RpcParameter(name = "characteristicIndex") Integer characteristicIndex

    ) throws Exception {
        if (mGattServiceList.get(index) != null) {
            if (mCharacteristicList.get(characteristicIndex) != null) {
                mGattServiceList.get(index).addCharacteristic(mCharacteristicList.get(
                        characteristicIndex));
            } else {
                throw new Exception("Invalid index input:" + index);
            }
        } else {
            throw new Exception("Invalid index input:" + index);
        }
    }

    /**
     * Close a bluetooth gatt
     *
     * @param index the bluetooth gatt index to close
     * @throws Exception
     */
    @Rpc(description = "Close a bluetooth gatt")
    public void gattServerClose(@RpcParameter(name = "index") Integer index) throws Exception {
        if (mBluetoothGattServerList.get(index) != null) {
            mBluetoothGattServerList.get(index).close();
        } else {
            throw new Exception("Invalid index input:" + index);
        }
    }

    /**
     * Get a list of Bluetooth Devices connnected to the bluetooth gatt
     *
     * @param index the bluetooth gatt index
     * @return List of BluetoothDevice Objects
     * @throws Exception
     */
    @Rpc(description = "Get a list of Bluetooth Devices connnected to the bluetooth gatt")
    public List<BluetoothDevice> gattGetConnectedDevices(
            @RpcParameter(name = "index") Integer index)
            throws Exception {
        if (mBluetoothGattServerList.get(index) != null) {
            return mBluetoothGattServerList.get(index).getConnectedDevices();
        } else {
            throw new Exception("Invalid index input:" + index);
        }
    }

    /**
     * Read the current transmitter PHY and receiver PHY of the connection.
     *
     * @param gattServerIndex the bluetooth gatt index
     * @throws Exception
     */
    @Rpc(description = "Read PHY")
    public void gattServerReadPhy(
            @RpcParameter(name = "gattServerIndex") Integer gattServerIndex,
            @RpcParameter(name = "bluetoothDeviceIndex") Integer bluetoothDeviceIndex)
                throws Exception {
        BluetoothGattServer gattServer = mBluetoothGattServerList.get(gattServerIndex);
        if (gattServer == null) {
            throw new Exception("Invalid gattServerIndex: " + Integer.toString(gattServerIndex));
        }
        List<BluetoothDevice> connectedDevices = mGattServerDiscoveredDevicesList.get(
                gattServerIndex);
        if (connectedDevices == null) {
            throw new Exception(
                    "Connected device list empty for gattServerIndex:" + Integer.toString(
                        gattServerIndex));
        }
        BluetoothDevice bluetoothDevice = connectedDevices.get(bluetoothDeviceIndex);
        if (bluetoothDevice == null) {
            throw new Exception(
                    "Invalid bluetoothDeviceIndex: " + Integer.toString(bluetoothDeviceIndex));
        }

        if (mBluetoothGattServerList.get(gattServerIndex) != null) {
            mBluetoothGattServerList.get(gattServerIndex).readPhy(bluetoothDevice);
        } else {
            throw new Exception("Invalid index input:" + gattServerIndex);
        }
    }

    /**
     * Set the preferred connection PHY.
     *
     * @param gattServerIndex the bluetooth gatt index
     * @throws Exception
     */
    @Rpc(description = "Set the preferred connection PHY")
    public void gattServerSetPreferredPhy(
            @RpcParameter(name = "gattServerIndex") Integer gattServerIndex,
            @RpcParameter(name = "bluetoothDeviceIndex") Integer bluetoothDeviceIndex,
            @RpcParameter(name = "txPhy") Integer txPhy,
            @RpcParameter(name = "rxPhy") Integer rxPhy,
            @RpcParameter(name = "txPhy") Integer phyOptions) throws Exception {
        BluetoothGattServer gattServer = mBluetoothGattServerList.get(gattServerIndex);
        if (gattServer == null) {
            throw new Exception("Invalid gattServerIndex: " + Integer.toString(gattServerIndex));
        }
        List<BluetoothDevice> connectedDevices = mGattServerDiscoveredDevicesList.get(
                gattServerIndex);
        if (connectedDevices == null) {
            throw new Exception(
                    "Connected device list empty for gattServerIndex:" + Integer.toString(
                        gattServerIndex));
        }
        BluetoothDevice bluetoothDevice = connectedDevices.get(bluetoothDeviceIndex);
        if (bluetoothDevice == null) {
            throw new Exception(
                    "Invalid bluetoothDeviceIndex: " + Integer.toString(bluetoothDeviceIndex));
        }

        if (mBluetoothGattServerList.get(gattServerIndex) != null) {
            mBluetoothGattServerList.get(gattServerIndex)
                .setPreferredPhy(bluetoothDevice, txPhy, rxPhy, phyOptions);
        } else {
            throw new Exception("Invalid index input:" + gattServerIndex);
        }
    }

    /**
     * Get GATT Server UUID by characteristic index
     *
     * @param charIndex of the characteristic
     * @return String Uuid
     * @throws Exception
     */
    @Rpc(description = "Get UUID of the input characteristic")
    public String gattServerGetCharacteristicUuid(
            @RpcParameter(name = "charIndex") Integer charIndex)
            throws Exception {
        BluetoothGattCharacteristic gattChar = mCharacteristicList.get(charIndex);
        if (gattChar == null) {
            throw new Exception("Invalid index input: " + charIndex);
        }
        return gattChar.getUuid().toString();
    }

    /**
     * Get GATT Server UUID by characteristic index
     *
     * @param charIndex of the characteristic
     * @return Integer instanceId
     * @throws Exception
     */
    @Rpc(description = "Get instanceId of the input characteristic")
    public Integer gattServerGetCharacteristicInstanceId(
            @RpcParameter(name = "charIndex") Integer charIndex)
            throws Exception {
        BluetoothGattCharacteristic gattChar = mCharacteristicList.get(charIndex);
        if (gattChar == null) {
            throw new Exception("Invalid index input: " + charIndex);
        }
        return gattChar.getInstanceId();
    }

    /**
     * Get the service from an input UUID
     *
     * @param index the bluetooth gatt index
     * @return BluetoothGattService related to the bluetooth gatt
     * @throws Exception
     */
    @Rpc(description = "Get the service from an input UUID")
    public ArrayList<String> gattGetServiceUuidList(@RpcParameter(name = "index") Integer index)
            throws Exception {
        if (mBluetoothGattServerList.get(index) != null) {
            ArrayList<String> serviceUuidList = new ArrayList<String>();
            for (BluetoothGattService service : mBluetoothGattServerList.get(index).getServices()) {
                serviceUuidList.add(service.getUuid().toString());
            }
            return serviceUuidList;
        } else {
            throw new Exception("Invalid index input:" + index);
        }
    }

    /**
     * Get the service from an input UUID
     *
     * @param index the bluetooth gatt index
     * @param uuid the String uuid that matches the service
     * @return BluetoothGattService related to the bluetooth gatt
     * @throws Exception
     */
    @Rpc(description = "Get the service from an input UUID")
    public BluetoothGattService gattGetService(@RpcParameter(name = "index") Integer index,
            @RpcParameter(name = "uuid") String uuid) throws Exception {
        if (mBluetoothGattServerList.get(index) != null) {
            return mBluetoothGattServerList.get(index).getService(UUID.fromString(uuid));
        } else {
            throw new Exception("Invalid index input:" + index);
        }
    }

    /**
     * Add a descriptor to a bluetooth gatt characteristic
     *
     * @param index the bluetooth gatt characteristic to add a descriptor to
     * @param descriptorIndex the descritor index to add to the characteristic
     * @throws Exception
     */
    @Rpc(description = "add descriptor to blutooth gatt characteristic")
    public void gattServerCharacteristicAddDescriptor(@RpcParameter(name = "index") Integer index,
            @RpcParameter(name = "descriptorIndex") Integer descriptorIndex) throws Exception {
        if (mCharacteristicList.get(index) != null) {
            if (mDescriptorList.get(descriptorIndex) != null) {
                mCharacteristicList.get(index).addDescriptor(mDescriptorList.get(descriptorIndex));
            } else {
                throw new Exception("Invalid descriptorIndex input:" + descriptorIndex);
            }
        } else {
            throw new Exception("Invalid index input:" + index);
        }
    }

    /**
     * Create a new Characteristic object
     *
     * @param characteristicUuid uuid The UUID for this characteristic
     * @param property Properties of this characteristic
     * @param permission permissions Permissions for this characteristic
     * @return
     */
    @Rpc(description = "Create a new Characteristic object")
    public int gattServerCreateBluetoothGattCharacteristic(
            @RpcParameter(name = "characteristicUuid") String characteristicUuid,
            @RpcParameter(name = "property") Integer property,
            @RpcParameter(name = "permission") Integer permission) {
        sCharacteristicCount += 1;
        int index = sCharacteristicCount;
        BluetoothGattCharacteristic characteristic =
                new BluetoothGattCharacteristic(
                    UUID.fromString(characteristicUuid), property, permission);
        mCharacteristicList.put(index, characteristic);
        return index;
    }

    /**
     * Set value to a bluetooth gatt characteristic
     *
     * @deprecated Use {@link #gattServerCharacteristicSetValueByInstanceId(
     * instanceId, value)} instead.
     * @param index the bluetooth gatt characteristic
     * @param value value
     * @throws Exception
     */
    @Deprecated
    @Rpc(description = "Set byte value to a Characteristic")
    public void gattServerCharacteristicSetValue(@RpcParameter(name = "index") Integer index,
            @RpcParameter(name = "value") byte[] value) throws Exception {
        if (mCharacteristicList.get(index) != null) {
            mCharacteristicList.get(index).setValue(value);
            } else {
            throw new Exception("Invalid index input:" + index);
        }
    }

    /**
     * Set value to a bluetooth gatt characteristic by instance id
     *
     * @param index the bluetooth gatt characteristic by instance id
     * @param value value
     * @throws Exception
     */
    @Rpc(description = "Set byte value to a Characteristic by instance id")
    public boolean gattServerCharacteristicSetValueByInstanceId(
            @RpcParameter(name = "instanceId") Integer instanceId,
            @RpcParameter(name = "value") byte[] value) throws Exception {
        for (BluetoothGattCharacteristic mGattChar : mCharacteristicList.values()) {
            //test this to be sure
            if (mGattChar.getInstanceId() == instanceId) {
                return mGattChar.setValue(value);
            }
        }
        throw new Exception("Cannot find instance ID:" + instanceId);
    }

    /**
     * Set value to a bluetooth gatt characteristic
     *
     * @param index the bluetooth gatt characteristic
     * @param value value
     * @throws Exception
     */
    @Rpc(description = "Set Characteristic Byte Value")
    public boolean gattServerCharacteristicSetByteValue(@RpcParameter(name = "index") Integer index,
            @RpcParameter(name = "value") byte[] value) throws Exception {
        if (mCharacteristicList.get(index) != null) {
            return mCharacteristicList.get(index).setValue(value);
        } else {
            throw new Exception("Invalid index input:" + index);
        }
    }

    /**
     * Set value to a bluetooth gatt descriptor
     *
     * @param index the bluetooth gatt descriptor
     * @param value byte[] value to set
     * @throws Exception
     */
    @Rpc(description = "Set Characteristic Byte Value")
    public boolean gattServerDescriptorSetByteValue(
            @RpcParameter(name = "index") Integer index,
            @RpcParameter(name = "value") byte[] value) throws Exception {
        if (mDescriptorList.get(index) != null) {
            return mDescriptorList.get(index).setValue(value);
        } else {
            throw new Exception("Invalid index input:" + index);
        }
    }

    /**
     * Get Read value by instance ID
     *
     * @param instanceId of the Characteristic of the Descriptor to get the value of
     * @throws Exception
     */
    @Rpc(description = "Returns the read value of a matching instanceId")
    public byte[] gattServerGetReadValueByInstanceId(
            @RpcParameter(name = "instanceId") Integer instanceId)
            throws Exception {
        for (BluetoothGattCharacteristic mGattChar : mCharacteristicList.values()) {
            if (mGattChar.getInstanceId() == instanceId) {
                        Log.i("Found Characteristic to get value. instanceId: "
                        + Integer.toString(instanceId)
                        + " UUID: " + mGattChar.getUuid().toString());
                return mGattChar.getValue();
            }
            List<BluetoothGattDescriptor> descList = mGattChar.getDescriptors();
            for (BluetoothGattDescriptor mGattDesc : descList) {
                if (mGattDesc.getInstanceId() == instanceId) {
                    Log.i("Found Descriptor to get value. instanceId: "
                        + Integer.toString(instanceId)
                        + " UUID: " + mGattDesc.getUuid().toString());
                    return mGattDesc.getValue();
                }
            }
        }
        throw new Exception("Cannot find instance ID:" + instanceId);
    }

    /**
     * Set value by instance ID
     *
     * @param instanceId of the Characteristic of the Descriptor to get the value of
     * @value value set bytearray value
     * @throws Exception
     */
    @Rpc(description = "Sets the value of a Characteristic or Descriptor by instance id")
    public boolean gattServerSetByteArrayValueByInstanceId(
            @RpcParameter(name = "instanceId") Integer instanceId,
            @RpcParameter(name = "value") byte[] value)
            throws Exception {
        for (BluetoothGattCharacteristic mGattChar : mCharacteristicList.values()) {
            if (mGattChar.getInstanceId() == instanceId) {
                        Log.i("Found Characteristic to get value. instanceId: "
                        + Integer.toString(instanceId)
                        + " UUID: " + mGattChar.getUuid().toString());
                return mGattChar.setValue(value);
            }
            List<BluetoothGattDescriptor> descList = mGattChar.getDescriptors();
            for (BluetoothGattDescriptor mGattDesc : descList) {
                if (mGattDesc.getInstanceId() == instanceId) {
                    Log.i("Found Descriptor to set value. instanceId: "
                        + Integer.toString(instanceId)
                        + " UUID: " + mGattDesc.getUuid().toString());
                    return mGattDesc.setValue(value);
                }
            }
        }
        throw new Exception("Cannot find instance ID:" + instanceId);
    }

    /**
     * Set BluetoothGattService instance ID to input value
     *
     * @param index the bluetooth gatt service
     * @param instanceId instanceId
     * @throws Exception
     */
    @Rpc(description = "GATT Server Set Instance ID of Service")
    public void gattServerServiceSetInstanceId(
            @RpcParameter(name = "serviceIndex") Integer serviceIndex,
            @RpcParameter(name = "instanceId") Integer instanceId) throws Exception {
        if (mGattServiceList.get(serviceIndex) == null) {
            throw new Exception("Invalid serviceIndex input:" + Integer.toString(serviceIndex));
        }
        Class bluetoothGattServiceClass = Class.forName("android.bluetooth.BluetoothGattService");
        Method setInstanceIdMethod = bluetoothGattServiceClass.getMethod(
            "setInstanceId", int.class);
        setInstanceIdMethod.invoke(mGattServiceList.get(serviceIndex), instanceId);
    }

    /**
     * GATT Server set the number of handles to reserve for this service
     *
     * @param index the bluetooth gatt service
     * @param numHandles number of handles to reserve
     * @throws Exception
     */
    @Rpc(description = "GATT Server Set the number of handles to reserve for this service")
    public void gattServerServiceSetHandlesToReserve(
            @RpcParameter(name = "serviceIndex") Integer serviceIndex,
            @RpcParameter(name = "numHandles") Integer numHandles) throws Exception {
        if (mGattServiceList.get(serviceIndex) == null) {
            throw new Exception("Invalid serviceIndex input:" + Integer.toString(serviceIndex));
        }
        Class bluetoothGattServiceClass = Class.forName("android.bluetooth.BluetoothGattService");
        Method setHandlesMethod = bluetoothGattServiceClass.getMethod(
            "setHandles", int.class);
        setHandlesMethod.invoke(mGattServiceList.get(serviceIndex), numHandles);
    }

    /**
     * GATT Server set service advertise perferred value
     *
     * @param index the bluetooth gatt service
     * @param isPreferred if advertisement is preferred or not
     * @throws Exception
     */
    @Rpc(description = "GATT Server Set the number of handles to reserve for this service")
    public void gattServerServiceIsAdvertisePreferred(
            @RpcParameter(name = "serviceIndex") Integer serviceIndex,
            @RpcParameter(name = "isPreferred") Boolean isPreferred) throws Exception {
        if (mGattServiceList.get(serviceIndex) == null) {
            throw new Exception("Invalid serviceIndex input:" + Integer.toString(serviceIndex));
        }
        Class bluetoothGattServiceClass = Class.forName("android.bluetooth.BluetoothGattService");
        Method setAdvertisePreferredMethod = bluetoothGattServiceClass.getMethod(
            "setAdvertisePrefereed", boolean.class);
        setAdvertisePreferredMethod.invoke(mGattServiceList.get(serviceIndex), isPreferred);
    }

    /**
     * GATT Service add included service.
     *
     * @param index the bluetooth gatt service
     * @param serviceIncludedIndex index of the service to be included
     * @throws Exception
     */
    @Rpc(description = "Gatt Server add included service")
    public void gattServerServiceaddIncludedService(
            @RpcParameter(name = "serviceIndex") Integer serviceIndex,
            @RpcParameter(name = "serviceIncludedIndex") Integer serviceIncludedIndex)
            throws Exception {
        if (mGattServiceList.get(serviceIndex) == null) {
            throw new Exception("Invalid serviceIndex input:" + Integer.toString(serviceIndex));
        }
        if (mGattServiceList.get(serviceIncludedIndex) == null) {
            throw new Exception("Invalid serviceIncludedIndex input:" + Integer.toString(
                serviceIncludedIndex));
        }
        Class bluetoothGattServiceClass = Class.forName("android.bluetooth.BluetoothGattService");
        Method addIncludedServiceMethod = bluetoothGattServiceClass.getMethod(
            "addIncludedService", BluetoothGattService.class);
        addIncludedServiceMethod.invoke(
            mGattServiceList.get(serviceIndex), mGattServiceList.get(serviceIncludedIndex));
    }

    /**
     * Set value to a bluetooth gatt characteristic
     *
     * @param index the bluetooth gatt characteristic
     * @param value value
     * @throws Exception
     */
    @Rpc(description = "add descriptor to blutooth gatt characteristic")
    public boolean gattServerCharacteristicSetStringValue(
            @RpcParameter(name = "index") Integer index,
            @RpcParameter(name = "value") String value) throws Exception {
        if (mCharacteristicList.get(index) != null) {
            return mCharacteristicList.get(index).setValue(value);
        } else {
            throw new Exception("Invalid index input:" + index);
        }
    }

    /**
     * Set value to a bluetooth gatt characteristic
     *
     * @param index the bluetooth gatt characteristic
     * @param value value
     * @throws Exception
     */
    @Rpc(description = "add descriptor to blutooth gatt characteristic")
    public boolean gattServerCharacteristicSetIntValue(@RpcParameter(name = "index") Integer index,
            @RpcParameter(name = "value") Integer value,
            @RpcParameter(name = "type") Integer type,
            @RpcParameter(name = "offset") Integer offset)
            throws Exception {
        if (mCharacteristicList.get(index) != null) {
            return mCharacteristicList.get(index).setValue(value, type, offset);
        } else {
            throw new Exception("Invalid index input:" + index);
        }
    }

    /**
     * Set the instance id of the Bluetooth Gatt Characteristic
     *
     * @param index the bluetooth gatt characteristic
     * @param instanceId the instanceId to set
     * @throws Exception
     */
    @Rpc(description = "Set Caracteristic Instance id")
    public void gattServerCharacteristicSetInstanceId(@RpcParameter(name = "index") Integer index,
            @RpcParameter(name = "instanceId") Integer instanceId)
            throws Exception {
        if (mCharacteristicList.get(index) != null) {
            mCharacteristicList.get(index).setInstanceId(instanceId);
        } else {
            throw new Exception("Invalid index input:" + index);
        }
    }

    /**
     * Set the instance id of the Bluetooth Gatt Descriptor
     *
     * @param index the bluetooth gatt descriptor
     * @param instanceId the instanceId to set
     * @throws Exception
     */
    @Rpc(description = "Set Descriptor Instance Id")
    public void gattServerDescriptorSetInstanceId(@RpcParameter(name = "index") Integer index,
            @RpcParameter(name = "instanceId") Integer instanceId)
            throws Exception {
        if (mDescriptorList.get(index) != null) {
            Class bluetoothGattDescriptorClass = Class.forName(
                "android.bluetooth.BluetoothGattDescriptor");
            Method setInstanceIdMethod = bluetoothGattDescriptorClass.getMethod(
                "setInstanceId", int.class);
            setInstanceIdMethod.invoke(
                mDescriptorList.get(index), instanceId);
        } else {
            throw new Exception("Invalid index input:" + index);
        }
    }

    /**
     * Get the instance id of the Bluetooth Gatt Characteristic
     *
     * @param index the bluetooth gatt characteristic
     * @throws Exception
     * @return the instance id of the characteristic
     */
    @Rpc(description = "add descriptor to blutooth gatt characteristic")
    public int gattServerCharacteristicGetInstanceId(
            @RpcParameter(name = "index") Integer index)
            throws Exception {
        if (mCharacteristicList.get(index) != null) {
            return mCharacteristicList.get(index).getInstanceId();
        } else {
            throw new Exception("Invalid index input:" + index);
        }
    }

    /**
     * Create a new GattCallback object
     *
     * @return the index of the callback object
     */
    @Rpc(description = "Create a new GattCallback object")
    public Integer gattServerCreateGattServerCallback() {
        sGattServerCallbackCount += 1;
        int index = sGattServerCallbackCount;
        mBluetoothGattServerCallbackList.put(index, new BtGattServerCallback(index));
        return index;
    }

    /**
     * Create a new Descriptor object
     *
     * @param descriptorUuid the UUID for this descriptor
     * @param permissions Permissions for this descriptor
     * @return the index of the Descriptor object
     */
    @Rpc(description = "Create a new Descriptor object")
    public int gattServerCreateBluetoothGattDescriptor(
            @RpcParameter(name = "descriptorUuid") String descriptorUuid,
            @RpcParameter(name = "permissions") Integer permissions) {
        sDescriptorCount += 1;
        int index = sDescriptorCount;
        BluetoothGattDescriptor descriptor =
                new BluetoothGattDescriptor(UUID.fromString(descriptorUuid), permissions);
        mDescriptorList.put(index, descriptor);
        return index;
    }

    private class BtGattServerCallback extends BluetoothGattServerCallback {
        private final Bundle mResults;
        private final int mIndex;
        private final String mEventType;

        BtGattServerCallback(int idx) {
            mResults = new Bundle();
            mEventType = "GattServer";
            mIndex = idx;
        }

        @Override
        public void onServiceAdded(int status, BluetoothGattService service) {
            Log.d("gatt_server change onServiceAdded " + mEventType + " " + mIndex);
            mResults.putString("serviceUuid", service.getUuid().toString());
            mResults.putInt("instanceId", service.getInstanceId());
            mEventFacade.postEvent(mEventType + mIndex + "onServiceAdded", mResults.clone());
            mResults.clear();
        }

        @Override
        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
                BluetoothGattCharacteristic characteristic) {
            Log.d("gatt_server change onCharacteristicReadRequest " + mEventType + " " + mIndex);
            mResults.putInt("requestId", requestId);
            mResults.putInt("offset", offset);
            mResults.putInt("instanceId", characteristic.getInstanceId());
            mResults.putInt("properties", characteristic.getProperties());
            mResults.putString("uuid", characteristic.getUuid().toString());
            mResults.putInt("permissions", characteristic.getPermissions());
            mEventFacade.postEvent(
                    mEventType + mIndex + "onCharacteristicReadRequest", mResults.clone());
            mResults.clear();
        }

        @Override
        public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId,
                BluetoothGattCharacteristic characteristic, boolean preparedWrite,
                boolean responseNeeded, int offset, byte[] value) {
            Log.d("gatt_server change onCharacteristicWriteRequest " + mEventType + " " + mIndex);
            mResults.putInt("requestId", requestId);
            mResults.putInt("offset", offset);
            mResults.putParcelable("BluetoothDevice", device);
            mResults.putBoolean("preparedWrite", preparedWrite);
            mResults.putBoolean("responseNeeded", responseNeeded);
            mResults.putByteArray("value", value);
            mResults.putInt("instanceId", characteristic.getInstanceId());
            mResults.putInt("properties", characteristic.getProperties());
            mResults.putString("uuid", characteristic.getUuid().toString());
            mResults.putInt("permissions", characteristic.getPermissions());
            mEventFacade.postEvent(
                    mEventType + mIndex + "onCharacteristicWriteRequest", mResults.clone());
            mResults.clear();

        }

        @Override
        public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset,
                BluetoothGattDescriptor descriptor) {
            Log.d("gatt_server change onDescriptorReadRequest " + mEventType + " " + mIndex);
            mResults.putInt("requestId", requestId);
            mResults.putInt("offset", offset);
            mResults.putParcelable("BluetoothDevice", device);
            mResults.putInt("instanceId", descriptor.getInstanceId());
            mResults.putInt("permissions", descriptor.getPermissions());
            mResults.putString("uuid", descriptor.getUuid().toString());
            mEventFacade.postEvent(
                    mEventType + mIndex + "onDescriptorReadRequest", mResults.clone());
            mResults.clear();
        }

        @Override
        public void onDescriptorWriteRequest(BluetoothDevice device, int requestId,
                BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded,
                int offset, byte[] value) {
            Log.d("gatt_server change onDescriptorWriteRequest " + mEventType + " " + mIndex);
            mResults.putInt("requestId", requestId);
            mResults.putInt("offset", offset);
            mResults.putParcelable("BluetoothDevice", device);
            mResults.putBoolean("preparedWrite", preparedWrite);
            mResults.putBoolean("responseNeeded", responseNeeded);
            mResults.putByteArray("value", value);
            mResults.putInt("instanceId", descriptor.getInstanceId());
            mResults.putInt("permissions", descriptor.getPermissions());
            mResults.putString("uuid", descriptor.getUuid().toString());
            mEventFacade.postEvent(
                    mEventType + mIndex + "onDescriptorWriteRequest", mResults.clone());
            mResults.clear();
        }

        @Override
        public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
            Log.d("gatt_server change onExecuteWrite " + mEventType + " " + mIndex);
            mResults.putParcelable("BluetoothDevice", device);
            mResults.putInt("requestId", requestId);
            mResults.putBoolean("execute", execute);
            mEventFacade.postEvent(mEventType + mIndex + "onExecuteWrite", mResults.clone());
            mResults.clear();
        }

        @Override
        public void onNotificationSent(BluetoothDevice device, int status) {
            Log.d("gatt_server change onNotificationSent " + mEventType + " " + mIndex);
            mResults.putParcelable("BluetoothDevice", device);
            mResults.putInt("status", status);
            mEventFacade.postEvent(mEventType + mIndex + "onNotificationSent", mResults.clone());
            mResults.clear();
        }

        @Override
        public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
            Log.d("gatt_server change onConnectionStateChange " + mEventType + " " + mIndex);
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                Log.d("State Connected to mac address " + device.getAddress() + " status "
                        + status);
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                Log.d("State Disconnected from mac address "
                        + device.getAddress() + " status " + status);
            }
            mResults.putParcelable("BluetoothDevice", device);
            mResults.putInt("status", status);
            mResults.putInt("newState", newState);
            mEventFacade.postEvent(
                    mEventType + mIndex + "onConnectionStateChange", mResults.clone());
            mResults.clear();
        }

        @Override
        public void onMtuChanged(BluetoothDevice device, int mtu) {
            Log.d("gatt_server change onMtuChanged " + mEventType + " " + mIndex);
            mResults.putParcelable("BluetoothDevice", device);
            mResults.putInt("MTU", mtu);
            mEventFacade.postEvent(mEventType + mIndex + "onMtuChanged", mResults.clone());
            mResults.clear();
        }

        @Override
        public void onPhyRead(BluetoothDevice device, int txPhy, int rxPhy, int status) {
            Log.d("gatt_server change onPhyRead " + mEventType + " " + mIndex);
            mResults.putParcelable("BluetoothDevice", device);
            mResults.putInt("TxPhy", txPhy);
            mResults.putInt("RxPhy", rxPhy);
            mResults.putInt("Status", status);
            mEventFacade.postEvent(mEventType + mIndex + "onPhyRead", mResults.clone());
            mResults.clear();
        }

        @Override
        public void onPhyUpdate(BluetoothDevice device, int txPhy, int rxPhy, int status) {
            Log.d("gatt_server change onPhyUpdate " + mEventType + " " + mIndex);
            mResults.putParcelable("BluetoothDevice", device);
            mResults.putInt("TxPhy", txPhy);
            mResults.putInt("RxPhy", rxPhy);
            mResults.putInt("Status", status);
            mEventFacade.postEvent(mEventType + mIndex + "onPhyUpdate", mResults.clone());
            mResults.clear();
        }

        public void onConnectionUpdated(BluetoothDevice device, int interval, int latency,
                                            int timeout, int status) {
            Log.d("gatt_server change onConnecitonUpdated " + mEventType + " " + mIndex
                    + ", interval: " + interval + ", latency: " + latency
                    + ", timeout: " + timeout + ", status: " + status);

            mResults.putInt("Status", status);
            mResults.putInt("Interval", interval);
            mResults.putInt("Latency", latency);
            mResults.putInt("Timeout", timeout);
            mEventFacade.postEvent(mEventType + mIndex + "onConnectionUpdated", mResults.clone());
            mResults.clear();
        }

    }

    @Override
    public void shutdown() {
        if (!mBluetoothGattServerList.isEmpty()) {
            if (mBluetoothGattServerList.values() != null) {
                for (BluetoothGattServer mBluetoothGattServer : mBluetoothGattServerList.values()) {
                    mBluetoothGattServer.close();
                }
            }
        }
    }
}
