#!/usr/bin/env python3
"""Make a delivered KiCad project self-contained.

Extracts the symbols embedded in the schematics' (lib_symbols ...) and the
footprints embedded in the .kicad_pcb into a project library named <libnick>,
re-points every lib_id / footprint reference to that library, and writes
project-local sym-lib-table / fp-lib-table (using ${KIPRJMOD}). Reads/writes the
files in <work>; prints JSON {root:[...], pretty:[...], symbols, footprints}.

usage: selfcontain.py <work_dir> <libnick> <file...>
"""
import sys, os, re, json

work, nick = sys.argv[1], sys.argv[2]
names = sys.argv[3:]


def balanced(s, start):
    d = 0
    for i in range(start, len(s)):
        if s[i] == '(':
            d += 1
        elif s[i] == ')':
            d -= 1
            if d == 0:
                return i + 1
    return len(s)


def top_symbols(section):
    """Top-level (symbol "NAME" ...) blocks inside a section (skips nested)."""
    out, i = [], 0
    while True:
        p = section.find('(symbol "', i)
        if p < 0:
            break
        ns = p + len('(symbol "')
        ne = section.find('"', ns)
        e = balanced(section, p)
        out.append((section[ns:ne], section[p:e]))
        i = e
    return out


def repoint(content, needle, nick):
    """Rewrite the library part of `needle "LIB:NAME"` occurrences to `nick`."""
    out, i = [], 0
    n = len(needle)
    while True:
        p = content.find(needle, i)
        if p < 0:
            out.append(content[i:])
            break
        vs = p + n
        ve = content.find('"', vs)
        val = content[vs:ve]
        out.append(content[i:vs])
        out.append(nick + ':' + val.split(':', 1)[1] if ':' in val else val)
        i = ve
    return ''.join(out)


schs = [n for n in names if n.endswith('.kicad_sch')]
pcbs = [n for n in names if n.endswith('.kicad_pcb')]

# ---- 1. collect symbols from every schematic's lib_symbols ----
symbols = {}
for f in schs:
    c = open(os.path.join(work, f), encoding='utf-8').read()
    ls = c.find('(lib_symbols')
    if ls < 0:
        continue
    section = c[ls:balanced(c, ls)]
    for name, block in top_symbols(section):
        bare = name.split(':', 1)[1] if ':' in name else name
        if bare not in symbols:
            symbols[bare] = block.replace('"' + name + '"', '"' + bare + '"', 1)

lib = '(kicad_symbol_lib\n\t(version 20231120)\n\t(generator "adom-lbr")\n'
for b in symbols.values():
    lib += '\t' + b + '\n'
lib += ')\n'
open(os.path.join(work, nick + '.kicad_sym'), 'w', encoding='utf-8').write(lib)

# ---- 2. re-point the schematics ----
for f in schs:
    path = os.path.join(work, f)
    c = open(path, encoding='utf-8').read()
    c = repoint(c, '(lib_id "', nick)
    c = repoint(c, '(symbol "', nick)                 # lib_symbols keys (the colon'd ones)
    c = repoint(c, '(property "Footprint" "', nick)
    open(path, 'w', encoding='utf-8').write(c)

# ---- 3. extract footprints from the PCB into <nick>.pretty ----
pretty = os.path.join(work, nick + '.pretty')
os.makedirs(pretty, exist_ok=True)
pfiles = []
UUID = re.compile(r'\s*\(uuid "[^"]*"\)')
NET = re.compile(r'\s*\(net \d+ "[^"]*"\)')
ATPLACE = re.compile(r'\(at [-\d.eE ]+\)')
HEADER = '\n\t(version 20240108)\n\t(generator "adom-lbr")\n\t(generator_version "9.0")'
for f in pcbs:
    path = os.path.join(work, f)
    c = open(path, encoding='utf-8').read()
    i, seen = 0, set()
    while True:
        p = c.find('(footprint "', i)
        if p < 0:
            break
        ns = p + len('(footprint "')
        ne = c.find('"', ns)
        name = c[ns:ne]
        bare = name.split(':', 1)[1] if ':' in name else name
        e = balanced(c, p)
        if bare not in seen:
            seen.add(bare)
            blk = c[p:e].replace('"' + name + '"', '"' + bare + '"', 1)
            blk = ATPLACE.sub('', blk, count=1)   # drop the footprint's board placement (first (at ...))
            blk = UUID.sub('', blk)               # drop uuids (instance-specific)
            blk = NET.sub('', blk)                # drop pad nets (instance-specific)
            # a *library* footprint needs a header right after the name
            blk = blk.replace('(footprint "' + bare + '"', '(footprint "' + bare + '"' + HEADER, 1)
            open(os.path.join(pretty, bare + '.kicad_mod'), 'w', encoding='utf-8').write(blk + '\n')
            pfiles.append(os.path.join(pretty, bare + '.kicad_mod'))
        i = e
    c = repoint(c, '(footprint "', nick)
    open(path, 'w', encoding='utf-8').write(c)

# ---- 4. project-local lib tables ----
open(os.path.join(work, 'sym-lib-table'), 'w', encoding='utf-8').write(
    '(sym_lib_table\n  (version 7)\n  (lib (name "%s")(type "KiCad")(uri "${KIPRJMOD}/%s.kicad_sym")(options "")(descr "adom-lbr self-contained project symbols"))\n)\n' % (nick, nick))
open(os.path.join(work, 'fp-lib-table'), 'w', encoding='utf-8').write(
    '(fp_lib_table\n  (version 7)\n  (lib (name "%s")(type "KiCad")(uri "${KIPRJMOD}/%s.pretty")(options "")(descr "adom-lbr self-contained project footprints"))\n)\n' % (nick, nick))

root = [os.path.join(work, f) for f in names] + [
    os.path.join(work, nick + '.kicad_sym'),
    os.path.join(work, 'sym-lib-table'),
    os.path.join(work, 'fp-lib-table'),
]
print(json.dumps({'root': root, 'pretty': pfiles, 'symbols': len(symbols), 'footprints': len(pfiles)}))