LTV = Struct(
    "length" / Byte,
    "type" / Byte,
    "value" / Switch(lambda ctx: ctx.type, {
        FLAGS_DATA_TYPE: AdvertisingFlags,
        SERVICE_DATA_TYPE: ServiceData,
        MANUFACTURER_SPECIFIC_DATA_TYPE: ManufacturerSpecificData,
    }, default=Array(lambda ctx: ctx.length - 1, Byte)),
) class EddystoneUIDFrame(object):
    """Eddystone UID frame."""

    def __init__(self, data):
        self._tx_power = data['tx_power']
        self._namespace = data_to_hexstring(data['namespace'])
        self._instance = data_to_hexstring(data['instance'])

    @property
    def tx_power(self):
        """Calibrated Tx power at 0 m."""
        return self._tx_power

    @property
    def namespace(self):
        """10-byte namespace identifier."""
        return self._namespace

    @property
    def instance(self):
        """6-byte instance identifier."""
        return self._instance

    @property
    def properties(self):
        """Get beacon properties."""
        return {'namespace': self.namespace, 'instance': self.instance}

    def __str__(self):
        return "EddystoneUIDFrame<tx_power: %d, namespace: %s, instance: %s>" \
               % (self.tx_power, self.namespace, self.instance) class EddystoneURLFrame(object):
    """Eddystone URL frame."""

    def __init__(self, data):
        self._tx_power = data['tx_power']
        url_scheme = EDDYSTONE_URL_SCHEMES[data['url_scheme']]
        url = data['url']

        # Replace url encodings with their expanded version
        for enc, tld in EDDYSTONE_TLD_ENCODINGS.items():
            url = url.replace(chr(enc), tld)

        self._url = url_scheme + url

    @property
    def tx_power(self):
        """Calibrated Tx power at 0 m."""
        return self._tx_power

    @property
    def url(self):
        """Transmitted URL."""
        return self._url

    def __str__(self):
        return "EddystoneURLFrame<tx_power: %d, url: %s>" \
               % (self.tx_power, self.url) class EddystoneEncryptedTLMFrame(object):
    """Eddystone encrypted TLM frame."""

    def __init__(self, data):
        self._encrypted_data = data_to_binstring(data['encrypted_data'])
        self._salt = data['salt']
        self._mic = data['mic']

    @property
    def encrypted_data(self):
        """Encrypted TLM data."""
        return self._encrypted_data

    @property
    def salt(self):
        """16-bit salt."""
        return self._salt

    @property
    def mic(self):
        """16-bit message integrity check."""
        return self._mic

    def __str__(self):
        return "EddystoneEncryptedTLMFrame<encrypted_data: %s, salt: %d, mic: %d>" \
               % (hexlify(self.encrypted_data), self.salt, self.mic) class EddystoneTLMFrame(object):
    """Eddystone TLM frame."""

    def __init__(self, data):
        self._voltage = data['voltage']
        self._temperature = data['temperature'] / float(256)
        self._advertising_count = data['advertising_count']
        self._seconds_since_boot = data['seconds_since_boot']

    @property
    def voltage(self):
        """Battery voltage measured in mV."""
        return self._voltage

    @property
    def temperature(self):
        """Temperature in degree Celsius."""
        return self._temperature

    @property
    def advertising_count(self):
        """Advertising PDU count."""
        return self._advertising_count

    @property
    def seconds_since_boot(self):
        """Time since power-on or reboot."""
        return self._seconds_since_boot

    def __str__(self):
        return "EddystoneTLMFrame<voltage: %d mV, temperature: %d Celsius, advertising count: %d,"\
               " seconds since boot: %d>" % (self.voltage, self.temperature, \
                self.advertising_count, self.seconds_since_boot) class EddystoneEIDFrame(object):
    """Eddystone EID frame."""

    def __init__(self, data):
        self._tx_power = data['tx_power']
        self._eid = data_to_binstring(data['eid'])

    @property
    def tx_power(self):
        """Calibrated Tx power at 0 m."""
        return self._tx_power

    @property
    def eid(self):
        """8-byte Ephemeral Identifier."""
        return self._eid

    def __str__(self):
        return "EddystoneEIDFrame<tx_power: %d, eid: %s>" \
               % (self.tx_power, hexlify(self.eid)) class IBeaconAdvertisement(object):
    """iBeacon advertisement."""

    def __init__(self, data):
        self._uuid = data_to_uuid(data['uuid'])
        self._major = data['major']
        self._minor = data['minor']
        self._tx_power = data['tx_power']

    @property
    def tx_power(self):
        """Calibrated Tx power at 0 m."""
        return self._tx_power

    @property
    def uuid(self):
        """16-byte uuid."""
        return self._uuid

    @property
    def major(self):
        """2-byte major identifier."""
        return self._major

    @property
    def minor(self):
        """2-byte minor identifier."""
        return self._minor

    @property
    def cypress_temperature(self):
        """Cypress iBeacon Sensor temperature in C."""
        return 175.72*((self._minor & 0xff)*256)/65536 - 46.85

    @property
    def cypress_humidity(self):
        """Cypress iBeacon Sensor humidity RH%."""
        return 125.0*(self._minor & 0xff00)/65536 - 6

    @property
    def properties(self):
        """Get beacon properties."""
        return {'uuid': self.uuid, 'major': self.major, 'minor': self.minor}

    def __str__(self):
        return "IBeaconAdvertisement<tx_power: %d, uuid: %s, major: %d, minor: %d>" \
               % (self.tx_power, self.uuid, self.major, self.minor) class EstimoteTelemetryFrameA(object):
    """Estimote telemetry subframe A."""

    def __init__(self, data, protocol_version):
        self._protocol_version = protocol_version
        self._identifier = data_to_hexstring(data['identifier'])
        sub = data['sub_frame']
        # acceleration: convert to tuple and normalize
        self._acceleration = tuple([v * 2 / 127.0 for v in sub['acceleration']])
        # motion states
        self._previous_motion_state = self.parse_motion_state(sub['previous_motion'])
        self._current_motion_state = self.parse_motion_state(sub['current_motion'])
        self._is_moving = (sub['combined_fields'][0] & 0b00000011) == 1
        # gpio
        states = []
        for i in range(4):
            states.append((sub['combined_fields'][0] & (1 << (4+i))) != 0)
        self._gpio_states = tuple(states)
        # error codes
        if self.protocol_version == 2:
            self._has_firmware_error = ((sub['combined_fields'][0] & 0b00000100) >> 2) == 1
            self._has_clock_error = ((sub['combined_fields'][0] & 0b00001000) >> 3) == 1
        elif self.protocol_version == 1:
            self._has_firmware_error = (sub['combined_fields'][1] & 0b00000001) == 1
            self._has_clock_error = ((sub['combined_fields'][1] & 0b00000010) >> 1) == 1
        else:
            self._has_firmware_error = None
            self._has_clock_error = None
        # pressure
        if self.protocol_version == 2:
            self._pressure = sub['combined_fields'][1] | \
                                sub['combined_fields'][2] << 8 | \
                                sub['combined_fields'][3] << 16 | \
                                sub['combined_fields'][4] << 24
            if self._pressure == 0xffffffff:
                self._pressure = None
            else:
                self._pressure /= 256.0
        else:
            self._pressure = None

    @staticmethod
    def parse_motion_state(val):
        """Convert motion state byte to seconds."""
        number = val & 0b00111111
        unit = (val & 0b11000000) >> 6
        if unit == 1:
            number *= 60 # minutes
        elif unit == 2:
            number *= 60 * 60 # hours
        elif unit == 3 and number < 32:
            number *= 60 * 60 * 24 # days
        elif unit == 3:
            number -= 32
            number *= 60 * 60 * 24 * 7 # weeks
        return number

    @property
    def protocol_version(self):
        """Protocol version of the packet."""
        return self._protocol_version

    @property
    def identifier(self):
        """First half of the identifier of the beacon (8 bytes)."""
        return self._identifier

    @property
    def acceleration(self):
        """Tuple of acceleration values for (X, Y, Z) axis, in g."""
        return self._acceleration

    @property
    def is_moving(self):
        """Whether the beacon is in motion at the moment (Bool)"""
        return self._is_moving

    @property
    def current_motion_state(self):
        """Duration of current motion state in seconds.
        E.g., if is_moving is True, this states how long the beacon is beeing moved already and
        previous_motion_state will tell how long it has been still before."""
        return self._current_motion_state


    @property
    def previous_motion_state(self):
        """Duration of previous motion state in seconds (see current_motion_state)."""
        return self._previous_motion_state

    @property
    def gpio_states(self):
        """Tuple with state of the GPIO pins 0-3 (True is high, False is low)."""
        return self._gpio_states

    @property
    def has_firmware_error(self):
        """If beacon has a firmware problem.
        Only available if protocol version > 0, None otherwise."""
        return self._has_firmware_error

    @property
    def has_clock_error(self):
        """If beacon has a clock problem. Only available if protocol version > 0, None otherwise."""
        return self._has_clock_error

    @property
    def pressure(self):
        """Atmosperic pressure in Pascal. None if all bits are set.
        Only available if protocol version is 2, None otherwise ."""
        return self._pressure

    @property
    def properties(self):
        """Get beacon properties."""
        return {'identifier': self.identifier, 'protocol_version': self.protocol_version}

    def __str__(self):
        return "EstimoteTelemetryFrameA<identifier: %s, protocol_version: %u>" \
            % (self.identifier, self.protocol_version) class EstimoteTelemetryFrameB(object):
    """Estimote telemetry subframe B."""

    def __init__(self, data, protocol_version):
        self._protocol_version = protocol_version
        self._identifier = data_to_hexstring(data['identifier'])
        sub = data['sub_frame']
        # magnetic field: convert to tuple and normalize
        if sub['magnetic_field'] == [-1, -1, -1]:
            self._magnetic_field = None
        else:
            self._magnetic_field = tuple([v / 128.0 for v in sub['magnetic_field']])
        # ambient light
        ambient_upper = (sub['ambient_light'] & 0b11110000) >> 4
        ambient_lower = sub['ambient_light'] & 0b00001111
        if ambient_upper == 0xf and ambient_lower == 0xf:
            self._ambient_light = None
        else:
            self._ambient_light = pow(2, ambient_upper) * ambient_lower * 0.72
        # uptime
        uptime_unit_code = (sub['combined_fields'][1] & 0b00110000) >> 4
        uptime_number = ((sub['combined_fields'][1] & 0b00001111) << 8) | \
                            sub['combined_fields'][0]
        if uptime_unit_code == 1:
            uptime_number *= 60 # minutes
        elif uptime_unit_code == 2:
            uptime_number *= 60 * 60 # hours
        elif uptime_unit_code == 3:
            uptime_number *= 60 * 60 * 24 # days
        else:
            uptime_number = 0
        self._uptime = uptime_number
        # temperature
        temperature = ((sub['combined_fields'][3] & 0b00000011) << 10) |   \
                        (sub['combined_fields'][2]               <<  2) |  \
                        ((sub['combined_fields'][1] & 0b11000000) >>  6)
        temperature = temperature - 4096 if temperature > 2047 else temperature
        self._temperature = temperature / 16.0
        # battery voltage
        voltage = (sub['combined_fields'][4] << 6) |  \
                    ((sub['combined_fields'][3] & 0b11111100) >> 2)
        self._voltage = None if voltage == 0b11111111111111 else voltage
        if self._protocol_version == 0:
            # errors (only protocol ver 0)
            self._has_firmware_error = (sub['battery_level'] & 0b00000001) == 1
            self._has_clock_error = (sub['battery_level'] & 0b00000010) == 0b10
            self._battery_level = None
        else:
            self._battery_level = None if sub['battery_level'] == 0xFF else sub['battery_level']
            self._has_clock_error = None
            self._has_firmware_error = None


    @property
    def protocol_version(self):
        """Protocol version of the packet."""
        return self._protocol_version

    @property
    def identifier(self):
        """First half of the identifier of the beacon (8 bytes)."""
        return self._identifier

    @property
    def magnetic_field(self):
        """Tuple of magnetic field values for (X, Y, Z) axis.
        Between -1 and 1 or None if all bits are set."""
        return self._magnetic_field

    @property
    def ambient_light(self):
        """Ambient light in lux."""
        return self._ambient_light

    @property
    def uptime(self):
        """Uptime in seconds."""
        return self._uptime

    @property
    def temperature(self):
        """Ambient temperature in celsius."""
        return self._temperature

    @property
    def has_firmware_error(self):
        """Whether beacon has a firmware problem.
        Only available if protocol version is 0, None otherwise."""
        return self._has_firmware_error

    @property
    def has_clock_error(self):
        """Whether beacon has a clock problem.
        Only available if protocol version is 0, None otherwise."""
        return self._has_clock_error

    @property
    def battery_level(self):
        """Beacon battery level between 0 and 100.
        None if protocol version is 0 or not measured yet."""
        return self._battery_level

    @property
    def properties(self):
        """Get beacon properties."""
        return {'identifier': self.identifier, 'protocol_version': self.protocol_version}

    def __str__(self):
        return "EstimoteTelemetryFrameB<identifier: %s, protocol_version: %u>" \
            % (self.identifier, self.protocol_version) class EstimoteNearable(object):
    """Estimote Nearable advertisement."""

    def __init__(self, data):
        self._identifier = data_to_hexstring(data['identifier'])
        self._hardware_version = data['hardware_version']
        self._firmware_version = data['firmware_version']

        # byte 13 and the first 4 bits of byte 14 is the temperature in signed,
        temperature_raw_value = (data['temperature'] & 0x0fff)
        if temperature_raw_value > 2047:
            # convert a 12-bit unsigned integer to a signed one
            temperature_raw_value = temperature_raw_value - 4096
        temperature = temperature_raw_value / 16.0
        self._temperature = temperature
        self._is_moving = data['is_moving'] & 0b01000000 != 0

    @property
    def identifier(self):
        """The Nearable identifier (8 bytes)."""
        return self._identifier

    @property
    def hardware_version(self):
        """The hardware version of the nearable."""
        return self._hardware_version

    @property
    def firmware_version(self):
        """The firmware version of the nearable."""
        return self._firmware_version

    @property
    def temperature(self):
        """The temperature reading taken by the nearable."""
        return self._temperature

    @property
    def is_moving(self):
        """Whether the beacon is in motion at the moment."""
        return self._is_moving

    @property
    def properties(self):
        """Get beacon properties."""
        return {'identifier': self.identifier, 'temperature': self.temperature,
                'is_moving': self._is_moving}

    def __str__(self):
        return "EstimoteNearable<identifier: %s>" \
               % self.identifier class CJMonitorAdvertisement(object):
    """CJ Monitor advertisement."""

    def __init__(self, frame):

        for ltv in frame:
            if ltv['type'] == MANUFACTURER_SPECIFIC_DATA_TYPE:
                msd = ltv['value']
                self._company_id = msd['company_identifier']
                self._beacon_type = msd['data']['beacon_type']
                if self._beacon_type == CJ_TEMPHUM_TYPE:
                    data = msd['data']['data']
                    self._temperature = data['temperature'] / 100.0
                    self._humidity = data['humidity']
                    self._light = mulaw_to_value(data['light']) / 10.0
            elif ltv['type'] == COMPLETE_LOCALE_NAME_DATA_TYPE:
                self._name = data_to_binstring(ltv['value']).decode("ascii")

    @property
    def name(self):
        """device name"""
        return self._name

    @property
    def humidity(self):
        """humidity in %"""
        return self._humidity

    @property
    def company_id(self):
        """company ID"""
        return self._company_id

    @property
    def beacon_type(self):
        """type of this beacon"""
        return self._beacon_type

    @property
    def temperature(self):
        """temperature in C."""
        return self._temperature

    @property
    def light(self):
        """light level in lux"""
        return self._light

    @property
    def properties(self):
        """Get Monitor properties."""
        return {'name': self.name,
                'temperature': self.temperature,
                'humidity': self.humidity,
                'light': self.light,
                'company_id': self.company_id,
                'beacon_type': self.beacon_type}

    def __str__(self):
        return "CJMonitorAdvertisement<name: {name}, temp: {temperature:.1f}," \
               "humidity: {humidity:d}, light: {light:.0f}>".format(
                   name=self.name, temperature=self.temperature,
                   humidity=self.humidity, light=self.light) class ExposureNotificationFrame(object):
    """COVID-19 Exposure Notification frame."""

    def __init__(self, data):
        self._identifier = data_to_hexstring(data['identifier'])
        self._encrypted_metadata = data_to_binstring(data['encrypted_metadata'])

    @property
    def identifier(self):
        """16 byte Rolling Proximity Identifier"""
        return self._identifier

    @property
    def encrypted_metadata(self):
        """4 byte encrypted data containing version info and transmission power"""
        return self._encrypted_metadata

    @property
    def properties(self):
        """Get beacon properties."""
        return {'identifier': self.identifier, 'encrypted_metadata' : self.encrypted_metadata}

    def __str__(self):
        return "ExposureNotificationFrame<identifier: %s>" % (self.identifier) EDDYSTONE_TLM_UNENCRYPTED = 0x00 EDDYSTONE_TLM_ENCRYPTED = 0x01 SERVICE_DATA_TYPE = 0x16 EDDYSTONE_UID_FRAME = 0x00 EDDYSTONE_TLM_FRAME = 0x20 EDDYSTONE_URL_FRAME = 0x10 EDDYSTONE_EID_FRAME = 0x30 EDDYSTONE_UUID = b"\xaa\xfe" ESTIMOTE_UUID = b"\x9a\xfe" ESTIMOTE_TELEMETRY_FRAME = 0x2 ESTIMOTE_TELEMETRY_SUBFRAME_A = 0 ESTIMOTE_TELEMETRY_SUBFRAME_B = 1 MANUFACTURER_SPECIFIC_DATA_TYPE = 0xFF ESTIMOTE_MANUFACTURER_ID = b"\x5d\x01" CJ_MANUFACTURER_ID = b"\x72\x04" IBEACON_MANUFACTURER_ID = b"\x4c\x00" EXPOSURE_NOTIFICATION_UUID = b"\x6f\xfd"
from construct import ConstructError
from .structs import LTVFrame
from .packet_types import EddystoneUIDFrame, EddystoneURLFrame, EddystoneEncryptedTLMFrame, \
                          EddystoneTLMFrame, EddystoneEIDFrame, IBeaconAdvertisement, \
                          EstimoteTelemetryFrameA, EstimoteTelemetryFrameB, EstimoteNearable, \
                          CJMonitorAdvertisement, ExposureNotificationFrame
from .const import EDDYSTONE_TLM_UNENCRYPTED, EDDYSTONE_TLM_ENCRYPTED, SERVICE_DATA_TYPE, \
                   EDDYSTONE_UID_FRAME, EDDYSTONE_TLM_FRAME, EDDYSTONE_URL_FRAME, \
                   EDDYSTONE_EID_FRAME, EDDYSTONE_UUID, ESTIMOTE_UUID, ESTIMOTE_TELEMETRY_FRAME, \
                   ESTIMOTE_TELEMETRY_SUBFRAME_A, ESTIMOTE_TELEMETRY_SUBFRAME_B, \
                   MANUFACTURER_SPECIFIC_DATA_TYPE, ESTIMOTE_MANUFACTURER_ID, CJ_MANUFACTURER_ID, \
                   IBEACON_MANUFACTURER_ID, EXPOSURE_NOTIFICATION_UUID
"""Beacon advertisement parser."""


# pylint: disable=invalid-name,too-many-return-statements

def parse_packet(packet):
    """Parse a beacon advertisement packet."""
    return parse_ltv_packet(packet)

def parse_ltv_packet(packet):
    """Parse a tag-length-value style beacon packet."""
    try:
        frame = LTVFrame.parse(packet)

        for ltv in frame:
            if ltv['type'] == SERVICE_DATA_TYPE:
                data = ltv['value']

                if data["service_identifier"] == EDDYSTONE_UUID:
                    return parse_eddystone_service_data(data['service_data'])

                elif data["service_identifier"] == ESTIMOTE_UUID:
                    return parse_estimote_service_data(data['service_data'])

                elif data["service_identifier"] == EXPOSURE_NOTIFICATION_UUID:
                    return ExposureNotificationFrame(data["service_data"])


            elif ltv['type'] == MANUFACTURER_SPECIFIC_DATA_TYPE:
                data = ltv["value"]

                if data["company_identifier"] == ESTIMOTE_MANUFACTURER_ID:
                    return EstimoteNearable(data['data'])

                elif data["company_identifier"] == CJ_MANUFACTURER_ID:
                    return CJMonitorAdvertisement(frame)

                elif data["company_identifier"] == IBEACON_MANUFACTURER_ID:
                    return IBeaconAdvertisement(data['data'])

    except ConstructError:
        return None

    return None

def parse_eddystone_service_data(data):
    """Parse Eddystone service data."""
    if data['frame_type'] == EDDYSTONE_UID_FRAME:
        return EddystoneUIDFrame(data['frame'])


    elif data['frame_type'] == EDDYSTONE_TLM_FRAME: