#!/usr/bin/env python3
"""
Parse an SDCC .map file produced by sdldz80 and verify that every named
bank fits inside its 16 KB window.

The Sprinter toolchain expects each `_BANKn` area (with n >= 1) to occupy
at most 16384 bytes — that is the size of CPU window 3 where banked code
runs at execution time.  The SDCC linker itself does not enforce this
limit, so we catch it post-link.

We also surface the HOME budget: anything left between the end of _CODE
and the start of window 2 (0x8000) is leftover space for adding code/data
without banking.

Usage:  check_banks.py <path-to.map>

Exits non-zero with a clear message if any bank is over its limit.
"""

import re
import sys

BANK_LIMIT = 16 * 1024
# HOME upper bound depends on layout:
#   HUGE: CODE at 0x4100 (W1), HOME may spill into W2 → ceiling 0xC000
#   BIG:  CODE at 0x8100 (W2), HOME stays in W2       → ceiling 0xC000
#   tiny/small: same ceiling — anything in W3 is bank territory.
# We pick the ceiling per-image based on where _CODE lives, so we don't
# falsely flag W2-resident code as "spilled into stack/heap".
HOME_CEILING = 0xC000


def parse_map(path):
    """
    Returns a dict {area_name: (addr, size)} for area lines like:
        _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 not m:
                continue
            name = m.group(1)
            addr = int(m.group(2), 16)
            size = int(m.group(3), 16)
            areas[name] = (addr, size)
    return areas


def main():
    if len(sys.argv) != 2:
        sys.exit("usage: check_banks.py <path-to.map>")

    areas = parse_map(sys.argv[1])

    fails = []
    bank_names = sorted(
        n for n in areas if re.fullmatch(r"_BANK\d+", 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))
        print(f"  {name:<12} @ 0x{addr:08X}  size {size:>5} / {BANK_LIMIT}  ({pct:5.1f}%) {marker}")

    # HOME budget — _CODE can land in W1 (huge/small) or W2 (big/tiny).
    if "_CODE" in areas:
        addr, size = areas["_CODE"]
        end = addr + size
        budget_remaining = HOME_CEILING - end
        which_window = "W2 (0x8000-0xBFFF)" if addr >= 0x8000 else "HOME (0x4100-0xBFFF)"
        print(f"  _CODE        @ 0x{addr:08X}  size {size:>5}  → ends at 0x{end:04X}, "
              f"{which_window} has {budget_remaining} bytes free before 0x{HOME_CEILING:04X}")
        if budget_remaining < 0:
            print(f"    ERROR: _CODE extends past 0x{HOME_CEILING:04X} (stack/heap territory)")
            fails.append(("_CODE", size))

    if fails:
        print()
        for name, size in fails:
            print(f"  {name} too big: {size} bytes (limit {BANK_LIMIT})")
        sys.exit(1)


if __name__ == "__main__":
    main()
