#!/usr/bin/env python3
"""Build Boolean-free continuous-surface candidates for the TN ring."""

from __future__ import annotations

import json
import math
import sys
from pathlib import Path

import bmesh
import bpy
from mathutils import Vector


CANDIDATES = {
    "soft": {
        "pinch": 0.57,
        "pinch_width_deg": 8.0,
        "cross_shift": 0.027,
        "groove_depth": 0.0105,
        "groove_width": 0.115,
        "front_bulge": 0.006,
    },
    "balanced": {
        "pinch": 0.67,
        "pinch_width_deg": 8.5,
        "cross_shift": 0.034,
        "groove_depth": 0.0135,
        "groove_width": 0.105,
        "front_bulge": 0.009,
    },
    "sculpted": {
        "pinch": 0.73,
        "pinch_width_deg": 9.5,
        "cross_shift": 0.042,
        "groove_depth": 0.0165,
        "groove_width": 0.095,
        "front_bulge": 0.012,
    },
    "knot": {
        "pinch": 0.80,
        "pinch_width_deg": 9.0,
        "cross_shift": 0.068,
        "cross_lift": 0.043,
        "knot_radius": 0.038,
        "knot_width_deg": 11.0,
        "groove_depth": 0.0185,
        "groove_width": 0.087,
        "front_bulge": 0.025,
    },
    "knot_soft": {
        "pinch": 0.75,
        "pinch_width_deg": 10.5,
        "cross_shift": 0.056,
        "cross_lift": 0.034,
        "knot_radius": 0.030,
        "knot_width_deg": 13.0,
        "groove_depth": 0.0160,
        "groove_width": 0.098,
        "front_bulge": 0.020,
    },
    "petal": {
        "pinch": 0.82,
        "pinch_width_deg": 8.0,
        "cross_shift": 0.060,
        "cross_lift": 0.030,
        "knot_radius": 0.030,
        "knot_width_deg": 10.0,
        "petal_tip": 0.075,
        "petal_width_deg": 7.0,
        "petal_height": 0.050,
        "groove_depth": 0.0180,
        "groove_width": 0.090,
        "front_bulge": 0.033,
    },
}


def smoothstep(edge0: float, edge1: float, value: float) -> float:
    t = max(0.0, min(1.0, (value - edge0) / (edge1 - edge0)))
    return t * t * (3.0 - 2.0 * t)


def wrap_angle(angle: float) -> float:
    return (angle + math.pi) % (2.0 * math.pi) - math.pi


def signed_power(value: float, power: float) -> float:
    return math.copysign(abs(value) ** power, value)


def polished_silver() -> bpy.types.Material:
    material = bpy.data.materials.get("TN Mirror Silver V2") or bpy.data.materials.new("TN Mirror Silver V2")
    material.use_nodes = True
    material.use_backface_culling = True
    material.diffuse_color = (0.96, 0.97, 0.985, 1.0)
    bsdf = material.node_tree.nodes.get("Principled BSDF")
    bsdf.inputs["Base Color"].default_value = (0.96, 0.97, 0.985, 1.0)
    bsdf.inputs["Metallic"].default_value = 1.0
    bsdf.inputs["Roughness"].default_value = 0.052
    if "Coat Weight" in bsdf.inputs:
        bsdf.inputs["Coat Weight"].default_value = 0.0
    return material


def make_ring(name: str, config: dict, ring_segments: int = 512, profile_segments: int = 96) -> bpy.types.Object:
    center_radius = 0.902
    half_radial = 0.087
    half_height = 0.155
    exponent = 3.15
    front_angle = -math.pi / 2.0
    pinch_width = math.radians(config["pinch_width_deg"])
    twist_width = math.radians(23.0)

    vertices: list[tuple[float, float, float]] = []
    faces: list[tuple[int, int, int, int]] = []

    for ring_index in range(ring_segments):
        theta = 2.0 * math.pi * ring_index / ring_segments
        delta = wrap_angle(theta - front_angle)
        radial = Vector((math.cos(theta), math.sin(theta), 0.0))
        center_z = -0.012 + 0.006 * math.cos(theta) + 0.070 * math.sin(theta)

        narrow_front = math.exp(-0.5 * (delta / pinch_width) ** 2)
        broad_front = math.exp(-0.5 * (delta / twist_width) ** 2)
        signed_cross = (delta / twist_width) * broad_front
        knot_width = math.radians(config.get("knot_width_deg", 10.0))
        knot_focus = math.exp(-0.5 * (delta / knot_width) ** 2)
        petal_width = math.radians(config.get("petal_width_deg", 7.0))
        petal_focus = math.exp(-0.5 * (delta / petal_width) ** 2)

        for profile_index in range(profile_segments):
            phi = 2.0 * math.pi * profile_index / profile_segments
            raw_x = math.cos(phi)
            raw_y = math.sin(phi)
            x = signed_power(raw_x, 2.0 / exponent)
            y = signed_power(raw_y, 2.0 / exponent)

            # Only the presentation face participates in the knot and grooves.
            outer = smoothstep(-0.10, 0.82, x)
            outer_focus = outer ** 1.55

            # The front becomes a smooth tapered wedge, while the inner wall
            # keeps its full height.  This creates the knot in one continuous
            # manifold surface instead of attaching a separate ornament.
            local_z = half_height * y
            local_z *= 1.0 - config["pinch"] * narrow_front * outer_focus
            local_z += -config["cross_shift"] * signed_cross * outer_focus
            # On the outer presentation face, the upper/lower halves sweep in
            # opposite directions and converge into a rounded central knot.
            local_z += config.get("cross_lift", 0.0) * math.sin(delta / knot_width) * knot_focus * y * outer_focus
            local_z += config.get("knot_radius", 0.0) * knot_focus * math.copysign(abs(y) ** 0.7, y) * outer_focus

            radial_offset = half_radial * x
            face_rounding = 0.0048 * outer_focus * max(0.0, 1.0 - y * y)
            radial_offset += face_rounding

            # Two polished channels sweep into one another near the front.
            # Their analytic Gaussian profiles keep reflection lines fair.
            approach = math.exp(-0.5 * (delta / math.radians(15.0)) ** 2)
            separation = 0.285 * (1.0 - 0.72 * approach)
            center_bias = -0.055 * math.sin(delta) * broad_front
            groove_a = separation + center_bias
            groove_b = -separation + center_bias
            groove_profile = (
                math.exp(-0.5 * ((y - groove_a) / config["groove_width"]) ** 2)
                + math.exp(-0.5 * ((y - groove_b) / config["groove_width"]) ** 2)
            )
            groove_fade = outer_focus * (0.60 + 0.40 * (1.0 - narrow_front))
            radial_offset -= config["groove_depth"] * groove_profile * groove_fade

            # A restrained crown gives the crossing enough silhouette without
            # creating the faceted, pasted-on look of the V1 Boolean junction.
            radial_offset += config["front_bulge"] * narrow_front * outer_focus * max(0.0, 1.0 - 0.5 * y * y)
            radial_offset += config.get("knot_radius", 0.0) * 0.55 * knot_focus * outer_focus * max(0.0, 1.0 - 0.75 * y * y)
            petal_profile = max(0.0, 1.0 - (abs(y) / 0.72) ** 1.8)
            radial_offset += config.get("petal_tip", 0.0) * petal_focus * outer_focus * petal_profile
            local_z += config.get("petal_height", 0.0) * petal_focus * outer_focus * y * petal_profile

            point = radial * (center_radius + radial_offset) + Vector((0.0, 0.0, center_z + local_z))
            vertices.append(tuple(point))

    for ring_index in range(ring_segments):
        next_ring = (ring_index + 1) % ring_segments
        for profile_index in range(profile_segments):
            next_profile = (profile_index + 1) % profile_segments
            a = ring_index * profile_segments + profile_index
            b = next_ring * profile_segments + profile_index
            c = next_ring * profile_segments + next_profile
            d = ring_index * profile_segments + next_profile
            faces.append((a, b, c, d))

    mesh = bpy.data.meshes.new(f"{name} mesh")
    mesh.from_pydata(vertices, [], faces)
    mesh.update()
    obj = bpy.data.objects.new(name, mesh)
    bpy.context.collection.objects.link(obj)
    mesh.materials.append(polished_silver())
    for polygon in mesh.polygons:
        polygon.use_smooth = True
    return obj


def topology(obj: bpy.types.Object) -> dict:
    editable = bmesh.new()
    editable.from_mesh(obj.data)
    result = {
        "vertices": len(editable.verts),
        "faces": len(editable.faces),
        "boundary_edges": sum(1 for edge in editable.edges if edge.is_boundary),
        "non_manifold_edges": sum(1 for edge in editable.edges if not edge.is_manifold),
    }
    editable.free()
    obj.data.calc_loop_triangles()
    result["triangles"] = len(obj.data.loop_triangles)
    return result


def export_glb(obj: bpy.types.Object, path: Path) -> None:
    bpy.ops.object.select_all(action="DESELECT")
    obj.select_set(True)
    bpy.context.view_layer.objects.active = obj
    bpy.ops.export_scene.gltf(
        filepath=str(path),
        export_format="GLB",
        use_selection=True,
        export_apply=True,
        export_yup=True,
        export_normals=True,
        export_tangents=False,
        export_texcoords=False,
        export_materials="EXPORT",
        export_animations=False,
        export_cameras=False,
        export_lights=False,
        export_draco_mesh_compression_enable=False,
    )


def main() -> None:
    args = sys.argv[sys.argv.index("--") + 1 :]
    output = Path(args[0]).resolve()
    output.mkdir(parents=True, exist_ok=True)
    bpy.ops.object.select_all(action="SELECT")
    bpy.ops.object.delete(use_global=False)
    results = {}
    for name, config in CANDIDATES.items():
        obj = make_ring(f"TN Silver V2 {name.title()}", config)
        path = output / f"tn-silver-v2-{name}.glb"
        export_glb(obj, path)
        results[name] = {
            **config,
            **topology(obj),
            "dimensions": [round(value, 6) for value in obj.dimensions],
            "bytes": path.stat().st_size,
            "path": str(path),
        }
        bpy.data.objects.remove(obj, do_unlink=True)
    manifest = output / "candidates.json"
    manifest.write_text(json.dumps(results, indent=2), encoding="utf-8")
    print("TN_V2_CANDIDATES", json.dumps(results))


if __name__ == "__main__":
    main()
