#!/usr/bin/env python3
"""Check the standalone bundle's pdfLaTeX memory budget and shipped examples.

No hosted Overleaf session is required. Each compile uses only the copied .sty,
an empty user TeX tree, main_memory=5000000, and no extra top/bottom memory.
The pressure case reserves a fixed preamble payload and loads pgfplots/circuitikz
so expanding anchor formulas 101 times cannot silently regress.
"""
from __future__ import annotations

import argparse
import json
import os
from pathlib import Path
import re
import shutil
import subprocess

ROOT = Path(__file__).resolve().parents[1]
MINIMAL = r"""\documentclass{article}
\tracingstats=2
\usepackage{tikzphysics}
\begin{document}
\begin{tikzpicture}
\node[carnot cycle] (C) {};
\foreach \family in {hot,expansion,cold,compression,volume-axis,pressure-axis}{%
 \foreach \p in {0,...,100}{\path (C.\family-\p);}}
\draw (C.hot-6)--(C.cold-94);
\end{tikzpicture}
\end{document}
"""
PRESSURE = MINIMAL.replace(
    r"\usepackage{tikzphysics}",
    r"""\usepackage{amsmath,amssymb}
\usepackage{pgfplots,circuitikz}
\pgfplotsset{compat=1.18}
\usetikzlibrary{decorations.text,graphs,shapes.geometric,shapes.symbols}
% 16 * 2^15 = 524288 persistent tokens, independent of a user's preamble.
\def\MemoryPayload{0123456789abcdef}
\newcount\PayloadDoublings
\loop\ifnum\PayloadDoublings<15
 \edef\MemoryPayload{\MemoryPayload\MemoryPayload}
 \advance\PayloadDoublings by1
\repeat
\usepackage{tikzphysics}""",
)


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--bundle', type=Path,
                        default=ROOT / 'output/overleaf/tikzphysics.sty')
    parser.add_argument('--output-dir', type=Path,
                        default=ROOT / 'tmp/overleaf-verification')
    parser.add_argument('--only', choices=('all', 'memory', 'examples'), default='all')
    args = parser.parse_args()
    if shutil.which('pdflatex') is None:
        raise SystemExit('pdflatex is required for standalone bundle verification')
    bundle = args.bundle.resolve().read_bytes()
    output = args.output_dir.resolve()
    output.mkdir(parents=True, exist_ok=True)
    empty_texmf = output / 'empty-texmf'
    empty_texmf.mkdir(exist_ok=True)
    env = dict(os.environ, TEXINPUTS='.' + os.pathsep, TEXMFHOME=str(empty_texmf),
               main_memory='5000000', extra_mem_top='0', extra_mem_bot='0')
    sources = {}
    if args.only != 'examples':
        sources.update({'memory-minimal': MINIMAL, 'memory-pressure': PRESSURE})
    if args.only != 'memory':
        sources.update({p.stem: p.read_text() for p in
                        sorted((ROOT / 'output/overleaf').glob('*.tex'))})
    evidence = []
    for name, source in sources.items():
        work = output / name
        work.mkdir(exist_ok=True)
        (work / 'tikzphysics.sty').write_bytes(bundle)
        (work / (name + '.tex')).write_text(source)
        proc = subprocess.run(
            ['pdflatex', '-interaction=nonstopmode', '-halt-on-error', '-recorder',
             name + '.tex'], cwd=work, env=env, capture_output=True, timeout=120)
        (work / 'compile.txt').write_bytes(proc.stdout + proc.stderr)
        if proc.returncode:
            raise SystemExit(f'FAIL {name}:\n{proc.stdout[-2500:].decode(errors="replace")}')
        log = (work / (name + '.log')).read_text(errors='replace')
        memory = re.search(r'(\d+) words of memory out of (\d+)', log)
        if memory is None or int(memory[2]) != 5000000:
            raise SystemExit(f'FAIL {name}: requested 5000000-word budget not confirmed')
        used = int(memory[1])
        # A load-only bound catches duplicated formulas even without the pressure
        # payload. Actual anchor coordinates are covered by the l3build fixtures.
        if name == 'memory-minimal' and used >= 2000000:
            raise SystemExit(f'FAIL {name}: standalone runtime uses {used} words (limit 2000000)')
        physics_inputs = []
        for line in (work / (name + '.fls')).read_text().splitlines():
            if line.startswith('INPUT '):
                path = line[6:]
                if 'tikzphysics' in Path(path).name:
                    physics_inputs.append(path)
                    if Path(path).resolve() != (work / 'tikzphysics.sty').resolve():
                        # Relative recorder paths are relative to the compiler cwd.
                        if (work / path).resolve() != (work / 'tikzphysics.sty').resolve():
                            raise SystemExit(f'FAIL {name}: nonlocal physics runtime: {path}')
        if not physics_inputs:
            raise SystemExit(f'FAIL {name}: no standalone runtime recorded')
        evidence.append({'name': name, 'memory_words': used,
                         'main_memory': 5000000, 'physics_inputs': physics_inputs})
        print(f'PASS: {name}, {used}/5000000 memory words, isolated runtime', flush=True)
    (output / 'evidence.json').write_text(json.dumps(evidence, indent=2))
    print(f'All {len(evidence)} standalone bundle checks passed.')


if __name__ == '__main__':
    main()
