import { BVH } from "../../../bvh2/bvh3/BVH.js";
import { bvh_query_user_data_overlaps_aabb } from "../../../bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js";
import { computeTriangleClosestPointToPointBarycentric } from "../triangle/computeTriangleClosestPointToPointBarycentric.js";
import { compute_tetrahedron_volume } from "../tetrahedra/compute_tetrahedron_volume.js";
import { TetrahedralMesh } from "../tetrahedra/TetrahedralMesh.js";
import { AbstractShape3D } from "./AbstractShape3D.js";

/**
 * Arbitrary triangle-mesh collider.
 *
 * The shape carries two synchronized representations:
 *   - flat (`positions`, `indices`) surface — used by GJK's support
 *     function and the per-triangle distance queries;
 *   - a {@link TetrahedralMesh} of the *interior* (built from the surface
 *     by `compute_tetrahedral_mesh_from_surface`) plus a {@link BVH}
 *     keyed by tet AABB — used by `contains_point` and any future
 *     volume-aware query.
 *
 * The tet decomposition gives us convex primitives. Two consequences:
 *   1. `contains_point` is a point-in-AABB BVH query followed by a
 *      per-tet `orient3d` test. Critically this works for **disconnected
 *      meshes** (two cubes welded into one buffer; a torus and a sphere
 *      handed to the same shape). A walking query started from one
 *      component can't reach the others — that was the previous
 *      implementation's silent bug.
 *   2. Per-point queries are O(log N_tets) on average — the BVH's
 *      traversal short-circuits cleanly when the query point is far
 *      from the mesh.
 *
 * Construct via {@link shape_mesh_from_geometry} rather than `new
 * MeshShape3D()` directly — the factory compacts the tet mesh, builds
 * the BVH, and caches the bbox / volume / surface area.
 *
 * Narrowphase routing:
 *   - GJK + EPA for the general case, using {@link support}.
 *   - The support function currently returns the deepest tet-mesh
 *     vertex, which gives GJK the *convex hull* of the mesh — not the
 *     true non-convex surface. For convex authored geometry that's
 *     fine; for highly concave shapes (a torus's hole, an L-bracket,
 *     two disconnected components) the convex-hull approximation
 *     overstates the collision volume. The accurate fix is a per-tet
 *     GJK loop in the narrowphase, which would consume `tet_mesh` and
 *     `tet_positions` directly — see PLAN.md's mesh-vs-convex
 *     closed-form item.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class MeshShape3D extends AbstractShape3D {

    constructor() {
        super();

        /**
         * Surface vertex positions, flat `(x, y, z)` per vertex. Authored
         * by the factory; mutate at your own risk (the cached bbox /
         * volume / surface area / BVH will go stale).
         * @type {Float32Array}
         */
        this.positions = new Float32Array(0);

        /**
         * Surface triangle indices, three uint32 per face referring to
         * {@link positions}. Same mutate-at-your-own-risk caveat.
         * @type {Uint32Array}
         */
        this.indices = new Uint32Array(0);

        /**
         * Tetrahedral decomposition of the interior. The factory
         * compacts the mesh before storing (low-index slots that the
         * carve pass freed get refilled), so iteration via
         * `tet_id in [0, tet_mesh.count)` is safe — no need for
         * `forEach` or `exists` filtering on the hot path.
         * @type {TetrahedralMesh}
         */
        this.tet_mesh = new TetrahedralMesh();

        /**
         * Flat positions array for {@link tet_mesh}. Includes the
         * surface vertices and any hole-closing centroids added during
         * tetrahedralisation. For closed airtight meshes this equals
         * {@link positions} element-wise; for meshes with boundary holes
         * (rare in authored content, common in mis-exported assets) the
         * trailing slots carry the closure centroids.
         * @type {Float32Array}
         */
        this.tet_positions = new Float32Array(0);

        /**
         * BVH over the tet mesh, keyed by tet index. Built by the
         * factory after compacting the tet mesh, so leaf user_data
         * values are valid tet indices that can be passed straight to
         * `tet_mesh.getVertexIndex`.
         * @type {BVH}
         */
        this.tet_bvh = new BVH();

        /**
         * Cached axis-aligned bounding box, layout `[minX, minY, minZ,
         * maxX, maxY, maxZ]`.
         * @private
         * @type {Float64Array}
         */
        this.__bbox = new Float64Array([0, 0, 0, 0, 0, 0]);

        /**
         * Cached total volume in the shape's local frame, computed from
         * the tetrahedral decomposition at construction.
         * @private
         * @type {number}
         */
        this.__volume = 0;

        /**
         * Cached total surface area, sum of triangle areas.
         * @private
         * @type {number}
         */
        this.__surface_area = 0;
    }

    get volume() {
        return this.__volume;
    }

    get surface_area() {
        return this.__surface_area;
    }

    compute_bounding_box(result) {
        const b = this.__bbox;
        result[0] = b[0]; result[1] = b[1]; result[2] = b[2];
        result[3] = b[3]; result[4] = b[4]; result[5] = b[5];
    }

    /**
     * Refresh the cached bounding box, volume, and surface area from the
     * current `positions` / `indices` / `tet_mesh`. Called by
     * {@link shape_mesh_from_geometry} at construction; call again after
     * mutating the underlying data.
     */
    recompute_cached() {
        const positions = this.positions;
        const indices = this.indices;
        const bbox = this.__bbox;

        let min_x = Infinity, min_y = Infinity, min_z = Infinity;
        let max_x = -Infinity, max_y = -Infinity, max_z = -Infinity;
        const v_count = positions.length / 3;
        for (let i = 0; i < v_count; i++) {
            const x = positions[i * 3];
            const y = positions[i * 3 + 1];
            const z = positions[i * 3 + 2];
            if (x < min_x) min_x = x; if (x > max_x) max_x = x;
            if (y < min_y) min_y = y; if (y > max_y) max_y = y;
            if (z < min_z) min_z = z; if (z > max_z) max_z = z;
        }
        if (v_count === 0) {
            min_x = min_y = min_z = max_x = max_y = max_z = 0;
        }
        bbox[0] = min_x; bbox[1] = min_y; bbox[2] = min_z;
        bbox[3] = max_x; bbox[4] = max_y; bbox[5] = max_z;

        // Surface area: ½‖e1 × e2‖ per triangle.
        let area_sum = 0;
        const tri_count = indices.length / 3;
        for (let i = 0; i < tri_count; i++) {
            const ia = indices[i * 3] * 3;
            const ib = indices[i * 3 + 1] * 3;
            const ic = indices[i * 3 + 2] * 3;
            const ax = positions[ia],     ay = positions[ia + 1], az = positions[ia + 2];
            const bx = positions[ib],     by = positions[ib + 1], bz = positions[ib + 2];
            const cx = positions[ic],     cy = positions[ic + 1], cz = positions[ic + 2];
            const e1x = bx - ax, e1y = by - ay, e1z = bz - az;
            const e2x = cx - ax, e2y = cy - ay, e2z = cz - az;
            const cx2 = e1y * e2z - e1z * e2y;
            const cy2 = e1z * e2x - e1x * e2z;
            const cz2 = e1x * e2y - e1y * e2x;
            area_sum += 0.5 * Math.sqrt(cx2 * cx2 + cy2 * cy2 + cz2 * cz2);
        }
        this.__surface_area = area_sum;

        // Volume: |Σ signed_tet_volume|. The mesh has been compacted by
        // the factory so iteration via a flat range works (no holes).
        let volume_sum = 0;
        const tet_positions = this.tet_positions;
        const tet_mesh = this.tet_mesh;
        const tet_count = tet_mesh.count;
        for (let t = 0; t < tet_count; t++) {
            volume_sum += compute_tetrahedron_volume(tet_mesh, tet_positions, t);
        }
        this.__volume = Math.abs(volume_sum);
    }

    /**
     * Support function: returns the deepest tet-mesh vertex along the
     * supplied direction. This gives GJK the **convex hull** of the
     * mesh — fine for convex authored geometry, an overestimate for
     * concave shapes (a torus's hole or two disconnected components
     * read as one filled blob through this function).
     *
     * Iterates `tet_positions` rather than the original surface
     * `positions` so any hole-closing centroid that the
     * tetrahedralisation added shows up. For airtight closed meshes the
     * two arrays match element-wise.
     *
     * Future work: an accurate non-convex `support` would have to be
     * per-tet (each tet is convex; the shape is the union). The
     * narrowphase would then iterate tets via the BVH. See PLAN.md.
     *
     * @param {number[]|Float32Array} result
     * @param {number} result_offset
     * @param {number} direction_x
     * @param {number} direction_y
     * @param {number} direction_z
     */
    support(result, result_offset, direction_x, direction_y, direction_z) {
        const positions = this.tet_positions;
        const v_count = positions.length / 3;
        if (v_count === 0) {
            result[result_offset] = 0;
            result[result_offset + 1] = 0;
            result[result_offset + 2] = 0;
            return;
        }
        let best_dot = -Infinity;
        let best_i = 0;
        for (let i = 0; i < v_count; i++) {
            const x = positions[i * 3];
            const y = positions[i * 3 + 1];
            const z = positions[i * 3 + 2];
            const d = direction_x * x + direction_y * y + direction_z * z;
            if (d > best_dot) {
                best_dot = d;
                best_i = i;
            }
        }
        result[result_offset]     = positions[best_i * 3];
        result[result_offset + 1] = positions[best_i * 3 + 1];
        result[result_offset + 2] = positions[best_i * 3 + 2];
    }

    /**
     * Inside test via the tet mesh. Queries the tet BVH for the leaves
     * whose AABB contains the point, then runs an `orient3d`-based
     * point-in-tet test on each candidate. Returns `true` on the first
     * hit.
     *
     * Crucially this works for **disconnected meshes** — a single
     * `MeshShape3D` holding two cubes welded into one buffer, or a
     * torus with a separate sphere, will correctly classify points
     * inside either component. The previous implementation walked tet
     * neighbours starting from a single seed, which trapped the query
     * in the seed's connected component and produced silent false
     * negatives in the other components.
     *
     * @param {number[]|Float32Array} point
     * @returns {boolean}
     */
    contains_point(point) {
        const px = point[0], py = point[1], pz = point[2];

        // Early-out: outside the shape's bbox there can't be any
        // containing tet, and the BVH would still report no overlap —
        // skip the BVH call to save a few ns on the common miss path.
        const bbox = this.__bbox;
        if (px < bbox[0] || py < bbox[1] || pz < bbox[2]
            || px > bbox[3] || py > bbox[4] || pz > bbox[5]) {
            return false;
        }

        scratch_query_aabb[0] = px; scratch_query_aabb[1] = py; scratch_query_aabb[2] = pz;
        scratch_query_aabb[3] = px; scratch_query_aabb[4] = py; scratch_query_aabb[5] = pz;

        const n = bvh_query_user_data_overlaps_aabb(
            scratch_candidates, 0, this.tet_bvh, scratch_query_aabb
        );

        const tet_mesh = this.tet_mesh;
        const positions = this.tet_positions;
        for (let i = 0; i < n; i++) {
            const tet_id = scratch_candidates[i];
            const a = tet_mesh.getVertexIndex(tet_id, 0);
            const b = tet_mesh.getVertexIndex(tet_id, 1);
            const c = tet_mesh.getVertexIndex(tet_id, 2);
            const d = tet_mesh.getVertexIndex(tet_id, 3);
            if (tet_contains_point_coords(positions, a, b, c, d, px, py, pz)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Closest world-space point on the surface to `reference`. Linear
     * scan over triangles using the barycentric closest-point helper.
     * For mesh sizes where this becomes a bottleneck, a triangle BVH
     * would be the next acceleration (sibling to {@link tet_bvh} but
     * keyed on surface triangles).
     *
     * @param {number[]|Float32Array} result
     * @param {number[]|Float32Array} reference
     */
    nearest_point_on_surface(result, reference) {
        const positions = this.positions;
        const indices = this.indices;
        const tri_count = indices.length / 3;
        const px = reference[0], py = reference[1], pz = reference[2];

        let best_d2 = Infinity;
        let best_x = 0, best_y = 0, best_z = 0;
        const baryc = scratch_barycentric;

        for (let i = 0; i < tri_count; i++) {
            const ia = indices[i * 3] * 3;
            const ib = indices[i * 3 + 1] * 3;
            const ic = indices[i * 3 + 2] * 3;
            const ax = positions[ia], ay = positions[ia + 1], az = positions[ia + 2];
            const bx = positions[ib], by = positions[ib + 1], bz = positions[ib + 2];
            const cx = positions[ic], cy = positions[ic + 1], cz = positions[ic + 2];

            computeTriangleClosestPointToPointBarycentric(
                baryc, 0,
                px, py, pz,
                ax, ay, az,
                bx, by, bz,
                cx, cy, cz
            );
            const u = baryc[0], v = baryc[1];
            const w = 1 - u - v;
            const qx = u * ax + v * bx + w * cx;
            const qy = u * ay + v * by + w * cy;
            const qz = u * az + v * bz + w * cz;

            const dx = qx - px, dy = qy - py, dz = qz - pz;
            const d2 = dx * dx + dy * dy + dz * dz;
            if (d2 < best_d2) {
                best_d2 = d2;
                best_x = qx; best_y = qy; best_z = qz;
            }
        }

        result[0] = best_x;
        result[1] = best_y;
        result[2] = best_z;
    }

    /**
     * Signed distance to the surface: positive outside the mesh,
     * negative inside. Linear scan over triangles for nearest unsigned
     * distance, then the BVH-backed {@link contains_point} flips the
     * sign for interior points.
     *
     * @param {number[]|Float32Array} point
     * @returns {number}
     */
    signed_distance_at_point(point) {
        const positions = this.positions;
        const indices = this.indices;
        const tri_count = indices.length / 3;
        const px = point[0], py = point[1], pz = point[2];

        let best_d2 = Infinity;
        const baryc = scratch_barycentric;

        for (let i = 0; i < tri_count; i++) {
            const ia = indices[i * 3] * 3;
            const ib = indices[i * 3 + 1] * 3;
            const ic = indices[i * 3 + 2] * 3;
            const ax = positions[ia], ay = positions[ia + 1], az = positions[ia + 2];
            const bx = positions[ib], by = positions[ib + 1], bz = positions[ib + 2];
            const cx = positions[ic], cy = positions[ic + 1], cz = positions[ic + 2];

            computeTriangleClosestPointToPointBarycentric(
                baryc, 0,
                px, py, pz,
                ax, ay, az,
                bx, by, bz,
                cx, cy, cz
            );
            const u = baryc[0], v = baryc[1];
            const w = 1 - u - v;
            const qx = u * ax + v * bx + w * cx;
            const qy = u * ay + v * by + w * cy;
            const qz = u * az + v * bz + w * cz;
            const dx = qx - px, dy = qy - py, dz = qz - pz;
            const d2 = dx * dx + dy * dy + dz * dz;
            if (d2 < best_d2) best_d2 = d2;
        }

        const dist = Math.sqrt(best_d2);
        return this.contains_point(point) ? -dist : dist;
    }

    /**
     * Reject-sample inside the bounding box until a point lands in the
     * mesh volume. For convex or near-convex shapes this terminates in
     * 1–3 attempts; for thin or pinched shapes the worst-case cost can
     * be high. Falls back to the bbox centre after 256 misses.
     *
     * @param {number[]|Float32Array} result
     * @param {number} result_offset
     * @param {function():number} random
     */
    sample_random_point_in_volume(result, result_offset, random) {
        const b = this.__bbox;
        const lx = b[0], ly = b[1], lz = b[2];
        const sx = b[3] - b[0];
        const sy = b[4] - b[1];
        const sz = b[5] - b[2];
        const probe = scratch_point;
        for (let attempt = 0; attempt < 256; attempt++) {
            probe[0] = lx + random() * sx;
            probe[1] = ly + random() * sy;
            probe[2] = lz + random() * sz;
            if (this.contains_point(probe)) {
                result[result_offset]     = probe[0];
                result[result_offset + 1] = probe[1];
                result[result_offset + 2] = probe[2];
                return;
            }
        }
        result[result_offset]     = lx + sx * 0.5;
        result[result_offset + 1] = ly + sy * 0.5;
        result[result_offset + 2] = lz + sz * 0.5;
    }

    /**
     * Numerical-gradient SDF gradient.
     *
     * @param {number[]|Float32Array} result
     * @param {number[]|Float32Array} point
     * @returns {number}
     */
    signed_distance_gradient_at_point(result, point) {
        const h = 1e-4;
        const d_center = this.signed_distance_at_point(point);
        const sample = scratch_point;
        sample[0] = point[0] + h; sample[1] = point[1]; sample[2] = point[2];
        const d_x = this.signed_distance_at_point(sample);
        sample[0] = point[0]; sample[1] = point[1] + h;
        const d_y = this.signed_distance_at_point(sample);
        sample[1] = point[1]; sample[2] = point[2] + h;
        const d_z = this.signed_distance_at_point(sample);
        const inv_h = 1 / h;
        result[0] = (d_x - d_center) * inv_h;
        result[1] = (d_y - d_center) * inv_h;
        result[2] = (d_z - d_center) * inv_h;
        return d_center;
    }

    /**
     * Topology equality: same vertex count, same triangle count, and
     * positions / indices match element-by-element.
     *
     * @param {MeshShape3D} other
     * @returns {boolean}
     */
    equals(other) {
        if (!super.equals(other)) return false;
        if (this.positions.length !== other.positions.length) return false;
        if (this.indices.length !== other.indices.length) return false;
        for (let i = 0; i < this.positions.length; i++) {
            if (this.positions[i] !== other.positions[i]) return false;
        }
        for (let i = 0; i < this.indices.length; i++) {
            if (this.indices[i] !== other.indices[i]) return false;
        }
        return true;
    }

    /**
     * Cheap stable hash: vertex count + triangle count + sampled coords.
     * @returns {number}
     */
    hash() {
        const v_count = this.positions.length / 3 | 0;
        const t_count = this.indices.length / 3 | 0;
        let h = (v_count * 31 + t_count) | 0;
        if (this.positions.length >= 3) {
            h = (h * 31 + Math.fround(this.positions[0]) * 1e6 | 0) | 0;
            h = (h * 31 + Math.fround(this.positions[this.positions.length - 1]) * 1e6 | 0) | 0;
        }
        return h | 0;
    }
}

/**
 * Fast type-check marker.
 * @readonly
 * @type {boolean}
 */
MeshShape3D.prototype.isMeshShape3D = true;

/**
 * Arbitrary triangle meshes are non-convex in the general case. The current
 * {@link support} implementation returns the convex hull's furthest vertex,
 * which silently overstates collision against meshes with concavities,
 * holes, or disconnected components. The narrowphase routes mesh pairs
 * through per-triangle GJK instead of feeding the whole mesh's support
 * into the Minkowski-difference loop.
 *
 * Authored geometry that happens to be convex is better expressed via a
 * dedicated hull shape — keeping this flag uniformly `false` avoids the
 * per-mesh convexity classification problem.
 *
 * @readonly
 * @type {boolean}
 */
MeshShape3D.prototype.is_convex = false;

// ── Module-local helpers ────────────────────────────────────────────────────

/**
 * Signed-volume-style orient3d test with the test point passed as raw
 * coordinates rather than an index into the positions array. Equivalent
 * to {@link orient3d_fast} but specialised so the caller doesn't have to
 * splice the query point into the positions buffer per call.
 *
 * @param {Float32Array|number[]} positions  flat (x, y, z) per vertex
 * @param {number} ai  index of plane vertex A
 * @param {number} bi  index of plane vertex B
 * @param {number} ci  index of plane vertex C
 * @param {number} ex  test point x
 * @param {number} ey
 * @param {number} ez
 * @returns {number}   positive iff E lies below the plane (CCW from above)
 */
function orient3d_with_point(positions, ai, bi, ci, ex, ey, ez) {
    const a3 = ai * 3, b3 = bi * 3, c3 = ci * 3;
    const adx = positions[a3]     - ex;
    const ady = positions[a3 + 1] - ey;
    const adz = positions[a3 + 2] - ez;
    const bdx = positions[b3]     - ex;
    const bdy = positions[b3 + 1] - ey;
    const bdz = positions[b3 + 2] - ez;
    const cdx = positions[c3]     - ex;
    const cdy = positions[c3 + 1] - ey;
    const cdz = positions[c3 + 2] - ez;
    return adx * (bdy * cdz - bdz * cdy)
        + bdx * (cdy * adz - cdz * ady)
        + cdx * (ady * bdz - adz * bdy);
}

/**
 * Point-in-tet test using four orient3d evaluations on the tet's four
 * faces. Winding-agnostic: rather than assume a specific tet winding
 * (Delaunay's output is CCW with positive signed volume; the existing
 * {@link tetrahedron_contains_point} helper, by contrast, was authored
 * against a CW convention), we accept the point as interior whenever
 * all four face-orient3d evaluations share a sign — i.e. the test
 * point is on the same side of every face. The function thus returns
 * true for both winding conventions; the caller doesn't need to know
 * which one its mesh uses.
 *
 * Takes the test point as raw coordinates so it can be called from a
 * hot loop without scratch-buffer plumbing into `positions`.
 *
 * @param {Float32Array|number[]} positions
 * @param {number} a   tet vertex index 0
 * @param {number} b   tet vertex index 1
 * @param {number} c   tet vertex index 2
 * @param {number} d   tet vertex index 3
 * @param {number} ex  test point x
 * @param {number} ey
 * @param {number} ez
 * @returns {boolean}
 */
function tet_contains_point_coords(positions, a, b, c, d, ex, ey, ez) {
    const v_a = orient3d_with_point(positions, b, d, c, ex, ey, ez);
    const v_b = orient3d_with_point(positions, c, d, a, ex, ey, ez);
    const v_c = orient3d_with_point(positions, d, b, a, ex, ey, ez);
    const v_d = orient3d_with_point(positions, a, b, c, ex, ey, ez);

    // Boundary points (any value == 0) count as inside — the >= / <=
    // bounds accept them under either winding.
    const all_nonneg = v_a >= 0 && v_b >= 0 && v_c >= 0 && v_d >= 0;
    const all_nonpos = v_a <= 0 && v_b <= 0 && v_c <= 0 && v_d <= 0;
    return all_nonneg || all_nonpos;
}

const scratch_barycentric = new Float64Array(2);
const scratch_point = new Float64Array(3);
const scratch_query_aabb = new Float64Array(6);

/**
 * Scratch for tet-id candidates from the BVH overlap query. Sized
 * generously — point-in-AABB queries typically hit 1–10 leaves for
 * non-degenerate meshes. Grows by reallocation if a query overflows.
 * @type {Uint32Array}
 */
let scratch_candidates = new Uint32Array(64);
