"""Speed / accuracy tradeoffs of the descriptor's geometric queries against the standard routes, CPU, 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/fcp_devel/.venv/bin/python scripts/tradeoffs.py base     # out/tr_base.csv: mesh baselines and references
    $T ~/code/data/meshes/fcp_2910_collision/.venv/bin/python scripts/tradeoffs.py o3d   # out/tr_o3d.csv
    $T ~/code/fcp_devel/.venv/bin/python scripts/tradeoffs.py grid     # out/tr_grid.csv: FCP configurations
    (sizes, figures and the section: tradeoffs_sizes.py, tradeoffs_figs.py)

Tasks: convex hull (qhull), volume (tetrahedron sum), AABB (Open3D / NumPy), OBB (trimesh / Open3D minimal, Open3D
principal axes), smallest sphere (qhull + exact ball; trimesh). FCP configurations: layers 64 / 128 / 256, atoms 512 /
2048 / 6144 (mesh encodes), directions M 16 / 32 / 64 / 128, ring samples 64 / 128 / 256, OBB frames own / own + world.
Every timing: median after a warm-up, one thread (numba, OpenMP and BLAS pinned), the load average logged per object.
"""
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"
LAYERS, ATOMS = (64, 128, 256), (512, 2048, 6144)
MS, NTS = (16, 32, 64, 128), (64, 128, 256)


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


def load():
    return os.getloadavg()[0]


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

    from bounds import min_sphere, _RNG
    from volume_sota import vol_nb

    assert os.environ.get("NUMBA_NUM_THREADS") == "1"
    rows = []
    for name, path in OBJECTS.items():
        m = trimesh.load(path, force="mesh")
        V = np.ascontiguousarray(m.vertices, np.float64)
        F = np.ascontiguousarray(m.faces, np.int64)
        H = ConvexHull(V)

        def exact_sphere():
            Hv = V[ConvexHull(V).vertices]
            return min_sphere(Hv[_RNG.permutation(len(Hv))])
        r = dict(obj=name, verts=len(V), faces=len(F), diag=float(np.linalg.norm(V.max(0) - V.min(0))), load=load(),
                 hull_vol=H.volume, qhull_ms=med(lambda: ConvexHull(V)),
                 volume=vol_nb(V, F), tetra_ms=med(lambda: vol_nb(V, F), 201),
                 aabb_np_ms=med(lambda: (V.min(0), V.max(0)), 201),
                 trimesh_obb_vol=float(np.prod(trimesh.bounds.oriented_bounds(m)[1])),
                 trimesh_obb_ms=med(lambda: trimesh.bounds.oriented_bounds(m), 5),
                 sphere_r=float(exact_sphere()[1]), sphere_exact_ms=med(exact_sphere),
                 trimesh_sphere_r=float(trimesh.nsphere.minimum_nsphere(m)[1]),
                 trimesh_sphere_ms=med(lambda: trimesh.nsphere.minimum_nsphere(m), 5))
        rows.append(r)
        print({k: (round(v, 4) if isinstance(v, float) else v) for k, v in r.items()}, flush=True)
    pd.DataFrame(rows).to_csv(OUT / "tr_base.csv", index=False)


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

    rows = []
    for name, path in OBJECTS.items():
        m = trimesh.load(path, force="mesh")
        pts = o3d.utility.Vector3dVector(np.asarray(m.vertices, float))
        r = dict(obj=name, load=load(), o3d_aabb_ms=med(lambda: o3d.geometry.AxisAlignedBoundingBox.create_from_points(pts), 201),
                 o3d_pca_vol=o3d.geometry.OrientedBoundingBox.create_from_points(pts).volume(),
                 o3d_pca_ms=med(lambda: o3d.geometry.OrientedBoundingBox.create_from_points(pts)),
                 o3d_min_vol=o3d.geometry.OrientedBoundingBox.create_from_points_minimal(pts).volume(),
                 o3d_min_ms=med(lambda: o3d.geometry.OrientedBoundingBox.create_from_points_minimal(pts), 3))
        rows.append(r)
        print(r, flush=True)
    pd.DataFrame(rows).to_csv(OUT / "tr_o3d.csv", index=False)


def grid():
    import pandas as pd
    import trimesh

    import fcp
    from bounds import outside
    from bounds_fast import fast_bounds3, hull_fast
    from volume_atoms import volume_atoms_fast

    assert os.environ.get("NUMBA_NUM_THREADS") == "1"
    base = pd.read_csv(OUT / "tr_base.csv").set_index("obj")
    rows = []
    for name, path in OBJECTS.items():
        m = trimesh.load(path, force="mesh")
        V0 = np.asarray(m.vertices, float)
        b = base.loc[name]
        from scipy.spatial import ConvexHull
        H0 = ConvexHull(V0)
        true = trimesh.Trimesh(V0, H0.simplices)
        true.fix_normals()
        S, _ = trimesh.sample.sample_surface(true, 5000, seed=0)  # points on the true hull, for the gap
        for L in LAYERS:
            for A in ATOMS:
                d = fcp.encode(m, atoms=A, layers=L, device="cpu")
                common = dict(obj=name, layers=L, atoms=A, atoms_held=d.n_atoms, nbytes=d.nbytes, load=load())
                rows.append(dict(common, task="volume", M=0, n_t=0, frames="",
                                 ms=med(lambda: volume_atoms_fast(d), 201), size_ratio=volume_atoms_fast(d) / b.volume))
                for M in MS:
                    for nt in NTS:
                        Vh, Th = hull_fast(d, M, nt)
                        h = trimesh.Trimesh(Vh, Th, process=False)
                        _, dist, _ = trimesh.proximity.closest_point(h, S)
                        rows.append(dict(common, task="hull", M=M, n_t=nt, frames="", ms=med(lambda: hull_fast(d, M, nt)),
                                         size_ratio=h.volume / b.hull_vol, gap_mean_pct=100 * dist.mean() / b.diag,
                                         gap_max_pct=100 * dist.max() / b.diag, out_pct=np.nan))
                        for task in ("aabb", "sphere"):
                            o = fast_bounds3(d, nt, M, which=(task,))
                            if task == "aabb":
                                lo, hi = o["aabb"]
                                ratio = float(np.prod(hi - lo) / np.prod(V0.max(0) - V0.min(0)))
                                out = outside(V0, aabb=o["aabb"])
                            else:
                                ratio = float(o["sphere"][1] / b.sphere_r)
                                out = outside(V0, sphere=o["sphere"])
                            rows.append(dict(common, task=task, M=M, n_t=nt, frames="", ms=med(lambda: fast_bounds3(d, nt, M, which=(task,))),
                                             size_ratio=ratio, out_pct=100 * out / b.diag))
                        for fr in ("own", "both"):
                            o = fast_bounds3(d, nt, M, frames=fr, which=("obb",))
                            rows.append(dict(common, task="obb", M=M, n_t=nt, frames=fr,
                                             ms=med(lambda: fast_bounds3(d, nt, M, frames=fr, which=("obb",))),
                                             size_ratio=float(np.prod(2 * o["obb"][2])),  # absolute volume: divided by the best reference later
                                             out_pct=100 * outside(V0, obb=o["obb"]) / b.diag))
                print(name, L, A, f"load {load():.2f}", flush=True)
        pd.DataFrame(rows).to_csv(OUT / "tr_grid.csv", index=False)  # after every object: a partial file survives
    pd.DataFrame(rows).to_csv(OUT / "tr_grid.csv", index=False)


def retime():
    """Re-time (not re-measure accuracy) every configuration of the objects whose grid run saw a load average above
    RETIME_LOAD; rewrite out/tr_grid.csv with the new times and loads (the old ones kept as ms_first, load_first)."""
    import pandas as pd
    import trimesh

    import fcp
    from bounds_fast import fast_bounds3, hull_fast
    from volume_atoms import volume_atoms_fast

    assert os.environ.get("NUMBA_NUM_THREADS") == "1"
    g = pd.read_csv(OUT / "tr_grid.csv")
    if "ms_first" not in g:
        g["ms_first"], g["load_first"] = g.ms, g.load
    limit = float(os.environ.get("RETIME_LOAD", 3))
    redo = [o for o, l in g.groupby("obj").load_first.max().items() if l > limit]
    print("re-timing", redo, flush=True)
    for name in redo:
        m = trimesh.load(OBJECTS[name], force="mesh")
        for L in LAYERS:
            for A in ATOMS:
                d = fcp.encode(m, atoms=A, layers=L, device="cpu")
                sel = g[(g.obj == name) & (g.layers == L) & (g.atoms == A)]
                for i, r in sel.iterrows():
                    M, nt, fr = int(r.M), int(r.n_t), r.frames if isinstance(r.frames, str) else ""
                    f = {"volume": lambda: volume_atoms_fast(d), "hull": lambda: hull_fast(d, M, nt),
                         "aabb": lambda: fast_bounds3(d, nt, M, which=("aabb",)),
                         "sphere": lambda: fast_bounds3(d, nt, M, which=("sphere",)),
                         "obb": lambda: fast_bounds3(d, nt, M, frames=fr, which=("obb",))}[r.task]
                    g.loc[i, "ms"] = med(f, 201 if r.task == "volume" else 21)
                    g.loc[i, "load"] = load()
                print(name, L, A, f"load {load():.2f}", flush=True)
        g.to_csv(OUT / "tr_grid.csv", index=False)


if __name__ == "__main__":
    {"base": base, "o3d": o3d_step, "grid": grid, "retime": retime}[STEP]()
