from __future__ import annotations

from dataclasses import dataclass
from enum import IntEnum
from typing import Iterable


ATT = 0x1F
HEADER_SIZE = 4


class PDUType(IntEnum):
    ping = 0x10
    request_info = 0x11
    read_voltage = 0x12

    open_channel = 0x30
    close_channel = 0x31
    open_fd_channel = 0x32
    send = 0x33
    set_arbitration = 0x34
    start_periodic_message = 0x35
    end_periodic_message = 0x36
    send_compressed = 0x37

    prepare_for_update = 0x40
    send_update_data = 0x41
    commit_update = 0x42
    reset = 0x43

    rpc_call = 0x50
    rpc_send_binary = 0x51

    ok = 0x80
    pong = 0x90
    info = 0x91
    voltage = 0x92

    channel_opened = 0xB0
    channel_closed = 0xB1
    received = 0xB2
    received_compressed = 0xB3

    periodic_message_started = 0xB5
    periodic_message_ended = 0xB6

    update_started_send_data = 0xC0
    update_data_received = 0xC1
    update_completed = 0xC2

    rpc_response = 0xD0
    rpc_binary_response = 0xD1

    error_unspecified = 0xE0
    error_hardware = 0xE1
    error_invalid_channel = 0xE2
    error_invalid_periodic = 0xE3
    error_no_response = 0xE4
    error_invalid_rpc = 0xE5
    error_invalid_command = 0xEF


class ChannelProtocol(IntEnum):
    raw = 0x00
    isotp = 0x01
    kline = 0x02
    raw_fd = 0x03
    isotp_fd = 0x04
    raw_with_fc = 0x05
    enet = 0x06


def _uint16_from_bytes(data: Iterable[int]) -> int:
    value = 0
    for byte in data:
        value = (value << 8) | (byte & 0xFF)
    return value


def _uint32_from_bytes(data: Iterable[int]) -> int:
    value = 0
    for byte in data:
        value = (value << 8) | (byte & 0xFF)
    return value


def _uint16_to_bytes(value: int) -> list[int]:
    return [(value >> 8) & 0xFF, value & 0xFF]


def _uint32_to_bytes(value: int) -> list[int]:
    return [
        (value >> 24) & 0xFF,
        (value >> 16) & 0xFF,
        (value >> 8) & 0xFF,
        value & 0xFF,
    ]


@dataclass(frozen=True)
class Arbitration:
    request: int
    reply_pattern: int
    reply_mask: int
    request_extension: int
    reply_extension: int

    @staticmethod
    def from_payload(payload: list[int], offset: int) -> tuple["Arbitration", int]:
        request = _uint32_from_bytes(payload[offset:offset + 4])
        offset += 4
        request_extension = payload[offset]
        offset += 1
        reply_pattern = _uint32_from_bytes(payload[offset:offset + 4])
        offset += 4
        reply_mask = _uint32_from_bytes(payload[offset:offset + 4])
        offset += 4
        reply_extension = payload[offset]
        offset += 1
        return (
            Arbitration(
                request=request,
                reply_pattern=reply_pattern,
                reply_mask=reply_mask,
                request_extension=request_extension,
                reply_extension=reply_extension,
            ),
            offset,
        )

    def to_payload(self) -> list[int]:
        payload: list[int] = []
        payload.extend(_uint32_to_bytes(self.request))
        payload.append(self.request_extension & 0xFF)
        payload.extend(_uint32_to_bytes(self.reply_pattern))
        payload.extend(_uint32_to_bytes(self.reply_mask))
        payload.append(self.reply_extension & 0xFF)
        return payload


@dataclass
class PDU:
    pdu_type: PDUType
    payload: list[int]

    @property
    def frame(self) -> bytes:
        length = len(self.payload)
        header = bytes([ATT, int(self.pdu_type)]) + bytes(_uint16_to_bytes(length))
        return header + bytes(self.payload)

    @staticmethod
    def contains_pdu(buffer: bytearray) -> int:
        try:
            start = buffer.index(ATT)
        except ValueError:
            return -len(buffer)
        if start != 0:
            return -start
        if len(buffer) < HEADER_SIZE:
            return 0
        payload_length = _uint16_from_bytes(buffer[2:4])
        total_length = HEADER_SIZE + payload_length
        if len(buffer) < total_length:
            return 0
        return total_length

    @staticmethod
    def from_bytes(data: bytes | bytearray) -> "PDU":
        if len(data) < HEADER_SIZE:
            raise ValueError("PDU header incomplete")
        if data[0] != ATT:
            raise ValueError("Invalid ATT header")
        payload_length = _uint16_from_bytes(data[2:4])
        if len(data) != HEADER_SIZE + payload_length:
            raise ValueError("PDU length mismatch")
        pdu_type = PDUType(data[1])
        payload = list(data[HEADER_SIZE:])
        return PDU(pdu_type=pdu_type, payload=payload)

    @staticmethod
    def ok() -> "PDU":
        return PDU(PDUType.ok, [])

    @staticmethod
    def error(pdu_type: PDUType) -> "PDU":
        return PDU(pdu_type, [])

    @staticmethod
    def pong(payload: list[int]) -> "PDU":
        return PDU(PDUType.pong, payload)

    @staticmethod
    def info(vendor: str, model: str, hardware: str, serial: str, firmware: str) -> "PDU":
        string = "\n".join([vendor, model, hardware, serial, firmware])
        return PDU(PDUType.info, list(string.encode("utf-8")))

    @staticmethod
    def voltage(millivolts: int) -> "PDU":
        return PDU(PDUType.voltage, _uint16_to_bytes(millivolts))

    @staticmethod
    def channel_opened(channel: int) -> "PDU":
        return PDU(PDUType.channel_opened, [channel & 0xFF])

    @staticmethod
    def channel_closed(channel: int) -> "PDU":
        return PDU(PDUType.channel_closed, [channel & 0xFF])

    @staticmethod
    def received(channel: int, can_id: int, extension: int, data: list[int]) -> "PDU":
        payload = [channel & 0xFF]
        payload.extend(_uint32_to_bytes(can_id))
        payload.append(extension & 0xFF)
        payload.extend(data)
        return PDU(PDUType.received, payload)

    @staticmethod
    def update_started_send_data() -> "PDU":
        return PDU(PDUType.update_started_send_data, [])

    @staticmethod
    def update_data_received() -> "PDU":
        return PDU(PDUType.update_data_received, [])

    @staticmethod
    def update_completed() -> "PDU":
        return PDU(PDUType.update_completed, [])
