import bpy
import bmesh
import json
import mathutils
import statistics
import sys
from pathlib import Path


source_path = Path(sys.argv[sys.argv.index("--") + 1])
output_dir = Path(sys.argv[sys.argv.index("--") + 2])
output_dir.mkdir(parents=True, exist_ok=True)

variants = {
    "fair-balanced": {"lambda_factor": 0.12, "iterations": 16},
    "fair-smooth": {"lambda_factor": 0.20, "iterations": 24},
    "fair-max": {"lambda_factor": 0.32, "iterations": 32},
}


def percentile(values, fraction):
    if not values:
        return 0.0
    ordered = sorted(values)
    return ordered[min(len(ordered) - 1, round((len(ordered) - 1) * fraction))]


def mesh_volume(mesh):
    bm = bmesh.new()
    bm.from_mesh(mesh)
    volume = abs(bm.calc_volume(signed=True))
    bm.free()
    return volume


report = {"source": str(source_path), "variants": {}}

for variant_name, settings in variants.items():
    bpy.ops.wm.read_factory_settings(use_empty=True)
    bpy.ops.import_scene.gltf(filepath=str(source_path))
    obj = next(item for item in bpy.context.scene.objects if item.type == "MESH")
    mesh = obj.data
    original_positions = [vertex.co.copy() for vertex in mesh.vertices]
    original_dimensions = obj.dimensions.copy()
    original_volume = mesh_volume(mesh)

    modifier = obj.modifiers.new(name="Constrained surface fairing", type="LAPLACIANSMOOTH")
    modifier.lambda_factor = settings["lambda_factor"]
    modifier.lambda_border = settings["lambda_factor"]
    modifier.iterations = settings["iterations"]
    modifier.use_volume_preserve = True
    modifier.use_normalized = True

    bpy.context.view_layer.objects.active = obj
    obj.select_set(True)
    bpy.ops.object.modifier_apply(modifier=modifier.name)
    for polygon in mesh.polygons:
        polygon.use_smooth = True
    mesh.update()
    mesh.calc_loop_triangles()
    area_weighted_normals = [mathutils.Vector((0.0, 0.0, 0.0)) for _ in mesh.vertices]
    for triangle in mesh.loop_triangles:
        weighted = triangle.normal * triangle.area
        for vertex_index in triangle.vertices:
            area_weighted_normals[vertex_index] += weighted
    for normal in area_weighted_normals:
        if normal.length_squared:
            normal.normalize()
    mesh.normals_split_custom_set_from_vertices(area_weighted_normals)
    mesh.update()
    bpy.context.view_layer.update()

    displacements = [
        (vertex.co - original).length
        for vertex, original in zip(mesh.vertices, original_positions)
    ]
    new_volume = mesh_volume(mesh)
    dimension_delta = obj.dimensions - original_dimensions

    output_path = output_dir / f"noble-ring-{variant_name}-desktop.glb"
    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(output_path),
        export_format="GLB",
        use_selection=True,
        export_normals=True,
        export_materials="NONE",
        export_yup=True,
    )

    report["variants"][variant_name] = {
        **settings,
        "output": str(output_path),
        "bytes": output_path.stat().st_size,
        "vertices": len(mesh.vertices),
        "polygons": len(mesh.polygons),
        "displacement": {
            "mean": statistics.fmean(displacements),
            "p50": percentile(displacements, 0.50),
            "p90": percentile(displacements, 0.90),
            "p99": percentile(displacements, 0.99),
            "max": max(displacements),
        },
        "dimension_delta": [dimension_delta.x, dimension_delta.y, dimension_delta.z],
        "volume_delta_percent": ((new_volume / original_volume) - 1.0) * 100,
    }

print("TN_FAIRING_REPORT_BEGIN")
print(json.dumps(report, indent=2))
print("TN_FAIRING_REPORT_END")
