"""Bake a low-frequency wearer lighting atlas for the diffuse study."""

from __future__ import annotations

import argparse
import sys
from pathlib import Path

import bpy


def math_value(nodes, links, operation, source, value):
    node = nodes.new("ShaderNodeMath")
    node.operation = operation
    links.new(source, node.inputs[0])
    node.inputs[1].default_value = value
    return node.outputs[0]


def math_pair(nodes, links, operation, first, second):
    node = nodes.new("ShaderNodeMath")
    node.operation = operation
    links.new(first, node.inputs[0])
    links.new(second, node.inputs[1])
    return node.outputs[0]


def add_constant(nodes, links, source, value):
    node = nodes.new("ShaderNodeMath")
    node.operation = "ADD"
    links.new(source, node.inputs[0])
    node.inputs[1].default_value = value
    return node.outputs[0]


def smooth_spot(nodes, links, x_socket, z_socket, center_x, center_z, radius):
    dx = add_constant(nodes, links, x_socket, -center_x)
    dz = add_constant(nodes, links, z_socket, -center_z)
    dx_squared = math_pair(nodes, links, "MULTIPLY", dx, dx)
    dz_squared = math_pair(nodes, links, "MULTIPLY", dz, dz)
    distance_squared = math_pair(nodes, links, "ADD", dx_squared, dz_squared)

    distance = nodes.new("ShaderNodeMath")
    distance.operation = "SQRT"
    links.new(distance_squared, distance.inputs[0])

    falloff = nodes.new("ShaderNodeMapRange")
    falloff.clamp = True
    falloff.interpolation_type = "SMOOTHERSTEP"
    falloff.inputs[1].default_value = 0.0
    falloff.inputs[2].default_value = radius
    falloff.inputs[3].default_value = 1.0
    falloff.inputs[4].default_value = 0.0
    links.new(distance.outputs[0], falloff.inputs[0])
    return falloff.outputs[0]


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--model", required=True, type=Path)
    parser.add_argument("--output", required=True, type=Path)
    parser.add_argument("--size", type=int, default=2048)
    argv = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else []
    args = parser.parse_args(argv)

    bpy.ops.wm.read_factory_settings(use_empty=True)
    bpy.ops.import_scene.gltf(filepath=str(args.model))
    meshes = [obj for obj in bpy.context.scene.objects if obj.type == "MESH"]
    if len(meshes) != 1:
        raise RuntimeError(f"Expected one mesh, found {len(meshes)}")
    wearer = meshes[0]

    bpy.ops.object.select_all(action="DESELECT")
    wearer.select_set(True)
    bpy.context.view_layer.objects.active = wearer
    bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)

    if not wearer.data.uv_layers:
        raise RuntimeError("The wearer GLB has no UV map")
    wearer.data.uv_layers.active_index = 0

    source_images = [
        node.image
        for material in wearer.data.materials
        if material and material.use_nodes
        for node in material.node_tree.nodes
        if node.type == "TEX_IMAGE" and node.image is not None
    ]
    if not source_images:
        raise RuntimeError("The wearer GLB has no embedded base-color texture")
    source = max(source_images, key=lambda image: image.size[0] * image.size[1])
    source.colorspace_settings.name = "sRGB"

    baked = bpy.data.images.new(
        "WearerLighting",
        width=args.size,
        height=args.size,
        alpha=True,
        float_buffer=False,
    )
    baked.colorspace_settings.name = "sRGB"
    baked.generated_color = (0.0, 0.0, 0.0, 1.0)

    material = bpy.data.materials.new("Wearer lighting bake")
    material.use_nodes = True
    nodes = material.node_tree.nodes
    links = material.node_tree.links
    nodes.clear()

    output = nodes.new("ShaderNodeOutputMaterial")
    emission = nodes.new("ShaderNodeEmission")
    source_texture = nodes.new("ShaderNodeTexImage")
    source_texture.image = source
    source_texture.interpolation = "Linear"
    source_texture.extension = "EXTEND"
    target_texture = nodes.new("ShaderNodeTexImage")
    target_texture.image = baked
    target_texture.select = True
    nodes.active = target_texture

    luminance = nodes.new("ShaderNodeRGBToBW")
    links.new(source_texture.outputs["Color"], luminance.inputs["Color"])

    hsv = nodes.new("ShaderNodeSeparateColor")
    hsv.mode = "HSV"
    links.new(source_texture.outputs["Color"], hsv.inputs["Color"])
    saturation = hsv.outputs[1]

    # The source texture contributes only broad semantic separation: dark hair,
    # mid-tone skin and restrained light clothing. Fine albedo detail is not
    # emitted directly into the final web composite.
    luma_term = math_value(nodes, links, "MULTIPLY", luminance.outputs["Val"], 0.12)
    saturation_term = math_value(nodes, links, "MULTIPLY", saturation, 0.60)
    semantic = math_pair(nodes, links, "ADD", luma_term, saturation_term)
    semantic = add_constant(nodes, links, semantic, 0.08)

    geometry = nodes.new("ShaderNodeNewGeometry")
    position = nodes.new("ShaderNodeSeparateXYZ")
    links.new(geometry.outputs["Position"], position.inputs["Vector"])

    # Object-space emphasis follows the approved order:
    # raised hand/ring, face, clothing, hair.
    hand_spot = smooth_spot(
        nodes,
        links,
        position.outputs["X"],
        position.outputs["Z"],
        center_x=-0.26,
        center_z=0.15,
        radius=0.34,
    )
    face_spot = smooth_spot(
        nodes,
        links,
        position.outputs["X"],
        position.outputs["Z"],
        center_x=-0.05,
        center_z=0.50,
        radius=0.43,
    )
    hand_spot = math_value(nodes, links, "MULTIPLY", hand_spot, 0.70)
    face_spot = math_value(nodes, links, "MULTIPLY", face_spot, 0.42)
    focus = math_pair(nodes, links, "ADD", hand_spot, face_spot)

    focus_gate = math_value(nodes, links, "MULTIPLY", semantic, 1.40)
    focus_gate = add_constant(nodes, links, focus_gate, 0.12)
    focus = math_pair(nodes, links, "MULTIPLY", focus, focus_gate)

    normal_dot = nodes.new("ShaderNodeVectorMath")
    normal_dot.operation = "DOT_PRODUCT"
    normal_dot.inputs[1].default_value = (0.0, -1.0, 0.0)
    links.new(geometry.outputs["Normal"], normal_dot.inputs[0])
    front = nodes.new("ShaderNodeMath")
    front.operation = "MAXIMUM"
    front.inputs[1].default_value = 0.0
    links.new(normal_dot.outputs["Value"], front.inputs[0])
    front_term = math_value(nodes, links, "MULTIPLY", front.outputs[0], 0.10)

    semantic_term = math_value(nodes, links, "MULTIPLY", semantic, 0.65)
    focus_term = math_value(nodes, links, "MULTIPLY", focus, 0.75)
    lighting = math_pair(nodes, links, "ADD", semantic_term, focus_term)
    lighting = math_pair(nodes, links, "ADD", lighting, front_term)

    # Suppress dark hair without carrying its fine texture into the result.
    luma_gate = math_value(nodes, links, "MULTIPLY", luminance.outputs["Val"], 0.65)
    luma_gate = add_constant(nodes, links, luma_gate, 0.35)
    lighting = math_pair(nodes, links, "MULTIPLY", lighting, luma_gate)
    lighting = math_value(nodes, links, "MULTIPLY", lighting, 0.32)

    clamp = nodes.new("ShaderNodeClamp")
    clamp.inputs[1].default_value = 0.0
    clamp.inputs[2].default_value = 1.0
    links.new(lighting, clamp.inputs[0])
    links.new(clamp.outputs[0], emission.inputs["Color"])
    links.new(emission.outputs["Emission"], output.inputs["Surface"])

    wearer.data.materials.clear()
    wearer.data.materials.append(material)

    scene = bpy.context.scene
    scene.render.engine = "CYCLES"
    scene.cycles.device = "CPU"
    scene.cycles.samples = 1
    scene.render.image_settings.file_format = "PNG"
    scene.render.image_settings.color_mode = "RGBA"
    scene.render.image_settings.color_depth = "8"
    scene.render.film_transparent = True
    bpy.ops.object.bake(type="EMIT", margin=32, use_clear=True)

    args.output.parent.mkdir(parents=True, exist_ok=True)
    baked.filepath_raw = str(args.output)
    baked.file_format = "PNG"
    baked.save()
    print(f"saved={args.output} source={source.name} size={args.size}x{args.size}")


if __name__ == "__main__":
    main()
