import { assert } from "../../../../../../assert.js";
import { BVH, NULL_NODE } from "../../../../../../bvh2/bvh3/BVH.js";
import { bvh_query_user_data_ray } from "../../../../../../bvh2/bvh3/query/bvh_query_user_data_ray.js";
import { seededRandom } from "../../../../../../math/random/seededRandom.js";
import { SurfacePoint3 } from "../../../../SurfacePoint3.js";
import { computeTriangleRayIntersection } from "../../../../triangle/computeTriangleRayIntersection.js";

// Three irrational, mutually unrelated ray directions for parity ray-cast
// majority vote. Same scheme as tetrahedral_mesh_carve_outside_surface — a
// single ray is fragile at grazing edges, three independent rays settle the
// vote correctly except in vanishingly improbable configurations.
const RAY_DIRECTIONS = [
    0.97123,  0.18927,  0.14431,
    0.34219,  0.91283, -0.22971,
   -0.51237,  0.27894,  0.81235,
];
const RAY_COUNT = RAY_DIRECTIONS.length / 3;

// Stratification offset cap (as a fraction of a cell). 0 = perfect grid
// (worst case for Delaunay sliver count), 0.5 = candidates can sit anywhere
// in the cell. 0.35 is the usual sweet spot: enough jitter to break all
// face/edge alignments without losing the locality benefits of the grid.
const JITTER = 0.35;

/**
 * Build a BVH whose leaves are the surface's triangle faces. User data on
 * each leaf is the source face ID.
 */
function build_surface_bvh(bvh, mesh) {
    bvh.release_all();

    const face_count = mesh.faces.size;
    const v = [0, 0, 0];

    for (let f = 0; f < face_count; f++) {
        if (!mesh.faces.is_allocated(f)) continue;

        const l0 = mesh.face_read_loop(f);
        const l1 = mesh.loop_read_next(l0);
        const l2 = mesh.loop_read_next(l1);

        const v0 = mesh.loop_read_vertex(l0);
        const v1 = mesh.loop_read_vertex(l1);
        const v2 = mesh.loop_read_vertex(l2);

        mesh.vertex_read_coordinate(v, 0, v0);
        let minx = v[0], miny = v[1], minz = v[2];
        let maxx = v[0], maxy = v[1], maxz = v[2];

        mesh.vertex_read_coordinate(v, 0, v1);
        if (v[0] < minx) minx = v[0]; else if (v[0] > maxx) maxx = v[0];
        if (v[1] < miny) miny = v[1]; else if (v[1] > maxy) maxy = v[1];
        if (v[2] < minz) minz = v[2]; else if (v[2] > maxz) maxz = v[2];

        mesh.vertex_read_coordinate(v, 0, v2);
        if (v[0] < minx) minx = v[0]; else if (v[0] > maxx) maxx = v[0];
        if (v[1] < miny) miny = v[1]; else if (v[1] > maxy) maxy = v[1];
        if (v[2] < minz) minz = v[2]; else if (v[2] > maxz) maxz = v[2];

        const node = bvh.allocate_node();
        bvh.node_set_user_data(node, f);
        bvh.node_set_aabb_primitive(node, minx, miny, minz, maxx, maxy, maxz);
        bvh.insert_leaf(node);
    }
}

const ray_hit = new SurfacePoint3();
const ray_candidates = [];
const va = [0, 0, 0];
const vb = [0, 0, 0];
const vc = [0, 0, 0];

/**
 * Parity ray-cast across `RAY_COUNT` directions; majority decides
 * inside/outside. Returns true if (strict majority of) rays cross the
 * surface an odd number of times.
 */
function point_is_inside(mesh, bvh, ox, oy, oz) {
    const root = bvh.root;
    if (root === NULL_NODE) return false;

    let inside_votes = 0;

    for (let r = 0; r < RAY_COUNT; r++) {
        const dx = RAY_DIRECTIONS[r * 3];
        const dy = RAY_DIRECTIONS[r * 3 + 1];
        const dz = RAY_DIRECTIONS[r * 3 + 2];

        const candidate_count = bvh_query_user_data_ray(
            bvh, root,
            ray_candidates, 0,
            ox, oy, oz,
            dx, dy, dz
        );

        let hits = 0;
        for (let i = 0; i < candidate_count; i++) {
            const f = ray_candidates[i];
            const l0 = mesh.face_read_loop(f);
            const l1 = mesh.loop_read_next(l0);
            const l2 = mesh.loop_read_next(l1);

            mesh.vertex_read_coordinate(va, 0, mesh.loop_read_vertex(l0));
            mesh.vertex_read_coordinate(vb, 0, mesh.loop_read_vertex(l1));
            mesh.vertex_read_coordinate(vc, 0, mesh.loop_read_vertex(l2));

            const hit = computeTriangleRayIntersection(
                ray_hit,
                ox, oy, oz,
                dx, dy, dz,
                va[0], va[1], va[2],
                vb[0], vb[1], vb[2],
                vc[0], vc[1], vc[2]
            );

            if (hit) hits++;
        }

        if ((hits & 1) === 1) inside_votes++;
    }

    return inside_votes * 2 > RAY_COUNT;
}

/**
 * Sample interior Steiner points for the volume enclosed by a closed
 * triangle surface, on a jittered axis-aligned grid sized by
 * `cells_per_dim`. Points outside the surface are discarded; what remains
 * is a Float32Array of (x, y, z) tuples that lies strictly inside the
 * surface (modulo classification noise on near-surface samples).
 *
 * Intended as input to `compute_delaunay_tetrahedral_mesh` alongside the
 * surface vertices, so the Delaunay tet mesh has interior detail rather
 * than being made entirely of surface-bound tets. Without these Steiner
 * points, every tet of a non-trivial volume straddles or touches the
 * surface, blowing up centroid-classification discretization error in
 * the carving step.
 *
 * Grid cells are scaled so the longest bounding-box axis gets
 * `cells_per_dim` cells; other axes get a proportional share rounded to
 * the nearest integer (minimum 1). Each cell contributes one candidate
 * point at its center, perturbed by a deterministic seeded jitter of up
 * to ±0.35 cell widths — enough to break Delaunay-pathological
 * alignments without losing the grid's spatial uniformity.
 *
 * @param {BinaryTopology} mesh closed surface
 * @param {number} cells_per_dim grid resolution along the longest bbox axis
 * @param {number} [seed] seed for the jitter RNG; same seed → same points
 * @returns {Float32Array} flat (x, y, z) interior point positions
 */
export function bt_mesh_sample_interior_grid_points(mesh, cells_per_dim, seed = 0) {
    assert.defined(mesh, 'mesh');
    assert.equal(mesh.isBinaryTopology, true, 'mesh.isBinaryTopology !== true');
    assert.isPositiveInteger(cells_per_dim, 'cells_per_dim');

    // Compute bounding box from vertex pool.
    const vertices = mesh.vertices;
    const vertex_count = vertices.size;
    const coord = [0, 0, 0];

    let min_x = Infinity, min_y = Infinity, min_z = Infinity;
    let max_x = -Infinity, max_y = -Infinity, max_z = -Infinity;

    for (let v = 0; v < vertex_count; v++) {
        if (!vertices.is_allocated(v)) continue;
        mesh.vertex_read_coordinate(coord, 0, v);
        if (coord[0] < min_x) min_x = coord[0]; if (coord[0] > max_x) max_x = coord[0];
        if (coord[1] < min_y) min_y = coord[1]; if (coord[1] > max_y) max_y = coord[1];
        if (coord[2] < min_z) min_z = coord[2]; if (coord[2] > max_z) max_z = coord[2];
    }

    if (!isFinite(min_x)) {
        // Empty mesh.
        return new Float32Array(0);
    }

    const dx = max_x - min_x;
    const dy = max_y - min_y;
    const dz = max_z - min_z;
    const longest = Math.max(dx, dy, dz);

    if (longest === 0) {
        // Degenerate: all vertices coincide. No interior to sample.
        return new Float32Array(0);
    }

    // Per-axis cell count, rounded so cells are roughly cubical.
    const cells_x = Math.max(1, Math.round((cells_per_dim * dx) / longest));
    const cells_y = Math.max(1, Math.round((cells_per_dim * dy) / longest));
    const cells_z = Math.max(1, Math.round((cells_per_dim * dz) / longest));

    const cell_w = dx / cells_x;
    const cell_h = dy / cells_y;
    const cell_d = dz / cells_z;

    // Build BVH over surface triangles for the inside-test.
    const bvh = new BVH();
    build_surface_bvh(bvh, mesh);

    const rng = seededRandom(seed);
    const out = []; // grow dynamically; copy to Float32Array at the end

    for (let iz = 0; iz < cells_z; iz++) {
        const cz0 = min_z + (iz + 0.5) * cell_d;
        for (let iy = 0; iy < cells_y; iy++) {
            const cy0 = min_y + (iy + 0.5) * cell_h;
            for (let ix = 0; ix < cells_x; ix++) {
                const cx0 = min_x + (ix + 0.5) * cell_w;

                // Jitter each axis independently within ±JITTER cells.
                const jx = (rng() * 2 - 1) * JITTER * cell_w;
                const jy = (rng() * 2 - 1) * JITTER * cell_h;
                const jz = (rng() * 2 - 1) * JITTER * cell_d;

                const px = cx0 + jx;
                const py = cy0 + jy;
                const pz = cz0 + jz;

                if (point_is_inside(mesh, bvh, px, py, pz)) {
                    out.push(px, py, pz);
                }
            }
        }
    }

    return new Float32Array(out);
}
