"""Original Clout Eevee shader study
Run with Blender 5: blender --background --python render-cel-shading-study.py -- /new/output
Authored geometry and native shading, no model inference
"""

import json
import math
import sys
from pathlib import Path

import bpy
from mathutils import Vector

out = Path(sys.argv[sys.argv.index("--") + 1])
out.mkdir(parents=True, exist_ok=False)
bpy.ops.wm.read_factory_settings(use_empty=True)
s = bpy.context.scene
s.render.engine = "BLENDER_EEVEE"
s.render.resolution_x = 960
s.render.resolution_y = 640
s.render.resolution_percentage = 100
s.render.image_settings.file_format = "PNG"
s.render.fps = 12
s.frame_start = 1
s.frame_end = 24
s.world = bpy.data.worlds.new("Dark studio")
s.world.use_nodes = True
s.world.node_tree.nodes.get("Background").inputs["Strength"].default_value = 0
s.view_settings.view_transform = "Standard"
s.view_settings.look = "None"
s.view_settings.exposure = 0
s.view_settings.gamma = 1
s.render.film_transparent = False


def flat(name, color):
    m = bpy.data.materials.new(name)
    m.use_nodes = True
    n = m.node_tree.nodes
    n.clear()
    e = n.new("ShaderNodeEmission")
    e.inputs["Color"].default_value = (*color, 1)
    o = n.new("ShaderNodeOutputMaterial")
    m.node_tree.links.new(e.outputs[0], o.inputs["Surface"])
    return m


palette = [
    (0.008, 0.04, 0.065, 1),
    (0.015, 0.16, 0.20, 1),
    (0.04, 0.42, 0.44, 1),
    (0.30, 0.82, 0.64, 1),
]
records = {
    "kind": "Original native Eevee toon shading study with no AI inference",
    "blender": bpy.app.version_string,
    "engine": s.render.engine,
    "viewTransform": "Standard",
    "materials": {},
}


def toon(name, stops, interpolation):
    m = bpy.data.materials.new(name)
    m.use_fake_user = True
    m.use_nodes = True
    n = m.node_tree.nodes
    n.clear()
    d = n.new("ShaderNodeBsdfDiffuse")
    d.location = (-600, 0)
    d.inputs["Color"].default_value = (1, 1, 1, 1)
    d.inputs["Roughness"].default_value = 0
    rgb = n.new("ShaderNodeShaderToRGB")
    rgb.location = (-380, 0)
    ramp = n.new("ShaderNodeValToRGB")
    ramp.location = (-150, 0)
    cr = ramp.color_ramp
    cr.interpolation = interpolation
    while len(cr.elements) > 1:
        cr.elements.remove(cr.elements[-1])
    for i, (pos, color) in enumerate(stops):
        e = cr.elements[0] if i == 0 else cr.elements.new(pos)
        e.position = pos
        e.color = color
    em = n.new("ShaderNodeEmission")
    em.location = (150, 0)
    o = n.new("ShaderNodeOutputMaterial")
    o.location = (380, 0)
    links = m.node_tree.links
    links.new(d.outputs[0], rgb.inputs[0])
    links.new(rgb.outputs["Color"], ramp.inputs["Fac"])
    links.new(ramp.outputs["Color"], em.inputs["Color"])
    links.new(em.outputs[0], o.inputs["Surface"])
    records["materials"][name] = {
        "interpolation": cr.interpolation,
        "stops": [{"position": e.position, "linearRGBA": list(e.color)} for e in cr.elements],
        "nodes": [x.bl_idname for x in n],
    }
    return m


mats = {
    "two-band": toon("Two band toon", [(0, palette[0]), (0.42, palette[3])], "CONSTANT"),
    "four-band": toon(
        "Four band toon",
        [(0, palette[0]), (0.20, palette[1]), (0.42, palette[2]), (0.67, palette[3])],
        "CONSTANT",
    ),
    "smooth": toon("Smooth ramp reference", [(0, palette[0]), (1, palette[3])], "LINEAR"),
}
body = []


def finish(o, name):
    o.name = name
    for p in o.data.polygons:
        p.use_smooth = True
    o.data.materials.append(mats["four-band"])
    body.append(o)
    return o


bpy.ops.mesh.primitive_uv_sphere_add(segments=64, ring_count=32, radius=1, location=(0, 0, 1.15))
o = finish(bpy.context.object, "Original kettle body")
o.scale = (1, 0.8, 0.9)
bpy.ops.mesh.primitive_cone_add(
    vertices=64, radius1=0.28, radius2=0.17, depth=0.85, location=(-1.05, 0, 1.55)
)
o = finish(bpy.context.object, "Short spout")
o.rotation_euler = (Vector((-1, 0, 0.55))).to_track_quat("Z", "Y").to_euler()
bpy.ops.mesh.primitive_uv_sphere_add(segments=48, ring_count=24, radius=1, location=(0, 0, 1.98))
o = finish(bpy.context.object, "Domed lid")
o.scale = (0.60, 0.49, 0.12)
bpy.ops.mesh.primitive_uv_sphere_add(segments=32, ring_count=16, radius=0.13, location=(0, 0, 2.16))
finish(bpy.context.object, "Lid knob")
bpy.ops.mesh.primitive_torus_add(
    major_segments=64,
    minor_segments=16,
    major_radius=0.55,
    minor_radius=0.105,
    location=(1.02, 0, 1.35),
    rotation=(math.pi / 2, 0, 0),
)
o = bpy.context.object
o.name = "Orange handle"
o.data.materials.append(flat("Handle color", (0.85, 0.25, 0.035)))
for p in o.data.polygons:
    p.use_smooth = True
bpy.ops.mesh.primitive_plane_add(size=200, location=(0, 0, 0.22))
bpy.context.object.name = "Unlit backdrop"
bpy.context.object.data.materials.append(flat("Backdrop", (0.012, 0.020, 0.035)))
bpy.ops.object.camera_add(location=(4, -7, 3.4))
c = bpy.context.object
c.rotation_euler = (Vector((0, 0, 1.2)) - c.location).to_track_quat("-Z", "Y").to_euler()
c.data.type = "ORTHO"
c.data.ortho_scale = 4.7
s.camera = c
bpy.ops.object.light_add(type="SUN")
light = bpy.context.object
light.name = "Animated key sun"
light.data.energy = 2.5
light.data.angle = 0
for frame, angle in [(1, -55), (24, 55)]:
    light.rotation_euler = (math.radians(35), math.radians(angle), math.radians(-25))
    light.keyframe_insert(data_path="rotation_euler", frame=frame)
# Constant angular rate within the authored two-second clip
for layer in light.animation_data.action.layers:
    for strip in layer.strips:
        for bag in strip.channelbags:
            for curve in bag.fcurves:
                for k in curve.keyframe_points:
                    k.interpolation = "LINEAR"
# Camera-parented label remains visibly identified as native authored work
font = bpy.data.curves.new("Native label", "FONT")
font.body = "Original Eevee shader study | No AI inference"
font.size = 0.095
label = bpy.data.objects.new("Native study label", font)
s.collection.objects.link(label)
label.parent = c
label.location = (-2.15, 1.36, -4)
label.data.materials.append(flat("Label", (0.7, 0.8, 0.9)))
for mode in ["two-band", "four-band", "smooth"]:
    for obj in body:
        obj.data.materials[0] = mats[mode]
    s.frame_set(1)
    s.render.filepath = str(out / f"cel-{mode}.png")
    bpy.ops.render.render(write_still=True)
for obj in body:
    obj.data.materials[0] = mats["four-band"]
s.frame_set(24)
s.render.filepath = str(out / "cel-light-end.png")
bpy.ops.render.render(write_still=True)
s.frame_set(1)
records["clip"] = {
    "fps": 12,
    "frames": 24,
    "durationSeconds": 2,
    "resolution": [960, 640],
    "lightEnergy": light.data.energy,
    "lightAnglesDegrees": [-55, 55],
    "material": "Four band toon",
    "bodyMeshes": [o.name for o in body],
}
bpy.ops.wm.save_as_mainfile(filepath=str(out / "cel-shading-study.blend"))
(out / "cel-shading-measurements.json").write_text(json.dumps(records, indent=2) + "\n")
(out / "frames").mkdir()
for frame in range(1, 25):
    s.frame_set(frame)
    s.render.filepath = str(out / "frames" / f"{frame:04}.png")
    bpy.ops.render.render(write_still=True)
print(json.dumps(records["clip"]))
