#!/usr/bin/env python3
"""Generate the article's four deterministic SVG figures using the standard library.

Download beside parkour_lab.py. Run without arguments to write assets/, or use
--check to compare existing figures without writing. Performance values are
transcribed from the paper's Table V, not generated by a model experiment.
"""
import argparse
from html import escape
from pathlib import Path
from parkour_lab import phase_weights, reward_memory

NAVY='#18344f'
BLUE='#247bb5'
PURPLE='#8064b9'
ORANGE='#d7862b'
TEAL='#208b83'
GRID='#dce5ed'


def text(x,y,value,size=20,fill=NAVY,anchor='start',weight='400'):
    return f'<text x="{x}" y="{y}" font-size="{size}" fill="{fill}" text-anchor="{anchor}" font-weight="{weight}">{escape(str(value))}</text>'


def line(x1,y1,x2,y2,color=GRID,width=1,dash=None):
    extra=f' stroke-dasharray="{dash}"' if dash else ''
    return f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="{color}" stroke-width="{width}"{extra}/>'


def poly(points,color):
    points=' '.join(f'{x:.3f},{y:.3f}' for x,y in points)
    return f'<polyline points="{points}" fill="none" stroke="{color}" stroke-width="3.5" stroke-linejoin="round"/>'


def svg(title,description,body,width=1000,height=520):
    return ('<svg xmlns="http://www.w3.org/2000/svg" width="'+str(width)+'" height="'+str(height)+
        '" viewBox="0 0 '+str(width)+' '+str(height)+'" role="img" aria-labelledby="title desc">\n'+
        '<title id="title">'+escape(title)+'</title><desc id="desc">'+escape(description)+'</desc>\n'+
        '<rect width="100%" height="100%" rx="18" fill="#fffdf9"/>\n'+
        '<g font-family="Arial, sans-serif">'+text(40,46,title,28,weight='700')+'\n'+
        '\n'.join(body)+'</g>\n</svg>\n')


def card(x,y,width,label,detail,fill):
    return (f'<rect x="{x}" y="{y}" width="{width}" height="94" rx="12" fill="{fill}" stroke="{GRID}"/>'+
        text(x+width/2,y+36,label,22,anchor='middle',weight='700')+
        text(x+width/2,y+65,detail,16,anchor='middle'))


def pipeline():
    body=[text(40,83,'Training uses extra information; deployment keeps one actor.',18),
        text(40,123,'TRAINING',17,PURPLE,weight='700'),
        card(40,140,270,'Height-scan teachers','Locomotion + individual skills','#e8f1fb'),
        card(365,140,270,'Unified height policy','Distillation + transition RL','#eee8fa'),
        card(690,140,270,'Depth student','Action labels + scan loss + RL','#fff0d9'),
        text(337,199,'→',36,anchor='middle'),text(662,199,'→',36,anchor='middle'),
        text(40,279,'DEPLOYMENT',17,TEAL,weight='700'),
        card(40,300,270,'Onboard observations','Depth + body state + command','#e8f1fb'),
        card(365,300,270,'Recurrent actor','Memory retained across steps','#eee8fa'),
        card(690,300,270,'Joint targets','PD tracking and robot motion','#fff0d9'),
        text(337,359,'→',36,anchor='middle'),text(662,359,'→',36,anchor='middle'),
        line(825,238,825,258,PURPLE,2),line(825,258,500,258,PURPLE,2),line(500,258,500,295,PURPLE,2),
        text(643,253,'Export trained actor',15,PURPLE,anchor='middle'),
        text(40,454,'Training-only experts, critic, references and scan decoder are omitted below.',18),
        text(40,486,'Schematic summary of the paper; not the complete third-party implementation.',16)]
    return svg('From training resources to one deployable policy','A training lane and a deployment lane distinguish supervision from runtime inputs.',body)


def phases():
    xmap=lambda x:90+x/3*840
    ymap=lambda y:410-y*280
    body=[text(40,82,'Smooth training-prior weights: triggers 1 and 2, inverse temperature 4.',18)]
    for y in (0,.25,.5,.75,1):
        body += [line(90,ymap(y),930,ymap(y)),text(73,ymap(y)+6,f'{y:g}',16,anchor='end')]
    for x in (0,.5,1,1.5,2,2.5,3):
        body += [text(xmap(x),438,f'{x:g}',16,anchor='middle')]
    for x in (1,2):
        body += [line(xmap(x),130,xmap(x),410,ORANGE,1,'6 5')]
    xs=[i*3/180 for i in range(181)]
    for index,(color,label) in enumerate([(BLUE,'Phase 0'),(PURPLE,'Phase 1'),(TEAL,'Phase 2')]):
        body += [poly([(xmap(x),ymap(phase_weights(x)[index])) for x in xs],color),
            line(115+index*270,109,145+index*270,109,color,4),text(155+index*270,115,label,18)]
    body += [text(500,470,'Position along the example track',18,anchor='middle'),
        text(40,500,'Hard mode changes phase only for x > trigger. These are reward weights, not actor gates.',16)]
    return svg('Phase-conditioned motion-prior weights','Three nonnegative smooth weights add to one for sorted triggers.',body)


def memory():
    values=reward_memory([20.]+[0.]*15)
    xmap=lambda x:90+x/15*840
    ymap=lambda y:410-y/20*270
    body=[text(40,82,'One foot reaches 50 m/s²; threshold 30, dt 0.02 s, tau 0.06 s.',18)]
    for y in (0,5,10,15,20):
        body += [line(90,ymap(y),930,ymap(y)),text(73,ymap(y)+6,str(y),16,anchor='end')]
    for t in (0,3,6,9,12,15):
        body += [text(xmap(t),438,f'{t*.02:.2f}',16,anchor='middle')]
    body += [poly([(xmap(i),ymap(y)) for i,y in enumerate(values)],PURPLE),
        text(400,110,'Positive excess accumulator',19,PURPLE,anchor='middle'),
        text(530,193,'First contribution: +0.2 with code default',18,ORANGE),
        text(530,223,'Paper penalty direction: −0.2',18,TEAL),
        text(500,471,'Seconds after the impulse',18,anchor='middle'),
        text(40,500,'Teaching values from the recurrence. New input is not multiplied by (1 − alpha).',16)]
    return svg('Foot-impact memory outlasts a single control step','The positive filtered acceleration excess decays geometrically after one impulse.',body)


def performance():
    heights=[60,65,70,75]
    series=[('Height-scan teacher',[99.9,99.2,99.2,98.6],PURPLE),
            ('Depth student',[99.2,98.8,90.,33.4],BLUE)]
    xmap=lambda x:110+(x-60)/15*760
    ymap=lambda y:410-y/100*270
    body=[text(40,82,'Climb-and-step: reported values from arXiv 2608.02653v1, Table V.',18)]
    for y in (0,25,50,75,100):
        body += [line(110,ymap(y),900,ymap(y)),text(92,ymap(y)+6,str(y),16,anchor='end')]
    for x in heights:
        body += [text(xmap(x),438,str(x),17,anchor='middle')]
    for i,(label,values,color) in enumerate(series):
        body += [line(140+i*390,108,170+i*390,108,color,4),text(180+i*390,115,label,18),
            poly([(xmap(x),ymap(y)) for x,y in zip(heights,values)],color)]
        for x,y in zip(heights,values):
            body += [f'<circle cx="{xmap(x)}" cy="{ymap(y)}" r="5" fill="{color}"/>']
        body += [text(xmap(75)-15,ymap(values[-1])+(-14 if i==0 else 30),str(values[-1])+'%',21,color,anchor='end',weight='700')]
    body += [text(500,471,'Obstacle height (cm); robot standing height = 90 cm',18,anchor='middle'),
        text(40,500,'Success rate (%). Connecting lines guide reading; no fitted model or confidence interval.',16)]
    return svg('Memory helps, but does not close every perception gap','Teacher and student success rates diverge sharply at the tallest tested obstacle.',body)


def main():
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--check',action='store_true')
    args=parser.parse_args()
    root=Path(__file__).resolve().parent/'assets'
    figures={'training-deployment.svg':pipeline(),'phase-prior.svg':phases(),
             'reward-memory.svg':memory(),'teacher-student-gap.svg':performance()}
    if not args.check:
        root.mkdir(exist_ok=True)
    for name,rendered in figures.items():
        path=root/name
        if args.check:
            if not path.exists() or path.read_text(encoding='utf-8') != rendered:
                raise SystemExit('Missing or stale figure: '+name)
        else:
            path.write_text(rendered,encoding='utf-8')
        print(name+': '+('matches generator' if args.check else 'written'))


if __name__=='__main__':
    main()
