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

/**
 * For a tet stored as (v0, v1, v2, v3), the vertices of the face opposite
 * vertex i, in counter-clockwise order viewed from outside the tet (i.e. the
 * standard outward-facing winding for a positively-oriented tet whose signed
 * volume is positive under the engine's `compute_tetrahedron_volume`).
 *
 * Derived from the volume formula vol = (1/6)·det[v0-v3, v1-v3, v2-v3]:
 *   - Face opposite v3 is the canonical case (v0, v1, v2).
 *   - Other faces follow by even-permutation parity to keep the implied tet
 *     (face, opposite_vertex) positively wound.
 */
const FACE_VERTICES_BY_OPPOSITE = [
    [1, 3, 2], // opposite v0
    [0, 2, 3], // opposite v1
    [0, 3, 1], // opposite v2
    [0, 1, 2], // opposite v3
];

/**
 * Perform a 2-3 flip on two tetrahedra that share a face.
 *
 * Input two tets t1, t2 sharing a triangle face. The flip removes that face
 * and replaces it with a new edge connecting the two "apex" vertices (the
 * vertex of each tet that's NOT on the shared face). The shared bipyramid is
 * re-tetrahedralised into three new tets, each containing the new edge plus
 * one edge of the original shared face.
 *
 * Counts go from (2 tets, 5 vertices total, 1 internal face) to (3 tets, 5
 * vertices, 3 internal faces). No vertex IDs change; the operation is purely
 * topological — it doesn't touch the points array.
 *
 * Geometric validity (positive volume of each new tet under any specific
 * embedding) is the caller's responsibility. The flip preserves orientation
 * — if t1 was positively wound the new tets are too — but the new edge
 * (d, e) must actually lie inside the convex region formed by the original
 * two tets for the result to be a valid embedding. Outside Delaunay /
 * constraint-recovery contexts this is the caller's domain knowledge.
 *
 * Implementation notes:
 *   - t1's storage slot is reused for one of the new tets ("T_alpha")
 *   - t2's slot for another ("T_beta")
 *   - one fresh slot is allocated for the third ("T_gamma")
 *   - all four external neighbour back-pointers around each of t1 and t2
 *     are rewritten to point at the right new tet/slot
 *
 * @param {TetrahedralMesh} mesh
 * @param {number} t1
 * @param {number} t2 must share a face with t1
 * @param {Uint32Array|number[]} out output storage; out[0..2] receive the 3
 *        new tet IDs. Caller provides — the function performs no allocation.
 *        Must have length ≥ 3.
 */
export function tetrahedral_mesh_flip_23(mesh, t1, t2, out) {
    assert.defined(mesh, 'mesh');
    assert.equal(mesh.isTetrahedralMesh, true, 'mesh.isTetrahedralMesh !== true');
    assert.isNonNegativeInteger(t1, 't1');
    assert.isNonNegativeInteger(t2, 't2');
    assert.notEqual(t1, t2, 't1 must not equal t2');
    assert.defined(out, 'out');

    // -----------------------------------------------------------------
    // Step 1: locate the shared face.
    // -----------------------------------------------------------------
    let shared_i1 = -1;
    let shared_i2 = -1;
    for (let i = 0; i < 4; i++) {
        const enc = mesh.getNeighbour(t1, i);
        if (enc === INVALID_NEIGHBOUR) continue;
        if ((enc >>> 2) === t2) {
            shared_i1 = i;
            shared_i2 = enc & 3;
            break;
        }
    }
    if (shared_i1 < 0) {
        throw new Error(`tets ${t1} and ${t2} do not share a face`);
    }

    // -----------------------------------------------------------------
    // Step 2: identify the 5 distinct vertices.
    //   d = apex of t1 (opposite the shared face)
    //   e = apex of t2
    //   (p, q, r) = shared face vertices, in CCW order seen from outside t1
    //               (i.e. the standard "outward face" ordering)
    // -----------------------------------------------------------------
    const d = mesh.getVertexIndex(t1, shared_i1);
    const e = mesh.getVertexIndex(t2, shared_i2);

    const face_t1 = FACE_VERTICES_BY_OPPOSITE[shared_i1];
    const pos_p_in_t1 = face_t1[0];
    const pos_q_in_t1 = face_t1[1];
    const pos_r_in_t1 = face_t1[2];

    const p = mesh.getVertexIndex(t1, pos_p_in_t1);
    const q = mesh.getVertexIndex(t1, pos_q_in_t1);
    const r = mesh.getVertexIndex(t1, pos_r_in_t1);

    // Find where p, q, r live in t2's vertex array. Needed to look up which
    // of t2's external neighbours sits opposite each of p, q, r.
    let pos_p_in_t2 = -1, pos_q_in_t2 = -1, pos_r_in_t2 = -1;
    for (let i = 0; i < 4; i++) {
        const v = mesh.getVertexIndex(t2, i);
        if (v === p) pos_p_in_t2 = i;
        else if (v === q) pos_q_in_t2 = i;
        else if (v === r) pos_r_in_t2 = i;
    }
    if (pos_p_in_t2 < 0 || pos_q_in_t2 < 0 || pos_r_in_t2 < 0) {
        throw new Error(
            `tets ${t1} and ${t2} are recorded as neighbours but their shared face vertices don't match — mesh integrity broken`
        );
    }

    // -----------------------------------------------------------------
    // Step 3: snapshot external neighbours BEFORE we start writing.
    //   t1's faces opposite p, q, r are external (the face opposite d is t2).
    //   Same for t2.
    // -----------------------------------------------------------------
    const ext_p_t1 = mesh.getNeighbour(t1, pos_p_in_t1);
    const ext_q_t1 = mesh.getNeighbour(t1, pos_q_in_t1);
    const ext_r_t1 = mesh.getNeighbour(t1, pos_r_in_t1);
    const ext_p_t2 = mesh.getNeighbour(t2, pos_p_in_t2);
    const ext_q_t2 = mesh.getNeighbour(t2, pos_q_in_t2);
    const ext_r_t2 = mesh.getNeighbour(t2, pos_r_in_t2);

    // -----------------------------------------------------------------
    // Step 4: allocate the third slot. Reuse t1 → T_alpha, t2 → T_beta.
    // -----------------------------------------------------------------
    const T_alpha = t1;
    const T_beta = t2;
    const T_gamma = mesh.allocate();

    // -----------------------------------------------------------------
    // Step 5: write vertices in canonical positive-volume order.
    //   T_alpha = (r, q, d, e)  — covers edge (q, r), missing p
    //   T_beta  = (p, r, d, e)  — covers edge (r, p), missing q
    //   T_gamma = (q, p, d, e)  — covers edge (p, q), missing r
    // Each ordering was derived (and verified) to keep the new tet's signed
    // volume the same sign as the original t1/t2 under the engine's
    // convention.
    // -----------------------------------------------------------------
    mesh.setVertexIndex(T_alpha, 0, r);
    mesh.setVertexIndex(T_alpha, 1, q);
    mesh.setVertexIndex(T_alpha, 2, d);
    mesh.setVertexIndex(T_alpha, 3, e);

    mesh.setVertexIndex(T_beta, 0, p);
    mesh.setVertexIndex(T_beta, 1, r);
    mesh.setVertexIndex(T_beta, 2, d);
    mesh.setVertexIndex(T_beta, 3, e);

    mesh.setVertexIndex(T_gamma, 0, q);
    mesh.setVertexIndex(T_gamma, 1, p);
    mesh.setVertexIndex(T_gamma, 2, d);
    mesh.setVertexIndex(T_gamma, 3, e);

    // -----------------------------------------------------------------
    // Step 6: internal neighbour links between the three new tets.
    //
    // Walking each new tet's faces:
    //   T_alpha = (r, q, d, e):
    //     slot 0 (opp r) face {q,d,e} — shared with T_gamma's slot 1 (opp p)
    //     slot 1 (opp q) face {r,d,e} — shared with T_beta's slot 0 (opp p)
    //     slot 2 (opp d) face {r,q,e} — external, was opposite p in t2
    //     slot 3 (opp e) face {r,q,d} — external, was opposite p in t1
    //
    //   T_beta = (p, r, d, e):
    //     slot 0 (opp p) face {r,d,e} — shared with T_alpha's slot 1 (opp q)
    //     slot 1 (opp r) face {p,d,e} — shared with T_gamma's slot 0 (opp q)
    //     slot 2 (opp d) face {p,r,e} — external, was opposite q in t2
    //     slot 3 (opp e) face {p,r,d} — external, was opposite q in t1
    //
    //   T_gamma = (q, p, d, e):
    //     slot 0 (opp q) face {p,d,e} — shared with T_beta's slot 1 (opp r)
    //     slot 1 (opp p) face {q,d,e} — shared with T_alpha's slot 0 (opp r)
    //     slot 2 (opp d) face {q,p,e} — external, was opposite r in t2
    //     slot 3 (opp e) face {q,p,d} — external, was opposite r in t1
    // -----------------------------------------------------------------
    mesh.setNeighbour(T_alpha, 0, (T_gamma << 2) | 1);
    mesh.setNeighbour(T_alpha, 1, (T_beta  << 2) | 0);
    mesh.setNeighbour(T_beta,  0, (T_alpha << 2) | 1);
    mesh.setNeighbour(T_beta,  1, (T_gamma << 2) | 0);
    mesh.setNeighbour(T_gamma, 0, (T_beta  << 2) | 1);
    mesh.setNeighbour(T_gamma, 1, (T_alpha << 2) | 0);

    // -----------------------------------------------------------------
    // Step 7: external neighbour links. The new tet inherits the SAME
    // encoded value the old tet held — the neighbour tet ID and its
    // back-slot don't change. Only the new tet's own slot needs the value.
    // -----------------------------------------------------------------
    mesh.setNeighbour(T_alpha, 2, ext_p_t2);
    mesh.setNeighbour(T_alpha, 3, ext_p_t1);
    mesh.setNeighbour(T_beta,  2, ext_q_t2);
    mesh.setNeighbour(T_beta,  3, ext_q_t1);
    mesh.setNeighbour(T_gamma, 2, ext_r_t2);
    mesh.setNeighbour(T_gamma, 3, ext_r_t1);

    // -----------------------------------------------------------------
    // Step 8: rewrite the external neighbours' back-pointers. Each one
    // currently points at t1 or t2; now they should point at the
    // corresponding new tet and slot.
    // -----------------------------------------------------------------
    rewrite_back_pointer(mesh, ext_p_t2, T_alpha, 2);
    rewrite_back_pointer(mesh, ext_p_t1, T_alpha, 3);
    rewrite_back_pointer(mesh, ext_q_t2, T_beta,  2);
    rewrite_back_pointer(mesh, ext_q_t1, T_beta,  3);
    rewrite_back_pointer(mesh, ext_r_t2, T_gamma, 2);
    rewrite_back_pointer(mesh, ext_r_t1, T_gamma, 3);

    out[0] = T_alpha;
    out[1] = T_beta;
    out[2] = T_gamma;
}

/**
 * Given an encoded neighbour reference (as stored in a tet's neighbour slot)
 * that used to point at some old tet, rewrite the OTHER end of that link so
 * it points at `new_tet` at `new_slot` instead. The low 2 bits of the
 * encoded value tell us the slot in the external tet that holds the
 * back-pointer.
 *
 * No-op if the encoded reference is INVALID_NEIGHBOUR (the old tet had no
 * neighbour on that face).
 */
function rewrite_back_pointer(mesh, encoded, new_tet, new_slot) {
    if (encoded === INVALID_NEIGHBOUR) return;
    const ext_tet = encoded >>> 2;
    const back_slot = encoded & 3;
    mesh.setNeighbour(ext_tet, back_slot, (new_tet << 2) | new_slot);
}
