# Lint as: python2, python3
# Copyright 2019 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Server side bluetooth GATT client helper class for testing"""

import base64
import json


class GATT_ClientFacade(object):
    """A wrapper for getting GATT application from GATT server"""

    def __init__(self, bluetooth_facade):
        """Initialize a GATT_ClientFacade

        @param bluetooth_facade: facade to communicate with adapter in DUT

        """
        self.bluetooth_facade = bluetooth_facade


    def browse(self, address):
        """Browse the application on GATT server

        @param address: a string of MAC address of the GATT server device

        @return: GATT_Application object

        """
        attr_map_json = json.loads(self.bluetooth_facade.\
                              get_gatt_attributes_map(address))
        application = GATT_Application()
        application.browse(attr_map_json, self.bluetooth_facade)

        return application


class GATT_Application(object):
    """A GATT client application class"""

    def __init__(self):
        """Initialize a GATT Application"""
        self.services = dict()


    def browse(self, attr_map_json, bluetooth_facade):
        """Browse the application on GATT server

        @param attr_map_json: a json object returned by
                              bluetooth_device_xmlrpc_server

        @bluetooth_facade: facade to communicate with adapter in DUT

        """
        servs_json = attr_map_json['services']
        for uuid in servs_json:
            path = servs_json[uuid]['path']
            service_obj = GATT_Service(uuid, path, bluetooth_facade)
            service_obj.read_properties()
            self.add_service(service_obj)

            chrcs_json = servs_json[uuid]['characteristics']
            for uuid in chrcs_json:
                path = chrcs_json[uuid]['path']
                chrc_obj = GATT_Characteristic(uuid, path, bluetooth_facade)
                chrc_obj.read_properties()
                service_obj.add_characteristic(chrc_obj)

                descs_json = chrcs_json[uuid]['descriptors']
                for uuid in descs_json:
                    path = descs_json[uuid]['path']
                    desc_obj = GATT_Descriptor(uuid, path, bluetooth_facade)
                    desc_obj.read_properties()
                    chrc_obj.add_descriptor(desc_obj)


    def find_by_uuid(self, uuid):
        """Find attribute under this application by specifying UUID

        @param uuid: string of UUID

        @return: Attribute object if found,
                 none otherwise
        """
        for serv_uuid, serv in self.services.items():
            found = serv.find_by_uuid(uuid)
            if found:
                return found
        return None


    def add_service(self, service):
        """Add a service into this application"""
        self.services[service.uuid] = service


    @staticmethod
    def diff(appl_a, appl_b):
        """Compare two Applications, and return their difference

        @param appl_a: the first application which is going to be compared

        @param appl_b: the second application which is going to be compared

        @return: a list of string, each describes one difference

        """
        result = []

        uuids_a = set(appl_a.services.keys())
        uuids_b = set(appl_b.services.keys())
        uuids = uuids_a.union(uuids_b)

        for uuid in uuids:
            serv_a = appl_a.services.get(uuid, None)
            serv_b = appl_b.services.get(uuid, None)

            if not serv_a or not serv_b:
                result.append("Service %s is not included in both Applications:"
                              "%s vs %s" % (uuid, bool(serv_a), bool(serv_b)))
            else:
                result.extend(GATT_Service.diff(serv_a, serv_b))
        return result


class GATT_Service(object):
    """GATT client service class"""
    PROPERTIES = ['UUID', 'Primary', 'Device', 'Includes']


    def __init__(self, uuid, object_path, bluetooth_facade):
        """Initialize a GATT service object

        @param uuid: string of UUID

        @param object_path: object path of this service

        @param bluetooth_facade: facade to communicate with adapter in DUT

        """
        self.uuid = uuid
        self.object_path = object_path
        self.bluetooth_facade = bluetooth_facade
        self.properties = dict()
        self.characteristics = dict()


    def add_characteristic(self, chrc_obj):
        """Add a characteristic attribute into service

        @param chrc_obj: a characteristic object

        """
        self.characteristics[chrc_obj.uuid] = chrc_obj


    def read_properties(self):
        """Read all properties in this service"""
        for prop_name in self.PROPERTIES:
            self.properties[prop_name] = self.read_property(prop_name)
        return self.properties


    def read_property(self, property_name):
        """Read a property in this service

        @param property_name: string of the name of the property

        @return: the value of the property

        """
        return self.bluetooth_facade.get_gatt_service_property(
                                        self.object_path, property_name)

    def find_by_uuid(self, uuid):
        """Find attribute under this service by specifying UUID

        @param uuid: string of UUID

        @return: Attribute object if found,
                 none otherwise

        """
        if self.uuid == uuid:
            return self

        for chrc_uuid, chrc in self.characteristics.items():
            found = chrc.find_by_uuid(uuid)
            if found:
                return found
        return None


    @staticmethod
    def diff(serv_a, serv_b):
        """Compare two Services, and return their difference

        @param serv_a: the first service which is going to be compared

        @param serv_b: the second service which is going to be compared

        @return: a list of string, each describes one difference

        """
        result = []

        for prop_name in GATT_Service.PROPERTIES:
            if serv_a.properties[prop_name] != serv_b.properties[prop_name]:
                result.append("Service %s is different in %s: %s vs %s" %
                              (serv_a.uuid, prop_name,
                              serv_a.properties[prop_name],
                              serv_b.properties[prop_name]))

        uuids_a = set(serv_a.characteristics.keys())
        uuids_b = set(serv_b.characteristics.keys())
        uuids = uuids_a.union(uuids_b)

        for uuid in uuids:
            chrc_a = serv_a.characteristics.get(uuid, None)
            chrc_b = serv_b.characteristics.get(uuid, None)

            if not chrc_a or not chrc_b:
                result.append("Characteristic %s is not included in both "
                              "Services: %s vs %s" % (uuid, bool(chrc_a),
                                                    bool(chrc_b)))
            else:
                result.extend(GATT_Characteristic.diff(chrc_a, chrc_b))
        return result


class GATT_Characteristic(object):
    """GATT client characteristic class"""

    PROPERTIES = ['UUID', 'Service', 'Value', 'Notifying', 'Flags']


    def __init__(self, uuid, object_path, bluetooth_facade):
        """Initialize a GATT characteristic object

        @param uuid: string of UUID

        @param object_path: object path of this characteristic

        @param bluetooth_facade: facade to communicate with adapter in DUT

        """
        self.uuid = uuid
        self.object_path = object_path
        self.bluetooth_facade = bluetooth_facade
        self.properties = dict()
        self.descriptors = dict()


    def add_descriptor(self, desc_obj):
        """Add a characteristic attribute into service

        @param desc_obj: a descriptor object

        """
        self.descriptors[desc_obj.uuid] = desc_obj


    def read_properties(self):
        """Read all properties in this characteristic"""
        for prop_name in self.PROPERTIES:
            self.properties[prop_name] = self.read_property(prop_name)
        return self.properties


    def read_property(self, property_name):
        """Read a property in this characteristic

        @param property_name: string of the name of the property

        @return: the value of the property

        """
        return self.bluetooth_facade.get_gatt_characteristic_property(
                                        self.object_path, property_name)


    def find_by_uuid(self, uuid):
        """Find attribute under this characteristic by specifying UUID

        @param uuid: string of UUID

        @return: Attribute object if found,
                 none otherwise

        """
        if self.uuid == uuid:
            return self

        for desc_uuid, desc in self.descriptors.items():
            if desc_uuid == uuid:
                return desc
        return None


    def read_value(self):
        """Perform ReadValue in DUT and store it in property 'Value'

        @return: bytearray of the value

        """
        value = self.bluetooth_facade.gatt_characteristic_read_value(
                                                self.uuid, self.object_path)
        self.properties['Value'] = bytearray(base64.standard_b64decode(value))
        return self.properties['Value']


    @staticmethod
    def diff(chrc_a, chrc_b):
        """Compare two Characteristics, and return their difference

        @param serv_a: the first service which is going to be compared

        @param serv_b: the second service which is going to be compared

        @return: a list of string, each describes one difference

        """
        result = []

        for prop_name in GATT_Characteristic.PROPERTIES:
            if chrc_a.properties[prop_name] != chrc_b.properties[prop_name]:
                result.append("Characteristic %s is different in %s: %s vs %s"
                              % (chrc_a.uuid, prop_name,
                              chrc_a.properties[prop_name],
                              chrc_b.properties[prop_name]))

        uuids_a = set(chrc_a.descriptors.keys())
        uuids_b = set(chrc_b.descriptors.keys())
        uuids = uuids_a.union(uuids_b)

        for uuid in uuids:
            desc_a = chrc_a.descriptors.get(uuid, None)
            desc_b = chrc_b.descriptors.get(uuid, None)

            if not desc_a or not desc_b:
                result.append("Descriptor %s is not included in both"
                              "Characteristic: %s vs %s" % (uuid, bool(desc_a),
                                                          bool(desc_b)))
            else:
                result.extend(GATT_Descriptor.diff(desc_a, desc_b))
        return result


class GATT_Descriptor(object):
    """GATT client descriptor class"""

    PROPERTIES = ['UUID', 'Characteristic', 'Value', 'Flags']

    def __init__(self, uuid, object_path, bluetooth_facade):
        """Initialize a GATT descriptor object

        @param uuid: string of UUID

        @param object_path: object path of this descriptor

        @param bluetooth_facade: facade to communicate with adapter in DUT

        """
        self.uuid = uuid
        self.object_path = object_path
        self.bluetooth_facade = bluetooth_facade
        self.properties = dict()


    def read_properties(self):
        """Read all properties in this characteristic"""
        for prop_name in self.PROPERTIES:
            self.properties[prop_name] = self.read_property(prop_name)
        return self.properties


    def read_property(self, property_name):
        """Read a property in this characteristic

        @param property_name: string of the name of the property

        @return: the value of the property

        """
        return self.bluetooth_facade.get_gatt_descriptor_property(
                                        self.object_path, property_name)


    def read_value(self):
        """Perform ReadValue in DUT and store it in property 'Value'

        @return: bytearray of the value

        """
        value = self.bluetooth_facade.gatt_descriptor_read_value(
                                                self.uuid, self.object_path)
        self.properties['Value'] = bytearray(base64.standard_b64decode(value))

        return self.properties['Value']


    @staticmethod
    def diff(desc_a, desc_b):
        """Compare two Descriptors, and return their difference

        @param serv_a: the first service which is going to be compared

        @param serv_b: the second service which is going to be compared

        @return: a list of string, each describes one difference

        """
        result = []

        for prop_name in desc_a.properties.keys():
            if desc_a.properties[prop_name] != desc_b.properties[prop_name]:
                result.append("Descriptor %s is different in %s: %s vs %s" %
                              (desc_a.uuid, prop_name,
                              desc_a.properties[prop_name],
                              desc_b.properties[prop_name]))

        return result


def UUID_Short2Full(uuid):
    """Transform 2 bytes uuid string to 16 bytes

    @param uuid: 2 bytes shortened UUID string in hex

    @return: full uuid string
    """
    uuid_template = '0000%s-0000-1000-8000-00805f9b34fb'
    return uuid_template % uuid


class GATT_HIDApplication(GATT_Application):
    """Default HID Application on Raspberry Pi GATT server
    """

    BatteryServiceUUID = UUID_Short2Full('180f')
    BatteryLevelUUID = UUID_Short2Full('2a19')
    CliChrcConfigUUID = UUID_Short2Full('2902')
    GenericAttributeProfileUUID = UUID_Short2Full('1801')
    ServiceChangedUUID = UUID_Short2Full('2a05')
    DeviceInfoUUID = UUID_Short2Full('180a')
    ManufacturerNameStrUUID = UUID_Short2Full('2a29')
    PnPIDUUID = UUID_Short2Full('2a50')
    GenericAccessProfileUUID = UUID_Short2Full('1800')
    DeviceNameUUID = UUID_Short2Full('2a00')
    AppearanceUUID = UUID_Short2Full('2a01')


    def __init__(self):
        """
        """
        GATT_Application.__init__(self)
        BatteryService = GATT_Service(self.BatteryServiceUUID, None, None)
        BatteryService.properties = {
                'UUID': BatteryService.uuid,
                'Primary': True,
                'Device': None,
                'Includes': []
        }
        self.add_service(BatteryService)

        BatteryLevel = GATT_Characteristic(self.BatteryLevelUUID, None, None)
        BatteryLevel.properties = {
                'UUID': BatteryLevel.uuid,
                'Service': None,
                'Value': [],
                'Notifying': False,
                'Flags': ['read', 'notify']
        }
        BatteryService.add_characteristic(BatteryLevel)

        CliChrcConfig = GATT_Descriptor(self.CliChrcConfigUUID, None, None)
        CliChrcConfig.properties = {
                'UUID': CliChrcConfig.uuid,
                'Characteristic': None,
                'Value': [],
                'Flags': None
        }

        BatteryLevel.add_descriptor(CliChrcConfig)

        GenericAttributeProfile = GATT_Service(self.GenericAttributeProfileUUID,
                                               None, None)
        GenericAttributeProfile.properties = {
                'UUID': GenericAttributeProfile.uuid,
                'Primary': True,
                'Device': None,
                'Includes': []
        }
        self.add_service(GenericAttributeProfile)

        ServiceChanged = GATT_Characteristic(self.ServiceChangedUUID, None,
                                             None)
        ServiceChanged.properties = {
                'UUID': ServiceChanged.uuid,
                'Service': None,
                'Value': [],
                'Notifying': False,
                'Flags': ['indicate']
        }
        GenericAttributeProfile.add_characteristic(ServiceChanged)

        CliChrcConfig = GATT_Descriptor(self.CliChrcConfigUUID, None, None)
        CliChrcConfig.properties = {
                'UUID': CliChrcConfig.uuid,
                'Characteristic': None,
                'Value': [],
                'Flags': None
        }
        ServiceChanged.add_descriptor(CliChrcConfig)

        DeviceInfo = GATT_Service(self.DeviceInfoUUID, None, None)
        DeviceInfo.properties = {
                'UUID': DeviceInfo.uuid,
                'Primary': True,
                'Device': None,
                'Includes': []
        }
        self.add_service(DeviceInfo)

        ManufacturerNameStr = GATT_Characteristic(self.ManufacturerNameStrUUID,
                                                  None, None)
        ManufacturerNameStr.properties = {
                'UUID': ManufacturerNameStr.uuid,
                'Service': None,
                'Value': [],
                'Notifying': None,
                'Flags': ['read']
        }
        DeviceInfo.add_characteristic(ManufacturerNameStr)

        PnPID = GATT_Characteristic(self.PnPIDUUID, None, None)
        PnPID.properties = {
                'UUID': PnPID.uuid,
                'Service': None,
                'Value': [],
                'Notifying': None,
                'Flags': ['read']
        }
        DeviceInfo.add_characteristic(PnPID)

        GenericAccessProfile = GATT_Service(self.GenericAccessProfileUUID,
                                            None, None)
        GenericAccessProfile.properties = {
                'UUID': GenericAccessProfile.uuid,
                'Primary': True,
                'Device': None,
                'Includes': []
        }
        self.add_service(GenericAccessProfile)

        DeviceName = GATT_Characteristic(self.DeviceNameUUID, None, None)
        DeviceName.properties = {
                'UUID': DeviceName.uuid,
                'Service': None,
                'Value': [],
                'Notifying': None,
                'Flags': ['read']
        }
        GenericAccessProfile.add_characteristic(DeviceName)

        Appearance = GATT_Characteristic(self.AppearanceUUID, None, None)
        Appearance.properties = {
                'UUID': Appearance.uuid,
                'Service': None,
                'Value': [],
                'Notifying': None,
                'Flags': ['read']
        }
        GenericAccessProfile.add_characteristic(Appearance)
