"""Intel HEX encode/decode.

Intel HEX itself is a public, well-documented format (unlike the Roboteq
bytecode it will eventually carry -- see docs/reverse_engineering.md), so
this module is implemented for real rather than stubbed. It's also useful
right now for reverse-engineering work: `read_intel_hex` can load .hex
files exported from RoborunPlus so they can be diffed byte-for-byte.

Only record types 00 (Data) and 01 (End Of File) are handled -- Roboteq
scripts are small enough that extended segment/linear address records
(02/03/04/05) are not expected to be needed, but that assumption should be
verified against real exported files during reverse engineering.
"""

from __future__ import annotations

from dataclasses import dataclass


class HexFormatError(Exception):
    pass


@dataclass
class HexRecord:
    byte_count: int
    address: int
    record_type: int
    data: bytes

    def checksum(self) -> int:
        total = (
            self.byte_count
            + (self.address >> 8)
            + (self.address & 0xFF)
            + self.record_type
            + sum(self.data)
        )
        return (-total) & 0xFF

    def to_line(self) -> str:
        body = bytes([self.byte_count, self.address >> 8, self.address & 0xFF, self.record_type]) + self.data
        return ":" + body.hex().upper() + f"{self.checksum():02X}"


def write_intel_hex(data: bytes, start_address: int = 0, bytes_per_line: int = 16) -> str:
    """Encode `data` as Intel HEX text (data records + trailing EOF record).

    CONFIRMED (gosub_distance corpus, 2026-07-28): real RoborunPlus-exported
    .hex always pads the final line out to a full `bytes_per_line` (16)
    bytes with 0xFF, rather than emitting a short last record for whatever
    remainder doesn't divide evenly. Every one of the 6 distance test cases
    (12/212/262/267/512/962 raw bytes) came back from RoborunPlus padded up
    to the next 16-byte boundary (16/224/272/272/... bytes) with the
    difference being solely trailing 0xFF -- 0 differing bytes in the shared
    range in all 6 cases, confirming GoSub codegen itself is correct
    (including across the 256-byte 1-vs-2-byte-address boundary). Padding
    here so our .hex output matches real compiler output byte-for-byte.
    """
    if bytes_per_line < 1 or bytes_per_line > 255:
        raise ValueError("bytes_per_line must be between 1 and 255")

    remainder = len(data) % bytes_per_line
    if remainder:
        data = data + b"\xff" * (bytes_per_line - remainder)

    lines: list[str] = []
    addr = start_address
    for offset in range(0, len(data), bytes_per_line):
        chunk = data[offset : offset + bytes_per_line]
        record = HexRecord(byte_count=len(chunk), address=addr & 0xFFFF, record_type=0x00, data=chunk)
        lines.append(record.to_line())
        addr += len(chunk)

    eof = HexRecord(byte_count=0, address=0, record_type=0x01, data=b"")
    lines.append(eof.to_line())
    return "\n".join(lines) + "\n"


def read_intel_hex(text: str) -> bytes:
    """Decode Intel HEX text back into a flat byte string (data records only,
    concatenated in file order -- does not attempt to place bytes at their
    absolute addresses since that's not needed for diffing bytecode dumps).
    """
    out = bytearray()
    for lineno, raw_line in enumerate(text.splitlines(), start=1):
        line = raw_line.strip()
        if not line:
            continue
        if not line.startswith(":"):
            raise HexFormatError(f"line {lineno}: missing ':' start code")
        try:
            body = bytes.fromhex(line[1:])
        except ValueError as exc:
            raise HexFormatError(f"line {lineno}: invalid hex data") from exc
        if len(body) < 5:
            raise HexFormatError(f"line {lineno}: record too short")

        byte_count, addr_hi, addr_lo, record_type = body[0], body[1], body[2], body[3]
        payload = body[4 : 4 + byte_count]
        checksum = body[4 + byte_count]

        record = HexRecord(byte_count=byte_count, address=(addr_hi << 8) | addr_lo, record_type=record_type, data=payload)
        if record.checksum() != checksum:
            raise HexFormatError(f"line {lineno}: checksum mismatch")

        if record_type == 0x00:
            out.extend(payload)
        elif record_type == 0x01:
            break
        else:
            raise HexFormatError(f"line {lineno}: unsupported record type 0x{record_type:02X}")

    return bytes(out)
