#!/usr/bin/env python3
"""Generate the in-TikZ reference and Markdown index from package declarations.
Run with --check in CI to reject stale generated files. No TeX is executed.
"""
from pathlib import Path
import argparse
import re
ROOT = Path(__file__).resolve().parent.parent

def group(s, start):
    assert s[start] == '{'
    level, i = 1, start + 1
    while level:
        if s[i] == '{' and s[i-1] != '\\': level += 1
        if s[i] == '}' and s[i-1] != '\\': level -= 1
        i += 1
    return s[start+1:i-1], i

def declarations(text, command):
    result = {}
    for m in re.finditer(re.escape('\\'+command)+r'\{([^}]+)\}\s*\{', text):
        value, _ = group(text,m.end()-1)
        value = re.sub(r'%[^\n]*','',value).strip()
        result[m[1]] = value
    return result

sources = {m:(ROOT/f'tikzlibrarytikzphysics.{m}.code.tex').read_text() for m in ('surface','ramps','mechanics','optics')}
text = '\n'.join(sources.values())
names = declarations(text,'tikzphysics@registerdisplayname')
keys = declarations(text,'tikzphysics@registerkeydefaults')
missing_labels = set(keys) - set(names)
if missing_labels:
    raise SystemExit('Missing public reference names: ' + ', '.join(sorted(missing_labels)))
shapes = declarations(text,'pgfdeclareshape')
base = ['center','north','south','east','west','north east','north west','south east','south west','base','base east','base west','mid','mid east','mid west','text']
# Styles wrapping standard shapes and logical variants of a shared shape.
actual = {'physicsblock':'rectangle','physicspulley':'circle','physicsparticle':'circle','physicsdisk':'circle','physicsring':'circle','physicsconcavemirror':'physicsmirror','physicsconvexmirror':'physicsmirror'}
# Path/pic reference declarations share the same registries as objects.
no_node = {'physicsspring':'path','physicsrope':'path','physicsforce':'path','physicsvelocity':'path','physicsacceleration':'path','physicstorque':'path','physicsrod':'path','physicspinsupport':'pic','physicsrollersupport':'pic','physicspendulum':'pic'}
# Parameterized declaration helpers have their concrete family names here.
extra_families = {
 'physicswedge':['surface','tangent-before','tangent-after','normal','right-surface','right-tangent-before','right-tangent-after','right-normal'],
 'physicsramp':['surface','tangent-before','tangent-after','normal'],
 'physicscurvedramp':['surface','curve','tangent-before','tangent-after','normal','curve-tangent-before','curve-tangent-after','curve-normal'],
}
notes = {
 'physicswedge':'surface runs left to right on the usable incline; top mode uses the left face. right-surface selects the right face in top mode. Legacy slope/right/base families keep their CCW directions. Directions and normals are local to the node.',
 'physicsramp':'surface follows the whole contact path by distance. At the sharp joint the tangent uses the incline side.',
 'physicscurvedramp':'surface follows floor plus arc by distance; curve follows the arc only.',
 'physicsspring':'Use draw[spring] (A)--(B). No private anchors. Lead lengths may be zero; amplitude and segment length must be positive.',
 'physicsrope':'Use draw[rope] (A) to[over pulley=P] (B). Endpoints must lie outside a circular pulley. Labels on to sit on the final straight segment. shortest changes wrap only, not the tangent pair.',
 'physicspulley':'Use a circular pulley with external rope endpoints. Set pulley axle radius=none to hide the axle.',
 'physicspendulum':'Use pic (P) {pendulum}. Angle is measured from downward vertical; positive swings right. Length is pivot to bob centre. Use (P-pivot) and (P-bob.center).',
 'physicspinsupport':'Use pic (S) {pin-support}; anchors are coordinates (S-pivot), (S-base), (S-left), (S-right).',
 'physicsrollersupport':'Use pic (S) {roller-support}; anchors are coordinates (S-pivot), (S-base), (S-left), (S-right).',
}

def clean(v):
    return v.replace('$','').replace('^\\circ',' degrees')
def tex(v):
    return v.replace('&',r'\&').replace('_',r'\_').replace('%',r'\%')

out = ['%% Generated by scripts/generate_reference.py; do not edit.','\\makeatletter']
md = ['# Feature reference','', 'Generated from the package declarations. Values below are defaults, not live node values. Short names are the normal interface. Existing `physics...` styles and `physics ...` key aliases remain supported.', '', 'Use `\\physicshelp{wedge}` inside a `tikzpicture` to display the same reference. For a named object use `show keys` or `show anchors`.','', 'All objects support ordinary TikZ styling; see the manual for transformation and sizing limitations. Percentage families accept integers 0 through 100.','']
manual = []
for logical, name in names.items():
    kind = no_node.get(logical,'node')
    shape = actual.get(logical,logical)
    body = shapes.get(shape,'')
    anchors = list(base) if shape in ('rectangle','circle') else re.findall(r'\\anchor\{([^{}#]+)\}|\\inheritanchor\[from=[^]]+\]\{([^{}#]+)\}',body)
    if anchors and isinstance(anchors[0],tuple): anchors=[a or b for a,b in anchors]
    anchors=list(dict.fromkeys(anchors + (['text'] if kind=='node' else [])))
    families=re.findall(r'\\tikzphysics@(?:declareedgeanchors|optics@declarearcanchors)\{([^}]+)\}',body)
    families+=extra_families.get(logical,[])
    families=list(dict.fromkeys(families))
    if kind == 'pic': anchors=['pivot','base','left','right'] if logical!='physicspendulum' else ['pivot','bob (node)']
    rows=[]
    for entry in keys.get(logical,'').split(','):
        if '/' in entry:
            k,v=entry.strip().split('/',1);rows.append((k,clean(v)))
    # Include unit-aware aliases whose implementations forward to native sizes.
    alias_prefixes={'physicsblock':'block','physicspulley':'pulley','physicswedge':'wedge','physicsplatformleft':'platform','physicsplatformright':'platform','physicsplatformboth':'platform','physicsground':'ground','physicsceiling':'ceiling','physicswallleft':'wall','physicswallright':'wall','physicsslab':'slab','physicsprism':'prism'}
    aliases=[]
    prefix=alias_prefixes.get(logical)
    if prefix:
        for m in re.finditer(r'physics ('+prefix+r' [^/\n]+)/\.code\s*=\s*\{\\tikzphysics@length@keyhandler\{/pgf/([^}]+)\}',text):
            aliases.append((m[1],m[2]))
    aliastext='; '.join(k+' = '+v for k,v in aliases)
    familytext=', '.join(f+'-0..100' for f in families)
    if logical in ('physicsramp','physicscurvedramp','physicsconcavemirror','physicsconvexmirror','physicsconvexlens','physicsconcavelens','physicsslab','physicsprism'):
        familytext+= '; legacy .0..100 shorthand also available'
    if shape in ('circle','rectangle'): familytext='Numeric anchors are angles in degrees, as in ordinary TikZ.'
    anchorstext=', '.join(anchors) or 'No private anchors; use path endpoints and nodes along the path.'
    common='draw, fill, line width, color, opacity, rotate, scale' + (', anchor, minimum width, minimum height, inner sep, outer sep' if kind=='node' else '')
    hook = 'platform' if name.startswith('platform') else ('support' if name in ('pin-support','roller-support') else name)
    contents=[r'\textbf{'+tex(name)+' ('+kind+r')}\par',r'\textbf{Keys: defaults}\par',r'\begin{tabular}{@{}ll@{}}']
    contents += [tex(k)+' & '+tex(v)+r'\\' for k,v in rows]
    contents += [r'\end{tabular}\par']
    if aliases:contents += [r'\textbf{Size aliases:} '+tex(aliastext)+r'.\par']
    contents += [r'\textbf{Anchors:} '+tex(anchorstext)+r'\par']
    if familytext:contents += [r'\textbf{Families:} '+tex(familytext)+r'\par']
    contents += [r'\textbf{Also:} '+tex(common)+r'.\par']
    contents += [r'\textbf{Defaults hook:} every '+hook+r'.\par']
    if logical in notes: contents += [tex(notes[logical])+r'\par']
    contents += [r'\textit{Use the manual for geometry rules and collision-safe aliases.}']
    out += ['\\expandafter\\def\\csname tikzphysics@reference@body@'+logical+'\\endcsname{%','\n'.join(contents),'}',
            '\\expandafter\\def\\csname tikzphysics@reference@anchors@'+logical+'\\endcsname{'+(','.join(anchors) if kind=='node' else '')+'}',
            '\\expandafter\\def\\csname tikzphysics@reference@families@'+logical+'\\endcsname{'+','.join(families)+'}']
    lookup=[name,logical]
    if kind in ('path','pic'):lookup+=['physics '+name]
    if logical=='physicsplatformboth':lookup+=['platform-both']
    if logical=='physicsplatformleft':lookup+=['platform-left-up']
    if logical=='physicsplatformright':lookup+=['platform-right-up']
    if logical=='physicsramp':lookup+=['ramp-left']
    if logical=='physicscurvedramp':lookup+=['curved-ramp-left']
    for alias in lookup:out += ['\\expandafter\\def\\csname tikzphysics@reference@lookup@'+alias+'\\endcsname{'+logical+'}']
    md += ['## '+name+' ('+kind+')','','| Key | Default |','| --- | --- |']+[f'| `{k}` | {v} |' for k,v in rows]
    if aliases:md+=['','Size aliases: '+aliastext+'.']
    md+=['','**Anchors:** '+anchorstext,'','**Families:** '+(familytext or 'None'),'']
    if logical in notes:md += [notes[logical],'']
    manual += [r'\subsection{'+tex(name)+' ('+kind+r')}', r'\begin{tabularx}{\linewidth}{@{}p{.45\linewidth}X@{}}',r'\toprule Key & Default \\',r'\midrule']
    manual += [r'\texttt{'+tex(k)+'} & '+tex(v)+r'\\' for k,v in rows]
    manual += [r'\bottomrule\end{tabularx}']
    if aliases: manual += [r'\textbf{Size aliases:} '+tex(aliastext)+r'.\par']
    manual += [r'\textbf{Anchors:} '+tex(anchorstext)+r'\par']
    if familytext: manual += [r'\textbf{Families:} '+tex(familytext)+r'\par']
    if logical in notes: manual += [tex(notes[logical])+r'\par']
    manual += ['']
    # The standard public hook is documented in both outputs.
    md += ['Default hook: `every '+hook+'`.','']
out += [r'\expandafter\def\csname tikzphysics@reference@lookup@index\endcsname{index}', r'\expandafter\def\csname tikzphysics@reference@body@index\endcsname{\textbf{Feature index}\par ' + ', '.join(names.values()) + r'\par Use \textbackslash physicshelp\{name\} for a complete card.}']
out += [r'\def\tikzphysics@reference@features{' + ','.join(names.values()) + '}', r'\def\tikzphysics@reference@nodes{' + ','.join(name for logical,name in names.items() if logical not in no_node) + '}']
out+=['\\makeatother','\\endinput','']
parser=argparse.ArgumentParser();parser.add_argument('--check',action='store_true');args=parser.parse_args()
outputs={'tikzlibrarytikzphysics.catalog.code.tex':'\n'.join(out),'docs/reference.md':'\n'.join(md)}
manual_path=ROOT/'tikzphysics.tex'
manual_source=manual_path.read_text()
if '% BEGIN GENERATED REFERENCE' in manual_source:
    outputs['tikzphysics.tex']=re.sub(r'% BEGIN GENERATED REFERENCE.*?% END GENERATED REFERENCE', lambda _: '% BEGIN GENERATED REFERENCE\n'+'\n'.join(manual)+'\n% END GENERATED REFERENCE',manual_source,flags=re.S)
for name,data in outputs.items():
    path=ROOT/name
    if args.check:
        if not path.exists() or path.read_text()!=data:raise SystemExit(f'Stale generated reference: {name}')
    else:path.write_text(data)
print(f'{len(names)} feature references '+('verified' if args.check else 'generated'))
