#!/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','elements','fluids','optics')}
text = '\n'.join(sources.values())
names = declarations(text,'tikzphysics@registerdisplayname')
pic_anchors = declarations(sources['fluids'],'tikzphysics@registeranchors')
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','physicsdifferentialsector':'physicspolarelement','physicsdifferentialring':'physicspolarelement','physicssphericalshell':'physicspolarelement','physicshollowsphere':'physicspolarelement','physicsrectangularstrip':'physicsrectangularelement','physicsrectangularsheet':'physicsrectangularelement','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','physicsdifferentialringdiagram':'pic','physicssphereshelldiagram':'pic','physicssphereslicediagram':'pic','physicshollowspherediagram':'pic','physicscylindershelldiagram':'pic','physicscylinderslicediagram':'pic','physicsconeslicediagram':'pic','physicssheetelementdiagram':'pic'}
no_node.update({'physicsutube':'pic','physicsgasmanometer':'pic','physicshydraulicpress':'pic','physicsfluidcylinder':'pic','physicsfluidtank':'pic','physicspressureelement':'pic','physicsrotatingtube':'pic','physicsrotatingfluid':'pic','physicsbuoyancy':'pic','physicsflowtube':'pic','physicsmeniscus':'pic','physicscapillary':'pic','physicssurfacetensionring':'pic','physicsliquidring':'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 = {
 'physicsplatformleft':'surface runs left to right across the floor; bottom, left, and right cover its other edges. wall-surface and wall-back run from root to tip; wall-base and wall-tip span the end caps. transition runs from pulley-center to wall-root. Upward walls require zero inset and drop. Use 50 for any edge midpoint.',
 'physicsplatformright':'surface runs left to right across the floor; bottom, left, and right cover its other edges. wall-surface and wall-back run from root to tip; wall-base and wall-tip span the end caps. transition runs from pulley-center to wall-root. Upward walls require zero inset and drop. Use 50 for any edge midpoint.',
 'physicsplatformboth':'surface runs left to right across the floor. Each wall has surface, back, base, and tip families with left-wall or right-wall prefixes; each transition runs from its pulley-center to wall-root. Upward walls require zero inset and drop. Use 50 for any edge midpoint.',
 'physicsground':'surface runs left to right on the top contact face; bottom, left, and right cover the other edges. Use 50 for an edge midpoint.',
 'physicsceiling':'surface runs left to right on the underside; top, left, and right cover the other edges. Use 50 for an edge midpoint.',
 'physicswallleft':'surface runs bottom to top on the right contact face; top, bottom, and left cover the other edges. Use 50 for an edge midpoint.',
 'physicswallright':'surface runs bottom to top on the left contact face; top, bottom, and right cover the other edges. Use 50 for an edge midpoint.',
 'physicswedge':'base, right, and slope follow the outline counterclockwise from bl. surface runs left to right on the primary usable incline; in top mode right-surface selects the second incline. right-mid equals right-50; slope-mid equals surface-50. A pulley edge is supported in br and bl modes, and its centroid follows the filled four-point body. Directions and normals are local to the node.',
 'physicsramp':'surface follows floor plus incline by distance; floor and incline select each component. wall-surface, wall-back, wall-tip, base, and end cover the remaining outline. At the sharp joint the tangent uses the incline side.',
 'physicscurvedramp':'surface follows floor plus arc by distance; curve follows the arc only. floor, start, base, back, and top cover the remaining boundary.',
 '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).',
 'physicspolarelement':'An annular sector from inner-start to outer-end. Set inner radius to zero for a sector or delta angle to 360 degrees for a full ring.',
 'physicsdifferentialsector':'A polar-element preset with zero inner radius. The outer arc represents r d theta and numeric families follow every boundary.',
 'physicsdifferentialring':'A polar-element preset with a 360-degree sweep. Use source element on an unwrapped ring to copy its radii.',
 'physicsunwrappedring':'A differential approximation with width 2 pi times the inner reference radius and height equal to the radial thickness. source element copies both radii from a named polar element.',
 'physicsdifferentialringdiagram':'Use pic (D) {differential ring diagram={...}}. It keeps the body circle, ring, centre mark, and unwrapped strip together. Components are (D-ring) and (D-strip).',
 'physicssphericalshell':'A concentric differential shell shown in section. Its full-sweep anchors and radial geometry match the spherical-shell construction.',
 'physicshollowsphere':'A finite spherical wall shown in section, with independent inner and outer radii.',
 'physicsrectangularelement':'A Cartesian differential area with independent width and height, complete boundary anchors, and optional dimension labels.',
 'physicsrectangularstrip':'A full-width strip preset. Change its width and differential thickness independently.',
 'physicsrectangularsheet':'A finite unshaded sheet node used alone or as the body in a sheet element diagram.',
 'physicssphereshelldiagram':'A solid sphere with a highlighted spherical shell. Components are (S-body) and (S-shell); (S-center) marks the common centre.',
 'physicshollowspherediagram':'A hollow sphere in section. Component (H-wall) exposes the inner and outer boundaries.',
 'physicssphereslicediagram':'A solid sphere with a projected disk slice at a signed normalized axial position. Components are (S-body) and (S-slice).',
 'physicscylindershelldiagram':'A projected cylinder with a radial cylindrical shell. Components are (C-body) and (C-shell).',
 'physicscylinderslicediagram':'A projected cylinder with an axial disk slice. Components are (C-body) and (C-slice).',
 'physicsconeslicediagram':'A projected cone with a thin disk slice. element position is measured from apex to base. Components are (K-body) and (K-slice).',
 'physicssheetelementdiagram':'A rectangular sheet with a named Cartesian element. Components are (A-body) and (A-element).',
}

notes.update({'physicsutube': 'Two-liquid U tube. level adds a second liquid only above left level. Use (F-origin), (F-center), and the listed (F-...) coordinates; these are pic coordinates, not node anchors.', 'physicsgasmanometer': 'Gas reservoir connected to a U tube. body label names the gas. Use (F-origin), (F-center), and the listed (F-...) coordinates; these are pic coordinates, not node anchors.', 'physicshydraulicpress': 'Large piston width is 0.32 times width; tube width sets the small piston. Use (F-origin), (F-center), and the listed (F-...) coordinates; these are pic coordinates, not node anchors.', 'physicsfluidcylinder': 'Projected cylinder with an elliptical liquid surface. width is the diameter. Use (F-origin), (F-center), and the listed (F-...) coordinates; these are pic coordinates, not node anchors.', 'physicsfluidtank': 'Open vessel. Equal endpoint levels give a horizontal free surface. Use (F-origin), (F-center), and the listed (F-...) coordinates; these are pic coordinates, not node anchors.', 'physicspressureelement': 'Projected horizontal fluid element with inward pressure forces. Use (F-origin), (F-center), and the listed (F-...) coordinates; these are pic coordinates, not node anchors.', 'physicsrotatingtube': 'Horizontal tube with a differential section. level locates its left face along width. Use (F-origin), (F-center), and the listed (F-...) coordinates; these are pic coordinates, not node anchors.', 'physicsrotatingfluid': 'Parabolic surface. level is the vertex height; bend is the wall-to-vertex rise divided by height. Use (F-origin), (F-center), and the listed (F-...) coordinates; these are pic coordinates, not node anchors.', 'physicsbuoyancy': 'Immersed body with schematic inward pressure arrows. Arrow lengths do not encode pressure magnitude. Use (F-origin), (F-center), and the listed (F-...) coordinates; these are pic coordinates, not node anchors.', 'physicsflowtube': 'Curved flow tube; outlet height is 0.55 times inlet height. bend controls vertical curvature. Use (F-origin), (F-center), and the listed (F-...) coordinates; these are pic coordinates, not node anchors.', 'physicsmeniscus': 'Positive bend is concave; negative bend is convex. Contact angle is measured through the liquid. Use (F-origin), (F-center), and the listed (F-...) coordinates; these are pic coordinates, not node anchors.', 'physicscapillary': 'Capillary rise. level is the reservoir surface and left level is the contact-line height. Use (F-origin), (F-center), and the listed (F-...) coordinates; these are pic coordinates, not node anchors.', 'physicssurfacetensionring': 'Projected circular contact line. width and height are the ellipse diameters. Use (F-origin), (F-center), and the listed (F-...) coordinates; these are pic coordinates, not node anchors.', 'physicsliquidring': 'Annular liquid sector. width is outer diameter, tube width is radial thickness. Angles use degrees counterclockwise from right. Use (F-origin), (F-center), and the listed (F-...) coordinates; these are pic coordinates, not node anchors.'})

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. Use `show keys` or `show anchors` on a node; a name is optional for the overlay.','', 'All objects support ordinary TikZ styling; see the manual for transformation and sizing limitations. Percentage families accept integers 0 through 100.','']
md += ['Start with the [debug overlay guide](debug-overlays.md) for `show anchors`, `show keys`, and `\\physicshelp` examples across nodes, paths, and pics. For boundary directions, see the [surface-anchor guide](surface-anchors.md), [wedge-anchor guide](wedge-anchors.md), and [ramp-anchor guide](ramp-anchors.md). Each has a rendered `show anchors` gallery.', '']
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|surface@declarewedgeboundaryanchors|elements@declarearcanchors|elements@declareedgeanchors)\{([^}]+)\}',body)
    families+=extra_families.get(logical,[])
    families=list(dict.fromkeys(families))
    if kind == 'pic':
        if logical in pic_anchors: anchors=pic_anchors[logical].split(',')
        elif logical == 'physicspendulum': anchors=['pivot','bob (node)']
        elif logical == 'physicsdifferentialringdiagram': anchors=['center','body-north','body-south','body-east','body-west','ring (node)','strip (node)']
        elif logical == 'physicssphereshelldiagram': anchors=['center','body (node)','shell (node)','formula-anchor']
        elif logical == 'physicshollowspherediagram': anchors=['center','body-north','body-south','wall (node)','formula-anchor']
        elif logical == 'physicssphereslicediagram': anchors=['center','body (node)','slice (node)','formula-anchor']
        elif logical in ('physicscylindershelldiagram','physicscylinderslicediagram'): anchors=['center','body (node)','shell or slice (node)','formula-anchor']
        elif logical == 'physicsconeslicediagram': anchors=['center','apex','base','body (node)','slice (node)','formula-anchor']
        elif logical == 'physicssheetelementdiagram': anchors=['center','body (node)','element (node)','formula-anchor']
        else: anchors=['pivot','base','left','right']
    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)
    if logical in pic_anchors and hook.endswith(' diagram'): hook = hook[:-8]
    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}\par\smallskip']
    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'))
