"""Clout original camera movement and case-state scenes
Run in a NEW background process only:
blender --background --factory-startup --python camera-movement-studies.py -- --output-root NEW_DIRECTORY
Writes PNGs and a manifest; never saves a Blender project or edits the foreground application.
"""

import argparse
import csv
import json
import math
import sys
from pathlib import Path

import bpy
from mathutils import Vector

if not bpy.app.background:
    raise RuntimeError("Use a new background factory-startup process, not an open project")
p = argparse.ArgumentParser()
p.add_argument("--output-root", required=True)
args = p.parse_args(sys.argv[sys.argv.index("--") + 1 :])
out = Path(args.output_root)
out.mkdir(parents=True, exist_ok=False)


def reset():
    bpy.ops.object.select_all(action="SELECT")
    bpy.ops.object.delete(use_global=False)
    s = bpy.context.scene
    s.render.engine = "CYCLES"
    s.cycles.device = "CPU"
    s.cycles.samples = 48
    s.cycles.use_denoising = True
    s.cycles.seed = 17
    s.render.resolution_x = 900
    s.render.resolution_y = 900
    s.render.resolution_percentage = 100
    s.render.image_settings.file_format = "PNG"
    s.world.color = (0.03, 0.03, 0.03)
    s.view_settings.view_transform = "AgX"
    s.render.film_transparent = False
    return s


def mat(name, color, sss=0, scale=0.12):
    m = bpy.data.materials.new(name)
    m.use_nodes = True
    bs = m.node_tree.nodes.get("Principled BSDF")
    bs.inputs["Base Color"].default_value = (*color, 1)
    bs.inputs["Roughness"].default_value = 0.36
    bs.inputs["Subsurface Weight"].default_value = sss
    bs.inputs["Subsurface Scale"].default_value = scale
    bs.inputs["Subsurface Radius"].default_value = (1, 0.35, 0.15)
    return m


def assign(obj, m):
    obj.data.materials.clear()
    obj.data.materials.append(m)


def camera(pos, target, scale):
    bpy.ops.object.camera_add(location=pos)
    c = bpy.context.object
    c.rotation_euler = (Vector(target) - c.location).to_track_quat("-Z", "Y").to_euler()
    c.data.type = "ORTHO"
    c.data.ortho_scale = scale
    bpy.context.scene.camera = c


def area(pos, target, power, size):
    bpy.ops.object.light_add(type="AREA", location=pos)
    light = bpy.context.object
    light.data.energy = power
    light.data.shape = "DISK"
    light.data.size = size
    light.rotation_euler = (Vector(target) - light.location).to_track_quat("-Z", "Y").to_euler()


def render(name):
    bpy.context.scene.render.filepath = str(out / (name + ".png"))
    bpy.ops.render.render(write_still=True)


def box(pos, scale, material):
    bpy.ops.mesh.primitive_cube_add(size=1, location=pos)
    ob = bpy.context.object
    ob.scale = scale
    assign(ob, material)
    return ob


def sphere(pos, radius, material):
    bpy.ops.mesh.primitive_uv_sphere_add(segments=20, ring_count=12, radius=radius, location=pos)
    ob = bpy.context.object
    assign(ob, material)
    for face in ob.data.polygons:
        face.use_smooth = True
    return ob


def aim(cam, target):
    cam.rotation_euler = (Vector(target) - cam.location).to_track_quat("-Z", "Y").to_euler()


s = reset()
s.render.resolution_x = 640
s.render.resolution_y = 360
s.cycles.samples = 8
s.render.fps = 24
teal = mat("Courier teal", (0.025, 0.33, 0.38))
orange = mat("Courier orange", (0.8, 0.3, 0.07))
cream = mat("Floor and trim", (0.42, 0.43, 0.39))
wall = mat("Archive wall", (0.14, 0.19, 0.22))
box((0, 1, -0.1), (16, 16, 0.2), cream)
box((0, 5, 2), (16, 0.2, 5), wall)
for x in [-4, -2, 0, 2, 4]:
    box((x, 4.83, 1.8), (0.12, 0.08, 3.1), orange)
for x in [-2.6, 2.6]:
    box((x, -2, 1.5), (0.25, 0.25, 3), wall)
for y in [-1, 1, 3]:
    box((0, y, 0.008), (12, 0.025, 0.015), teal)
body = box((0, 0, 0.9), (0.65, 0.45, 0.9), teal)
head = sphere((0, 0, 1.6), 0.27, orange)
left = box((-0.19, 0, 0.3), (0.22, 0.3, 0.6), wall)
right = box((0.19, 0, 0.3), (0.22, 0.3, 0.6), wall)
subject = [body, head, left, right]
rest = {o.name: o.location.copy() for o in subject}
area((-3, -4, 7), (0, 0, 1), 1400, 5)
camera((0, -6, 1), (0, 0, 1), 8)
cam = s.camera
cam.data.type = "PERSP"
cam.data.lens = 32
cam.data.sensor_width = 36
rows = []
for mode in ["pan", "track"]:
    folder = out / mode
    folder.mkdir()
    for f in range(96):
        x = -1.5 + 3 * f / 95
        for ob in subject:
            ob.location = rest[ob.name] + Vector((x, 0, 0))
        cam.location = (0 if mode == "pan" else x, -6, 1)
        aim(cam, (x, 0, 1))
        bpy.context.view_layer.update()
        rows.append(
            {
                "mode": mode,
                "frame": f,
                "seconds": f / 24,
                "subjectX": x,
                "cameraX": cam.location.x,
                "cameraY": -6,
                "cameraZ": 1,
                "targetX": x,
                "targetY": 0,
                "targetZ": 1,
                "rightPositiveAimDegrees": math.degrees(math.atan2(x - cam.location.x, 6)),
            }
        )
        s.render.filepath = str(folder / f"{f:03d}.png")
        bpy.ops.render.render(write_still=True)
with (out / "camera-movement-actual-path.csv").open("w", newline="") as file:
    writer = csv.DictWriter(file, fieldnames=list(rows[0]), lineterminator="\n")
    writer.writeheader()
    writer.writerows(rows)
# A separate still scene visualizes a desired object-state edit, not any AI output
s = reset()
s.render.resolution_x = 1280
s.render.resolution_y = 720
s.cycles.samples = 32
teal = mat("Case shell", (0.02, 0.25, 0.29))
inside = mat("Case lining", (0.26, 0.1, 0.035))
cream = mat("Neutral table", (0.4, 0.39, 0.34))
brass = mat("Single body latch", (0.5, 0.28, 0.06))
box((0, 0, -0.04), (10, 10, 0.08), cream)
box((0, 0, 0.13), (2.6, 1.6, 0.22), teal)
for pos, scale in [
    ((0, -0.73, 0.5), (2.6, 0.14, 0.65)),
    ((0, 0.73, 0.5), (2.6, 0.14, 0.65)),
    ((-1.23, 0, 0.5), (0.14, 1.6, 0.65)),
    ((1.23, 0, 0.5), (0.14, 1.6, 0.65)),
]:
    box(pos, scale, teal)
box((0, 0, 0.26), (2.28, 1.28, 0.04), inside)
box((0, -0.82, 0.62), (0.23, 0.06, 0.22), brass)
bpy.ops.object.empty_add(type="PLAIN_AXES", location=(0, 0.8, 0.86))
hinge = bpy.context.object
lid = box((0, 0, 0), (2.6, 1.6, 0.12), teal)
lid.parent = hinge
lid.location = (0, -0.8, 0)
area((-3, -4, 6), (0, 0, 0.5), 900, 4)
camera((3.7, -5.4, 3.4), (0, 0, 1), 6)
render("chronoedit-case-closed-source")
hinge.rotation_euler.x = math.radians(-65)
render("chronoedit-case-open-target")
manifest = {
    "author": "Clout",
    "blenderVersion": bpy.app.version_string,
    "engine": "Cycles CPU",
    "cameraStudy": {
        "frames": 96,
        "fps": 24,
        "durationSeconds": 4,
        "resolution": [640, 360],
        "samples": 8,
        "path": "subject X -1.5 to 1.5 linearly over frame indices 0 through 95",
        "pan": "Camera position fixed at (0,-6,1), aim follows subject",
        "track": "Camera X follows subject at fixed offset (0,-6,1), aim follows subject",
        "lensMm": 32,
        "sensorWidthMm": 36,
        "motionBlur": False,
    },
    "caseStudy": {
        "resolution": [1280, 720],
        "samples": 32,
        "change": "Only the lid hinge rotates from 0 to -65 degrees about X",
        "unchanged": "Body geometry, single body latch, camera, materials and light",
        "note": "Authored desired edit states, not ChronoEdit outputs or physical consistency benchmarks",
    },
}
(out / "camera-edit-render-manifest.json").write_text(json.dumps(manifest, indent=2))
