#!/usr/bin/env python3
"""
Отчёт по раскладке памяти образа + проверка лимитов, по .map-файлу
sdldz80.  Печатается для ЛЮБОЙ модели памяти (не только при --bank):

  - _CODE       — основной код (W1 0x4100 или W2 0x8100), размер, конец;
  - данные      — _DATA/_BSS/_INITIALIZED/… цепочкой за кодом, окно, конец;
  - статика/куча/стек — где кончается статика и сколько свободно под кучу
    (до ___sdcc_heap_end) и стек;
  - _W3CODE     — резидентный код окна W3 (--w3), если есть;
  - _BANKn      — банки (big/huge), каждый ≤ 16 КБ (размер окна W3/W1).

Раскладка режимов — docs/memory-management.md §4.  Линкер сам лимиты не
проверяет — ловим здесь пост-линк.

Usage: check_banks.py <path.map> [--mode MODE] [--heap-top 0xNNNN]
                                  [--stack 0xNNNN]

Выходит с ненулевым кодом, если банк не влез в окно или статика заехала
за потолок кучи/окна.
"""

import argparse
import re
import sys

BANK_LIMIT = 16 * 1024          # окно W3/W1 под банк
W_CEILING  = 0xC000             # верх W2 (дальше — только W3/банки)

# Области .map, составляющие HOME (код + данные), в порядке цепочки линкера.
# Всё, что не банк и не резидент W3 и лежит < 0xC000.
_BANK_RE = re.compile(r"_BANK\d+$")


def win_label(addr):
    """Окно по адресу (0-based физического образа игнорируем — берём Z80-view)."""
    a = addr & 0xFFFF
    if 0x4000 <= a < 0x8000:
        return "W1"
    if 0x8000 <= a < 0xC000:
        return "W2"
    if 0xC000 <= a <= 0xFFFF:
        return "W3"
    return "??"


def parse_map(path):
    """{area_name: (addr, size)} для строк вида `_CODE  00004100  00000313 = ...`."""
    line_re = re.compile(r"^\s*(_\w+)\s+([0-9A-Fa-f]{8})\s+([0-9A-Fa-f]{8})\s*=")
    areas = {}
    with open(path) as f:
        for ln in f:
            m = line_re.match(ln)
            if m:
                areas[m.group(1)] = (int(m.group(2), 16), int(m.group(3), 16))
    return areas


MODE_DESC = {
    "tiny":  "CODE+DATA в W2 (0x8100)",
    "small": "CODE в W1 (0x4100), DATA→W2",
    "big":   "CODE+DATA в W2 (0x8100), банки в W1",
    "huge":  "CODE в W1 (0x4100), DATA→W2, банки в W3",
    "manual": "ручная раскладка",
}


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("mapfile")
    ap.add_argument("--mode", default="")
    ap.add_argument("--heap-top", default="0xBB00")
    ap.add_argument("--stack", default="0xBFFE")
    args = ap.parse_args()

    areas = parse_map(args.mapfile)
    heap_top = int(args.heap_top, 0)
    stack_top = int(args.stack, 0)

    fails = []

    desc = MODE_DESC.get(args.mode, "")
    hdr = f"memory: {args.mode}" if args.mode else "memory:"
    if desc:
        hdr += f" — {desc}"
    print(hdr)

    # --- HOME: код + данные (всё < 0xC000, кроме банков) --------------
    home = {n: v for n, v in areas.items()
            if not _BANK_RE.match(n) and n != "_W3CODE" and (v[0] & 0xFFFF) < W_CEILING}

    if "_CODE" in areas:
        caddr, csize = areas["_CODE"]
        cend = caddr + csize
        print(f"  _CODE         @ 0x{caddr & 0xFFFF:04X}  size {csize:>6}  "
              f"({win_label(caddr)})  → 0x{cend & 0xFFFF:04X}")

        # данные = все HOME-области, кроме _CODE (цепляются за кодом)
        data = {n: v for n, v in home.items() if n != "_CODE"}
        if data:
            dstart = min(v[0] for v in data.values())
            dend = max(v[0] + v[1] for v in data.values())
            print(f"  данные        @ 0x{dstart & 0xFFFF:04X}  size {dend - dstart:>6}  "
                  f"({win_label(dstart)})  → 0x{dend & 0xFFFF:04X}"
                  f"   [_DATA/_BSS/_INITIALIZED/…]")
        else:
            dend = cend

        # статика/куча/стек
        static_end = max(v[0] + v[1] for v in home.values())
        heap_free = heap_top - static_end
        stack_room = stack_top + 1 - heap_top
        print(f"  статика до 0x{static_end & 0xFFFF:04X}  —  куча "
              f"0x{static_end & 0xFFFF:04X}..0x{heap_top & 0xFFFF:04X} ({heap_free} Б), "
              f"стек 0x{heap_top & 0xFFFF:04X}..0x{stack_top & 0xFFFF:04X} ({stack_room} Б)")
        if static_end > heap_top:
            print(f"    ОШИБКА: статика заехала за heap_top 0x{heap_top & 0xFFFF:04X} "
                  f"(нет места под кучу/стек)")
            fails.append(("_HOME", static_end))
        elif static_end > W_CEILING:
            print(f"    ОШИБКА: статика вышла за 0x{W_CEILING:04X}")
            fails.append(("_HOME", static_end))

    # --- Резидентный код окна W3 (--w3) -------------------------------
    if "_W3CODE" in areas:
        waddr, wsize = areas["_W3CODE"]
        wend = waddr + wsize
        wfree = 0x10000 - wend
        marker = "OK"
        if wend > 0x10000:
            marker = "OVERFLOW"
            fails.append(("_W3CODE", wsize))
        print(f"  _W3CODE       @ 0x{waddr & 0xFFFF:04X}  size {wsize:>6}  "
              f"(резидент W3)  → 0x{wend & 0xFFFF:04X}, {wfree} Б свободно до 0x10000  {marker}")

    # --- Банки (big/huge) ---------------------------------------------
    bank_names = sorted(n for n in areas if _BANK_RE.match(n))
    for name in bank_names:
        addr, size = areas[name]
        pct = size * 100.0 / BANK_LIMIT
        marker = "OK"
        if size > BANK_LIMIT:
            marker = "OVERFLOW"
            fails.append((name, size))
        # виртуальный адрес: старший байт = номер банка, младшие 16 — окно
        free = BANK_LIMIT - size
        print(f"  {name:<12} @ 0x{addr:08X}  size {size:>6} / {BANK_LIMIT} "
              f"({pct:4.1f}%)  → {free} Б свободно  {marker}")

    if fails:
        print()
        for name, size in fails:
            if name in ("_HOME", "_W3CODE"):
                print(f"  {name}: раскладка не влезла (конец 0x{size & 0xFFFF:04X})")
            else:
                print(f"  {name} слишком большой: {size} Б (лимит {BANK_LIMIT})")
        sys.exit(1)


if __name__ == "__main__":
    main()
