#!/usr/bin/env python3
"""Analyze (and optionally normalize) a KiCad project for wiki publishing.

Given a project directory (already pulled from the user's machine), report the
render files, the 3D-model refs (standard vs custom — the custom ones must be
bundled into <Board>.3dshapes/), and the component list with MPN -> slug so the
library's dependencies can be filled.

With --normalize, rewrite every CUSTOM (model "...") ref in the .kicad_pcb to a
project-relative <Board>.3dshapes/<basename> path. This is REQUIRED before
publishing: machine-specific absolute paths (/home/<user>/…, C:\\Users\\<user>\\…)
resolve nowhere on the render service or on anyone else's machine. Relative paths
with forward slashes work on every OS and KiCad resolves them next to the .pcb.
Only the path string is changed; each model's offset/scale/rotate is untouched,
and ${KICAD*_3DMODEL_DIR} standard refs are left alone.

usage: kicad_board_bundle.py <project-dir> [--normalize]
prints JSON: { pro, pcb, schematics[], sym, pretty[], board_name,
               models_std[], models_custom[], components[{ref,value,mpn,slug}],
               normalized? }
"""
import sys, os, re, json, glob


def slugify(s):
    return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")


def basename(path):
    return re.split(r"[\\/]", path)[-1]


def normalize_pcb(pcb_path, shapes_dir):
    """Rewrite custom (model "...") refs to <shapes_dir>/<basename>. Returns the
    count of refs actually changed. ${...} standard refs are left untouched."""
    txt = open(pcb_path, encoding="utf-8", errors="ignore").read()
    changed = [0]

    def repl(m):
        path = m.group(1)
        if path.startswith("${"):
            return m.group(0)
        newp = f"{shapes_dir}/{basename(path)}"
        if newp != path:
            changed[0] += 1
        return f'(model "{newp}"'

    out = re.sub(r'\(model\s+"([^"]+)"', repl, txt)
    if changed[0]:
        open(pcb_path, "w", encoding="utf-8").write(out)
    return changed[0]


def main(d, normalize=False):
    g = lambda pat: sorted(glob.glob(os.path.join(d, "**", pat), recursive=True))
    pro = (g("*.kicad_pro") or [None])[0]
    pcb = (g("*.kicad_pcb") or [None])[0]
    schs = g("*.kicad_sch")
    sym = (g("*.kicad_sym") or [None])[0]
    pretty = sorted(p for p in g("*.pretty") if os.path.isdir(p))
    board_name = os.path.splitext(os.path.basename(pro or pcb or "board"))[0]

    result = {
        "pro": pro, "pcb": pcb, "schematics": schs, "sym": sym, "pretty": pretty,
        "board_name": board_name,
    }

    # normalize BEFORE reporting so the reported refs reflect the fixed file
    if normalize and pcb:
        result["normalized"] = normalize_pcb(pcb, f"{board_name}.3dshapes")

    # 3D model refs from the PCB: ${KICAD*_3DMODEL_DIR}/... resolve on the render
    # service; everything else is custom and must be bundled into <Board>.3dshapes/.
    models_std, models_custom = set(), set()
    if pcb:
        txt = open(pcb, encoding="utf-8", errors="ignore").read()
        for m in re.findall(r'\(model\s+"([^"]+)"', txt):
            (models_std if m.startswith("${") else models_custom).add(m)
    result["models_std"] = sorted(models_std)
    result["models_custom"] = sorted(models_custom)

    # components: each schematic symbol instance's Reference + an MPN-like property
    mpn_keys = ("MPN", "Manufacturer Part Number", "Mfr Part #", "Mfr. Part #",
                "LCSC", "LCSC Part", "LCSC Part #", "Part Number", "Value")
    comps = {}
    for sch in schs:
        t = open(sch, encoding="utf-8", errors="ignore").read()
        for block in re.split(r"\n\s*\(symbol\s", t):
            ref_m = re.search(r'\(property\s+"Reference"\s+"([^"]+)"', block)
            if not ref_m:
                continue
            ref = ref_m.group(1)
            if ref.startswith("#") or ref in comps:  # power symbols / dupes
                continue
            mpn = None
            for k in mpn_keys:
                mm = re.search(r'\(property\s+"' + re.escape(k) + r'"\s+"([^"]+)"', block)
                if mm and mm.group(1).strip() and mm.group(1) not in ("~", ""):
                    mpn = mm.group(1).strip()
                    break
            val_m = re.search(r'\(property\s+"Value"\s+"([^"]+)"', block)
            comps[ref] = {
                "ref": ref,
                "value": val_m.group(1) if val_m else "",
                "mpn": mpn,
                "slug": slugify(mpn) if mpn else None,
            }
    result["components"] = list(comps.values())

    print(json.dumps(result, indent=2))


if __name__ == "__main__":
    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    if not args:
        sys.exit("usage: kicad_board_bundle.py <project-dir> [--normalize]")
    main(args[0], normalize="--normalize" in sys.argv)