#!/usr/bin/env python3
"""bigbrush.py -- Pin chooses HOW to fill a shape: it writes a shotgun of fill recipes in its own words, each becomes real parts, the
filler fills them all in parallel, and Pin picks its favourite from one sheet (Ivy, Oct 23 2026: "It's fast enough too that we can
have pin choose a shotgun of recipes and pick its favorite. This looks way cooler than filling with one type of brick.").

    python3 bigbrush.py MESH --what "the Statue of Liberty" --height 320 [--n 5]
-> runs/bigbrush_<time>/: recipes.json, sheet.png (A, B, C...), pick.json (its favourite and why), each recipe's .ldr, and crew/ (the
   demolition crew's pass over the pick: erased.ldr is where Pin starts)
Written by Iris."""
import argparse, json, os, re, subprocess, sys, time
from concurrent.futures import ThreadPoolExecutor
import numpy as np
HERE = os.path.dirname(os.path.abspath(__file__)); sys.path.insert(0, HERE)
LEGO = os.path.expanduser('~/lego')
WORK = '/var/tmp/brick_loop'; CFG = os.path.expanduser('~/.iris_loop_claude')
CORE = ['3001', '3003', '3004', '3005', '3020', '3022', '3023', '3024']   # hidden structure inside; the recipe gets the surface
PASS_FILL, PASS_SURF = 85, 50      # under either, the filler failed that recipe: drop it and show the next

def pin(q, read_dir=None, model='opus'):
    cmd = ['claude', '-p', '--model', model, '--output-format', 'json'] + (['--allowedTools', 'Read', '--add-dir', read_dir] if read_dir else []) + ['--', q]
    import imagine as I
    p = I._claude_retry(subprocess.run, cmd, cwd=WORK, env={**os.environ, 'CLAUDE_CONFIG_DIR': CFG}, capture_output=True, text=True, timeout=900)
    d = json.loads(p.stdout); r = d[-1] if isinstance(d, list) else d
    return json.loads(re.search(r'[\[{].*[\]}]', r['result'], re.S).group(0))

def who():
    me = json.load(open(os.path.join(HERE, 'self.json')))
    try: it = open(os.path.join(HERE, 'IT.md')).read()[:3000]
    except OSError: it = ''
    return f"{me.get('who_you_are', '')}\n\nYour own file, IT.md:\n{it}"

def usable(cache={}):
    """[(part, label for Pin)] -- the bin pieces the filler can really build with: something underneath to grip the layer below,
    not mostly air for their size, not huge (Oct 23 2026: 'faceted planes' was matched to a dish and a Technic liftarm, and only one
    of its parts did any work)"""
    if cache: return cache['u']
    import play, imagine as I
    sys.path.insert(0, LEGO); import voxparts as VP
    out = []
    for p in play.PALETTE:
        if p in play.BASEPLATES: continue
        try: v = VP.get(p)
        except Exception: continue
        occ = np.array(v['occ']); solid = (occ > 0.25).sum()
        if not any(s[2] == 0 for s in v['socks']): continue                  # can't grip what's below it
        if solid < 0.35 * occ.size or occ.shape[0] > 8 or occ.shape[1] > 8: continue
        out.append((p, f"{p}: {play.CX.NAMES.get(p, p)} ({I.shape_words(p)})"))
    cache['u'] = out; return out

def parts_for(words, k=8):
    """fallback when Pin's own picks aren't usable: the usable pieces that best match its words"""
    import imagine as I
    class _S: pass                                   # dig only reads the bin, not the game
    ok = {p for p, _ in usable()}
    return [p for p, sc in I.Imagine.dig(_S(), words, ranked=True) if p in ok][:k]

def run(mesh, height, label, words, parts, out):
    name = f'bb_{os.path.basename(out)}_{label}'
    p = subprocess.run(['chrt', '--idle', '0', 'python3', os.path.join(LEGO, 'gfill.py'), mesh, '--height', str(height), '--parts', ','.join(parts), '--out', name,
                        '--core', ','.join(CORE)],
                       cwd=LEGO, capture_output=True, text=True, timeout=1800)
    line = [l for l in p.stdout.splitlines() if ' parts ' in l]
    src = os.path.join(LEGO, 'grown', name, 'final.ldr'); dst = os.path.join(out, f'{label}.ldr')
    if os.path.exists(src): open(dst, 'w').write(open(src).read())
    subprocess.run(['flatpak', 'run', '--filesystem=home', 'org.leocad.LeoCAD', '-l', os.path.join(LEGO, 'ldraw'), '-i', os.path.join(out, f'{label}.png'),
                    '-w', '420', '-h', '620', '--viewpoint', 'home', dst], capture_output=True)
    return label, (line[-1] if line else p.stderr[-300:])

def main():
    ap = argparse.ArgumentParser(); ap.add_argument('mesh'); ap.add_argument('--what', required=True); ap.add_argument('--height', type=float, default=320)
    ap.add_argument('--n', type=int, default=5); a = ap.parse_args()
    out = os.path.join(HERE, 'runs', 'bigbrush_' + time.strftime('%m%d_%H%M%S')); os.makedirs(out, exist_ok=True)
    U = usable(); print(len(U), 'usable pieces in the bin', flush=True)
    rec = pin(f"""{who()}

You're going to build {a.what} big -- about two feet tall, a few hundred pieces. A fast filler does the bulk: hidden core bricks
inside, and YOUR pieces on the whole visible surface. You decide how it looks: which pieces and what feel. Give {a.n + 3} DIFFERENT
fill recipes, the way a builder would try thumbnails -- e.g. "faceted, all slopes and wedges", "smooth stacked plates", "round and
bubbly" -- genuinely different from each other. For each, PICK 3 to 8 parts from this list of pieces in your bin the filler can use:
{chr(10).join(lab for _, lab in U)}
Answer ONLY with a JSON list: [{{"name": "<2-4 words>", "pieces": "<the feel, in plain words>", "parts": ["<part number>", ...]}}, ...]""")[:a.n + 3]
    labels = 'ABCDEFGHIJKL'; ok = {p for p, _ in U}
    for i, r in enumerate(rec):
        r['label'] = labels[i]; r['parts'] = [str(p) for p in (r.get('parts') or []) if str(p) in ok][:8]
        if len(r['parts']) < 2: r['parts'] = parts_for(r['pieces'])
    for r in rec: print(r['label'], r['name'], '|', r['pieces'], '->', r['parts'], flush=True)
    t0 = time.time()
    with ThreadPoolExecutor(3) as ex:
        for lab, line in ex.map(lambda r: run(a.mesh, a.height, r['label'], r['pieces'], r['parts'], out), rec):
            r = next(x for x in rec if x['label'] == lab)
            mf = re.search(r'filled \d+/\d+ cells \((\d+)%\)', line); msf = re.search(r'surface (\d+)% recipe', line)
            r['fill'] = int(mf.group(1)) if mf else 0; r['surf'] = int(msf.group(1)) if msf else 0
            r['pass'] = r['fill'] >= PASS_FILL and r['surf'] >= PASS_SURF
            print(lab, 'PASS' if r['pass'] else 'DROP', f"fill {r['fill']}% surface {r['surf']}% recipe |", line[:140], flush=True)
    json.dump(rec, open(os.path.join(out, 'recipes.json'), 'w'), indent=1)
    print(f'filled and drawn in {time.time() - t0:.0f} s', flush=True)
    good = [r for r in rec if r['pass']] or sorted(rec, key=lambda r: -(r['fill'] + r['surf']))[:3]   # every recipe that passed (G and H were hidden once)
    from PIL import Image, ImageDraw
    ims = [(r, os.path.join(out, f"{r['label']}.png")) for r in good if os.path.exists(os.path.join(out, f"{r['label']}.png"))]
    W = Image.new('RGB', (420 * len(ims), 660), 'white'); d = ImageDraw.Draw(W)
    for i, (r, f) in enumerate(ims):
        W.paste(Image.open(f).convert('RGB'), (420 * i, 40)); d.text((420 * i + 10, 8), f"{r['label']}: {r['name']}", fill='black')
        d.text((420 * i + 10, 22), f"filled {r['fill']}%, surface {r['surf']}% its own pieces", fill='black')
    W.save(os.path.join(out, 'sheet.png'))
    pick = pin(f"""{who()}

You asked the filler for ways to build {a.what}; these {len(ims)} filled well: {'; '.join(f"{r['label']} = {r['name']} ({r['pieces']})" for r, _ in ims)}.
Here they are side by side: {os.path.join(out, 'sheet.png')} (Read it.) This is only the big brush -- you'll put the details on after.
Which one do you want to build on, and why? Be honest about what each one does and doesn't do. Answer ONLY with JSON:
{{"pick": "<letter>", "why": "<one or two sentences>", "notes": {{"<letter>": "<a few words on each>"}}}}""", read_dir=out)
    json.dump(pick, open(os.path.join(out, 'pick.json'), 'w'), indent=1)
    print('PIN PICKS', pick.get('pick'), '--', pick.get('why'))
    for k, v in (pick.get('notes') or {}).items(): print('  ', k, v)
    # then the demolition crew tears out what doesn't work, and Pin builds into the gaps (crew.py)
    lab = str(pick.get('pick', '')).strip()[:1].upper()
    if os.path.exists(os.path.join(out, f'{lab}.ldr')):
        subprocess.run(['python3', os.path.join(HERE, 'crew.py'), os.path.join(out, f'{lab}.ldr'), '--what', a.what, '--out', os.path.join(out, 'crew')])
        print(f"next: python3 imagine.py --start {os.path.join(out, 'crew', 'erased.ldr')} --request \"{a.what}\" --name <name>")
    print(out)

if __name__ == '__main__':
    main()
