#!/usr/bin/env python3
"""
run_ucsim.py — прогнать тестовый .ihx под ucsim_z80 и разобрать результат.

Как это устроено (см. crt0_ucsim.s и tcheck.h): тестовый бинарь кладёт итог
в структуру tc_result в ОЗУ и заканчивается инструкцией halt.  Мы находим
адрес _tc_result в карте линковки (.noi), гоняем ucsim, дампим структуру и
печатаем отчёт.

Через simif сознательно не идём: номера его команд меняются от версии к
версии, а halt + dump работают одинаково везде.

Код возврата: 0 — все проверки прошли, 1 — есть провалы или сбой прогона.
"""

import re
import subprocess
import sys
from pathlib import Path

TC_MAGIC = 0x5A5A
HDR = 8              # magic, total, failed, logn — по 2 байта
LOGSZ = 768          # TC_LOGSZ из tcheck.h
BYTES_PER_LINE = 16

# Строка дампа ucsim: "0x8000   5a 5a 07 00 ...  ZZ......"
# ASCII-хвост тоже может выглядеть как hex, поэтому берём РОВНО столько
# токенов, сколько байт положено в строке, а не «сколько нашлось».
DUMP_RE = re.compile(r"^0x([0-9A-Fa-f]+)\s+((?:[0-9A-Fa-f]{2}\s+)+)")


def find_symbol(noi_path: Path, name: str) -> int:
    m = re.search(rf"^DEF {re.escape(name)} (0x[0-9A-Fa-f]+)$",
                  noi_path.read_text(), re.M)
    if not m:
        raise SystemExit(f"{noi_path.name}: символ {name} не найден "
                         f"(тест не слинкован с tcheck.c?)")
    return int(m.group(1), 16)


def run(ucsim: Path, ihx: Path, start: int, length: int, timeout: int):
    stop = start + length - 1
    script = (f'file "{ihx}"\n'
              f'reset\n'
              f'run\n'
              f'dump 0x{start:04X} 0x{stop:04X} {BYTES_PER_LINE}\n'
              f'quit\n')
    try:
        p = subprocess.run([str(ucsim), "-t", "z80"], input=script,
                           capture_output=True, text=True, timeout=timeout)
    except subprocess.TimeoutExpired:
        raise SystemExit(f"ТАЙМАУТ {timeout} с — тест зациклился "
                         f"(нет halt / бесконечный цикл в коде)")
    return p.stdout


def parse_dump(out: str, start: int, length: int) -> bytes:
    mem = bytearray(length)
    seen = bytearray(length)
    for line in out.splitlines():
        m = DUMP_RE.match(line.strip())
        if not m:
            continue
        addr = int(m.group(1), 16)
        off = addr - start
        if off < 0 or off >= length:
            continue
        want = min(BYTES_PER_LINE, length - off)
        toks = m.group(2).split()[:want]
        for i, t in enumerate(toks):
            mem[off + i] = int(t, 16)
            seen[off + i] = 1
    if not any(seen):
        raise SystemExit("не удалось разобрать дамп ucsim:\n" + out)
    return bytes(mem)


def main() -> int:
    if len(sys.argv) < 4:
        print("usage: run_ucsim.py <ucsim> <test.ihx> <test.noi> [timeout]",
              file=sys.stderr)
        return 2
    ucsim, ihx, noi = (Path(a) for a in sys.argv[1:4])
    timeout = int(sys.argv[4]) if len(sys.argv) > 4 else 60
    name = ihx.stem

    addr = find_symbol(noi, "_tc_result")
    out = run(ucsim, ihx, addr, HDR + LOGSZ, timeout)

    if "Halted" not in out:
        print(f"[{name}] ПРОГОН НЕ ЗАВЕРШИЛСЯ штатным halt:\n{out}")
        return 1

    mem = parse_dump(out, addr, HDR + LOGSZ)
    u16 = lambda o: mem[o] | (mem[o + 1] << 8)
    magic, total, failed, logn = u16(0), u16(2), u16(4), u16(6)

    if magic != TC_MAGIC:
        print(f"[{name}] tc_result не заполнен (magic=0x{magic:04X}) — "
              f"main() не позвал ни одной проверки?")
        return 1

    if failed:
        log = mem[HDR:HDR + min(logn, LOGSZ)].decode("utf-8", "replace")
        print(f"[{name}] ПРОВАЛ: {failed} из {total}")
        for line in log.rstrip("\n").split("\n"):
            print(f"    {line}")
        return 1

    print(f"[{name}] ok: {total}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
