"""The mesh-size sweep: four objects resampled to about 1k-1M vertices (Open3D: quadric decimation down, Loop
subdivision then decimation up); per mesh the standard routes' time and FCP's (encode separately), one thread.

    cd ~/code/data/meshes/fcp_3010_hull
    T="env -u PYTHONPATH NUMBA_NUM_THREADS=1 OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 MKL_NUM_THREADS=1"
    $T ~/code/data/meshes/fcp_2910_collision/.venv/bin/python scripts/tradeoffs_sizes.py make   # out/sizes/<obj>_<n>.ply
    $T ~/code/data/meshes/fcp_2910_collision/.venv/bin/python scripts/tradeoffs_sizes.py o3d    # out/tr_sizes_o3d.csv
    $T ~/code/fcp_devel/.venv/bin/python scripts/tradeoffs_sizes.py run                         # out/tr_sizes.csv
"""
import os
import sys
import time
from pathlib import Path

import numpy as np

STEP = sys.argv[1]
sys.argv = [sys.argv[0], "x"]
sys.path.insert(0, str(Path(__file__).parent))
from hull import OBJECTS  # noqa: E402

ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "out"
SIZES_DIR = OUT / "sizes"
SWEEP = ["mug", "dino", "horse", "knot"]
TARGETS = [1000, 3000, 10000, 30000, 100000, 300000, 1000000]  # vertices
ENC = [(64, 512), (128, 2048), (256, 6144)]  # layers, atoms
QUERY = [("fast", 16, 64), ("default", 64, 128), ("accurate", 128, 256)]  # name, directions M, ring samples


def med(f, n=11):
    f()
    t = []
    for _ in range(n):
        t0 = time.perf_counter()
        f()
        t.append(time.perf_counter() - t0)
    return 1e3 * float(np.median(t))


def make():
    import open3d as o3d
    import trimesh

    SIZES_DIR.mkdir(parents=True, exist_ok=True)
    for name in SWEEP:
        m = trimesh.load(OBJECTS[name], force="mesh")
        o = o3d.geometry.TriangleMesh(o3d.utility.Vector3dVector(m.vertices), o3d.utility.Vector3iVector(m.faces))
        for n in TARGETS:
            src = o
            while len(src.vertices) < n:
                src = src.subdivide_loop(1)
            res = src.simplify_quadric_decimation(2 * n) if len(src.vertices) > 1.05 * n else src
            res.remove_unreferenced_vertices()
            t = trimesh.Trimesh(np.asarray(res.vertices), np.asarray(res.triangles), process=True)
            t.export(SIZES_DIR / f"{name}_{n}.ply")
            print(name, n, len(t.vertices), "watertight", t.is_watertight, flush=True)


def o3d_step():
    import open3d as o3d
    import pandas as pd
    import trimesh

    rows = []
    for f in sorted(SIZES_DIR.glob("*.ply")):
        m = trimesh.load(f, force="mesh")
        pts = o3d.utility.Vector3dVector(np.asarray(m.vertices, float))
        r = dict(mesh=f.stem, verts=len(m.vertices), load=os.getloadavg()[0],
                 o3d_aabb_ms=med(lambda: o3d.geometry.AxisAlignedBoundingBox.create_from_points(pts), 51),
                 o3d_pca_ms=med(lambda: o3d.geometry.OrientedBoundingBox.create_from_points(pts)),
                 o3d_pca_vol=o3d.geometry.OrientedBoundingBox.create_from_points(pts).volume())
        rows.append(r)
        print(r, flush=True)
    pd.DataFrame(rows).to_csv(OUT / "tr_sizes_o3d.csv", index=False)


def run():
    import pandas as pd
    import trimesh
    from scipy.spatial import ConvexHull

    import fcp
    from bounds import min_sphere, outside, _RNG
    from bounds_fast import fast_bounds3, hull_fast
    from volume_atoms import volume_atoms_fast
    from volume_sota import vol_nb

    assert os.environ.get("NUMBA_NUM_THREADS") == "1"
    rows = []
    for f in sorted(SIZES_DIR.glob("*.ply"), key=lambda p: (p.stem.rsplit("_", 1)[0], int(p.stem.rsplit("_", 1)[1]))):
        name, n = f.stem.rsplit("_", 1)
        m = trimesh.load(f, force="mesh")
        V = np.ascontiguousarray(m.vertices, np.float64)
        F = np.ascontiguousarray(m.faces, np.int64)
        diag = float(np.linalg.norm(V.max(0) - V.min(0)))
        H = ConvexHull(V)
        true = trimesh.Trimesh(V, H.simplices)
        true.fix_normals()
        S, _ = trimesh.sample.sample_surface(true, 5000, seed=0)

        def exact_sphere():
            Hv = V[ConvexHull(V).vertices]
            return min_sphere(Hv[_RNG.permutation(len(Hv))])
        vol = vol_nb(V, F)
        sr = exact_sphere()[1]
        tob = float(np.prod(trimesh.bounds.oriented_bounds(m)[1]))
        base = dict(obj=name, target=int(n), verts=len(V), faces=len(F), load=os.getloadavg()[0],
                    qhull_ms=med(lambda: ConvexHull(V)), tetra_ms=med(lambda: vol_nb(V, F), 51),
                    aabb_np_ms=med(lambda: (V.min(0), V.max(0)), 51), sphere_exact_ms=med(exact_sphere),
                    trimesh_obb_ms=med(lambda: trimesh.bounds.oriented_bounds(m), 3))
        for L, A in ENC:
            t0 = time.perf_counter()
            d = fcp.encode(m, atoms=A, layers=L, device="cpu")
            enc = 1e3 * (time.perf_counter() - t0)
            enc = min(enc, med(lambda: fcp.encode(m, atoms=A, layers=L, device="cpu"), 2))
            r = dict(base, layers=L, atoms=A, encode_ms=enc, volume_ms=med(lambda: volume_atoms_fast(d), 51),
                     volume_ratio=volume_atoms_fast(d) / vol)
            for q, M, nt in QUERY:
                Vh, Th = hull_fast(d, M, nt)
                h = trimesh.Trimesh(Vh, Th, process=False)
                _, dist, _ = trimesh.proximity.closest_point(h, S)
                o = fast_bounds3(d, nt, M)
                r.update({f"{q}_hull_ms": med(lambda: hull_fast(d, M, nt)), f"{q}_hull_ratio": h.volume / H.volume,
                          f"{q}_hull_gap_max_pct": 100 * dist.max() / diag,
                          f"{q}_obb_ms": med(lambda: fast_bounds3(d, nt, M, which=("obb",))),
                          f"{q}_obb_ratio": float(np.prod(2 * o["obb"][2]) / tob),
                          f"{q}_sphere_ms": med(lambda: fast_bounds3(d, nt, M, which=("sphere",))),
                          f"{q}_sphere_ratio": float(o["sphere"][1] / sr),
                          f"{q}_out_pct": 100 * max(outside(V, aabb=o["aabb"]), outside(V, obb=o["obb"]), outside(V, sphere=o["sphere"])) / diag})
            rows.append(r)
            print({k: (round(v, 3) if isinstance(v, float) else v) for k, v in r.items() if not k.startswith(("fast_", "accurate_"))}, flush=True)
        pd.DataFrame(rows).to_csv(OUT / "tr_sizes.csv", index=False)


if __name__ == "__main__":
    {"make": make, "o3d": o3d_step, "run": run}[STEP]()
