#!/usr/bin/env python3
"""Build a clean parametric ring body and merge it with the original front ornament."""

from __future__ import annotations

import json
import math
import sys
from pathlib import Path

import bmesh
import bpy
from mathutils import Vector


def clear() -> None:
    bpy.ops.object.select_all(action="SELECT")
    bpy.ops.object.delete(use_global=False)


def make_material() -> bpy.types.Material:
    material = bpy.data.materials.new("TN Polished Silver Reconstructed")
    material.use_nodes = True
    material.use_backface_culling = True
    node = material.node_tree.nodes.get("Principled BSDF")
    node.inputs["Base Color"].default_value = (0.972, 0.960, 0.915, 1.0)
    node.inputs["Metallic"].default_value = 1.0
    node.inputs["Roughness"].default_value = 0.055
    return material


def make_clean_body() -> bpy.types.Object:
    ring_segments = 512
    profile_segments = 64
    center_radius = 0.900
    half_radial = 0.089
    half_height = 0.158
    exponent = 3.35
    vertices = []
    faces = []
    for ring_index in range(ring_segments):
        angle = 2 * math.pi * ring_index / ring_segments
        radial = Vector((math.cos(angle), math.sin(angle), 0.0))
        center_z = -0.012 + 0.006 * math.cos(angle) + 0.070 * math.sin(angle)
        center = radial * center_radius + Vector((0.0, 0.0, center_z))
        for profile_index in range(profile_segments):
            profile_angle = 2 * math.pi * profile_index / profile_segments
            cosine = math.cos(profile_angle)
            sine = math.sin(profile_angle)
            radial_offset = half_radial * math.copysign(abs(cosine) ** (2.0 / exponent), cosine)
            z_offset = half_height * math.copysign(abs(sine) ** (2.0 / exponent), sine)
            point = center + radial * radial_offset + Vector((0.0, 0.0, z_offset))
            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("Reconstructed clean body mesh")
    mesh.from_pydata(vertices, [], faces)
    mesh.update()
    obj = bpy.data.objects.new("Reconstructed clean body", mesh)
    bpy.context.collection.objects.link(obj)
    for polygon in mesh.polygons:
        polygon.use_smooth = True
    return obj


def make_box(name: str, center_y: float, size_y: float) -> bpy.types.Object:
    bpy.ops.mesh.primitive_cube_add(size=1.0, location=(0.0, center_y, 0.0))
    obj = bpy.context.object
    obj.name = name
    obj.dimensions = (3.0, size_y, 1.5)
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    return obj


def boolean_apply(target: bpy.types.Object, operand: bpy.types.Object, operation: str) -> None:
    bpy.context.view_layer.objects.active = target
    target.select_set(True)
    modifier = target.modifiers.new(name=f"{operation} {operand.name}", type="BOOLEAN")
    modifier.operation = operation
    modifier.solver = "EXACT"
    modifier.object = operand
    bpy.ops.object.modifier_apply(modifier=modifier.name)
    bpy.data.objects.remove(operand, do_unlink=True)


def import_original(path: Path) -> bpy.types.Object:
    bpy.ops.import_scene.gltf(filepath=str(path))
    obj = max((candidate for candidate in bpy.context.scene.objects if candidate.type == "MESH"), key=lambda item: len(item.data.vertices))
    obj.name = "Preserved original ornament"
    return obj


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 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()
    return result


def main() -> None:
    args = sys.argv[sys.argv.index("--") + 1 :]
    source = Path(args[0]).resolve()
    output = Path(args[1]).resolve()
    output.parent.mkdir(parents=True, exist_ok=True)
    clear()
    body = make_clean_body()
    original = import_original(source)

    # Remove the front section from the clean body and retain a slightly larger
    # original section so the exact union has a robust overlap zone.
    front_cut = make_box("Front body cut", center_y=-1.15, size_y=1.10)
    boolean_apply(body, front_cut, "DIFFERENCE")
    ornament_box = make_box("Ornament selection", center_y=-1.18, size_y=1.34)
    boolean_apply(original, ornament_box, "INTERSECT")
    boolean_apply(body, original, "UNION")

    body.name = "TN Silver Reconstructed V1"
    body.data.name = "TN Silver Reconstructed V1 Mesh"
    body.data.materials.clear()
    body.data.materials.append(make_material())
    for polygon in body.data.polygons:
        polygon.use_smooth = True
    export_glb(body, output)
    result = {
        "source": str(source), "output": str(output), "bytes": output.stat().st_size,
        "dimensions": [round(value, 6) for value in body.dimensions], **topology(body),
    }
    output.with_suffix(".json").write_text(json.dumps(result, indent=2), encoding="utf-8")
    print("TN_HYBRID", json.dumps(result))


if __name__ == "__main__":
    main()
