import collections
import enum
import logging
import struct
from functools import reduce
from typing import Iterator

import attr

logger = logging.getLogger(__name__)


def bytes_debug_str(data: bytes):
    return "[%s]" % " ".join([hex(x) for x in bytearray(data)])


class ChecksumError(Exception):
    pass


class FuncCode(enum.Enum):
    STATUS = 0x50
    CTL_POWER = 0x31
    CTL_TEMPERATURE = 0x32
    CTL_OPERATION = 0x33
    CTL_FAN_MODE = 0x34


class CtlStatus(enum.Enum):
    ONE = 0x01
    MULTI = 0x0F
    ONLINE = 0x02
    ALL = 0xFF


class StatusSwitch(enum.Enum):
    ON = 0x01
    OFF = 0x00

    @classmethod
    def new_status_switch(cls, value):
        return cls(value % 2)


class StatusOperation(enum.Enum):
    COOL = 0x01
    DRY = 0x02
    FAN_ONLY = 0x04
    HEAT = 0x08


class StatusFanMode(enum.Enum):
    HIGH = 0x01
    MID = 0x02
    LOW = 0x04
    MIDHIGH = 0x03
    MIDLOW = 0x05


def _enum_or_none(enum_cls, value):
    """Convert a raw byte to an enum member, or None if not defined.

    Gateways occasionally report values that are not covered by the protocol
    specification (for example fan speed 0x00 right after an indoor unit is
    switched off).  A strict converter would raise and kill the whole listener
    thread, so unknown values are tolerated and surfaced as None instead.
    """
    try:
        return enum_cls(value)
    except ValueError:
        return None


STATUS_PAYLOAD_LEN = 10
STATUS_ONLINE_PAYLOAD_LEN = 3
AC_ADDR_LEN = 2


@attr.s
class ZhongHongDataStruct:
    @staticmethod
    def _to_value(element):
        if isinstance(element, enum.Enum):
            return int(element.value)
        return int(element)

    def export(self):
        return list(map(self._to_value, attr.astuple(self)))

    @staticmethod
    def _sum(init, element):
        return init + ZhongHongDataStruct._to_value(element)

    @property
    def checksum(self):
        return reduce(self._sum, self.export(), 0) % 256

    def encode(self):
        length = len(self.export())
        return struct.pack("B" * length, *self.export())


@attr.s(slots=True, hash=True)
class Header(ZhongHongDataStruct):
    gw_addr = attr.ib()
    _func_code = attr.ib(converter=ZhongHongDataStruct._to_value)
    _ctl_code = attr.ib(converter=ZhongHongDataStruct._to_value)
    ac_num = attr.ib(cmp=False)

    @property
    def is_valid(self):
        try:
            self.check()
        except ValueError:
            logger.debug("header not valid: %s", self.export())
            return False
        return True

    def check(self):
        self.func_code
        self.ctl_code

    @classmethod
    def get_header_from_frame(cls, data_frame):
        if len(data_frame) < 4:
            return None
        return cls(*struct.unpack("BBBB", data_frame[:4]))

    @property
    def func_code(self):
        return FuncCode(self._func_code)

    @property
    def ctl_code(self):
        if self.func_code == FuncCode.STATUS:
            return CtlStatus(self._ctl_code)
        elif self.func_code == FuncCode.CTL_POWER:
            return StatusSwitch(self._ctl_code)
        elif self.func_code == FuncCode.CTL_TEMPERATURE:
            return self._ctl_code
        elif self.func_code == FuncCode.CTL_FAN_MODE:
            return StatusFanMode(self._ctl_code)
        elif self.func_code == FuncCode.CTL_OPERATION:
            return StatusOperation(self._ctl_code)
        return None

    def __str__(self):
        return "Header: gw_addr %s, func: %s, ctl: %s, ac_num: %s" % (
            self.gw_addr,
            self.func_code,
            self.ctl_code,
            self.ac_num,
        )

    def is_status_update(self):
        if self.func_code != FuncCode.STATUS:
            return False
        if self.ctl_code not in (CtlStatus.ALL, CtlStatus.ONE, CtlStatus.MULTI):
            return False
        return True

    @property
    def length(self):
        return 4

    @property
    def payload_length(self):
        if self.func_code == FuncCode.STATUS:
            if self.ctl_code in (CtlStatus.ONE, CtlStatus.MULTI, CtlStatus.ALL):
                payload_length = STATUS_PAYLOAD_LEN * self.ac_num
            elif self.ctl_code == CtlStatus.ONLINE:
                payload_length = STATUS_ONLINE_PAYLOAD_LEN * self.ac_num
            else:
                raise Exception("unknown ctrl code: %s", self.header.export())
        elif self.func_code in (
            FuncCode.CTL_POWER,
            FuncCode.CTL_TEMPERATURE,
            FuncCode.CTL_OPERATION,
            FuncCode.CTL_FAN_MODE,
        ):
            payload_length = AC_ADDR_LEN * self.ac_num
        else:
            raise Exception("unknown func code: %s", self.header.export())

        return payload_length

    @property
    def checksum_position(self):
        return self.length + self.payload_length


@attr.s(slots=True, hash=True)
class AcAddr(ZhongHongDataStruct):
    addr_out = attr.ib()
    addr_in = attr.ib()

    def __str__(self):
        return "AC %s-%s" % (self.addr_out, self.addr_in)


@attr.s(slots=True, hash=True)
class AcOnline(ZhongHongDataStruct):
    addr_out = attr.ib()
    addr_in = attr.ib()
    online_status = attr.ib(cmp=False)

    @property
    def ac_addr(self):
        return AcAddr(self.addr_out, self.addr_in)

    def __str__(self):
        return "%s online_status: %s" % (self.ac_addr, self.online_status)


@attr.s(slots=True)
class AcStatus(ZhongHongDataStruct):
    addr_out = attr.ib()
    addr_in = attr.ib()
    switch_status = attr.ib(converter=StatusSwitch.new_status_switch)
    target_temperature = attr.ib()
    current_operation = attr.ib(converter=lambda v: _enum_or_none(StatusOperation, v))
    current_fan_mode = attr.ib(converter=lambda v: _enum_or_none(StatusFanMode, v))
    current_temperature = attr.ib()
    error_code = attr.ib()
    padding1 = attr.ib()
    padding2 = attr.ib()

    @property
    def ac_addr(self):
        return AcAddr(self.addr_out, self.addr_in)

    def __str__(self):
        return (
            "AC %s-%s power %s, current_operation %s, speed %s, target_temp %s, room_temp %s"
            % (
                self.addr_out,
                self.addr_in,
                self.switch_status,
                self.current_operation,
                self.current_fan_mode,
                self.target_temperature,
                self.current_temperature,
            )
        )


@attr.s(slots=True)
class AcData(collections.abc.Iterable):
    header = attr.ib(init=False)  # type: Header
    payload = attr.ib(
        attr.Factory(collections.deque), init=False
    )  # type: List[ZhongHongDataStruct]
    request = attr.ib(True)

    def add(self, data):
        self.payload.append(data)

    def __str__(self):
        return "\n".join([str(self.header)] + [str(x) for x in self.payload])

    def __iter__(self) -> Iterator[ZhongHongDataStruct]:
        yield from iter(self.payload)

    @property
    def length(self):
        header_length = self.header.length
        checksum_length = 1

        if self.func_code == FuncCode.STATUS:
            if self.ctl_code in (CtlStatus.ONE, CtlStatus.MULTI, CtlStatus.ALL):
                payload_length = STATUS_PAYLOAD_LEN * self.ac_num
            elif self.ctl_code in (CtlStatus.ONLINE):
                payload_length = STATUS_ONLINE_PAYLOAD_LEN * self.ac_num
            else:
                raise Exception("unknown ctrl code: %s", self.header.export())
        elif self.func_code in (
            FuncCode.CTL_POWER,
            FuncCode.CTL_TEMPERATURE,
            FuncCode.CTL_OPERATION,
            FuncCode.CTL_FAN_MODE,
        ):
            payload_length = AcAddr * self.ac_num
        else:
            raise Exception("unknown func code: %s", self.header.export())

        return header_length + checksum_length + payload_length

    @property
    def ac_num(self):
        return self.header.ac_num

    @property
    def func_code(self):
        return self.header.func_code

    @property
    def ctl_code(self):
        return self.header.ctl_code

    @property
    def is_request(self):
        """Is this data a Request or Response."""
        return self.request

    @property
    def checksum(self):
        return (
            self.header.checksum + sum([item.checksum for item in self.payload])
        ) % 256

    @property
    def bin_checksum(self):
        return struct.pack("B", self.checksum)

    def encode(self):
        return b"".join(
            [self.header.encode()]
            + [x.encode() for x in self.payload]
            + [self.bin_checksum]
        )

    def hex(self):
        return bytes_debug_str(self.encode())
