import { build_piece, face_key, FACE_OPP } from "./convex_decomposition.js";

/**
 * Convex fast path for a triangulated {@link MeshShape3D}: if the whole mesh is
 * convex, represent it as a single coplanar-merged convex piece so it collides
 * via one GJK + EPA + face-clip manifold (O(support queries)) instead of being
 * greedily decomposed into many small SAT pieces.
 *
 * Why this matters: the greedy convex decomposition fragments even a convex mesh
 * (a partial union of tets is generally NOT convex, so the order-dependent grow
 * can't reconstruct the single convex region — a curved convex surface splits
 * into dozens of pieces). Detecting global convexity and emitting ONE piece
 * collapses that to a single collider, the path to large convex meshes.
 *
 * The piece is built by {@link build_piece} from the mesh's whole boundary, so
 * its faces are coplanar-MERGED polygons (a box → 6 quads, not 12 triangles) —
 * essential for the clipper to produce a full face-on-face patch rather than
 * triangle slivers. A `support()` is attached (linear scan over the piece's
 * localised surface vertices, so it stays O(surface), not O(all tets)).
 *
 * Cached on the (immutable) shape as `__convex_hull`: the piece if convex, or
 * `null` if concave. Returned verbatim on subsequent calls.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */

/**
 * @param {{tet_mesh, tet_positions, compute_bounding_box}} meshShape
 * @returns {{vertices:Float32Array, face_offsets:Uint32Array, face_loops:Uint32Array, aabb:Float64Array, support:Function}|null}
 *   a single coplanar-merged convex piece (with support) if the mesh is convex, else null
 */
export function get_mesh_convex_hull(meshShape) {
    if (meshShape.__convex_hull !== undefined) return meshShape.__convex_hull;
    const hull = build_if_convex(meshShape);
    meshShape.__convex_hull = hull;
    return hull;
}

function build_if_convex(meshShape) {
    const tm = meshShape.tet_mesh;
    const tp = meshShape.tet_positions;
    const N = tm.count;
    if (N === 0) return null;

    const bb = new Float64Array(6);
    meshShape.compute_bounding_box(bb);
    const diag = Math.hypot(bb[3] - bb[0], bb[4] - bb[1], bb[5] - bb[2]) || 1;
    const EPS = 1e-6 * diag;

    // Boundary faces = those owned by exactly one tet. Keep the owning tet's
    // opposite vertex to orient the face outward for the convexity test.
    const owners = new Map(); // key → { a, b, c, opp, count }
    for (let t = 0; t < N; t++) {
        for (let f = 0; f < 4; f++) {
            const o = FACE_OPP[f];
            const a = tm.getVertexIndex(t, o[0]);
            const b = tm.getVertexIndex(t, o[1]);
            const c = tm.getVertexIndex(t, o[2]);
            const opp = tm.getVertexIndex(t, o[3]);
            const k = face_key(a, b, c);
            const e = owners.get(k);
            if (e === undefined) owners.set(k, { a, b, c, opp, count: 1 });
            else e.count++;
        }
    }

    // Convexity: every vertex behind (or on) every boundary-face plane.
    const nVerts = tp.length / 3;
    const boundary = new Map(); // key → {a,b,c} for build_piece
    let boundaryCount = 0;
    for (const [k, fc] of owners) {
        if (fc.count !== 1) continue;
        boundaryCount++;
        const ax = tp[fc.a * 3], ay = tp[fc.a * 3 + 1], az = tp[fc.a * 3 + 2];
        const e1x = tp[fc.b * 3] - ax, e1y = tp[fc.b * 3 + 1] - ay, e1z = tp[fc.b * 3 + 2] - az;
        const e2x = tp[fc.c * 3] - ax, e2y = tp[fc.c * 3 + 1] - ay, e2z = tp[fc.c * 3 + 2] - az;
        let nx = e1y * e2z - e1z * e2y, ny = e1z * e2x - e1x * e2z, nz = e1x * e2y - e1y * e2x;
        const ox = tp[fc.opp * 3] - ax, oy = tp[fc.opp * 3 + 1] - ay, oz = tp[fc.opp * 3 + 2] - az;
        if (nx * ox + ny * oy + nz * oz > 0) { nx = -nx; ny = -ny; nz = -nz; } // outward
        const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
        const ix = nx / len, iy = ny / len, iz = nz / len;
        for (let v = 0; v < nVerts; v++) {
            const d = ix * (tp[v * 3] - ax) + iy * (tp[v * 3 + 1] - ay) + iz * (tp[v * 3 + 2] - az);
            if (d > EPS) return null; // a vertex in front of a face → concave
        }
        boundary.set(k, { a: fc.a, b: fc.b, c: fc.c });
    }
    if (boundaryCount < 4) return null; // not a closed volume

    // Coplanar-merged hull piece + a linear-scan support over its local verts.
    const piece = build_piece(tp, boundary);
    const v = piece.vertices;
    const vc = v.length / 3;
    piece.support = function (result, result_offset, dx, dy, dz) {
        let bi = 0, best = -Infinity;
        for (let i = 0; i < vc; i++) {
            const d = dx * v[i * 3] + dy * v[i * 3 + 1] + dz * v[i * 3 + 2];
            if (d > best) { best = d; bi = i; }
        }
        result[result_offset] = v[bi * 3];
        result[result_offset + 1] = v[bi * 3 + 1];
        result[result_offset + 2] = v[bi * 3 + 2];
    };
    return piece;
}
