import { assert } from "../../../assert.js";
import {
    tetrahedral_mesh_build_vertex_to_tets_map
} from "./tetrahedral_mesh_build_vertex_to_tets_map.js";
import { compute_tetrahedron_quality } from "./compute_tetrahedron_quality.js";
import {
    tetrahedral_mesh_find_tets_around_edge
} from "./tetrahedral_mesh_find_tets_around_edge.js";
import { tetrahedral_mesh_flip_23 } from "./tetrahedral_mesh_flip_23.js";
import { tetrahedral_mesh_flip_32 } from "./tetrahedral_mesh_flip_32.js";
import {
    tetrahedral_mesh_smooth_vertex
} from "./tetrahedral_mesh_smooth_vertex.js";
import {
    tetrahedral_mesh_vertex_is_boundary
} from "./tetrahedral_mesh_vertex_is_boundary.js";
import { tetrahedron_compute_quality } from "./tetrahedron_compute_quality.js";
import { INVALID_NEIGHBOUR } from "./TetrahedralMesh.js";

// Face-vertex layout for a positively-oriented tet (identical to the
// table inside tetrahedral_mesh_flip_23.js). Inlined here so we can score
// a hypothetical 2-3 flip without doing-and-undoing it on the live mesh.
const FACE_VERTICES_BY_OPPOSITE = [
    [1, 3, 2],
    [0, 2, 3],
    [0, 3, 1],
    [0, 1, 2],
];

// Scratch for the 2-3 / 3-2 flip out-parameters. Module-level — the driver
// is not re-entrant.
const scratch_out_23 = new Uint32Array(3);
const scratch_out_32 = new Uint32Array(2);
const scratch_edge_ring = new Array(16);
const scratch_ring_closed = [0];

// The 6 unique vertex-pair (i, j) slots in a 4-vertex tet, enumerating
// every edge once. Used to iterate the 6 edges of a candidate tet during
// 3-2 flip search.
const EDGE_SLOT_PAIRS = [
    [0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3],
];

/**
 * Driver: improve the shape quality of an existing tetrahedral mesh by
 * locally smoothing low-quality tets and applying 2-3 / 3-2 flips that
 * monotonically increase the per-tet minimum quality.
 *
 * Algorithm — one pass:
 *   1. Scan every live tet, compute its radius-ratio quality.
 *   2. For each tet below `target_quality`, try in order:
 *      a. Laplacian smoothing on each non-boundary vertex. Accept the
 *         move only if the worst-of-the-1-ring quality strictly improves;
 *         otherwise revert the position.
 *      b. 2-3 flip across each of the tet's 4 internal faces. Tentatively
 *         apply the flip, score the 3 new tets; accept only if the
 *         minimum-of-three exceeds the original min-of-two. Otherwise
 *         apply the inverse 3-2 flip to roll back.
 *   3. Repeat until no operation in a pass improves anything, or
 *      `max_passes` is reached.
 *
 * The "do then maybe undo" pattern for flips keeps the driver simple: no
 * separate "preview the flip" code path. The cost is a wasted reverse-flip
 * per rejected candidate. In practice most candidate flips on bad tets are
 * either clearly beneficial (accepted) or clearly bad (rejected after one
 * round-trip), so the overhead is small.
 *
 * Mesh topology and points are mutated in place. Surface vertices (any
 * vertex incident to a face with INVALID_NEIGHBOUR) are NEVER moved by
 * smoothing — moving them would change the surface shape, which is
 * usually a hard constraint for the caller.
 *
 * @param {TetrahedralMesh} mesh — mutated in place
 * @param {Float32Array|number[]} points — flat (x,y,z) positions, mutated
 * @param {{
 *   target_quality?: number,
 *   max_passes?: number,
 *   smoothing_weight?: number,
 *   enable_flips?: boolean,
 *   enable_smoothing?: boolean,
 * }} [options]
 * @returns {{
 *   passes_run: number,
 *   smoothings_committed: number,
 *   smoothings_reverted: number,
 *   flips_23_committed: number,
 *   flips_23_rejected: number,
 *   flips_32_committed: number,
 *   flips_32_rejected: number,
 *   initial_min_quality: number,
 *   final_min_quality: number,
 * }}
 */
export function tetrahedral_mesh_improve_quality(mesh, points, options = {}) {
    assert.defined(mesh, 'mesh');
    assert.defined(points, 'points');

    const target_quality   = options.target_quality   ?? 0.1;
    const max_passes       = options.max_passes       ?? 5;
    const smoothing_weight = options.smoothing_weight ?? 0.5;
    const enable_flips     = options.enable_flips     ?? true;
    const enable_smoothing = options.enable_smoothing ?? true;

    const stats = {
        passes_run: 0,
        smoothings_committed: 0,
        smoothings_reverted: 0,
        flips_23_committed: 0,
        flips_23_rejected: 0,
        flips_32_committed: 0,
        flips_32_rejected: 0,
        initial_min_quality: scan_min_quality(mesh, points),
        final_min_quality: 1,
    };

    for (let pass = 0; pass < max_passes; pass++) {
        let any_improvement = false;

        // Snapshot the live tet IDs so iteration is stable even as flips
        // delete and re-introduce tet IDs underneath us.
        const live = mesh.getLive();

        // Build a vertex->tets cache once per pass. Used by the boundary
        // check (flip-invariant — safe to use for the whole pass) and by
        // the local quality eval (filtered through mesh.exists; may miss
        // newly-flipped tets which is fine: the live-snapshot iteration
        // already misses them too, and they'll be picked up next pass).
        const vertex_to_tets = tetrahedral_mesh_build_vertex_to_tets_map(mesh);

        for (let li = 0; li < live.length; li++) {
            const tet = live[li];
            if (!mesh.exists(tet)) continue;

            let q = compute_tetrahedron_quality(mesh, points, tet);
            if (q >= target_quality) continue;

            // ---- Smoothing ---------------------------------------------------
            if (enable_smoothing) {
                for (let i = 0; i < 4; i++) {
                    if (!mesh.exists(tet)) break;
                    const v_id = mesh.getVertexIndex(tet, i);
                    if (tetrahedral_mesh_vertex_is_boundary(mesh, v_id, vertex_to_tets)) continue;

                    const v3 = v_id * 3;
                    const old_x = points[v3];
                    const old_y = points[v3 + 1];
                    const old_z = points[v3 + 2];

                    const old_min = local_min_quality_around_vertex(mesh, points, v_id, vertex_to_tets);

                    const moved = tetrahedral_mesh_smooth_vertex(mesh, points, v_id, smoothing_weight, vertex_to_tets);
                    if (!moved) continue;

                    const new_min = local_min_quality_around_vertex(mesh, points, v_id, vertex_to_tets);
                    if (new_min > old_min) {
                        stats.smoothings_committed++;
                        any_improvement = true;
                        // Re-fetch quality of `tet` — it may have improved.
                        q = compute_tetrahedron_quality(mesh, points, tet);
                        if (q >= target_quality) break;
                    } else {
                        points[v3]     = old_x;
                        points[v3 + 1] = old_y;
                        points[v3 + 2] = old_z;
                        stats.smoothings_reverted++;
                    }
                }
            }

            if (!mesh.exists(tet)) continue;
            q = compute_tetrahedron_quality(mesh, points, tet);
            if (q >= target_quality) continue;

            // ---- 2-3 flip across each internal face -------------------------
            // We SCORE the candidate flip by computing what the 3 new tets'
            // vertex IDs WOULD BE, evaluating quality from the indexed
            // primitive — without touching mesh topology. The actual flip is
            // only applied on accepted candidates, so the mesh never sees a
            // bad transient state and we don't need to invert anything.
            if (enable_flips) {
                for (let face_slot = 0; face_slot < 4; face_slot++) {
                    if (!mesh.exists(tet)) break;

                    const enc = mesh.getNeighbour(tet, face_slot);
                    if (enc === INVALID_NEIGHBOUR) continue;
                    const neighbour_tet = enc >>> 2;
                    if (!mesh.exists(neighbour_tet)) continue;
                    const shared_i2 = enc & 3;

                    const q_before = Math.min(
                        compute_tetrahedron_quality(mesh, points, tet),
                        compute_tetrahedron_quality(mesh, points, neighbour_tet)
                    );

                    // Derive the would-be vertex IDs of the 3 new tets.
                    // d / e: the two apex vertices (the one opposite the
                    // shared face in each of tet and neighbour). p / q / r:
                    // the shared face's vertices in CCW-from-outside-tet
                    // order.
                    const d = mesh.getVertexIndex(tet,           face_slot);
                    const e = mesh.getVertexIndex(neighbour_tet, shared_i2);
                    const face = FACE_VERTICES_BY_OPPOSITE[face_slot];
                    const p = mesh.getVertexIndex(tet, face[0]);
                    const q = mesh.getVertexIndex(tet, face[1]);
                    const r = mesh.getVertexIndex(tet, face[2]);

                    // Canonical layouts: T_α = (r, q, d, e); T_β = (p, r, d, e);
                    // T_γ = (q, p, d, e). Mirrors tetrahedral_mesh_flip_23.
                    const q_alpha = tetrahedron_compute_quality(points, r, q, d, e);
                    const q_beta  = tetrahedron_compute_quality(points, p, r, d, e);
                    const q_gamma = tetrahedron_compute_quality(points, q, p, d, e);
                    const q_after = Math.min(q_alpha, q_beta, q_gamma);

                    if (q_after > q_before) {
                        // Commit: actually perform the flip.
                        tetrahedral_mesh_flip_23(mesh, tet, neighbour_tet, scratch_out_23);
                        stats.flips_23_committed++;
                        any_improvement = true;
                        // `tet` may have been re-used and now represents a
                        // different shape; stop probing its faces and let
                        // the next outer loop iteration pick up the new
                        // low-quality tets.
                        break;
                    }

                    stats.flips_23_rejected++;
                }
            }

            if (!mesh.exists(tet)) continue;
            q = compute_tetrahedron_quality(mesh, points, tet);
            if (q >= target_quality) continue;

            // ---- 3-2 flip on each edge whose ring has exactly 3 tets --------
            // For each of the 6 edges of `tet`, walk the surrounding ring.
            // If exactly 3 tets sit around that edge, we have a valid 3-2
            // candidate. Score it analytically (using the same "compute
            // ring layout from t1's canonical winding" rule as
            // tetrahedral_mesh_flip_32) and commit only if the new pair's
            // min quality beats the old triple's.
            if (enable_flips) {
                for (let ep = 0; ep < EDGE_SLOT_PAIRS.length; ep++) {
                    if (!mesh.exists(tet)) break;

                    const slot_a = EDGE_SLOT_PAIRS[ep][0];
                    const slot_b = EDGE_SLOT_PAIRS[ep][1];
                    const v_a = mesh.getVertexIndex(tet, slot_a);
                    const v_b = mesh.getVertexIndex(tet, slot_b);

                    const ring_count = tetrahedral_mesh_find_tets_around_edge(
                        scratch_edge_ring, 0, mesh, tet, v_a, v_b, scratch_ring_closed
                    );
                    // 3-2 flips are only defined on a CLOSED ring of
                    // exactly 3 tets. The ring walker may return 3 tets
                    // for an open chain on the mesh boundary (e.g. a
                    // vertex where two boundary faces meet at an edge);
                    // those aren't a valid 3-2 input — flip_32 would
                    // throw on them.
                    if (ring_count !== 3 || scratch_ring_closed[0] !== 1) continue;

                    const r0 = scratch_edge_ring[0];
                    const r1 = scratch_edge_ring[1];
                    const r2 = scratch_edge_ring[2];

                    const q_before_32 = Math.min(
                        compute_tetrahedron_quality(mesh, points, r0),
                        compute_tetrahedron_quality(mesh, points, r1),
                        compute_tetrahedron_quality(mesh, points, r2)
                    );

                    // Compute the 3-2 flip's would-be new tet vertex IDs.
                    // Mirrors the derivation in tetrahedral_mesh_flip_32:
                    // read r0's face opposite v_b in CCW-from-outside order,
                    // locate v_a within that triple, and the next two
                    // cyclic positions give the t1-side ring (v_X, v_Y) in
                    // CCW-from-v_a order. v_missing is the third ring vertex
                    // — present in r1 or r2 but not r0.
                    const slot_a_r0 = find_slot(mesh, r0, v_a);
                    const slot_b_r0 = find_slot(mesh, r0, v_b);
                    const face_opp_b = FACE_VERTICES_BY_OPPOSITE[slot_b_r0];
                    const f0 = mesh.getVertexIndex(r0, face_opp_b[0]);
                    const f1 = mesh.getVertexIndex(r0, face_opp_b[1]);
                    const f2 = mesh.getVertexIndex(r0, face_opp_b[2]);
                    let pos_a = -1;
                    if (f0 === v_a) pos_a = 0;
                    else if (f1 === v_a) pos_a = 1;
                    else if (f2 === v_a) pos_a = 2;
                    const face_array = [f0, f1, f2];
                    const v_X = face_array[(pos_a + 1) % 3];
                    const v_Y = face_array[(pos_a + 2) % 3];

                    // v_missing is the only ring vertex not in r0. It's in
                    // both r1 and r2; finding it requires walking another
                    // tet's vertices.
                    let v_missing = -1;
                    for (let i = 0; i < 4; i++) {
                        const v = mesh.getVertexIndex(r1, i);
                        if (v !== v_a && v !== v_b && v !== v_X && v !== v_Y) {
                            v_missing = v;
                            break;
                        }
                    }
                    if (v_missing < 0) continue; // shouldn't happen, but guard

                    // Canonical new layouts (mirrors flip_32):
                    //   new_tet_a = (v_X, v_missing, v_Y, v_a)
                    //   new_tet_b = (v_X, v_Y, v_missing, v_b)
                    const q_new_a = tetrahedron_compute_quality(points, v_X, v_missing, v_Y, v_a);
                    const q_new_b = tetrahedron_compute_quality(points, v_X, v_Y, v_missing, v_b);
                    const q_after_32 = Math.min(q_new_a, q_new_b);

                    if (q_after_32 > q_before_32) {
                        tetrahedral_mesh_flip_32(mesh, r0, r1, r2, scratch_out_32);
                        stats.flips_32_committed++;
                        any_improvement = true;
                        break; // tet's identity may now be one of the new tets
                    }

                    stats.flips_32_rejected++;
                }
            }
        }

        stats.passes_run = pass + 1;
        if (!any_improvement) break;
    }

    stats.final_min_quality = scan_min_quality(mesh, points);
    return stats;
}

/**
 * Index of `vertex_id` within `tet`'s 4 slots, or -1 if absent.
 */
function find_slot(mesh, tet, vertex_id) {
    for (let i = 0; i < 4; i++) {
        if (mesh.getVertexIndex(tet, i) === vertex_id) return i;
    }
    return -1;
}

/**
 * Min quality across every live tet in the mesh.
 */
function scan_min_quality(mesh, points) {
    let min_q = 1;
    mesh.forEach((tet, m) => {
        const q = compute_tetrahedron_quality(m, points, tet);
        if (q < min_q) min_q = q;
    });
    return min_q;
}

/**
 * Min quality across all tets containing `v_id`. Used to score the local
 * effect of moving (or undoing the move of) a single vertex.
 *
 * Reads the incident tets directly from the pass-level cache and filters
 * out tets that have since been deleted. Cache entries whose vertex set
 * no longer contains `v_id` (because a flip re-used the slot) are
 * silently skipped — those tets are no longer part of `v_id`'s 1-ring
 * anyway.
 */
function local_min_quality_around_vertex(mesh, points, v_id, vertex_to_tets) {
    const tets = vertex_to_tets.get(v_id);
    if (tets === undefined) return 1;

    let min_q = 1;
    for (let i = 0; i < tets.length; i++) {
        const tet = tets[i];
        if (!mesh.exists(tet)) continue;
        // Tet may have been re-used by a flip with a different vertex set;
        // confirm v_id is still incident before counting it.
        let still_incident = false;
        for (let s = 0; s < 4; s++) {
            if (mesh.getVertexIndex(tet, s) === v_id) {
                still_incident = true;
                break;
            }
        }
        if (!still_incident) continue;

        const q = compute_tetrahedron_quality(mesh, points, tet);
        if (q < min_q) min_q = q;
    }
    return min_q;
}
