import { assert } from "../../../assert.js";
import {
    tetrahedral_mesh_find_tets_attached_to_vertex
} from "./tetrahedral_mesh_find_tets_attached_to_vertex.js";

// Module-level scratch buffers, sized for typical vertex valence (~20-40
// incident tets, ~30-60 1-ring neighbours). Grow on demand. Re-used across
// calls — the function is therefore NOT re-entrant.
let scratch_tets = new Array(64);
let scratch_neighbours = new Array(128);

/**
 * Move a vertex toward the centroid of its 1-ring neighbours (Laplacian
 * smoothing), at the given weight: `w == 0` keeps the vertex where it is,
 * `w == 1` snaps it to the centroid, `0 < w < 1` blends. Default `w = 1`.
 *
 * Mutates `points` in place. Does NOT touch the mesh topology.
 *
 * The 1-ring is computed by finding every tet that contains `vertex_id` and
 * collecting the OTHER 3 vertices of each tet, deduplicated.
 *
 * If `vertex_to_tets_map` is provided (a `Map<vertex_id, tet_id[]>` from
 * {@link tetrahedral_mesh_build_vertex_to_tets_map}), the incident-tet
 * lookup runs in O(deg) instead of O(N). Entries pointing to deleted tets
 * or to tets that no longer contain `vertex_id` (e.g. flipped) are
 * silently filtered.
 *
 * Does NOT check whether the vertex sits on the mesh boundary — the caller
 * is responsible for skipping boundary vertices (via
 * {@link tetrahedral_mesh_vertex_is_boundary}) if surface preservation
 * matters. Moving a boundary vertex changes the mesh's external shape.
 *
 * Returns `true` if the vertex was actually moved (i.e. it had ≥ 1 neighbour
 * and the centroid differs from the current position), `false` otherwise
 * (isolated vertex, or w == 0, or already at the centroid). The boolean
 * lets a quality-improvement driver short-circuit on "nothing happened".
 *
 * @param {TetrahedralMesh} mesh
 * @param {Float32Array|number[]} points flat (x,y,z) positions — mutated in place
 * @param {number} vertex_id
 * @param {number} [weight] move fraction, default 1.0
 * @param {Map<number, number[]>} [vertex_to_tets_map] optional adjacency cache
 * @returns {boolean} true if `points` was modified
 */
export function tetrahedral_mesh_smooth_vertex(mesh, points, vertex_id, weight = 1.0, vertex_to_tets_map) {
    assert.defined(mesh, 'mesh');
    assert.defined(points, 'points');
    assert.isNonNegativeInteger(vertex_id, 'vertex_id');
    assert.isNumber(weight, 'weight');

    if (weight === 0) return false;

    // 1. Find tets containing the vertex.
    let tet_count;
    if (vertex_to_tets_map !== undefined) {
        const cached = vertex_to_tets_map.get(vertex_id);
        if (cached === undefined) return false;
        if (scratch_tets.length < cached.length) {
            scratch_tets = new Array(Math.max(cached.length, scratch_tets.length * 2));
        }
        // Filter stale entries (deleted tets, or tets that no longer
        // contain vertex_id because a flip re-used the slot).
        tet_count = 0;
        for (let i = 0; i < cached.length; i++) {
            const tet = cached[i];
            if (!mesh.exists(tet)) continue;
            let still_incident = false;
            for (let s = 0; s < 4; s++) {
                if (mesh.getVertexIndex(tet, s) === vertex_id) {
                    still_incident = true;
                    break;
                }
            }
            if (still_incident) scratch_tets[tet_count++] = tet;
        }
    } else {
        if (scratch_tets.length < mesh.count) {
            scratch_tets = new Array(Math.max(mesh.count, scratch_tets.length * 2));
        }
        tet_count = tetrahedral_mesh_find_tets_attached_to_vertex(
            scratch_tets, 0, mesh, vertex_id
        );
    }

    if (tet_count === 0) return false;

    // 2. Collect 1-ring neighbours (the other 3 vertices of each incident
    // tet), deduplicated. Linear search dedup — fine for typical valences
    // (the neighbour list rarely exceeds a few dozen).
    let neighbour_count = 0;
    for (let i = 0; i < tet_count; i++) {
        const tet = scratch_tets[i];
        for (let j = 0; j < 4; j++) {
            const v = mesh.getVertexIndex(tet, j);
            if (v === vertex_id) continue;
            let already = false;
            for (let k = 0; k < neighbour_count; k++) {
                if (scratch_neighbours[k] === v) { already = true; break; }
            }
            if (!already) {
                if (neighbour_count >= scratch_neighbours.length) {
                    scratch_neighbours.length = scratch_neighbours.length * 2;
                }
                scratch_neighbours[neighbour_count++] = v;
            }
        }
    }

    if (neighbour_count === 0) return false;

    // 3. Compute 1-ring centroid.
    let cx = 0, cy = 0, cz = 0;
    for (let i = 0; i < neighbour_count; i++) {
        const v3 = scratch_neighbours[i] * 3;
        cx += points[v3];
        cy += points[v3 + 1];
        cz += points[v3 + 2];
    }
    const inv_n = 1 / neighbour_count;
    cx *= inv_n;
    cy *= inv_n;
    cz *= inv_n;

    // 4. Blend toward centroid.
    const v3 = vertex_id * 3;
    const old_x = points[v3];
    const old_y = points[v3 + 1];
    const old_z = points[v3 + 2];

    const new_x = old_x + weight * (cx - old_x);
    const new_y = old_y + weight * (cy - old_y);
    const new_z = old_z + weight * (cz - old_z);

    if (new_x === old_x && new_y === old_y && new_z === old_z) return false;

    points[v3]     = new_x;
    points[v3 + 1] = new_y;
    points[v3 + 2] = new_z;
    return true;
}
