import { assert } from "../../../../../../assert.js";
import { clamp01 } from "../../../../../../math/clamp01.js";
import { inverseLerp } from "../../../../../../math/inverseLerp.js";
import {
    line3_compute_segment_point_distance_eikonal
} from "../../../../line/line3_compute_segment_point_distance_eikonal.js";
import { NULL_POINTER } from "../BinaryTopology.js";
import { bt_face_get_incenter } from "../query/bt_face_get_incenter.js";
import { bt_mesh_build_boundary_distance_field } from "../query/bt_mesh_build_boundary_distance_field.js";
import { bt_mesh_cleanup_faceless_references } from "./bt_mesh_cleanup_faceless_references.js";
import { bt_edge_split } from "./edge/bt_edge_split.js";
import { bt_face_kill } from "./face/bt_face_kill.js";
import { bt_mesh_face_poke } from "./face/bt_face_poke.js";

/**
 * Shrinks an island of faces by a given distance.
 * Conceptually, this moves the outline of the island inwards.
 * Assumes the input mesh is triangulated, see {@link bt_mesh_triangulate}.
 *
 * It is assumed that the island does not share any edges/vertices with any other part of the mesh. If this assumption is violated - expect undefined behavior.
 * An island is not restricted to all faces being co-planar, it may curve.
 * The erosion distance may be smaller than the smallest triangle height in the island, or it may be significantly larger - there is no restriction.
 *
 * @param {BinaryTopology} mesh
 * @param {number[]} faces
 * @param {number} erode_distance
 */
export function bt_mesh_face_island_erode(
    mesh,
    faces,
    erode_distance
) {

    if (faces.length === 0 || erode_distance <= 0) {
        // no work to do
        return;
    }

    /**
     *
     * @type {Set<number>}
     */
    const island_vertices = new Set();

    /**
     *
     * @type {Set<number>}
     */
    const island_edges = new Set();

    // 1. Map all vertices and edges of the island
    for (let i = 0; i < faces.length; i++) {
        const f = faces[i];
        const l_first = mesh.face_read_loop(f);
        if (l_first === NULL_POINTER) continue;

        let l_curr = l_first;
        do {
            island_vertices.add(mesh.loop_read_vertex(l_curr));
            island_edges.add(mesh.loop_read_edge(l_curr));
            l_curr = mesh.loop_read_next(l_curr);
        } while (l_curr !== l_first);
    }

    // 2. Identify boundary vertices (edges with only one loop)
    const boundary_vertices = new Set();
    const boundary_edges_set = new Set();

    for (const e of island_edges) {
        const l = mesh.edge_read_loop(e);
        if (mesh.loop_read_radial_next(l) === l) {
            boundary_vertices.add(mesh.edge_read_vertex1(e));
            boundary_vertices.add(mesh.edge_read_vertex2(e));

            boundary_edges_set.add(e);
        }
    }

    if (boundary_vertices.size === 0) {
        // no boundary vertices found
        // should never happen if the mesh is valid
        return;
    }

    // 3. Compute Geodesic Distance Field
    /**
     * Vertex -> Distance from boundary
     * @type {Map<number, number>}
     */
    const vertex_boundary_distances = new Map();
    bt_mesh_build_boundary_distance_field(vertex_boundary_distances, mesh, island_vertices, boundary_vertices);
    // ---------------------------------------------------------
    // 3.5. PEAK RESCUE PASS (The Fix)
    // Handle cases where a face is "submerged" by vertex values
    // but geometrically contains a safe zone (e.g. single triangle).
    // ---------------------------------------------------------

    // We snapshot the faces list because we might modify topology (poke)
    const faces_to_check = [...faces];
    const centroid = [];
    const scratch_v1 = [];
    const scratch_v2 = [];

    for (const f of faces_to_check) {
        // Collect face vertices and check if they are all "dead"
        let l_curr = mesh.face_read_loop(f);
        const l_end = l_curr;
        let all_dead = true;

        do {
            const v = mesh.loop_read_vertex(l_curr);
            if ((vertex_boundary_distances.get(v) || 0) >= erode_distance) {
                all_dead = false;
            }
            l_curr = mesh.loop_read_next(l_curr);
        } while (l_curr !== l_end);

        if (all_dead) {
            // This face is slated for total deletion.
            // Check if we should rescue it by adding a peak vertex.

            // 1. Calculate Centroid (incenter only for triangle, falls back to centroid for other N-gons)
            bt_face_get_incenter(centroid, 0, mesh, f);

            // 2. Calculate Centroid Distance to Boundary
            // (For a single island face, min dist to its own boundary edges)
            let min_dist = Infinity;
            // Iterate edges of this face
            l_curr = mesh.face_read_loop(f);
            do {
                const e = mesh.loop_read_edge(l_curr);

                const v1 = mesh.edge_read_vertex1(e);
                const v2 = mesh.edge_read_vertex2(e);

                if (island_vertices.has(v1) && island_vertices.has(v2)) {

                    mesh.vertex_read_coordinate(scratch_v1, 0, v1);
                    mesh.vertex_read_coordinate(scratch_v2, 0, v2);

                    const d1 = vertex_boundary_distances.get(v1);
                    const d2 = vertex_boundary_distances.get(v2);

                    if (d1 < erode_distance || d2 < erode_distance) {
                        // only consider edges that could cross the erosion threshold

                        const d = line3_compute_segment_point_distance_eikonal(
                            d1, d2,
                            scratch_v1[0], scratch_v1[1], scratch_v1[2],
                            scratch_v2[0], scratch_v2[1], scratch_v2[2],
                            centroid[0], centroid[1], centroid[2]
                        );

                        if (d < min_dist) {
                            min_dist = d;
                        }

                    }

                }

                l_curr = mesh.loop_read_next(l_curr);
            } while (l_curr !== l_end);

            // If min_dist is Infinity, it means this face has NO boundary edges
            // (it's internal). It should have inherited values from neighbors.
            // We ignore it, or assume linear interpolation was correct.
            // We only rescue faces touching the boundary that collapsed.

            if (min_dist !== Infinity && min_dist > erode_distance) {
                // RESCUE: The center is safe!
                // Poke the face to create a vertex at the centroid.
                const new_vert = bt_mesh_face_poke(mesh, f, centroid[0], centroid[1], centroid[2]);

                // Update Data
                island_vertices.add(new_vert);
                vertex_boundary_distances.set(new_vert, min_dist);

                // Read the new edges connected to the center and add to processing set
                let e_first = mesh.vertex_read_edge(new_vert);

                let e_scan = e_first;
                do {
                    island_edges.add(e_scan);

                    const v1 = mesh.edge_read_vertex1(e_scan);
                    const is_v1 = v1 === new_vert;

                    // DEBUG checks
                    const v2 = mesh.edge_read_vertex2(e_scan);
                    if (is_v1) {
                        assert.equal(v1, new_vert, 'edge vertex does not match new vertex');
                        assert.notEqual(v2, new_vert, 'edge vertex does not match new vertex');
                    } else {
                        assert.equal(v2, new_vert, 'edge vertex does not match new vertex');
                        assert.notEqual(v1, new_vert, 'edge vertex does not match new vertex');
                    }

                    e_scan = is_v1 ? mesh.edge_read_v1_disk_next(e_scan) : mesh.edge_read_v2_disk_next(e_scan);
                } while (e_scan !== e_first);
            }
        }
    }
    // ---------------------------------------------------------

    // 4. Trace the Contour & Split Edges

    /**
     *
     * @type {{e: number, t: number}[]}
     */
    const edges_to_split = [];

    for (const e of island_edges) {

        assert.equal(mesh.edges.is_allocated(e), true, 'edge is not allocated');

        const v1 = mesh.edge_read_vertex1(e);
        const v2 = mesh.edge_read_vertex2(e);

        const d1 = vertex_boundary_distances.get(v1);
        const d2 = vertex_boundary_distances.get(v2);

        // If the edge strictly crosses the erosion threshold
        const crosses_erosion_boundary = (d1 < erode_distance && d2 > erode_distance) || (d2 < erode_distance && d1 > erode_distance);

        if (crosses_erosion_boundary) {
            // Calculate exact interpolation parameter
            const t = clamp01(inverseLerp(d1, d2, erode_distance));
            edges_to_split.push({ e, t });
        }
    }

    // Execute splits
    for (const { e, t } of edges_to_split) {
        // splits the edge as well as adjacent faces.
        bt_edge_split(mesh, e, t);
    }

    // Cull Dead Geometry
    // Any vertex that had a distance < erode_distance is dead.
    // Any face connected to a dead vertex must be destroyed.
    /**
     *
     * @type {Set<number>}
     */
    const faces_to_kill = new Set();

    for (const [vertex, distance] of vertex_boundary_distances.entries()) {
        const EPSILON = 1e-17;

        if (distance >= erode_distance - EPSILON) {
            // too far, survives
            continue;
        }

        const e_first = mesh.vertex_read_edge(vertex);

        if (e_first === NULL_POINTER) {
            continue;
        }

        let e_curr = e_first;
        do {
            // Traverse radial loops to get all connected faces
            const l_first = mesh.edge_read_loop(e_curr);
            if (l_first !== NULL_POINTER) {
                let l_curr = l_first;
                do {
                    faces_to_kill.add(mesh.loop_read_face(l_curr));
                    l_curr = mesh.loop_read_radial_next(l_curr);
                } while (l_curr !== l_first);
            }

            // Move around disk cycle
            const v1 = mesh.edge_read_vertex1(e_curr);
            e_curr = (v1 === vertex) ? mesh.edge_read_v1_disk_next(e_curr) : mesh.edge_read_v2_disk_next(e_curr);
        } while (e_curr !== e_first && e_curr !== NULL_POINTER);
    }

    // Eradicate the dead faces
    for (const f of faces_to_kill) {
        bt_face_kill(mesh, f);
    }

    // 6. Cleanup floating vertices and edges
    if (faces_to_kill.size > 0) {
        bt_mesh_cleanup_faceless_references(mesh);
    }
}