#!/usr/bin/env python3
"""
Convert flux-ai/soic-16-n-to-8-pin-fpc-flex-adapter.ipc2581c to KiCad .kicad_pcb format.

Key design facts from IPC-2581C analysis:
- TSSOP_L1-L8: castellated through-holes at y=-15.874, x=10.555..19.445 (1.27mm pitch)
- TSSOP_R1-R8: castellated through-holes at y=-11.474, x=10.555..19.445 (1.27mm pitch)
- FPC_1-10: SMD finger pads at x=30.5, y=-15.924..-11.424 (0.5mm pitch), 3mm x 0.35mm
- Board outline: roughly 10..32 x -16.424..-10.924 (22mm x 5.5mm)
"""

import xml.etree.ElementTree as ET
import math

IPC_FILE = 'soic-16-n-to-8-pin-fpc-flex-adapter.ipc2581c'
OUT_FILE = '../soic-16-n-to-8-pin-fpc-flex-adapter.kicad_pcb'

NS = 'http://webstds.ipc.org/2581'
def Q(name): return f'{{{NS}}}{name}'
def untag(el): return el.tag.replace(f'{{{NS}}}', '')


def parse_ipc(path):
    tree = ET.parse(path)
    root = tree.getroot()

    # --- Line width dictionary ---
    line_widths = {}
    for dl in root.iter(Q('DictionaryLineDesc')):
        for entry in dl:
            lid = entry.get('id')
            ld = entry.find(Q('LineDesc'))
            if ld is not None:
                line_widths[lid] = float(ld.get('lineWidth', '0'))

    # --- Standard primitives (CUSTOM_1 = castellation copper, CUSTOM_2 = tiny) ---
    std_prims = {}
    for ds in root.iter(Q('DictionaryStandard')):
        for entry in ds:
            sid = entry.get('id')
            pts = []
            for pb in entry.iter(Q('PolyBegin')):
                pts.append((float(pb.get('x')), float(pb.get('y'))))
            for ps in entry.iter(Q('PolyStepSegment')):
                pts.append((float(ps.get('x')), float(ps.get('y'))))
            std_prims[sid] = pts

    # --- User primitives (UPOLY_N = copper fills, UCIRCLE_1 = castellation ring) ---
    upoly_defs = {}   # id -> [(x,y), ...]
    ucircle_defs = {} # id -> diameter
    for du in root.iter(Q('DictionaryUser')):
        for entry in du:
            uid = entry.get('id')
            pts = []
            for pb in entry.iter(Q('PolyBegin')):
                pts.append((float(pb.get('x')), float(pb.get('y'))))
            for ps in entry.iter(Q('PolyStepSegment')):
                pts.append((float(ps.get('x')), float(ps.get('y'))))
            if pts:
                upoly_defs[uid] = pts
            for c in entry.iter(Q('Circle')):
                ucircle_defs[uid] = float(c.get('diameter'))

    # --- Layer features ---
    # pads: {pin_name: {x, y, prim_id, layer}}
    pads = {}
    # upoly placements per layer: [(uid, offset_x, offset_y)]
    layer_upolys = {}
    # lines per layer: [(x1,y1,x2,y2,width)]
    layer_lines = {}
    # through-holes: [(x, y, diameter)]
    through_holes = []

    for lf in root.iter(Q('LayerFeature')):
        layer = lf.get('layerRef')
        layer_upolys.setdefault(layer, [])
        layer_lines.setdefault(layer, [])

        sets = list(lf)
        for s in sets:
            if untag(s) != 'Set':
                continue

            # Pad elements (appear in Set[0] of each layer, only F.Cu has pin refs)
            for pad_el in s.findall(Q('Pad')):
                loc = pad_el.find(Q('Location'))
                if loc is None:
                    continue
                x = float(loc.get('x'))
                y = float(loc.get('y'))
                pin_ref = pad_el.find(Q('PinRef'))
                pin = pin_ref.get('pin') if pin_ref is not None else ''
                sp_ref = pad_el.find(Q('StandardPrimitiveRef'))
                up_ref = pad_el.find(Q('UserPrimitiveRef'))
                prim_id = (sp_ref.get('id') if sp_ref is not None
                           else (up_ref.get('id') if up_ref is not None else None))
                if pin and pin not in pads:
                    pads[pin] = {'x': x, 'y': y, 'prim_id': prim_id, 'layer': layer}

            # Features blocks (UPOLY refs and lines)
            for feat in s.findall(Q('Features')):
                loc = feat.find(Q('Location'))
                ox = float(loc.get('x', '0')) if loc is not None else 0
                oy = float(loc.get('y', '0')) if loc is not None else 0

                up_ref = feat.find(Q('UserPrimitiveRef'))
                if up_ref is not None:
                    layer_upolys[layer].append((up_ref.get('id'), ox, oy))

                for line in feat.iter(Q('Line')):
                    if line.get('startX') is None:
                        continue
                    lw_ref = line.find(Q('LineDescRef'))
                    lw = line_widths.get(lw_ref.get('id'), 0.1) if lw_ref is not None else 0.1
                    layer_lines[layer].append((
                        float(line.get('startX')), float(line.get('startY')),
                        float(line.get('endX')), float(line.get('endY')), lw
                    ))

                for us in feat.findall(Q('UserSpecial')):
                    for line in us.findall(Q('Line')):
                        ld = line.find(Q('LineDesc'))
                        lw = float(ld.get('lineWidth', '0.05')) if ld is not None else 0.05
                        layer_lines[layer].append((
                            float(line.get('startX')), float(line.get('startY')),
                            float(line.get('endX')), float(line.get('endY')), lw
                        ))
                    for poly in us.findall(Q('Polyline')):
                        ld = poly.find(Q('LineDesc'))
                        lw = float(ld.get('lineWidth', '0.05')) if ld is not None else 0.05
                        pts = []
                        pb = poly.find(Q('PolyBegin'))
                        if pb is not None:
                            pts.append((float(pb.get('x')), float(pb.get('y'))))
                        for ps in poly.findall(Q('PolyStepSegment')):
                            pts.append((float(ps.get('x')), float(ps.get('y'))))
                        for i in range(len(pts) - 1):
                            layer_lines[layer].append((*pts[i], *pts[i+1], lw))

        # Through-holes (F.Cu_B.Cu layer)
        if layer == 'F.Cu_B.Cu':
            for s in sets:
                if untag(s) != 'Set':
                    continue
                for hole in s.iter(Q('Hole')):
                    through_holes.append((
                        float(hole.get('x')), float(hole.get('y')),
                        float(hole.get('diameter'))
                    ))

    return {
        'pads': pads,
        'upoly_defs': upoly_defs,
        'ucircle_defs': ucircle_defs,
        'std_prims': std_prims,
        'layer_upolys': layer_upolys,
        'layer_lines': layer_lines,
        'through_holes': through_holes,
        'line_widths': line_widths,
    }


def bbox(pts):
    xs = [p[0] for p in pts]
    ys = [p[1] for p in pts]
    return min(xs), max(xs), min(ys), max(ys)


def upoly_is_fpc(pts):
    """FPC pads span ~3mm in x (from ~29 to ~32)."""
    xmin, xmax, _, _ = bbox(pts)
    return (xmax - xmin) > 2.5


def upoly_center_size(pts):
    xmin, xmax, ymin, ymax = bbox(pts)
    cx = (xmin + xmax) / 2
    cy = (ymin + ymax) / 2
    w = xmax - xmin
    h = ymax - ymin
    return cx, cy, w, h


def generate_kicad(data, out_path):
    pads = data['pads']
    upoly_defs = data['upoly_defs']
    layer_upolys = data['layer_upolys']
    layer_lines = data['layer_lines']
    through_holes = data['through_holes']

    # KiCad standard layer mapping (for 2-layer board)
    kicad_layers = [
        (0,  'F.Cu',    'signal'),
        (31, 'B.Cu',    'signal'),
        (35, 'F.Paste', 'user'),
        (34, 'B.Paste', 'user'),
        (37, 'F.SilkS', 'user', 'F.Silkscreen'),
        (36, 'B.SilkS', 'user', 'B.Silkscreen'),
        (39, 'F.Mask',  'user'),
        (38, 'B.Mask',  'user'),
        (44, 'Edge.Cuts', 'user'),
        (45, 'Margin',  'user'),
        (47, 'F.CrtYd', 'user', 'F.Courtyard'),
        (46, 'B.CrtYd', 'user', 'B.Courtyard'),
        (49, 'F.Fab',   'user', 'F.Fabrication'),
        (48, 'B.Fab',   'user', 'B.Fabrication'),
    ]

    lines = []
    lines.append('(kicad_pcb')
    lines.append('\t(version 20241229)')
    lines.append('\t(generator "ipc2581_to_kicad")')
    lines.append('\t(generator_version "2.0")')
    lines.append('\t(general')
    lines.append('\t\t(thickness 1.6)')
    lines.append('\t\t(legacy_teardrops no)')
    lines.append('\t)')
    lines.append('\t(paper "A4")')
    lines.append('\t(title_block')
    lines.append('\t\t(title "SOIC-16-N to 8-pin FPC Flex Adapter")')
    lines.append('\t\t(date "2026-05-28")')
    lines.append('\t\t(rev "1.0")')
    lines.append('\t)')

    # Layers
    lines.append('\t(layers')
    for layer_def in kicad_layers:
        if len(layer_def) == 4:
            lines.append(f'\t\t({layer_def[0]} "{layer_def[1]}" {layer_def[2]} "{layer_def[3]}")')
        else:
            lines.append(f'\t\t({layer_def[0]} "{layer_def[1]}" {layer_def[2]})')
    lines.append('\t)')

    lines.append('\t(net 0 "")')
    lines.append('\t(net 1 "Net-(Pad1)")')
    lines.append('')

    # --- Board outline (Edge.Cuts) ---
    # Board outline
    edge_seen = set()
    for (x1, y1, x2, y2, lw) in layer_lines.get('Edge.Cuts', []):
        key = (round(x1, 4), round(y1, 4), round(x2, 4), round(y2, 4))
        key2 = (round(x2, 4), round(y2, 4), round(x1, 4), round(y1, 4))
        if key in edge_seen or key2 in edge_seen:
            continue
        edge_seen.add(key)
        lines.append(f'\t(gr_line')
        lines.append(f'\t\t(start {x1:.6f} {y1:.6f})')
        lines.append(f'\t\t(end {x2:.6f} {y2:.6f})')
        lines.append(f'\t\t(stroke (width 0.0500) (type solid))')
        lines.append(f'\t\t(layer "Edge.Cuts")')
        lines.append(f'\t)')

    lines.append('')

    # --- Silkscreen lines ---
    # Silkscreen
    silk_lines = layer_lines.get('F.SilkS', [])
    # Deduplicate (some lines appear twice due to duplicate sets)
    seen = set()
    for (x1, y1, x2, y2, lw) in silk_lines:
        key = (round(x1, 4), round(y1, 4), round(x2, 4), round(y2, 4))
        key2 = (round(x2, 4), round(y2, 4), round(x1, 4), round(y1, 4))
        if key in seen or key2 in seen:
            continue
        seen.add(key)
        lw = max(lw, 0.01)
        lines.append(f'\t(gr_line')
        lines.append(f'\t\t(start {x1:.6f} {y1:.6f})')
        lines.append(f'\t\t(end {x2:.6f} {y2:.6f})')
        lines.append(f'\t\t(stroke (width {lw:.4f}) (type solid))')
        lines.append(f'\t\t(layer "F.SilkS")')
        lines.append(f'\t)')

    lines.append('')

    # --- Footprint ---
    lines.append('\t(footprint "soic16-fpc-adapter"')
    lines.append('\t\t(layer "F.Cu")')
    lines.append('\t\t(at 0 0)')
    lines.append('\t\t(fp_text reference "U?" (at 21 -13.67) (layer "F.SilkS")')
    lines.append('\t\t\t(effects (font (size 1 1) (thickness 0.15)))')
    lines.append('\t\t)')
    lines.append('\t\t(fp_text value "SOIC16-FPC-Adapter" (at 21 -19) (layer "F.Fab")')
    lines.append('\t\t\t(effects (font (size 1 1) (thickness 0.15)))')
    lines.append('\t\t)')

    # -- Through-hole castellation pads (TSSOP pads) --
    # Collect unique hole positions and match to pad names
    lines.append('')
    # Copper pad diameter = 0.636396mm (from UCIRCLE_1), drill = 0.45mm
    copper_d = 0.636396
    drill_d = 0.45

    # Build a map from (x,y) to pin name
    pad_pos_to_pin = {}
    for pin, p in pads.items():
        key = (round(p['x'], 3), round(p['y'], 3))
        pad_pos_to_pin[key] = pin

    hole_set = set()
    for (hx, hy, hd) in through_holes:
        key = (round(hx, 3), round(hy, 3))
        if key in hole_set:
            continue
        hole_set.add(key)
        pin = pad_pos_to_pin.get(key, '?')
        lines.append(f'\t\t(pad "{pin}" thru_hole circle')
        lines.append(f'\t\t\t(at {hx:.6f} {hy:.6f})')
        lines.append(f'\t\t\t(size {copper_d:.6f} {copper_d:.6f})')
        lines.append(f'\t\t\t(drill {drill_d:.3f})')
        lines.append(f'\t\t\t(layers "*.Cu" "*.Mask")')
        lines.append(f'\t\t)')

    # -- SMD FPC finger pads --
    lines.append('')
    # Collect FPC pads from F.Cu UPOLY placements
    fpc_pads_added = set()
    for (uid, ox, oy) in layer_upolys.get('F.Cu', []):
        if uid not in upoly_defs:
            continue
        pts = upoly_defs[uid]
        if not upoly_is_fpc(pts):
            continue
        cx, cy, w, h = upoly_center_size(pts)
        cx += ox
        cy += oy
        # Find matching pin name
        key = (round(cx, 3), round(cy, 3))
        # FPC pad center y is between ymin and ymax of the upoly
        # Match to pad by proximity
        pin = '?'
        for pname, p in pads.items():
            if pname.startswith('FPC') and abs(p['y'] - cy) < 0.01 and abs(p['x'] - cx) < 0.1:
                pin = pname
                break
        if pin in fpc_pads_added:
            continue
        fpc_pads_added.add(pin)
        lines.append(f'\t\t(pad "{pin}" smd rect')
        lines.append(f'\t\t\t(at {cx:.6f} {cy:.6f})')
        lines.append(f'\t\t\t(size {w:.6f} {h:.6f})')
        lines.append(f'\t\t\t(layers "F.Cu" "F.Paste" "F.Mask")')
        lines.append(f'\t\t)')

    lines.append('\t)')  # end footprint
    lines.append('')

    # --- Copper fills as gr_poly on F.Cu and B.Cu ---
    # Only output non-FPC (castellation) copper fills to avoid clutter
    layer_map = {'F.Cu': 'F.Cu', 'B.Cu': 'B.Cu'}
    for ipc_layer, kicad_layer in layer_map.items():
        upoly_list = layer_upolys.get(ipc_layer, [])
        written = set()
        for (uid, ox, oy) in upoly_list:
            if uid not in upoly_defs:
                continue
            pts = upoly_defs[uid]
            if upoly_is_fpc(pts):
                continue  # FPC pads are already in footprint
            if uid in written:
                continue
            written.add(uid)
            cx, cy, w, h = upoly_center_size(pts)
            # Output as filled polygon
            lines.append(f'\t(gr_poly')
            lines.append(f'\t\t(pts')
            # Use simplified rect for octagonal pads to keep file clean
            hw = w / 2
            hh = h / 2
            px = cx + ox
            py = cy + oy
            lines.append(f'\t\t\t(xy {px-hw:.4f} {py-hh:.4f})')
            lines.append(f'\t\t\t(xy {px+hw:.4f} {py-hh:.4f})')
            lines.append(f'\t\t\t(xy {px+hw:.4f} {py+hh:.4f})')
            lines.append(f'\t\t\t(xy {px-hw:.4f} {py+hh:.4f})')
            lines.append(f'\t\t)')
            lines.append(f'\t\t(stroke (width 0) (type solid))')
            lines.append(f'\t\t(fill solid)')
            lines.append(f'\t\t(layer "{kicad_layer}")')
            lines.append(f'\t)')

    lines.append(')')  # end kicad_pcb

    with open(out_path, 'w') as f:
        f.write('\n'.join(lines) + '\n')
    print(f"Written: {out_path}")
    print(f"  Edge.Cuts lines: {len(edge_seen)}")
    print(f"  Silkscreen lines: {len(seen)}")
    print(f"  Through-holes: {len(hole_set)}")
    print(f"  FPC SMD pads: {len(fpc_pads_added)}")


if __name__ == '__main__':
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))
    ipc_path = os.path.join(script_dir, IPC_FILE)
    out_path = os.path.join(script_dir, OUT_FILE)
    data = parse_ipc(ipc_path)
    print(f"Parsed: {len(data['pads'])} pads, "
          f"{sum(len(v) for v in data['layer_upolys'].values())} upoly placements, "
          f"{len(data['through_holes'])} holes")
    generate_kicad(data, out_path)
