"""Internet Paradise: make a Blender scene light enough for the town, and export it as a GLB.

Run it without opening Blender's window (Blender 4.1 or newer; tested with 4.1, 4.5 and 5.2):

  blender -b --python teztopia_optimize.py -- house.blend house.glb --triangles 100000 --texture 2048
  blender -b --python teztopia_optimize.py -- chair.fbx chair.glb --triangles 3000 --texture 1024 --bake

The input may be a .blend, .glb, .gltf, .fbx or .obj. What it does, in order:
  1. Removes lights and cameras (models cannot light the shared town).
  2. Applies every modifier, then merges duplicate vertices.
  3. Decimates every mesh by the same ratio until the whole model is under --triangles.
  4. Joins meshes that use the same materials, so each material is drawn once.
  5. With --bake: bakes every material's colour into ONE atlas texture and one material.
  6. Shrinks every texture to at most --texture pixels on its longest side.
  7. Exports a GLB: WebP textures, no Draco, no lights or cameras, modifiers applied.
Then check the result with the Internet Paradise MCP tool inspect_model (or https://internetparadise.xyz/docs/check).
"""

import sys
import bmesh
import bpy

argv = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else []
if len(argv) < 2:
    raise SystemExit("Usage: blender -b --python teztopia_optimize.py -- INPUT OUTPUT.glb "
                     "[--triangles N] [--texture PX] [--bake]")
source, output = argv[0], argv[1]
option = lambda name, default: int(argv[argv.index(name) + 1]) if name in argv else default
TRIANGLES = option("--triangles", 100_000)  # Internet Paradise refuses a GLB above 200,000
TEXTURE = option("--texture", 2048)  # Internet Paradise's import aims for 2048 px; 4096 is the hard limit
BAKE = "--bake" in argv

# 0. Open or import the source into an empty scene.
if source.lower().endswith(".blend"):
    bpy.ops.wm.open_mainfile(filepath=source)
else:
    bpy.ops.wm.read_factory_settings(use_empty=True)
    ext = source.lower().rsplit(".", 1)[-1]
    if ext in ("glb", "gltf"):
        bpy.ops.import_scene.gltf(filepath=source)
    elif ext == "fbx":
        bpy.ops.import_scene.fbx(filepath=source)
    elif ext == "obj":
        bpy.ops.wm.obj_import(filepath=source)
    else:
        raise SystemExit(f"Cannot read .{ext}; use .blend, .glb, .gltf, .fbx or .obj")
scene = bpy.context.scene
if bpy.context.object and bpy.context.object.mode != "OBJECT":
    bpy.ops.object.mode_set(mode="OBJECT")


def meshes():
    return [o for o in scene.objects if o.type == "MESH"]


def triangles(objects):
    total = 0
    for o in objects:
        o.data.calc_loop_triangles()
        total += len(o.data.loop_triangles)
    return total


def select(objects, active=None):
    bpy.ops.object.select_all(action="DESELECT")
    for o in objects:
        o.select_set(True)
    bpy.context.view_layer.objects.active = active or (objects[0] if objects else None)


# 1. No lights or cameras.
for o in [o for o in scene.objects if o.type in ("LIGHT", "CAMERA")]:
    bpy.data.objects.remove(o, do_unlink=True)

# 2. Apply modifiers (convert to mesh) and merge duplicate vertices.
select(meshes())
if meshes():
    bpy.ops.object.convert(target="MESH")
for o in meshes():
    bm = bmesh.new()
    bm.from_mesh(o.data)
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
    bm.to_mesh(o.data)
    bm.free()
before = triangles(meshes())

# 3. Decimate everything by one ratio until the model fits. Shape keys block Decimate.
for _ in range(3):
    total = triangles(meshes())
    if total <= TRIANGLES:
        break
    ratio = max(0.01, TRIANGLES / total * 0.98)
    for o in meshes():
        if o.data.shape_keys or len(o.data.polygons) < 50:
            continue
        mod = o.modifiers.new("Internet Paradise decimate", "DECIMATE")
        mod.decimate_type = "COLLAPSE"
        mod.ratio = ratio
        mod.use_collapse_triangulate = True
    select(meshes())
    bpy.ops.object.convert(target="MESH")

# 4. Join meshes that share the same materials (rigged or shape-keyed meshes stay apart).
groups = {}
for o in meshes():
    if o.data.shape_keys or o.find_armature():
        continue
    key = tuple(sorted(m.name for m in o.data.materials if m))
    groups.setdefault(key, []).append(o)
for group in groups.values():
    if len(group) > 1:
        select(group)
        bpy.ops.object.join()

# 5. Optional: bake all colour into one atlas with Cycles, one material for the whole model.
if BAKE and meshes():
    select(meshes())
    if len(meshes()) > 1:
        bpy.ops.object.join()
    target = bpy.context.view_layer.objects.active
    uv = target.data.uv_layers.new(name="Atlas")
    target.data.uv_layers.active = uv
    bpy.ops.object.mode_set(mode="EDIT")
    bpy.ops.mesh.select_all(action="SELECT")
    bpy.ops.uv.smart_project(island_margin=0.003)
    bpy.ops.object.mode_set(mode="OBJECT")
    atlas = bpy.data.images.new("Atlas", TEXTURE, TEXTURE, alpha=False)
    for slot in target.material_slots:
        material = slot.material
        if not material:
            continue
        material.use_nodes = True
        node = material.node_tree.nodes.new("ShaderNodeTexImage")
        node.image = atlas
        material.node_tree.nodes.active = node  # the bake writes into the active image node
    scene.render.engine = "CYCLES"
    scene.cycles.samples = 16
    scene.render.bake.use_pass_direct = False
    scene.render.bake.use_pass_indirect = False
    scene.render.bake.margin = 4
    bpy.ops.object.bake(type="DIFFUSE", pass_filter={"COLOR"}, use_clear=True)
    atlas.pack()
    baked = bpy.data.materials.new("Atlas")
    baked.use_nodes = True
    nodes, links = baked.node_tree.nodes, baked.node_tree.links
    image = nodes.new("ShaderNodeTexImage")
    image.image = atlas
    coords = nodes.new("ShaderNodeUVMap")
    coords.uv_map = "Atlas"
    links.new(coords.outputs["UV"], image.inputs["Vector"])
    links.new(image.outputs["Color"], nodes["Principled BSDF"].inputs["Base Color"])
    nodes["Principled BSDF"].inputs["Roughness"].default_value = 0.8
    target.data.materials.clear()
    target.data.materials.append(baked)
    for layer in [l for l in target.data.uv_layers if l.name != "Atlas"]:
        target.data.uv_layers.remove(layer)
    target.data.uv_layers["Atlas"].active_render = True

# 6. No texture larger than --texture on its longest side.
for image in bpy.data.images:
    w, h = image.size
    if image.users and max(w, h) > TEXTURE:
        k = TEXTURE / max(w, h)
        image.scale(max(1, round(w * k)), max(1, round(h * k)))
        image.pack()

# 7. Export a GLB the town accepts.
select(meshes())
bpy.ops.export_scene.gltf(
    filepath=output,
    export_format="GLB",
    export_apply=True,
    export_yup=True,
    export_image_format="WEBP",
    export_draco_mesh_compression_enable=False,
    export_cameras=False,
    export_lights=False,
)
after = triangles(meshes())
materials = len({m.name for o in meshes() for m in o.data.materials if m})
print(f"Internet Paradise: {before:,} -> {after:,} triangles, {len(meshes())} meshes, "
      f"{materials} materials. Wrote {output}")
