#!/usr/bin/env python3
"""Build selectively faired ring-body candidates while preserving the front ornament."""

from __future__ import annotations

import json
import math
import sys
from pathlib import Path

import bpy


CANDIDATES = {
    "gentle": (0.18, 16),
    "balanced": (0.24, 32),
    "strong": (0.30, 52),
}


def angular_distance(a: float, b: float) -> float:
    return abs((a - b + math.pi) % (2 * math.pi) - math.pi)


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


def import_ring(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))
    bpy.context.view_layer.objects.active = obj
    obj.select_set(True)
    return obj


def assign_body_weights(obj: bpy.types.Object) -> bpy.types.VertexGroup:
    group = obj.vertex_groups.new(name="Body fairing mask")
    front = -math.pi / 2
    protected = math.radians(42)
    fully_faired = math.radians(72)
    for vertex in obj.data.vertices:
        coordinate = obj.matrix_world @ vertex.co
        distance = angular_distance(math.atan2(coordinate.y, coordinate.x), front)
        if distance <= protected:
            weight = 0.0
        elif distance >= fully_faired:
            weight = 1.0
        else:
            t = (distance - protected) / (fully_faired - protected)
            weight = t * t * (3.0 - 2.0 * t)
        group.add([vertex.index], weight, "REPLACE")
    return group


def apply_fairing(obj: bpy.types.Object, strength: float, iterations: int) -> None:
    group = assign_body_weights(obj)
    modifier = obj.modifiers.new(name="Body curvature reconstruction", type="LAPLACIANSMOOTH")
    modifier.vertex_group = group.name
    modifier.lambda_factor = strength
    modifier.lambda_border = strength * 0.25
    modifier.iterations = iterations
    modifier.use_volume_preserve = True
    modifier.use_normalized = True
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.modifier_apply(modifier=modifier.name)
    for polygon in obj.data.polygons:
        polygon.use_smooth = True


def export(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 :]
    source = Path(args[0]).resolve()
    output = Path(args[1]).resolve()
    output.mkdir(parents=True, exist_ok=True)
    result = {}
    for name, (strength, iterations) in CANDIDATES.items():
        clear()
        obj = import_ring(source)
        apply_fairing(obj, strength, iterations)
        path = output / f"reconstructed-{name}.glb"
        export(obj, path)
        obj.data.calc_loop_triangles()
        result[name] = {
            "path": str(path),
            "strength": strength,
            "iterations": iterations,
            "vertices": len(obj.data.vertices),
            "triangles": len(obj.data.loop_triangles),
            "bytes": path.stat().st_size,
        }
    (output / "candidates.json").write_text(json.dumps(result, indent=2), encoding="utf-8")
    print("TN_CANDIDATES", json.dumps(result))


if __name__ == "__main__":
    main()
