from pathlib import Path
import shutil, json, hashlib, re, math
import wx
app = wx.App(False)
import pcbnew

src = Path(__file__).resolve().parent
out = src.parent / 'lamp_buck_lmr51450_a2_costdown'
if out.exists():
    raise SystemExit(f'refusing to overwrite {out}')
out.mkdir(parents=True)
copy_files = [
    'lamp_buck_lmr51450_a1.kicad_sch',
    'lamp_buck_lmr51450_a1.kicad_pcb',
    'lamp_buck_lmr51450_a1.kicad_pro',
    'sym-lib-table', 'fp-lib-table', 'Project.kicad_sym',
]
for name in copy_files:
    shutil.copy2(src / name, out / name)
if (src / 'Lamp_Power.pretty').exists():
    shutil.copytree(src / 'Lamp_Power.pretty', out / 'Lamp_Power.pretty')

oldstem = 'lamp_buck_lmr51450_a1'
newstem = 'lamp_buck_lmr51450_a2_costdown'
for ext in ('.kicad_sch', '.kicad_pcb', '.kicad_pro'):
    (out / f'{oldstem}{ext}').rename(out / f'{newstem}{ext}')

# S-expression helpers preserve the existing schematic's formatting and UUIDs.
def balanced_end(s, start):
    depth=0; quoted=False; escaped=False
    for i in range(start,len(s)):
        c=s[i]
        if quoted:
            if escaped: escaped=False
            elif c=='\\': escaped=True
            elif c=='"': quoted=False
        else:
            if c=='"': quoted=True
            elif c=='(': depth += 1
            elif c==')':
                depth -= 1
                if depth==0: return i+1
    raise ValueError('unbalanced sexpr')

def sexpr_blocks(s, token):
    p=0
    while True:
        i=s.find(token,p)
        if i<0: return
        j=balanced_end(s,i)
        yield i,j,s[i:j]
        p=j

schpath=out/f'{newstem}.kicad_sch'
sch=schpath.read_text(encoding='utf-8')
updates={
    'C5': {'value':'33uF 16V X7R', 'footprint':'Capacitor_SMD:C_1812_4532Metric', 'mpn':'TDK C4532X7R1C336M250KC', 'dnp':False},
    'C6': {'value':'33uF 16V X7R', 'footprint':'Capacitor_SMD:C_1812_4532Metric', 'mpn':'TDK C4532X7R1C336M250KC', 'dnp':False},
    'C7': {'value':'100uF 10V X6S OPTIONAL DNP', 'footprint':'Capacitor_SMD:C_1210_3225Metric', 'mpn':'TDK C3225X6S1A107M250AC optional', 'dnp':True},
    'C8': {'value':'100uF 10V X6S OPTIONAL DNP', 'footprint':'Capacitor_SMD:C_1210_3225Metric', 'mpn':'TDK C3225X6S1A107M250AC optional', 'dnp':True},
}
replacements=[]
for a,b,block in sexpr_blocks(sch,'(symbol (lib_id '):
    refm=re.search(r'\(property "Reference" "([^"]+)"',block)
    if not refm or refm.group(1) not in updates: continue
    ref=refm.group(1); spec=updates[ref]
    mod=block
    for key,value in [('Value',spec['value']),('Footprint',spec['footprint'])]:
        pat=rf'(\(property "{key}" ")[^"]*(")'
        mod,n=re.subn(pat,lambda m:m.group(1)+value+m.group(2),mod,count=1)
        if n!=1: raise RuntimeError(f'{ref}: expected one {key} property')
    # Keep exact MPN data machine-readable but hidden from the drawing.
    m=re.search(r'\(property "Footprint" ',mod)
    pe=balanced_end(mod,m.start())
    mpn=f' (property "MPN" "{spec["mpn"]}" (at 0 0 0) (effects (font (size 1 1)) (hide yes)))'
    if '(property "MPN" ' not in mod: mod=mod[:pe]+mpn+mod[pe:]
    if spec['dnp']:
        mod=mod.replace('(in_bom yes) (on_board yes) (dnp no)', '(in_bom no) (on_board yes) (dnp yes)',1)
    replacements.append((a,b,mod))
for a,b,mod in reversed(replacements): sch=sch[:a]+mod+sch[b:]
if len(replacements)!=4: raise RuntimeError(f'expected 4 component updates, got {len(replacements)}')
sch=sch.replace('AI lamp power LMR51450 EVT candidate A1','AI lamp power LMR51450 EVT candidate A2 cost-down')
sch=sch.replace('"A1-dev"','"A2-costdown-dev"').replace('AI MINI POWER BOARD A1','AI MINI POWER BOARD A2 COST-DOWN')
sch=sch.replace('(project "lamp_power"','(project "lamp_buck_lmr51450_a2_costdown"')
schpath.write_text(sch,encoding='utf-8')

# Change only C5/C6 from 1210 to the exact 1812 library geometry, preserve all
# schematic paths and net assignments, and extend tracks whose endpoints were
# attached to the old pad centers.
boardpath=out/f'{newstem}.kicad_pcb'
board=pcbnew.LoadBoard(str(boardpath))
libdir=Path('/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints/Capacitor_SMD.pretty')
changed={}
def mm(v): return pcbnew.ToMM(v)
for old in list(board.GetFootprints()):
    old=pcbnew.Cast_to_FOOTPRINT(old)
    ref=old.GetReference()
    if ref not in ('C5','C6'): continue
    pos=old.GetPosition(); orient=old.GetOrientation()
    oldpads={p.GetNumber():p for p in old.Pads()}
    oldcenters={num:pad.GetPosition() for num,pad in oldpads.items()}
    fp=pcbnew.FootprintLoad(str(libdir),'C_1812_4532Metric')
    if not fp: raise RuntimeError('could not load 1812 footprint')
    fp.SetReference(ref); fp.SetValue('33uF 16V X7R')
    fp.SetPosition(pos); fp.SetOrientation(orient)
    fp.SetFPID(pcbnew.LIB_ID('Capacitor_SMD','C_1812_4532Metric'))
    fp.SetPath(old.GetPath())
    # Preserve visible field locations, layers, and orientation.
    for fieldname in ('Reference','Value'):
        oldfield=old.GetField(fieldname)
        newfield=fp.GetField(fieldname)
        newfield.SetPosition(oldfield.GetPosition())
        newfield.SetTextAngle(oldfield.GetTextAngle())
        newfield.SetLayer(oldfield.GetLayer())
        newfield.SetVisible(oldfield.IsVisible())
    for pad in fp.Pads():
        prior=oldpads.get(pad.GetNumber())
        if not prior: raise RuntimeError(f'{ref}: footprint pad mismatch')
        pad.SetNetCode(prior.GetNetCode())
        pad.SetNet(prior.GetNet())
    newcenters={p.GetNumber():p.GetPosition() for p in fp.Pads()}
    # Move copper endpoints/vias that terminate at old pad centers.
    moved=0
    for item in list(board.GetTracks()):
        if isinstance(item,pcbnew.PCB_VIA):
            for num,oldp in oldcenters.items():
                if item.GetPosition()==oldp and item.GetNetCode()==oldpads[num].GetNetCode():
                    item.SetPosition(newcenters[num]); moved+=1
        else:
            for num,oldp in oldcenters.items():
                if item.GetStart()==oldp and item.GetNetCode()==oldpads[num].GetNetCode():
                    item.SetStart(newcenters[num]); moved+=1
                if item.GetEnd()==oldp and item.GetNetCode()==oldpads[num].GetNetCode():
                    item.SetEnd(newcenters[num]); moved+=1
    board.Remove(old)
    board.Add(fp)
    changed[ref]={
      'old_fp':'C_1210_3225Metric','new_fp':'C_1812_4532Metric',
      'position_mm':[mm(pos.x),mm(pos.y)],'old_pad_centers_mm':{k:[mm(v.x),mm(v.y)] for k,v in oldcenters.items()},
      'new_pad_centers_mm':{k:[mm(v.x),mm(v.y)] for k,v in newcenters.items()},'copper_endpoints_moved':moved,
      'pad_nets':{k:oldpads[k].GetNetname() for k in oldpads},
    }
# Mark optional sites clearly on board but retain pads for EVT rework.
for fp in board.GetFootprints():
    fp=pcbnew.Cast_to_FOOTPRINT(fp)
    if fp.GetReference() in ('C7','C8'):
        fp.SetValue('OPTIONAL DNP 100uF 10V X6S')
pcbnew.ZONE_FILLER(board).Fill(board.Zones())
if not pcbnew.SaveBoard(str(boardpath),board): raise RuntimeError('failed to save A2 PCB')
(out/'board-change.json').write_text(json.dumps({'status':'A2_BOARD_EDITED_DRC_PENDING','changed':changed,'dnp_sites':['C7','C8']},indent=2)+'\n')
print(json.dumps({'output':str(out),'changed':changed},indent=2))
