"""Generate controlled SVG inputs and export their line/circular-arc contours.
This fixture exporter is not a general SVG/CAM engine. Run beside decode-dxf.py.
"""
from pathlib import Path
import json,math,sys,datetime,xml.etree.ElementTree as ET,re
import ezdxf
ROOT=Path(__file__).resolve().parent
CASES=[
 {'id':'closed-rectangle','width_mm':40,'height_mm':20,'paths':['M 0 0 L 40 0 L 40 20 L 0 20 Z'],'purpose':'One closed path, no transforms'},
 {'id':'circular-corners','width_mm':40,'height_mm':20,'paths':['M 4 0 L 36 0 A 4 4 0 0 1 40 4 L 40 16 A 4 4 0 0 1 36 20 L 4 20 A 4 4 0 0 1 0 16 L 0 4 A 4 4 0 0 1 4 0 Z'],'purpose':'Four genuine circular corner arcs, no tessellation'},
 {'id':'two-path-overlap','width_mm':40,'height_mm':20,'paths':['M 0 0 L 40 0 L 40 20 L 0 20 Z','M 0 0 L 40 0'],'purpose':'Closed rectangle plus intentionally open path overlapping its top edge'}
]
def convert(source,destination):
 root=ET.parse(source).getroot()
 # Only accept the explicit physical scaling used by these fixtures.
 if root.attrib.get('width')!='40mm' or root.attrib.get('height')!='20mm' or root.attrib.get('viewBox')!='0 0 40 20':raise ValueError('This reproducible fixture exporter requires width=40mm height=20mm viewBox="0 0 40 20".')
 if any(el.tag.split('}')[-1]!='path' for el in list(root)):raise ValueError('Only path children supported.')
 doc=ezdxf.new('R2010');doc.units=4;doc.header['$MEASUREMENT']=1;space=doc.modelspace()
 for child in root:
  if 'transform' in child.attrib:raise ValueError('Transforms not supported in this experiment.')
  data=child.attrib['d'];tokens=re.findall(r'[A-Za-z]|[-+]?(?:\d*\.\d+|\d+\.?\d*)(?:[eE][-+]?\d+)?',data);i=0;points=[];first=None;current=None;closed=False
  if re.sub(r'[\s,]+','',data)!=''.join(tokens):raise ValueError('Unsupported SVG syntax.')
  while i<len(tokens):
   cmd=tokens[i];i+=1
   if cmd=='M':
    if first is not None:raise ValueError('One subpath per path element required.')
    current=(float(tokens[i]),float(tokens[i+1]));first=current;i+=2
   elif cmd in ['L','A']:
    if current is None:raise ValueError('Start with M.')
    if cmd=='L':end=(float(tokens[i]),float(tokens[i+1]));bulge=0;i+=2
    else:
     rx,ry,rot,large,sweep,x,y=map(float,tokens[i:i+7]);i+=7;end=(x,y)
     if rx<=0 or rx!=ry or rot!=0 or large not in [0,1] or sweep not in [0,1]:raise ValueError('Only unrotated circular arcs supported.')
     chord=math.dist(current,end)
     if chord==0 or chord>2*rx:raise ValueError('Unsupported circular arc endpoints.')
     angle=2*math.asin(chord/(2*rx));angle=2*math.pi-angle if large else angle
     bulge=(-1 if sweep else 1)*math.tan(angle/4)
    points.append((current[0],-current[1],bulge));current=end
   elif cmd=='Z':
    if current!=first:points.append((current[0],-current[1],0))
    current=first;closed=True
    if i!=len(tokens):raise ValueError('Z must end the contour.')
   else:raise ValueError('Fixture exporter accepts explicit absolute M, L, circular A, Z only.')
  if not closed:points.append((current[0],-current[1],0))
  space.add_lwpolyline(points,format='xyb',close=closed)
 doc.saveas(destination)
if __name__=='__main__':
 if len(sys.argv)==3:convert(Path(sys.argv[1]),Path(sys.argv[2]));sys.exit()
 runs=[]
 for case in CASES:
  source=ROOT/(case['id']+'.svg');target=ROOT/(case['id']+'.dxf');paths=''.join('<path d="'+d+'"/>' for d in case['paths']);source.write_text('<svg xmlns="http://www.w3.org/2000/svg" width="40mm" height="20mm" viewBox="0 0 40 20">'+paths+'</svg>',encoding='utf-8');convert(source,target);runs.append({'id':case['id'],'source':source.name,'output':target.name,'source_bytes':source.stat().st_size,'output_bytes':target.stat().st_size,'exported_at':datetime.datetime.now(datetime.timezone.utc).isoformat()})
 (ROOT/'fixtures.json').write_text(json.dumps(CASES,indent=2));(ROOT/'runs.json').write_text(json.dumps({'engine':'Local restricted absolute M/L/circular A/Z parser + ezdxf DXF writing','ezdxf_version':ezdxf.__version__,'runs':runs},indent=2));print(json.dumps(runs,indent=2))
