"""Common Lab033 link packet shared by commands, telemetry, and video.

The fixed 32-byte header uses network byte order and no implicit padding::

    !4sBBBBHIQHII

    Offset  Size  Field
       0      4   magic (b"SLP1")
       4      1   version
       5      1   traffic_class
       6      1   direction
       7      1   flags
       8      2   stream_id
      10      4   sequence_number
      14      8   generation_time_us
      22      2   deadline_ms
      24      4   payload_length
      28      4   packet_crc32

``packet_crc32`` is IEEE CRC32 over the complete header with that field set
to zero, followed by the complete payload.
"""

from __future__ import annotations

from dataclasses import dataclass, replace
from enum import IntEnum
import struct
import zlib


MAGIC = b"SLP1"
VERSION = 1
HEADER_FORMAT = "!4sBBBBHIQHII"
HEADER_SIZE = struct.calcsize(HEADER_FORMAT)
SUPPORTED_FLAGS_MASK = 0


class TrafficClass(IntEnum):
    """Traffic classes in descending scheduling priority."""

    EMERGENCY = 1
    CONTROL = 2
    TELEMETRY = 3
    VIDEO = 4


class Direction(IntEnum):
    """Logical direction of a packet on the shared modelled resource."""

    GROUND_TO_ROVER = 1
    ROVER_TO_GROUND = 2


class LinkPacketError(ValueError):
    """Base class for malformed Lab033 link packets."""


class LinkPacketCRCError(LinkPacketError):
    """The Lab033 packet header or payload failed CRC32 validation."""


@dataclass(frozen=True)
class LinkPacket:
    """Decoded common packet, including its CRC-validated payload."""

    traffic_class: TrafficClass
    direction: Direction
    stream_id: int
    sequence_number: int
    generation_time_us: int
    deadline_ms: int
    payload: bytes
    flags: int = 0
    version: int = VERSION
    packet_crc32: int = 0


def crc32(data: bytes) -> int:
    """Return unsigned IEEE CRC32."""

    return zlib.crc32(data) & 0xFFFFFFFF


def _validated(packet: LinkPacket) -> LinkPacket:
    if not isinstance(packet, LinkPacket):
        raise TypeError("packet must be LinkPacket")
    try:
        traffic_class = TrafficClass(packet.traffic_class)
        direction = Direction(packet.direction)
    except ValueError as error:
        raise LinkPacketError("unsupported enumeration value") from error
    if packet.version != VERSION:
        raise LinkPacketError("unsupported link packet version")
    if packet.flags & ~SUPPORTED_FLAGS_MASK:
        raise LinkPacketError("unsupported link packet flags")
    if not 0 <= packet.stream_id <= 0xFFFF:
        raise LinkPacketError("stream_id is outside uint16")
    if not 0 <= packet.sequence_number <= 0xFFFFFFFF:
        raise LinkPacketError("sequence_number is outside uint32")
    if not 0 <= packet.generation_time_us <= 0xFFFFFFFFFFFFFFFF:
        raise LinkPacketError("generation_time_us is outside uint64")
    if not 0 <= packet.deadline_ms <= 0xFFFF:
        raise LinkPacketError("deadline_ms is outside uint16")
    if not isinstance(packet.payload, (bytes, bytearray)):
        raise TypeError("payload must be bytes or bytearray")
    payload = bytes(packet.payload)
    if len(payload) > 0xFFFFFFFF:
        raise LinkPacketError("payload is outside uint32 length")
    return replace(
        packet,
        traffic_class=traffic_class,
        direction=direction,
        payload=payload,
    )


def _pack_header(packet: LinkPacket, crc_value: int) -> bytes:
    return struct.pack(
        HEADER_FORMAT,
        MAGIC,
        packet.version,
        int(packet.traffic_class),
        int(packet.direction),
        packet.flags,
        packet.stream_id,
        packet.sequence_number,
        packet.generation_time_us,
        packet.deadline_ms,
        len(packet.payload),
        crc_value,
    )


def encode_link_packet(packet: LinkPacket) -> bytes:
    """Serialize a common packet and calculate its CRC32."""

    packet = _validated(packet)
    header_without_crc = _pack_header(packet, 0)
    packet_crc32 = crc32(header_without_crc + packet.payload)
    return _pack_header(packet, packet_crc32) + packet.payload


def decode_link_packet(wire_packet: bytes) -> LinkPacket:
    """Deserialize a common packet and validate its format and CRC32."""

    if not isinstance(wire_packet, (bytes, bytearray)):
        raise TypeError("wire_packet must be bytes or bytearray")
    wire_packet = bytes(wire_packet)
    if len(wire_packet) < HEADER_SIZE:
        raise LinkPacketError("link packet is shorter than its header")
    (
        magic,
        version,
        traffic_class,
        direction,
        flags,
        stream_id,
        sequence_number,
        generation_time_us,
        deadline_ms,
        payload_length,
        received_crc32,
    ) = struct.unpack(HEADER_FORMAT, wire_packet[:HEADER_SIZE])
    if magic != MAGIC:
        raise LinkPacketError("invalid link packet magic")
    if len(wire_packet) != HEADER_SIZE + payload_length:
        raise LinkPacketError("payload_length does not match packet length")
    try:
        packet = LinkPacket(
            traffic_class=TrafficClass(traffic_class),
            direction=Direction(direction),
            stream_id=stream_id,
            sequence_number=sequence_number,
            generation_time_us=generation_time_us,
            deadline_ms=deadline_ms,
            payload=wire_packet[HEADER_SIZE:],
            flags=flags,
            version=version,
            packet_crc32=received_crc32,
        )
    except ValueError as error:
        raise LinkPacketError("unsupported enumeration value") from error
    packet = _validated(packet)
    calculated_crc32 = crc32(_pack_header(packet, 0) + packet.payload)
    if calculated_crc32 != received_crc32:
        raise LinkPacketCRCError(
            "link packet CRC mismatch: "
            f"received 0x{received_crc32:08X}, "
            f"calculated 0x{calculated_crc32:08X}"
        )
    return packet


assert HEADER_SIZE == 32
