"""Common utility module.

common module provides utilities and auxiliary methods for
extraction of data from DPA bytes and Daemon API JSON messages.
"""

__all__ = ['Common']

import math
from typing import List, Union
from typeguard import typechecked
from iqrfpy.enums.commands import *
from iqrfpy.enums.message_types import *
from iqrfpy.enums.peripherals import *
from iqrfpy.exceptions import InvalidPeripheralValueError, InvalidPeripheralCommandValueError, \
    JsonMsgidMissingError, JsonMTypeMissingError, JsonNadrMissingError, JsonHwpidMissingError, JsonRCodeMissingError, \
    JsonDpaValueMissingError, JsonResultMissingError, JsonStatusMissingError, JsonGenericDataMissingError, \
    UnsupportedMessageTypeError, UnsupportedPeripheralError, UnsupportedPeripheralCommandError
import iqrfpy.utils.dpa as dpa_constants


@typechecked
class Common:
    """Common class provides auxiliary methods for handling DPA and Daemon API JSON messages."""

    # DPA

    @staticmethod
    def hwpid_from_dpa(high: int, low: int) -> int:
        """Convert DPA HWPID bytes to a single 16bit unsigned integer.

        Args:
            high (int): HWPID high byte
            low (int): HWPID low byte
        Returns:
            :obj:`int`: 16bit unsigned integer HWPID value
        Raises:
            ValueError: Raised if input values are not between 0 and 255
        """
        if high > dpa_constants.BYTE_MAX or low > dpa_constants.BYTE_MAX:
            raise ValueError('Argument value exceeds maximum allowed value of 255.')
        if high < dpa_constants.BYTE_MIN or low < dpa_constants.BYTE_MIN:
            raise ValueError('Negative argument values are not allowed.')
        return (high << 8) + low

    @staticmethod
    def pnum_from_dpa(pnum: int) -> Peripheral:
        """Return peripheral enum value based on DPA peripheral data byte.

        Args:
            pnum (int): Peripheral number data byte
        Returns:
            :obj:`Peripheral`: Peripheral enum member
        Raises:
            InvalidPeripheralValueError: Raised if pnum value is not between 0 and 255
            UnsupportedPeripheralError: Raised if pnum parameter value is not recognized as a member of any peripheral enum
        """
        if pnum < 0 or pnum > 255:
            raise InvalidPeripheralValueError('Peripheral value out of range 0-255.')
        if EmbedPeripherals.has_value(pnum):
            return EmbedPeripherals(pnum)
        if Standards.has_value(pnum):
            return Standards(pnum)
        raise UnsupportedPeripheralError('Unknown or unsupported peripheral.')

    @staticmethod
    def request_pcmd_from_dpa(pnum: Peripheral, pcmd: int) -> Command:
        """Return request command based on DPA peripheral and command data byte.

        Args:
            pnum (Peripheral): Peripheral enum member
            pcmd (int): Command data byte value
        Returns:
            :obj:`Command`: Request command enum value
        Raises:
            InvalidPeripheralCommandValueError: Raised if pcmd is a negative value, or if pcmd is not a value between 0 and 127
            UnsupportedPeripheralError: Raised if pnum parameter value is not recognized as a member of any peripheral enum
            UnsupportedPeripheralCommandError: Raised if pcmd parameter value is not recognized as a member of any peripheral command enum
        """
        if pcmd < dpa_constants.REQUEST_PCMD_MIN:
            raise InvalidPeripheralCommandValueError('Negative peripheral command values are not allowed.')
        if pcmd > dpa_constants.REQUEST_PCMD_MAX:
            raise InvalidPeripheralCommandValueError('Peripheral command value exceeds maximum allowed value of 127.')
        commands = None
        match pnum:
            case EmbedPeripherals.COORDINATOR:
                commands = CoordinatorRequestCommands
            case EmbedPeripherals.NODE:
                commands = NodeRequestCommands
            case EmbedPeripherals.OS:
                commands = OSRequestCommands
            case EmbedPeripherals.EEPROM:
                commands = EEPROMRequestCommands
            case EmbedPeripherals.EEEPROM:
                commands = EEEPROMRequestCommands
            case EmbedPeripherals.RAM:
                commands = RAMRequestCommands
            case EmbedPeripherals.LEDR | EmbedPeripherals.LEDG:
                commands = LEDRequestCommands
            case EmbedPeripherals.IO:
                commands = IORequestCommands
            case EmbedPeripherals.THERMOMETER:
                commands = ThermometerRequestCommands
            case EmbedPeripherals.UART:
                commands = UartRequestCommands
            case EmbedPeripherals.FRC:
                commands = FrcRequestCommands
            case EmbedPeripherals.EXPLORATION:
                commands = ExplorationRequestCommands
            case Standards.DALI:
                commands = DALIRequestCommands
            case Standards.BINARY_OUTPUT:
                commands = BinaryOutputRequestCommands
            case Standards.SENSOR:
                commands = SensorRequestCommands
            case Standards.LIGHT:
                commands = LightRequestCommands
            case _:
                raise UnsupportedPeripheralError('Unknown or unsupported peripheral.')

        if commands is not None and commands.has_value(pcmd):
            return commands(pcmd)
        raise UnsupportedPeripheralCommandError('Unknown or unsupported peripheral command.')

    @staticmethod
    def response_pcmd_from_dpa(pnum: Peripheral, pcmd: int) -> Command:
        """Return response command based on DPA peripheral and command data byte.

        Args:
            pnum (Peripheral): Peripheral enum member
            pcmd (int): Command data byte value
        Returns:
            :obj:`Command`: Response command enum member
        Raises:
            InvalidPeripheralCommandValueError: Raised if pcmd is a negative value, or if pcmd is not a value between 128 and 255
            UnsupportedPeripheralError: Raised if pnum parameter value is not recognized as a member of any peripheral enum
            UnsupportedPeripheralCommandError: Raised if pcmd parameter value is not recognized as a member of any peripheral command enum
        """
        if pcmd < dpa_constants.REQUEST_PCMD_MIN:
            raise InvalidPeripheralCommandValueError('Negative peripheral command values are not allowed.')
        if pcmd <= dpa_constants.REQUEST_PCMD_MAX or pcmd > dpa_constants.RESPONSE_PCMD_MAX:
            raise InvalidPeripheralCommandValueError('Response peripheral command should be value between 128 and 255.')
        commands = None
        match pnum:
            case EmbedPeripherals.COORDINATOR:
                commands = CoordinatorResponseCommands
            case EmbedPeripherals.NODE:
                commands = NodeResponseCommands
            case EmbedPeripherals.OS:
                commands = OSResponseCommands
            case EmbedPeripherals.EEPROM:
                commands = EEPROMResponseCommands
            case EmbedPeripherals.EEEPROM:
                commands = EEEPROMResponseCommands
            case EmbedPeripherals.RAM:
                commands = RAMResponseCommands
            case EmbedPeripherals.LEDR | EmbedPeripherals.LEDG:
                commands = LEDResponseCommands
            case EmbedPeripherals.IO:
                commands = IOResponseCommands
            case EmbedPeripherals.THERMOMETER:
                commands = ThermometerResponseCommands
            case EmbedPeripherals.UART:
                commands = UartResponseCommands
            case EmbedPeripherals.FRC:
                commands = FrcResponseCommands
            case EmbedPeripherals.EXPLORATION:
                commands = ExplorationResponseCommands
            case Standards.DALI:
                commands = DALIResponseCommands
            case Standards.BINARY_OUTPUT:
                commands = BinaryOutputResponseCommands
            case Standards.SENSOR:
                commands = SensorResponseCommands
            case Standards.LIGHT:
                commands = LightResponseCommands
            case _:
                raise UnsupportedPeripheralError('Unknown or unsupported peripheral.')

        if commands is not None and commands.has_value(pcmd):
            return commands(pcmd)
        raise UnsupportedPeripheralCommandError('Unknown or unsupported peripheral command.')

    @staticmethod
    def pdata_from_dpa(dpa: bytes) -> Union[List[int], None]:
        """Return PDATA from DPA response bytes.

        Args:
            dpa (bytes): DPA response message
        Returns:
            :obj:`list` of :obj:`int` or :obj:`None`: PDATA integer list or None of there are no PDATA
        """
        if len(dpa) > 8:
            return list(dpa[8:])
        return None

    # json

    @staticmethod
    def msgid_from_json(json: dict) -> str:
        """Return response msgid from Daemon API JSON response.

        Args:
            json (dict): JSON API response
        Returns:
            :obj:`str`: JSON API response message ID
        Raises:
            JsonMsgidMissingError: Raised if Daemon API response does not contain the msgId key
        """
        try:
            return json['data']['msgId']
        except KeyError as err:
            raise JsonMsgidMissingError(f'Object does not contain property {str(err)}') from err

    @staticmethod
    def mtype_str_from_json(json: dict) -> str:
        """Return message type from Daemon API JSON response.

        Args:
            json (dict): JSON API response
        Returns:
            :obj:`str`: JSON API response message type string
        Raises:
            JsonMTypeMissingError: Raised if Daemon API response does not contain the mType key
        """
        try:
            return json['mType']
        except KeyError as err:
            raise JsonMTypeMissingError(f'Object does not contain property {str(err)}') from err

    @staticmethod
    def nadr_from_json(json: dict) -> int:
        """Return response nadr from Daemon API JSON response.

        Args:
            json (dict): JSON API response
        Returns:
            :obj:`int`: JSON API response device address
        Raises:
            JsonNadrMissingError: Raised if Daemon API response does not contain the nAdr key
        """
        try:
            return json['data']['rsp']['nAdr']
        except KeyError as err:
            raise JsonNadrMissingError(f'Object does not contain property {str(err)}') from err

    @staticmethod
    def pnum_from_json(json: dict) -> int:
        """Return response pnum from Daemon API JSON response.

        Args:
            json (dict): JSON API response
        Returns:
            :obj:`int`: JSON API response peripheral number
        Raises:
            JsonNadrMissingError: Raised if Daemon API response does not contain the pnum key
        """
        try:
            return json['data']['rsp']['pnum']
        except KeyError as err:
            raise JsonNadrMissingError(f'Object does not contain property {str(err)}') from err

    @staticmethod
    def pcmd_from_json(json: dict) -> int:
        """Return response pcmd from Daemon API JSON response.

        Args:
            json (dict): JSON API response
        Returns:
            :obj:`int`: JSON API response peripheral command
        Raises:
            JsonNadrMissingError: Raised if Daemon API response does not contain the pcmd key
        """
        try:
            return json['data']['rsp']['pcmd']
        except KeyError as err:
            raise JsonNadrMissingError(f'Object does not contain property {str(err)}') from err

    @staticmethod
    def hwpid_from_json(json: dict) -> int:
        """Return response hwpid from Daemon API JSON response.

        Args:
            json (dict): JSON API response
        Returns:
            :obj:`int`: JSON API response hardware profile ID
        Raises:
            JsonHwpidMissingError: Raised if Daemon API response does not contain the hwpId key
        """
        try:
            return json['data']['rsp']['hwpId']
        except KeyError as err:
            raise JsonHwpidMissingError(f'Object does not contain property {str(err)}') from err

    @staticmethod
    def rcode_from_json(json: dict) -> int:
        """Return response rcode from Daemon API JSON response.

        Args:
            json (dict): JSON API response
        Returns:
            :obj:`int`: JSON API response DPA rcode
        Raises:
            JsonRCodeMissingError: Raised if Daemon API response does not contain the rcode key
        """
        try:
            return json['data']['rsp']['rCode']
        except KeyError as err:
            raise JsonRCodeMissingError(f'Object does not contain property {str(err)}') from err

    @staticmethod
    def dpa_value_from_json(json: dict) -> int:
        """Return response DPA value from Daemon API JSON response.

        Args:
            json (dict): JSON API response
        Returns:
            :obj:`int`: JSON API response dpa value
        Raises:
            JsonDpaValueMissingError: Raised if Daemon API response does not contain the dpaVal key
        """
        try:
            return json['data']['rsp']['dpaVal']
        except KeyError as err:
            raise JsonDpaValueMissingError(f'Object does not contain property {str(err)}') from err

    @staticmethod
    def pdata_from_json(json: dict) -> Union[List[int], None]:
        """Return pdata from Daemon API JSON response if available.

        Args:
            json (dict): JSON API response
        Returns:
            :obj:`Union[List[int], None]`: JSON API response pdata or None if there are no PDATA
        """
        pdata = None
        try:
            raw = json['data']['raw']
            response = None
            if isinstance(raw, list):
                response = raw[0]['response']
            elif isinstance(raw, dict):
                response = raw['response']
            response = response.split('.')
            if len(response) > 8:
                pdata = [int(x, 16) for x in response[8:]]
        except KeyError:
            pdata = None
        finally:
            return pdata

    @staticmethod
    def result_from_json(json: dict) -> dict:
        """Return response result from Daemon API JSON response.

        Args:
            json (dict): JSON API response
        Returns:
            :obj:`dict`: JSON API response result object
        Raises:
            JsonResultMissingError: Raised if JSON API response does not contain the result key
        """
        try:
            return json['data']['rsp']['result']
        except KeyError as err:
            raise JsonResultMissingError(f'Object does not contain property {str(err)}') from err

    @staticmethod
    def status_from_json(json: dict) -> int:
        """Return response status from Daemon API JSON response.

        Args:
            json (dict): JSON API response
        Returns:
            :obj:`int`: JSON API response status code
        Raises:
            JsonStatusMissingError: Raised if JSON API response does not contain the status key
        """
        try:
            return json['data']['status']
        except KeyError as err:
            raise JsonStatusMissingError(f'Object does not contain property {str(err)}') from err

    @staticmethod
    def generic_rdata_from_json(json: dict) -> str:
        """Return generic response data from Daemon API JSON response.

        Args:
            json (dict): JSON API response
        Returns:
            :obj:`int`: JSON API response status code
        Raises:
            JsonStatusMissingError: Raised if JSON API response does not contain path to rData.
        """
        try:
            return json['data']['rsp']['rData']
        except KeyError as err:
            raise JsonGenericDataMissingError(f'Object does not contain property {str(err)}') from err

    @staticmethod
    def string_to_mtype(string: str) -> MessageType:
        """Convert message type string to message type enum member value.

        Args:
            string (str): Message type string
        Returns:
            :obj:`MessageType`: Message type enum member
        Raises:
            UnsupportedMessageTypeError: Raised if message type is not recognized as a member of any message type enum
        """
        messages = [GenericMessages, ExplorationMessages, CoordinatorMessages, NodeMessages, OSMessages, EEPROMMessages,
                    EEEPROMMessages, RAMMessages, LEDRMessages, LEDGMessages, IOMessages, ThermometerMessages,
                    UartMessages, FrcMessages, DALIMessages, BinaryOutputMessages, SensorMessages, LightMessages]
        for item in messages:
            if item.has_value(string):
                return item(string)
        raise UnsupportedMessageTypeError(f'Unknown or unsupported message type: {string}.')

    # general

    @staticmethod
    def bitmap_to_nodes(bitmap: List[int], coordinator_shift: bool = False) -> List[int]:
        """Convert node bitmap to list of nodes.

        Args:
            bitmap (List[int]): Node bitmap represented by list of integers
            coordinator_shift (bool): Bitmap contains dummy coordinator value
        Returns:
            :obj:`list` of :obj:`int`: List of node addresses from bitmap
        """
        nodes = []
        start = 0 if not coordinator_shift else 1
        for i in range(start, len(bitmap * 8)):
            if bitmap[int(i / 8)] & (1 << (i % 8)):
                nodes.append(i)
        return nodes

    @staticmethod
    def nodes_to_bitmap(nodes: List[int]) -> List[int]:
        """Convert list of nodes to node bitmap.

        Args:
            nodes (List[int]): List of node addresses
        Returns:
            :obj:`list` of :obj:`int`: Nodes bitmap represented by list of 30 integers
        """
        bitmap = [0] * 30
        for node in nodes:
            bitmap[math.floor(node / 8)] |= (1 << (node % 8))
        return bitmap

    @staticmethod
    def bitmap_4byte_to_indexes(bitmap: List[int]) -> List[int]:
        """Convert 4 byte bitmap to list of indexes.

        Args:
            bitmap (List[int]): Index bitmap.
        Returns:
            :obj:`List` of :obj:`int`: List of indexes.
        """
        indexes = [0] * 32
        for i in range(0, len(bitmap * 8)):
            if bitmap[int(i / 8)] & (1 << (i % 8)):
                indexes[i] = 1
        return indexes

    @staticmethod
    def indexes_to_4byte_bitmap(values: List[int]) -> List[int]:
        """Convert list of indexes to a 4 byte bitmap.

        Args:
            values (List[int]): List of indexes
        Returns:
            :obj:`list` of :obj:`int`: Index bitmap represented by list of 4 integers
        """
        bitmap = [0] * 4
        for value in values:
            bitmap[math.floor(value / 8)] |= (1 << (value % 8))
        return bitmap

    @staticmethod
    def is_hex_string(string: str) -> bool:
        """Check if string contains only hexadecimal characters.

        Args:
            string (str): Input string
        Returns:
            :obj:`bool`: True if string contains only hexadecimal characters, False otherwise
        """
        if len(string) == 0:
            return False
        return not set(string) - set('0123456789abcdefABCDEF')

    @staticmethod
    def hex_string_to_list(string: str) -> List[int]:
        """Convert hexadecimal string to list of unsigned integers.

        Args:
            string (str): Hexadecimal string
        Returns:
            :obj:`list` of :obj:`int`: List of integers from hexadecimal string
        Raises:
            ValueError: Raised if string is of uneven length or contains non-hexadecimal characters
        """
        if not len(string) % 2 == 0:
            raise ValueError('Argument should be even length.')
        if not Common.is_hex_string(string):
            raise ValueError('Argument is not a hexadecimal string.')
        return [int(string[i:i + 2], base=16) for i in range(0, len(string), 2)]

    @staticmethod
    def list_to_hex_string(values: List[int]) -> str:
        """Convert list of unsigned integers to hexadecimal string.

        Args:
            values (List[int]): List of unsigned integers
        Returns:
            :obj:`string`: Hexadecimal string representation of the list
        """
        return ''.join(f'{value:02x}' for value in values)

    @staticmethod
    def peripheral_list_to_bitmap(values: List[int]) -> List[int]:
        """Convert list of peripheral numbers into a bitmap of peripherals.

        For example, the coordinator peripheral would be represented by the 1st bit in bitmap.

        Args:
            values (List[int]): List of peripheral numbers
        Returns:
            :obj:`list` of :obj:`int`: Peripheral bitmap represented by list of 4 integers
        """
        bitmap = [0 for _ in range(32)]
        for value in values:
            bitmap[value] = 1
        byte_list = []
        for bits in [bitmap[i:i + 8] for i in range(0, len(bitmap), 8)]:
            bits.reverse()
            byte = 0
            for bit in bits:
                byte = (byte << 1) | bit
            byte_list.append(byte)
        return byte_list

    @staticmethod
    def values_in_byte_range(values: List[int]) -> bool:
        """Check if list elements are within unsigned integer byte range.

        Args:
            values (List[int]): Input data
        Returns:
            :obj:`bool`: True if values are in range, False otherwise
        """
        return len([value for value in values if value < 0 or value > 255]) == 0

    @staticmethod
    def byte_complement(value: int):
        """Convert unsigned 1B value into a signed 1B value.

        Args:
            value (int): Input unsigned 1B value
        Returns:
            :obj:`int`: Signed 1B value
        """
        if not 0 <= value <= 0xFF:
            raise ValueError('Not an unsigned 1B value.')
        if value < 0x80:
            return value
        return value - 0x100

    @staticmethod
    def word_complement(value: int) -> int:
        """Convert unsigned 2B value into a signed 2B value.

        Args:
            value (int): Input unsigned 2B value
        Returns:
            :obj:`int`: Signed 2B value
        """
        if not 0 <= value <= 0xFFFF:
            raise ValueError('Not an unsigned 2B value.')
        if value < 0x8000:
            return value
        return value - 0x10000
