#!/usr/bin/env python3
import csv,hashlib,json,re,shutil,subprocess,sys,uuid,xml.etree.ElementTree as ET
from pathlib import Path

ROOT=Path(__file__).resolve().parent
BASE=ROOT.parent
SRC=BASE/'lamp_buck_lmr51450_a2_costdown'
DST=ROOT/'lamp_a3_r13_candidate'
OLD='lamp_buck_lmr51450_a2_costdown'
NEW='lamp_buck_lmr51450_a3_r13'
CLI='/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli'
LIBID='Lamp_Power:R0603_Panasonic_ERJPA3_Recommended'
sys.path.insert(0,str(BASE/'parity_metadata_closure_A22'))
from prepare_candidates import forms

def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest()
def run(args,log):
    p=subprocess.run([CLI,*args],text=True,capture_output=True)
    log.write_text(p.stdout+'\nSTDERR:\n'+p.stderr)
    assert p.returncode==0,(args,p.stdout,p.stderr)
def pins(path):
    r=ET.parse(path).getroot();d={}
    for n in r.findall('./nets/net'):
        for p in n.findall('node'):d[(p.attrib['ref'],p.attrib['pin'])]=n.attrib['name']
    return d
def patch_pads(block):
    for st,en,pad in reversed(list(forms(block,'pad'))):
        num=re.match(r'\(pad\s+"([^"]+)"',pad).group(1)
        assert num in ['1','2']
        oldat=re.search(r'\(at\s+(-?[\d.]+)\s+0\)',pad)
        oldsize=re.search(r'\(size\s+([\d.]+)\s+([\d.]+)\)',pad)
        assert oldat and abs(float(oldat.group(1)))==0.825
        assert oldsize and float(oldsize.group(1))==0.8 and float(oldsize.group(2))==0.95
        p=re.sub(r'\(at\s+-?[\d.]+\s+0\)', '(at '+('-0.75' if num=='1' else '0.75')+' 0)',pad,count=1)
        p=re.sub(r'\(size\s+[\d.]+\s+[\d.]+\)','(size 0.65 0.95)',p,count=1)
        # Retain the original absolute 0.20 mm corner radius. Keeping only
        # the old ratio would reduce radius and grow the inner corner copper
        # toward the neighboring opposite-net track.
        p=re.sub(r'\(roundrect_rratio\s+[\d.]+\)','(roundrect_rratio 0.3076923076923)',p,count=1)
        block=block[:st]+p+block[en:]
    return block

DST.mkdir(parents=True,exist_ok=True)
for f in ['fp-lib-table','sym-lib-table','Project.kicad_sym']:
    shutil.copyfile(SRC/f,DST/f)
shutil.copytree(SRC/'Lamp_Power.pretty',DST/'Lamp_Power.pretty',dirs_exist_ok=True)
shutil.copyfile(SRC/(OLD+'.kicad_pro'),DST/(NEW+'.kicad_pro'))
original=(SRC/(OLD+'.kicad_pcb')).read_text()
board=original
r13=[]
for st,en,block in forms(original,'footprint'):
    if '(property "Reference" "R13"' in block:r13.append((st,en,block))
assert len(r13)==1
st,en,block=r13[0]
newblock=patch_pads(block)
newblock=newblock.replace('(footprint "Resistor_SMD:R_0603_1608Metric"','(footprint '+json.dumps(LIBID),1)
dp=[row for row in forms(newblock,'property') if row[2].startswith('(property "Datasheet"')]
assert len(dp)==1
ds,de,prop=dp[0]
mpn=prop.replace('"Datasheet" ""','"MPN" "ERJPA3F1000V"',1)
mpn=re.sub(r'\(uuid\s+"[^"]+"\)', '(uuid '+json.dumps(str(uuid.uuid5(uuid.NAMESPACE_DNS,'lamp-a24-R13-Panasonic-MPN-property')))+')',mpn,count=1)
newblock=newblock[:de]+'\n'+mpn+newblock[de:]
board=original[:st]+newblock+original[en:]
# Local footprint library uses KiCad's same original standard library geometry,
# then changes only these pad dimensions and the name. All other copper/routes
# are retained in the PCB. This is a new engineering candidate, never a release.
libsrc=Path('/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints/Resistor_SMD.pretty/R_0603_1608Metric.kicad_mod')
lib=patch_pads(libsrc.read_text())
lib=re.sub(r'^(\(footprint\s+)"[^"]+"',r'\1"R0603_Panasonic_ERJPA3_Recommended"',lib,count=1)
(DST/'Lamp_Power.pretty/R0603_Panasonic_ERJPA3_Recommended.kicad_mod').write_text(lib)
(DST/(NEW+'.kicad_pcb')).write_text(board)
s=(SRC/(OLD+'.kicad_sch')).read_text()
s=re.sub(r'\(project\s+"[^"]+"',lambda m:'(project '+json.dumps(NEW),s)
changed_sch=0
for st,en,block in reversed(list(forms(s,'symbol'))):
    if not re.match(r'\(symbol\s+\(lib_id',block):continue
    if '(property "Reference" "R13"' not in block:continue
    updated=block.replace('(property "Footprint" "Resistor_SMD:R_0603_1608Metric"','(property "Footprint" '+json.dumps(LIBID),1)
    assert updated!=block
    fp=[row for row in forms(updated,'property') if row[2].startswith('(property "Footprint"')]
    assert len(fp)==1
    ps,pe,prop=fp[0]
    mpnprop=prop.replace('"Footprint"','"MPN"',1).replace(json.dumps(LIBID),'"ERJPA3F1000V"',1)
    updated=updated[:pe]+'\n'+mpnprop+updated[pe:]
    s=s[:st]+updated+s[en:];changed_sch+=1
assert changed_sch==1
(DST/(NEW+'.kicad_sch')).write_text(s)
pcb=DST/(NEW+'.kicad_pcb');sch=DST/(NEW+'.kicad_sch')
run(['sch','export','netlist','--format','kicadxml','--output',str(DST/'netlist.xml'),str(sch)],DST/'netlist.log')
run(['sch','export','netlist','--format','kicadxml','--output',str(DST/'source_netlist.xml'),str(SRC/(OLD+'.kicad_sch'))],DST/'source-netlist.log')
assert pins(DST/'netlist.xml')==pins(DST/'source_netlist.xml')
run(['sch','erc','--severity-all','--format','json','--output',str(DST/'erc.json'),str(sch)],DST/'erc.log')
run(['pcb','drc','--severity-all','--all-track-errors','--refill-zones','--schematic-parity','--format','json','--output',str(DST/'drc-parity.json'),str(pcb)],DST/'drc.log')
d=json.loads((DST/'drc-parity.json').read_text())
erc=json.loads((DST/'erc.json').read_text())
assert not d['violations'],d['violations']
assert not d['unconnected_items']
assert not [v for sheet in erc['sheets'] for v in sheet['violations']]
assert len(d['schematic_parity'])==4 and all(x['type']=='missing_footprint' for x in d['schematic_parity'])
assert all(any(ref in x['description'] for ref in ['C9','C10','D1','R5']) for x in d['schematic_parity'])
# Confirm the unchanged PCB exterior and every non-R13 footprint/track/zone byte.
assert original[:r13[0][0]]==board[:r13[0][0]]
assert original[r13[0][1]:]==board[r13[0][0]+len(newblock):]
gap=2*0.75-0.65;outer=2*0.75+0.65;width=0.95
assert 0.7<=gap<=0.9 and 2.0<=outer<=2.2 and 0.8<=width<=1.0
# A conservative application envelope. This is an explicit hypothesis, not a
# measured output-voltage or temperature result.
vmax=5.5;rmin=100*(1-0.01);pmax=vmax*vmax/rmin
assert pmax<0.33
def allowed(t):return 0.33 if t<=130 else 0.33*max(0,(155-t)/25)
checks={'release':'A24','candidate_board_sha256':sha(pcb),'source_board_sha256':sha(SRC/(OLD+'.kicad_pcb')),'source_circuit_pin_mapping_unchanged':True,'only_board_change':'R13 footprint library ID and its two copper/mask/paste pad sizes/centers/roundrect ratios; other board bytes unchanged','r13_mpn':'ERJPA3F1000V','manufacturer':'Panasonic Industry','resistance_ohm':100,'tolerance_percent':1,'rated_ambient_W_at_105C':0.25,'rated_terminal_W_at_130C':0.33,'temperature_derating':'Above 130C terminal, derate linearly to 0W at 155C. Terminal temperature takes priority if method is uncertain. Case must stay below upper category temperature.','application_envelope_assumed_not_measured':{'max_output_V':vmax,'max_terminal_C':130,'minimum_resistance_ohm':rmin,'zero_internal_qod_resistance_bound_W':pmax,'terminal_rating_W':0.33,'rating_to_bound_ratio':0.33/pmax},'envelope_examples':[{'terminal_C':t,'allowed_W':allowed(t),'under_assumed_voltage_resistance_envelope':pmax<=allowed(t)} for t in [105,125,130,135,140]],'panasonic_recommended_land_mm':{'a_gap':[0.7,0.9],'b_outer':[2.0,2.2],'c_width':[0.8,1.0]},'candidate_land_mm':{'a_gap':gap,'b_outer':outer,'c_width':width,'centers':[-0.75,0.75],'pad_size':[0.65,0.95],'corner_radius_mm':0.2},'erc_violations':0,'physical_drc_violations':0,'unconnected_items':0,'schematic_parity_warnings':4,'remaining_parity':'Intentional DNP C9/C10/D1/R5','qualification':'CONDITIONAL_CAD_AND_RATING_CANDIDATE_NOT_PHYSICAL_APPROVAL','physical_validation':'NOT_RUN_NO_SAMPLE','manufacturing_status':'FABRICATION_HOLD','remaining_gates':['measure maximum output including overshoot and backfeed; hypothesis max 5.5V','measure terminal/case temperatures with actual enclosure/load','verify discharge time/energy/repetition with actual downstream capacitance','assembly/reflow/DFM acceptance of new R13 land pattern','factory quote and authenticity/stock verification']}
(ROOT/'r13_candidate_checks.json').write_text(json.dumps(checks,indent=2)+'\n')
checks['candidate_schematic_sha256']=sha(sch)
(ROOT/'r13_candidate_checks.json').write_text(json.dumps(checks,indent=2)+'\n')
print(json.dumps(checks,indent=2))
