import { computeTriangleClosestPointToPointBarycentric } from "../../../core/geom/3d/triangle/computeTriangleClosestPointToPointBarycentric.js";

/**
 * Closest-point on triangle, written into the first two floats of `out`
 * as `(alpha_A, alpha_B)` — `alpha_C = 1 - alpha_A - alpha_B`. Reused
 * across the {@link sphere_triangle_contact} body to avoid allocation.
 * @type {Float64Array}
 */
const scratch_bary = new Float64Array(2);

/**
 * Closed-form contact generation for a sphere vs. a triangle.
 *
 * Algorithm: find the closest point on the triangle to the sphere centre
 * via Ericson barycentric voronoi-region projection, then compare the
 * distance to the sphere radius. The result is closed-form, exact for
 * the geometric primitives involved, and avoids the precision wall that
 * GJK+EPA hits on {@link Triangle3D} — whose support function is
 * degenerate along the face normal (all 3 vertices project to the same
 * value) and produces noisy depths at small penetrations.
 *
 * Sphere-vs-triangle is the building block for sphere-vs-concave
 * (mesh, heightmap) narrowphase: the concave dispatch in
 * {@link narrowphase_step} decomposes the concave shape into world-space
 * triangles and runs this function per triangle.
 *
 * Output convention (mirrors {@link sphere_box_contact} and
 * {@link sphere_sphere_contact}): the normal in `out[0..2]` points
 * from the triangle's surface toward the sphere centre — i.e. the
 * direction the sphere should be pushed to separate; `out[3]` is the
 * positive penetration depth; `out[4..6]` is the world contact on
 * the sphere's surface; `out[7..9]` is the world contact on the
 * triangle's surface (the closest point).
 *
 * Singular case: when the sphere centre lies exactly on the triangle's
 * plane (distance == 0), the normal direction is geometrically undefined.
 * We fall back to the triangle's face normal (CCW winding), which is the
 * direction the surrounding solid is "outward" from for closed meshes
 * and heightmaps.
 *
 * @param {number[]|Float64Array} out length >= 10
 * @param {number} sx sphere centre x (world)
 * @param {number} sy
 * @param {number} sz
 * @param {number} radius
 * @param {number} ax triangle vertex A x (world)
 * @param {number} ay
 * @param {number} az
 * @param {number} bx triangle vertex B x (world)
 * @param {number} by
 * @param {number} bz
 * @param {number} cx triangle vertex C x (world)
 * @param {number} cy
 * @param {number} cz
 * @returns {boolean} true if sphere overlaps the triangle
 */
export function sphere_triangle_contact(
    out,
    sx, sy, sz, radius,
    ax, ay, az,
    bx, by, bz,
    cx, cy, cz
) {
    // Degenerate-triangle guard. For zero-area triangles (colinear or
    // duplicate vertices) the closest-point algorithm still returns
    // a point on the degenerate "line" but the surrounding geometry
    // has no meaningful face — emitting a contact would mislead the
    // solver. Catch this upfront via the face-normal cross product;
    // the same magnitude is reused for the dist=0 fallback below.
    //
    // Cost: 9 flops + 1 sqrt. Negligible vs. the rest of the function
    // and saves the caller from having to pre-filter its input.
    const e1x = bx - ax, e1y = by - ay, e1z = bz - az;
    const e2x = cx - ax, e2y = cy - ay, e2z = cz - az;
    const fnx = e1y * e2z - e1z * e2y;
    const fny = e1z * e2x - e1x * e2z;
    const fnz = e1x * e2y - e1y * e2x;
    const fn_mag_sqr = fnx * fnx + fny * fny + fnz * fnz;
    if (fn_mag_sqr === 0) return false;

    // Closest-point on triangle to sphere centre — barycentric output.
    computeTriangleClosestPointToPointBarycentric(
        scratch_bary, 0,
        sx, sy, sz,
        ax, ay, az,
        bx, by, bz,
        cx, cy, cz
    );
    const alpha_a = scratch_bary[0];
    const alpha_b = scratch_bary[1];
    const alpha_c = 1 - alpha_a - alpha_b;

    // Reconstruct the closest world-space point.
    const px = alpha_a * ax + alpha_b * bx + alpha_c * cx;
    const py = alpha_a * ay + alpha_b * by + alpha_c * cy;
    const pz = alpha_a * az + alpha_b * bz + alpha_c * cz;

    // Vector from closest point to sphere centre.
    const dx = sx - px;
    const dy = sy - py;
    const dz = sz - pz;
    const dist_sqr = dx * dx + dy * dy + dz * dz;

    if (dist_sqr >= radius * radius) {
        return false;
    }

    const dist = Math.sqrt(dist_sqr);
    let nx, ny, nz;
    if (dist > 0) {
        const inv = 1 / dist;
        nx = dx * inv;
        ny = dy * inv;
        nz = dz * inv;
    } else {
        // Sphere centre lies on the triangle's surface — direction is
        // undefined. Fall back to the triangle's face normal (CCW
        // winding: (B - A) × (C - A)), which is the outward direction
        // for closed meshes and heightmaps. The cross product was
        // already computed for the degeneracy guard at the top of
        // the function; reuse fn_mag_sqr.
        const fn_mag = Math.sqrt(fn_mag_sqr);
        const inv_fn = 1 / fn_mag;
        nx = fnx * inv_fn;
        ny = fny * inv_fn;
        nz = fnz * inv_fn;
    }

    out[0] = nx;
    out[1] = ny;
    out[2] = nz;
    out[3] = radius - dist;
    // Sphere surface point: centre minus normal*radius (the point on
    // the sphere nearest the triangle).
    out[4] = sx - nx * radius;
    out[5] = sy - ny * radius;
    out[6] = sz - nz * radius;
    // Triangle surface point: the closest point we already computed.
    out[7] = px;
    out[8] = py;
    out[9] = pz;
    return true;
}
