import { assert } from "../../../assert.js";
import { INVALID_NEIGHBOUR } from "./TetrahedralMesh.js";

/**
 * Return true if the given vertex lies on the boundary of the
 * tetrahedral mesh — equivalently, if it sits on any face that has no
 * neighbouring tet (INVALID_NEIGHBOUR).
 *
 * Used by quality-improvement passes to skip boundary vertices when
 * smoothing or collapsing, because moving them would change the mesh's
 * external surface (which usually represents some geometric constraint
 * the caller wants preserved).
 *
 * Cost without `vertex_to_tets_map`: O(N) over the live tets, same as
 * {@link tetrahedral_mesh_find_tets_attached_to_vertex} which it shares
 * the same scan structure with.
 *
 * Cost with `vertex_to_tets_map`: O(deg(vertex)) — usually a small
 * constant. For repeated calls over many vertices in a tight loop
 * (e.g. a sliver-removal pass), build the map once via
 * {@link tetrahedral_mesh_build_vertex_to_tets_map} and pass it in.
 *
 * Note: boundary status is invariant under interior 2-3 / 3-2 flips, so
 * a once-per-pass cache stays valid for boundary queries even as the
 * pass commits flips, as long as a freshly-snapshotted cache is built
 * for each NEW pass.
 *
 * @param {TetrahedralMesh} mesh
 * @param {number} vertex_id
 * @param {Map<number, number[]>} [vertex_to_tets_map] optional cache
 * @returns {boolean}
 */
export function tetrahedral_mesh_vertex_is_boundary(mesh, vertex_id, vertex_to_tets_map) {
    assert.defined(mesh, 'mesh');
    assert.isNonNegativeInteger(vertex_id, 'vertex_id');

    if (vertex_to_tets_map !== undefined) {
        const tets = vertex_to_tets_map.get(vertex_id);
        if (tets === undefined) return false;

        for (let ti = 0; ti < tets.length; ti++) {
            const tet = tets[ti];
            // Tets in the cache may have been deleted since it was built.
            if (!mesh.exists(tet)) continue;

            // Find the slot of vertex_id in this tet — present by
            // construction unless a flip re-used the tet ID with
            // different vertices, in which case skip.
            let v_slot = -1;
            for (let i = 0; i < 4; i++) {
                if (mesh.getVertexIndex(tet, i) === vertex_id) {
                    v_slot = i;
                    break;
                }
            }
            if (v_slot < 0) continue;

            // The 3 faces containing vertex_id are the ones NOT opposite v_slot.
            for (let j = 0; j < 4; j++) {
                if (j === v_slot) continue;
                if (mesh.getNeighbour(tet, j) === INVALID_NEIGHBOUR) {
                    return true;
                }
            }
        }
        return false;
    }

    // Slow path: scan every live tet.
    let is_boundary = false;

    mesh.forEach((tet, m) => {
        if (is_boundary) return;

        // Find the slot of vertex_id in this tet (if any).
        let v_slot = -1;
        for (let i = 0; i < 4; i++) {
            if (m.getVertexIndex(tet, i) === vertex_id) {
                v_slot = i;
                break;
            }
        }
        if (v_slot < 0) return;

        // The 3 faces containing vertex_id are the ones NOT opposite v_slot.
        // (The face opposite v_slot is the only face that excludes vertex_id.)
        for (let j = 0; j < 4; j++) {
            if (j === v_slot) continue;
            if (m.getNeighbour(tet, j) === INVALID_NEIGHBOUR) {
                is_boundary = true;
                return;
            }
        }
    });

    return is_boundary;
}
