"""Billet 117: the rat cellar's ceiling ladder and the metro service hatch.

cellar_ladder is built here from code: an iron service ladder leaning a little
to the north, from the floor up to a dark hatch in the ceiling. Its rungs follow
the hero's climb (RoomElement.PASSAGE_LADDER_HEIGHT, 260 units = 3.47 m, from the
south face to 0.3 r past the centre, for size 1.5), set back 0.25 m so his body
stays in front of them.

service_hatch is the casino's rat_trapdoor (scene of metro_passages.blend) with
its wood swapped for painted steel and its frame for safety yellow: same cage,
same "Walled"/"Open" nodes, so the same placement numbers fit.

Run headless from big-egg-3d/ (the .blend is read, never saved):
  blender --background tools/blender/metro_passages.blend --python tools/blender/metro_ladder_hatch.py [-- --preview <dir>]
"""
import bpy, bmesh, math, os, sys
from mathutils import Vector, Euler

ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
OUT = os.path.join(ROOT, "assets", "models", "rooms")

# The ladder, in metres. Blender +Y is north (glTF -Z, the plan's -y).
FOOT_Y = -0.19
SLOPE = 0.164
TOP = 5.2
HATCH_Z = 4.95
RUNG_STEP = 0.3


def material(name, rgb, metal, rough, glow=None):
    m = bpy.data.materials.get(name) or bpy.data.materials.new(name)
    m.use_nodes = True
    p = next(n for n in m.node_tree.nodes if n.type == 'BSDF_PRINCIPLED')
    p.inputs["Base Color"].default_value = (*rgb, 1.0)
    p.inputs["Metallic"].default_value = metal
    p.inputs["Roughness"].default_value = rough
    m.diffuse_color = (*rgb, 1.0)
    if glow:
        p.inputs["Emission Color"].default_value = (*glow, 1.0)
        p.inputs["Emission Strength"].default_value = 3.0
    return m


def part(scene, root, name, mesh_fill, mat, location, rotation=(0.0, 0.0, 0.0)):
    me = bpy.data.meshes.new(name)
    bm = bmesh.new()
    mesh_fill(bm)
    bm.to_mesh(me)
    bm.free()
    me.materials.append(mat)
    obj = bpy.data.objects.new(name, me)
    scene.collection.objects.link(obj)
    obj.parent = root
    obj.location = location
    obj.rotation_euler = Euler(rotation)
    return obj


def box_fill(size):
    def fill(bm):
        bmesh.ops.create_cube(bm, size=1.0)
        for v in bm.verts:
            v.co = Vector((v.co.x * size[0], v.co.y * size[1], v.co.z * size[2]))
    return fill


def rod_fill(radius, depth, segments=10):
    def fill(bm):
        bmesh.ops.create_cone(bm, cap_ends=True, segments=segments, radius1=radius, radius2=radius, depth=depth)
    return fill


def build_ladder(scene):
    iron = material("ladder_iron", (0.075, 0.058, 0.048), 0.6, 0.65)
    rung = material("ladder_rung", (0.26, 0.245, 0.22), 0.7, 0.45)
    frame = material("ladder_hatch_frame", (0.16, 0.16, 0.17), 0.2, 0.9)
    dark = bpy.data.materials.get("edicule_dark") or material("edicule_dark", (0.003, 0.004, 0.005), 0.0, 1.0)
    lamp = bpy.data.materials.get("trap_lantern") or material("trap_lantern", (1.0, 0.62, 0.14), 0.0, 0.4, (1.0, 0.62, 0.14))
    root = bpy.data.objects.new("cellar_ladder", None)
    scene.collection.objects.link(root)
    lean = math.atan(SLOPE)
    for side, x in (("West", -0.24), ("East", 0.24)):
        part(scene, root, "Rail" + side, box_fill((0.05, 0.07, TOP / math.cos(lean))), iron,
            (x, FOOT_Y + SLOPE * TOP / 2, TOP / 2), (-lean, 0.0, 0.0))
        part(scene, root, "Foot" + side, box_fill((0.14, 0.16, 0.024)), iron, (x, FOOT_Y, 0.012))
    index = 0
    z = RUNG_STEP
    while z < TOP - 0.15:
        part(scene, root, "Rung%02d" % index, rod_fill(0.019, 0.5), rung,
            (0.0, FOOT_Y + SLOPE * z, z), (0.0, math.pi / 2, 0.0))
        z += RUNG_STEP
        index += 1
    # The hatch in the ceiling: a concrete frame around a dark shaft, lid swung up.
    hatch_y = FOOT_Y + SLOPE * HATCH_Z
    for name, at, size in (("HatchFrameSouth", (0.0, hatch_y - 0.46, HATCH_Z), (1.0, 0.08, 0.12)),
            ("HatchFrameNorth", (0.0, hatch_y + 0.46, HATCH_Z), (1.0, 0.08, 0.12)),
            ("HatchFrameWest", (-0.46, hatch_y, HATCH_Z), (0.08, 0.84, 0.12)),
            ("HatchFrameEast", (0.46, hatch_y, HATCH_Z), (0.08, 0.84, 0.12))):
        part(scene, root, name, box_fill(size), frame, at)
    part(scene, root, "HatchShaft", box_fill((0.86, 0.86, 0.5)), dark, (0.0, hatch_y, HATCH_Z + 0.3))
    part(scene, root, "HatchLid", box_fill((0.82, 0.04, 0.7)), iron, (0.0, hatch_y + 0.5, HATCH_Z + 0.35))
    # A caged work lamp clamped to the west rail, halfway up.
    lamp_z = 2.3
    part(scene, root, "LampClamp", box_fill((0.08, 0.05, 0.05)), iron, (-0.31, FOOT_Y + SLOPE * lamp_z, lamp_z))
    part(scene, root, "Lamp", rod_fill(0.045, 0.11), lamp, (-0.36, FOOT_Y + SLOPE * lamp_z - 0.02, lamp_z - 0.06))
    return scene, root


def build_hatch(scene):
    root = bpy.data.scenes["rat_trapdoor"].objects["rat_trapdoor"]
    # Exported from the active scene: another scene has no depsgraph in
    # background, and the exporter crashes on it.
    for obj in [root] + list(root.children_recursive):
        if obj.name not in scene.collection.objects:
            scene.collection.objects.link(obj)
    # Wood becomes steel; the bricks of the shaft, concrete. The frame and the
    # stringers turn safety yellow, the metro's colour for a service opening.
    material("trap_plank", (0.06, 0.075, 0.07), 0.6, 0.55)
    material("trap_plank_light", (0.16, 0.17, 0.17), 0.7, 0.45)
    material("trap_frame", (0.45, 0.30, 0.03), 0.3, 0.6)
    material("trap_brick", (0.11, 0.105, 0.1), 0.0, 0.95)
    root.name = "service_hatch"
    return scene, root


def bounds(root):
    bpy.context.view_layer.update()
    low = Vector((1e9,) * 3)
    high = Vector((-1e9,) * 3)
    for obj in [root] + list(root.children_recursive):
        if obj.type != 'MESH':
            continue
        for corner in obj.bound_box:
            world = obj.matrix_world @ Vector(corner)
            low = Vector(map(min, low, world))
            high = Vector(map(max, high, world))
    return low, high


def export(scene, root, name):
    layer = bpy.context.view_layer
    for obj in scene.objects:
        obj.select_set(False, view_layer=layer)
    for obj in [root] + list(root.children_recursive):
        obj.select_set(True, view_layer=layer)
    folder = os.path.join(OUT, name)
    os.makedirs(folder, exist_ok=True)
    path = os.path.join(folder, name + ".glb")
    bpy.ops.export_scene.gltf(filepath=path, export_format='GLB', use_selection=True, use_active_scene=True,
        export_apply=True, export_cameras=False, export_lights=False)
    low, high = bounds(root)
    tall = high.z - low.z
    centre = (low + high) * 0.5
    # model_3d numbers, as the other passages: the factory fits the height to
    # 32 units x scale and recentres; the offsets put the cage back in place.
    print("MODEL %s scale=%.4f offset_x=%.2f offset_y=%.2f offset_z=%.2f size=%s" % (
        name, tall * 75.0 / 32.0, centre.x * 75.0, low.z * 75.0, -centre.y * 75.0,
        [round(v, 3) for v in (high - low)]))


def preview(scene, root, path, target, distance):
    shown = set([root] + list(root.children_recursive))
    for obj in scene.objects:
        obj.hide_render = obj not in shown
    camera = bpy.data.objects.new(root.name + "PreviewCamera", bpy.data.cameras.new(root.name + "PreviewCamera"))
    camera.data.lens = 102.08
    scene.collection.objects.link(camera)
    angle = math.radians(34.0)
    camera.location = Vector(target) + Vector((0.0, -math.cos(angle) * distance, math.sin(angle) * distance))
    camera.rotation_euler = (math.radians(56.0), 0.0, 0.0)
    scene.camera = camera
    scene.render.engine = 'BLENDER_WORKBENCH'
    scene.display.shading.color_type = 'MATERIAL'
    scene.display.shading.light = 'STUDIO'
    scene.render.resolution_x = 520
    scene.render.resolution_y = 520
    scene.render.filepath = path
    bpy.ops.render.render(write_still=True)


if __name__ == "__main__":
    args = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
    preview_dir = args[args.index("--preview") + 1] if "--preview" in args else ""
    for name, builder, target, distance in (("cellar_ladder", build_ladder, (0.0, 0.3, 2.4), 24.0),
            ("service_hatch", build_hatch, (0.0, 0.0, 0.0), 14.0)):
        scene = bpy.context.scene
        scene, root = builder(scene)
        export(scene, root, name)
        if preview_dir:
            preview(scene, root, os.path.join(preview_dir, name + ".png"), target, distance)
