"""
SceneShift proxy exporter  (v2 — Blender 3.x / 4.x incl. 4.2+)
--------------------------------------------------------------
Builds one named, axis-aligned bounding box per semantic object and exports
ONLY those boxes. Geometry is written in world coordinates with identity node
transforms. Every box carries explicit POSITION min/max.

Writes BOTH  <OUT>.glb  and  <OUT>.gltf  (self-contained, base64 buffer).
Try the .gltf in the dashboard first, then the .glb.

Run from Blender's Scripting tab.
"""

import bpy, os, struct, json, base64
from mathutils import Vector

# ----------------------------------------------------------------- settings
OUT      = os.path.expanduser("~/Downloads/sceneshift-livingroom")  # <- no extension; both get written
MODE     = 'COLLECTIONS'   # 'COLLECTIONS' = one box per top-level collection
                           # 'SELECTED'    = one box per selected object
CAMERA   = True            # include the render camera
MIN_SIZE = 0.01            # ignore anything smaller than this, in metres
# ---------------------------------------------------------------------------

PROXY_COLL = "SS_PROXY"
GEOM = {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}


def world_bbox(objs, dg):
    mn = Vector((1e18, 1e18, 1e18))
    mx = Vector((-1e18, -1e18, -1e18))
    hit = False
    for ob in objs:
        if ob.type not in GEOM or not ob.visible_get():
            continue
        ev = ob.evaluated_get(dg)
        try:
            corners = ev.bound_box
        except (RuntimeError, AttributeError):
            continue
        mw = ev.matrix_world
        for c in corners:
            w = mw @ Vector(c)
            for i in range(3):
                if w[i] < mn[i]: mn[i] = w[i]
                if w[i] > mx[i]: mx[i] = w[i]
            hit = True
    return (mn, mx) if hit else None


def make_box(name, mn, mx, coll):
    x0, y0, z0 = mn
    x1, y1, z1 = mx
    verts = [(x0, y0, z0), (x1, y0, z0), (x1, y1, z0), (x0, y1, z0),
             (x0, y0, z1), (x1, y0, z1), (x1, y1, z1), (x0, y1, z1)]
    faces = [(0, 1, 2, 3), (7, 6, 5, 4), (0, 4, 5, 1),
             (1, 5, 6, 2), (2, 6, 7, 3), (3, 7, 4, 0)]
    me = bpy.data.meshes.new(name)
    me.from_pydata(verts, [], faces)
    me.validate()
    me.update()
    ob = bpy.data.objects.new(name, me)
    coll.objects.link(ob)
    return ob


def glb_to_gltf(glb_path, gltf_path):
    """Fold the GLB binary chunk into the JSON as a base64 data URI."""
    data = open(glb_path, 'rb').read()
    if data[:4] != b'glTF':
        raise ValueError("not a GLB")
    off, js, bin_ = 12, None, None
    while off + 8 <= len(data):
        clen, ctype = struct.unpack_from('<II', data, off)
        off += 8
        chunk = data[off:off + clen]
        off += clen
        if ctype == 0x4E4F534A:
            js = json.loads(chunk.decode('utf-8'))
        elif ctype == 0x004E4942:
            bin_ = chunk
    if js is None:
        raise ValueError("no JSON chunk in GLB")
    if bin_ is not None and js.get('buffers'):
        n = js['buffers'][0].get('byteLength', len(bin_))
        js['buffers'][0]['uri'] = ('data:application/octet-stream;base64,'
                                   + base64.b64encode(bin_[:n]).decode())
    with open(gltf_path, 'w') as f:
        json.dump(js, f)
    return js


def main():
    scene = bpy.context.scene
    dg = bpy.context.evaluated_depsgraph_get()

    old = bpy.data.collections.get(PROXY_COLL)
    if old:
        for ob in list(old.objects):
            bpy.data.objects.remove(ob, do_unlink=True)
        bpy.data.collections.remove(old)

    proxy = bpy.data.collections.new(PROXY_COLL)
    scene.collection.children.link(proxy)

    groups = []
    if MODE == 'COLLECTIONS':
        for c in scene.collection.children:
            if c.name == PROXY_COLL:
                continue
            objs = [o for o in c.all_objects if o.type in GEOM]
            if objs:
                groups.append((c.name, objs))
    else:
        for o in bpy.context.selected_objects:
            if o.type in GEOM:
                groups.append((o.name, [o]))

    if not groups:
        raise RuntimeError(
            "Nothing to proxy. COLLECTIONS mode needs top-level collections "
            "holding meshes; SELECTED mode needs objects selected."
        )

    made = []
    print("\n--- proxy boxes (metres) ---")
    for name, objs in groups:
        bb = world_bbox(objs, dg)
        if bb is None:
            print(f"  skip {name}: no visible geometry")
            continue
        mn, mx = bb
        if max(mx[i] - mn[i] for i in range(3)) < MIN_SIZE:
            print(f"  skip {name}: below MIN_SIZE")
            continue
        safe = name.replace(" ", "_")
        made.append(make_box(safe, mn, mx, proxy))
        size = tuple(round(mx[i] - mn[i], 2) for i in range(3))
        ctr = tuple(round((mx[i] + mn[i]) / 2, 2) for i in range(3))
        print(f"  {safe:<26} size {size}  centre {ctr}")

    bpy.ops.object.select_all(action='DESELECT')
    for ob in made:
        ob.select_set(True)
    if CAMERA and scene.camera:
        scene.camera.select_set(True)
        print(f"\n  camera: {tuple(round(v, 2) for v in scene.camera.location)}")
    bpy.context.view_layer.objects.active = made[0] if made else scene.camera

    # only pass kwargs this Blender version actually knows about
    wanted = dict(
        use_selection=True,
        export_apply=True,
        export_cameras=CAMERA,
        export_lights=False,
        export_animations=False,
        export_materials='NONE',
        export_yup=True,
    )
    valid = set(bpy.ops.export_scene.gltf.get_rna_type().properties.keys())
    kwargs = {k: v for k, v in wanted.items() if k in valid}
    dropped = sorted(set(wanted) - valid)
    if dropped:
        print(f"  (this Blender ignores: {', '.join(dropped)})")

    base = os.path.splitext(OUT)[0]
    glb_path, gltf_path = base + ".glb", base + ".gltf"

    bpy.ops.export_scene.gltf(filepath=glb_path, export_format='GLB', **kwargs)
    js = glb_to_gltf(glb_path, gltf_path)

    print(f"\n{len(made)} boxes · {len(js.get('nodes', []))} nodes · "
          f"{len(js.get('meshes', []))} meshes")
    print(f"  {glb_path}   {os.path.getsize(glb_path):,} bytes")
    print(f"  {gltf_path}  {os.path.getsize(gltf_path):,} bytes")


main()