import copy
import logging
import struct

from .protocol import (
    AcAddr,
    AcData,
    AcOnline,
    AcStatus,
    ChecksumError,
    CtlStatus,
    FuncCode,
    Header,
)

logger = logging.getLogger(__name__)


def validate(data_frame):
    """checksum of data frame."""
    header = Header.get_header_from_frame(data_frame)
    if not header.is_valid:
        return False

    data_checksum = (
        sum(
            struct.unpack(
                "B" * header.checksum_position, data_frame[: header.checksum_position]
            )
        )
        % 256
    )
    pos = header.checksum_position
    data_checksum = struct.unpack("B", data_frame[pos : pos + 1])[0]
    return data_checksum == data_checksum


def get_data_frame(data):
    """find frame in raw data.

    Arguments:
        data {bytes} -- raw bytes read from wire

    Yields:
        {bytes} -- a valid frame (checksum checked)

    Returns:
        None -- [description]
    """
    data = copy.copy(data)
    while data:
        try:
            if len(data) <= 5:
                return data
            header = Header.get_header_from_frame(data)
            header.check()

        except ValueError:
            logger.debug("header code unknown: %s", data[:4])
            data = data[1:]
            continue

        try:
            payload_length = header.payload_length
        except Exception:
            logger.error("unknown header, drop frame: %s", data[:4].hex())
            data = data[1:]
            continue
        total_length = header.length + payload_length + 1
        if len(data) < total_length:
            logger.error("date length not enough")
            return data

        date_frame = data[:total_length]
        if validate(date_frame):
            yield date_frame
        else:
            logger.error("checksum error and drop this frame: %s", date_frame)
        data = data[total_length:]


def parse_data(data_frame):
    if not validate(data_frame):
        raise ChecksumError("checksum error")

    ac_data = AcData(request=False)
    ac_data.header = header = Header.get_header_from_frame(data_frame)
    logger.debug(str(header))

    if header.func_code == FuncCode.STATUS:
        if header.ctl_code in (CtlStatus.ONE, CtlStatus.MULTI, CtlStatus.ALL):
            for idx in range(header.ac_num):
                start = 4 + idx * 10
                end = 4 + (idx + 1) * 10
                ac_status = AcStatus(*struct.unpack("B" * 10, data_frame[start:end]))
                ac_data.add(ac_status)
        elif header.ctl_code == CtlStatus.ONLINE:
            for idx in range(header.ac_num):
                start = 4 + idx * 3
                end = 4 + (idx + 1) * 3
                ac_address = AcOnline(*struct.unpack("BBB", data_frame[start:end]))
                ac_data.add(ac_address)
        else:
            raise TypeError("not support type: %s" % header)

    elif header.func_code in (
        FuncCode.CTL_POWER,
        FuncCode.CTL_OPERATION,
        FuncCode.CTL_FAN_MODE,
        FuncCode.CTL_TEMPERATURE,
    ):
        if header.ac_num != 1:
            raise TypeError("not support ac control more than one: %s", header.ac_num)

        start = 4
        end = start + 2
        ac_addr = AcAddr(*struct.unpack("BB", data_frame[start:end]))
        ac_data.add(ac_addr)

    else:
        raise TypeError("not support type: %s" % header)

    return ac_data


def get_ac_data(data: bytes) -> AcData:
    for data_frame in get_data_frame(data):
        try:
            yield parse_data(data_frame)
        except Exception as e:
            # One malformed frame must not kill the listener: log it and keep
            # parsing the remaining frames in the buffer.
            logger.error("Failed to parse frame %s: %s", data_frame.hex(), e)
            continue
