#!/usr/bin/env python3
"""
size_check.py — размерный регресс-тест: сравнение _CODE всех тестов и
приложений с эталоном docs/size_baseline.tsv.

Использование:
    python3 toolchain/size_check.py            # сверка с эталоном
    python3 toolchain/size_check.py --update   # перезаписать эталон

Логика: собираем _CODE из .map каждой программы (tests/*, examples/*),
сравниваем с эталоном.  Любое отличие печатается; выход 1 — если есть
РОСТ (случайное разжирение libc), 0 — если только уменьшения/без
изменений.  Новые программы (нет в эталоне) — предупреждение, добавить
через --update.  Запускать после `make all`.
"""

import glob
import os
import re
import sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASELINE = os.path.join(ROOT, "docs", "size_baseline.tsv")


def collect():
    """Имя программы → размер _CODE (байт) по свежим .map."""
    sizes = {}
    pats = [
        os.path.join(ROOT, "tests", "*", ".sprinter-cc-*", "*.map"),
        os.path.join(ROOT, "examples", "*", ".sprinter-cc-*", "*.map"),
    ]
    for pat in pats:
        for map_path in glob.glob(pat):
            name = os.path.splitext(os.path.basename(map_path))[0]
            # .map должен лежать в своей папке .sprinter-cc-<name>
            if os.path.basename(os.path.dirname(map_path)) != ".sprinter-cc-" + name:
                continue
            with open(map_path, encoding="utf-8", errors="replace") as f:
                for line in f:
                    m = re.match(r"^_CODE\s+([0-9A-Fa-f]+)\s+([0-9A-Fa-f]+)", line)
                    if m:
                        sizes[name] = int(m.group(2), 16)
                        break
    return sizes


def load_baseline():
    base = {}
    if os.path.exists(BASELINE):
        with open(BASELINE) as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                name, size = line.split("\t")
                base[name] = int(size)
    return base


def save_baseline(sizes):
    with open(BASELINE, "w") as f:
        f.write("# Эталон размеров _CODE (байт); обновление: "
                "python3 toolchain/size_check.py --update\n")
        for name in sorted(sizes):
            f.write(f"{name}\t{sizes[name]}\n")


def main():
    update = "--update" in sys.argv
    sizes = collect()
    if not sizes:
        print("size_check: .map не найдены — сначала `make all`")
        return 2

    if update:
        save_baseline(sizes)
        print(f"size_check: эталон обновлён ({len(sizes)} программ) → "
              f"{os.path.relpath(BASELINE, ROOT)}")
        return 0

    base = load_baseline()
    if not base:
        print("size_check: эталона нет — создать: size_check.py --update")
        return 2

    grew = 0
    for name in sorted(set(sizes) | set(base)):
        new, old = sizes.get(name), base.get(name)
        if new is None:
            print(f"  {name}: исчез из сборки (был {old} Б)")
        elif old is None:
            print(f"  {name}: НОВЫЙ ({new} Б) — добавить в эталон (--update)")
        elif new != old:
            mark = "РОСТ" if new > old else "ок, меньше"
            print(f"  {name}: {old} -> {new} ({new - old:+d} Б)  [{mark}]")
            if new > old:
                grew += 1
    if grew:
        print(f"size_check: {grew} программ(ы) ВЫРОСЛИ — проверить правки libc")
        return 1
    print(f"size_check: OK ({len(sizes)} программ, роста нет)")
    return 0


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