import { computeHashFloat } from "../../../primitives/numbers/computeHashFloat.js";
import { computeTriangleClosestPointToPointBarycentric } from "../triangle/computeTriangleClosestPointToPointBarycentric.js";
import { AbstractShape3D } from "./AbstractShape3D.js";
import { compute_signed_distance_gradient_by_sampling } from "./util/compute_signed_distance_gradient_by_sampling.js";

/**
 * Convex polyhedron collider.
 *
 * Holds a convex triangle surface (`vertices` + outward-CCW `indices`) and the
 * derived face half-spaces. Because it is convex (`is_convex === true`, the
 * default), it routes straight through the narrowphase's GJK + EPA path via
 * {@link support} — no tetrahedralisation, no per-triangle decomposition, no
 * BVH. This is the right shape for authored convex geometry (props, crates,
 * gems, low-poly rocks, an icosphere) and is far cheaper to construct than
 * {@link MeshShape3D}, which tetrahedralises its interior.
 *
 * Contract:
 *   - `support` is exact for any convex point set (deepest vertex along the
 *     direction) — the precondition GJK relies on.
 *   - `contains_point` is the convex half-space test (behind every face plane).
 *   - `signed_distance` / `nearest_point_on_surface` scan the surface triangles
 *     (O(F)); these are query-path helpers, not on the collision hot path.
 *
 * The input is assumed convex with outward-facing CCW winding — build it via
 * {@link ConvexHullShape3D.from}. A genuine non-convex mesh belongs in
 * {@link MeshShape3D} (and, once decomposition lands, will be represented as a
 * set of these pieces).
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class ConvexHullShape3D extends AbstractShape3D {

    constructor() {
        super();

        /**
         * Hull vertex positions, flat `(x, y, z)` per vertex, in the body's
         * local frame. Fed to {@link support}.
         * @type {Float32Array}
         */
        this.vertices = new Float32Array(0);

        /**
         * Surface triangle indices, three uint32 per face (outward CCW). Used by
         * the surface-scan query helpers (signed distance / nearest point).
         * @type {Uint32Array}
         */
        this.indices = new Uint32Array(0);

        /**
         * Polygon-face representation for the contact clipper (CSR): face `f`'s
         * vertex loop is `face_loops[face_offsets[f] .. face_offsets[f+1]]`,
         * ordered CCW (outward). Currently one triangle per face; a coplanar-merge
         * pass will collapse coplanar triangles into larger polygon faces.
         * @type {Uint32Array}
         */
        this.face_offsets = new Uint32Array(0);
        /** @type {Uint32Array} */
        this.face_loops = new Uint32Array(0);

        /**
         * Face half-spaces, four doubles per face `(nx, ny, nz, d)` with unit
         * outward normal and `d = n · v` — point is inside iff `n · p − d ≤ 0`
         * for every face. Derived in {@link recompute}.
         * @private
         * @type {Float64Array}
         */
        this.__planes = new Float64Array(0);

        /** @private @type {Float64Array} */
        this.__bbox = new Float64Array(6);
        /** @private @type {number} */
        this.__volume = 0;
        /** @private @type {number} */
        this.__surface_area = 0;
    }

    /**
     * Build from a convex triangle surface. Vertices must lie on the hull and
     * faces must wind CCW when viewed from outside.
     *
     * @param {Float32Array} vertices flat `(x, y, z)` per vertex
     * @param {Uint32Array}  indices  three uint32 per triangle, outward CCW
     * @returns {ConvexHullShape3D}
     */
    static from(vertices, indices) {
        const s = new ConvexHullShape3D();
        s.vertices = vertices;
        s.indices = indices;
        s.recompute();
        return s;
    }

    /**
     * Refresh the cached bbox / face planes / volume / surface area from the
     * current `vertices` / `indices`.
     */
    recompute() {
        const v = this.vertices;
        const idx = this.indices;

        let mnx = Infinity, mny = Infinity, mnz = Infinity;
        let mxx = -Infinity, mxy = -Infinity, mxz = -Infinity;
        const vc = v.length / 3;
        for (let i = 0; i < vc; i++) {
            const x = v[i * 3], y = v[i * 3 + 1], z = v[i * 3 + 2];
            if (x < mnx) mnx = x; if (x > mxx) mxx = x;
            if (y < mny) mny = y; if (y > mxy) mxy = y;
            if (z < mnz) mnz = z; if (z > mxz) mxz = z;
        }
        if (vc === 0) { mnx = mny = mnz = mxx = mxy = mxz = 0; }
        const b = this.__bbox;
        b[0] = mnx; b[1] = mny; b[2] = mnz; b[3] = mxx; b[4] = mxy; b[5] = mxz;

        const tri = idx.length / 3;
        const planes = new Float64Array(tri * 4);
        let area = 0;
        let vol6 = 0; // 6× the signed volume (Σ a·(b×c) over outward faces)
        for (let f = 0; f < tri; f++) {
            const ia = idx[f * 3] * 3, ib = idx[f * 3 + 1] * 3, ic = idx[f * 3 + 2] * 3;
            const ax = v[ia], ay = v[ia + 1], az = v[ia + 2];
            const bx = v[ib], by = v[ib + 1], bz = v[ib + 2];
            const cx = v[ic], cy = v[ic + 1], cz = v[ic + 2];

            const e1x = bx - ax, e1y = by - ay, e1z = bz - az;
            const e2x = cx - ax, e2y = cy - ay, e2z = cz - az;
            let nx = e1y * e2z - e1z * e2y;
            let ny = e1z * e2x - e1x * e2z;
            let nz = e1x * e2y - e1y * e2x;
            const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
            area += 0.5 * len;
            vol6 += ax * (by * cz - bz * cy) + ay * (bz * cx - bx * cz) + az * (bx * cy - by * cx);

            nx /= len; ny /= len; nz /= len;
            const o = f * 4;
            planes[o] = nx; planes[o + 1] = ny; planes[o + 2] = nz;
            planes[o + 3] = nx * ax + ny * ay + nz * az;
        }
        this.__planes = planes;
        this.__surface_area = area;
        this.__volume = Math.abs(vol6) / 6;

        // Polygon faces for the clipper. v1: one triangle per face (the contact
        // clipper consumes polygon loops; a tetrahedral piece is exactly this).
        const fo = new Uint32Array(tri + 1);
        for (let f = 0; f <= tri; f++) fo[f] = f * 3;
        this.face_offsets = fo;
        this.face_loops = idx;
    }

    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];
    }

    /**
     * GJK support: the hull vertex furthest along the direction. Exact for a
     * convex point set. Linear scan over the vertices — fine for the modest
     * vertex counts of authored hulls; a hill-climb over vertex adjacency is
     * the acceleration for very large hulls.
     */
    support(result, result_offset, direction_x, direction_y, direction_z) {
        const v = this.vertices;
        const vc = v.length / 3;
        if (vc === 0) {
            result[result_offset] = 0; result[result_offset + 1] = 0; result[result_offset + 2] = 0;
            return;
        }
        let best = -Infinity;
        let bi = 0;
        for (let i = 0; i < vc; i++) {
            const d = direction_x * v[i * 3] + direction_y * v[i * 3 + 1] + direction_z * 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];
    }

    /**
     * Convex half-space test: inside iff the point is behind (or on) every face
     * plane. Exact for a convex polytope.
     */
    contains_point(point) {
        const px = point[0], py = point[1], pz = point[2];
        const b = this.__bbox;
        if (px < b[0] || py < b[1] || pz < b[2] || px > b[3] || py > b[4] || pz > b[5]) {
            return false;
        }
        const planes = this.__planes;
        const f = planes.length / 4;
        const EPS = 1e-6;
        for (let i = 0; i < f; i++) {
            const o = i * 4;
            if (planes[o] * px + planes[o + 1] * py + planes[o + 2] * pz - planes[o + 3] > EPS) {
                return false;
            }
        }
        return true;
    }

    /**
     * Unsigned nearest-surface distance via a per-triangle scan, signed by
     * {@link contains_point}. Same approach as {@link MeshShape3D}; O(F).
     */
    signed_distance_at_point(point) {
        const v = this.vertices;
        const idx = this.indices;
        const tri = idx.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; i++) {
            const ia = idx[i * 3] * 3, ib = idx[i * 3 + 1] * 3, ic = idx[i * 3 + 2] * 3;
            const ax = v[ia], ay = v[ia + 1], az = v[ia + 2];
            const bx = v[ib], by = v[ib + 1], bz = v[ib + 2];
            const cx = v[ic], cy = v[ic + 1], cz = v[ic + 2];
            computeTriangleClosestPointToPointBarycentric(baryc, 0, px, py, pz, ax, ay, az, bx, by, bz, cx, cy, cz);
            const u = baryc[0], w = baryc[1], t = 1 - u - w;
            const qx = u * ax + w * bx + t * cx;
            const qy = u * ay + w * by + t * cy;
            const qz = u * az + w * bz + t * 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;
    }

    signed_distance_gradient_at_point(result, point) {
        return compute_signed_distance_gradient_by_sampling(result, this, point);
    }

    nearest_point_on_surface(result, reference) {
        const v = this.vertices;
        const idx = this.indices;
        const tri = idx.length / 3;
        const px = reference[0], py = reference[1], pz = reference[2];

        let best_d2 = Infinity;
        let bx_ = 0, by_ = 0, bz_ = 0;
        const baryc = scratch_barycentric;
        for (let i = 0; i < tri; i++) {
            const ia = idx[i * 3] * 3, ib = idx[i * 3 + 1] * 3, ic = idx[i * 3 + 2] * 3;
            const ax = v[ia], ay = v[ia + 1], az = v[ia + 2];
            const bx = v[ib], by = v[ib + 1], bz = v[ib + 2];
            const cx = v[ic], cy = v[ic + 1], cz = v[ic + 2];
            computeTriangleClosestPointToPointBarycentric(baryc, 0, px, py, pz, ax, ay, az, bx, by, bz, cx, cy, cz);
            const u = baryc[0], w = baryc[1], t = 1 - u - w;
            const qx = u * ax + w * bx + t * cx;
            const qy = u * ay + w * by + t * cy;
            const qz = u * az + w * bz + t * 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; bx_ = qx; by_ = qy; bz_ = qz; }
        }
        result[0] = bx_; result[1] = by_; result[2] = bz_;
    }

    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], sy = b[4] - b[1], 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;
    }

    /**
     * @param {ConvexHullShape3D} other
     * @returns {boolean}
     */
    equals(other) {
        if (!super.equals(other)) return false;
        if (this.vertices.length !== other.vertices.length) return false;
        if (this.indices.length !== other.indices.length) return false;
        for (let i = 0; i < this.vertices.length; i++) {
            if (this.vertices[i] !== other.vertices[i]) return false;
        }
        for (let i = 0; i < this.indices.length; i++) {
            if (this.indices[i] !== other.indices[i]) return false;
        }
        return true;
    }

    hash() {
        const v_count = this.vertices.length / 3 | 0;
        const t_count = this.indices.length / 3 | 0;
        let h = (v_count * 31 + t_count) | 0;
        if (this.vertices.length >= 3) {
            h = (h * 31 + (computeHashFloat(this.vertices[0]))) | 0;
            h = (h * 31 + (computeHashFloat(this.vertices[this.vertices.length - 1]))) | 0;
        }
        return h | 0;
    }
}

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

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