import bpy
import json
import numpy as np
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 = {
    "curve-light": {"factor": 0.18, "iterations": 8, "max_displacement": 0.0015},
    "curve-balanced": {"factor": 0.20, "iterations": 16, "max_displacement": 0.0030},
    "curve-smooth": {"factor": 0.22, "iterations": 28, "max_displacement": 0.0050},
}


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


def signed_volume(vertices, triangles):
    first = vertices[triangles[:, 0]]
    second = vertices[triangles[:, 1]]
    third = vertices[triangles[:, 2]]
    return np.einsum("ij,ij->i", first, np.cross(second, third)).sum() / 6.0


def vertex_normals(vertices, triangles):
    first = vertices[triangles[:, 0]]
    second = vertices[triangles[:, 1]]
    third = vertices[triangles[:, 2]]
    face_normals = np.cross(second - first, third - first)
    normals = np.zeros_like(vertices)
    for corner in range(3):
        np.add.at(normals, triangles[:, corner], face_normals)
    lengths = np.linalg.norm(normals, axis=1)
    valid = lengths > 1e-12
    normals[valid] /= lengths[valid, None]
    return normals


def cotangent_edges(vertices, triangles):
    p0 = vertices[triangles[:, 0]]
    p1 = vertices[triangles[:, 1]]
    p2 = vertices[triangles[:, 2]]
    double_area = np.linalg.norm(np.cross(p1 - p0, p2 - p0), axis=1)
    double_area = np.maximum(double_area, 1e-12)
    cot0 = np.einsum("ij,ij->i", p1 - p0, p2 - p0) / double_area
    cot1 = np.einsum("ij,ij->i", p2 - p1, p0 - p1) / double_area
    cot2 = np.einsum("ij,ij->i", p0 - p2, p1 - p2) / double_area
    # Clamping obtuse-triangle weights keeps the explicit fairing stable on the scan-like mesh.
    weights = np.concatenate((np.clip(cot2, 0.0, 8.0), np.clip(cot0, 0.0, 8.0), np.clip(cot1, 0.0, 8.0)))
    edge_from = np.concatenate((triangles[:, 0], triangles[:, 1], triangles[:, 2]))
    edge_to = np.concatenate((triangles[:, 1], triangles[:, 2], triangles[:, 0]))
    return edge_from, edge_to, weights


def laplacian(vertices, edge_from, edge_to, weights):
    weighted_sum = np.zeros_like(vertices)
    weight_sum = np.zeros(vertices.shape[0], dtype=np.float64)
    np.add.at(weighted_sum, edge_from, vertices[edge_to] * weights[:, None])
    np.add.at(weighted_sum, edge_to, vertices[edge_from] * weights[:, None])
    np.add.at(weight_sum, edge_from, weights)
    np.add.at(weight_sum, edge_to, weights)
    valid = weight_sum > 1e-12
    result = np.zeros_like(vertices)
    result[valid] = weighted_sum[valid] / weight_sum[valid, None] - vertices[valid]
    return result


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
    mesh.calc_loop_triangles()

    original = np.array([vertex.co[:] for vertex in mesh.vertices], dtype=np.float64)
    triangles = np.array([triangle.vertices[:] for triangle in mesh.loop_triangles], dtype=np.int64)
    original_dimensions = np.ptp(original, axis=0)
    original_volume = abs(signed_volume(original, triangles))
    center = original.mean(axis=0)
    edge_from, edge_to, weights = cotangent_edges(original, triangles)
    triangle_area = np.linalg.norm(
        np.cross(
            original[triangles[:, 1]] - original[triangles[:, 0]],
            original[triangles[:, 2]] - original[triangles[:, 0]],
        ),
        axis=1,
    ) * 0.5
    protected_vertices = np.unique(triangles[triangle_area < 4e-6])
    positions = original.copy()

    for _ in range(settings["iterations"]):
        delta = laplacian(positions, edge_from, edge_to, weights)
        normals = vertex_normals(positions, triangles)
        normal_delta = normals * np.einsum("ij,ij->i", delta, normals)[:, None]
        normal_delta[protected_vertices] = 0.0
        positions += settings["factor"] * normal_delta
        offset = positions - original
        offset_length = np.linalg.norm(offset, axis=1)
        over_limit = offset_length > settings["max_displacement"]
        if np.any(over_limit):
            offset[over_limit] *= (settings["max_displacement"] / offset_length[over_limit])[:, None]
            positions[over_limit] = original[over_limit] + offset[over_limit]

    new_volume = abs(signed_volume(positions, triangles))
    volume_scale = (original_volume / new_volume) ** (1.0 / 3.0)
    positions = center + (positions - center) * volume_scale

    for vertex, coordinate in zip(mesh.vertices, positions):
        vertex.co = coordinate
    for polygon in mesh.polygons:
        polygon.use_smooth = True
    mesh.update()
    mesh.calc_loop_triangles()

    normals = vertex_normals(positions, triangles)
    mesh.normals_split_custom_set_from_vertices(normals)
    mesh.update()
    bpy.context.view_layer.update()

    displacements = np.linalg.norm(positions - original, axis=1)
    dimensions = np.ptp(positions, axis=0)
    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,
        "displacement": {
            "mean": float(displacements.mean()),
            "p50": percentile(displacements.tolist(), 0.50),
            "p90": percentile(displacements.tolist(), 0.90),
            "p99": percentile(displacements.tolist(), 0.99),
            "max": float(displacements.max()),
        },
        "dimension_delta": (dimensions - original_dimensions).tolist(),
        "volume_delta_percent": ((abs(signed_volume(positions, triangles)) / original_volume) - 1.0) * 100,
    }

print("TN_CURVATURE_FAIRING_REPORT_BEGIN")
print(json.dumps(report, indent=2))
print("TN_CURVATURE_FAIRING_REPORT_END")
