import { assert } from "../../../../../../../assert.js";
import { NULL_POINTER } from "../../BinaryTopology.js";
import { bt_disk_edge_remove } from "../bt_disk_edge_remove.js";
import { bt_loop_kill } from "../bt_loop_kill.js";
import { bt_radial_loop_add } from "../bt_radial_loop_add.js";
import { bt_radial_loop_remove } from "../bt_radial_loop_remove.js";
import { bt_kill_only_edge } from "../edge/bt_kill_only_edge.js";

/**
 * Localized counterpart to {@link bt_mesh_fuse_duplicate_edges} that inspects only
 * `v`'s disk cycle. Any two edges in that cycle sharing the same other endpoint
 * are fused: loops move from the victim to the master, the victim is untangled
 * from both endpoint disk cycles, and its memory is released.
 *
 * If transferring a loop would leave the master edge with two loops on the same
 * face (i.e. the face has collapsed to a degenerate two-edge polygon), that face
 * is killed as well.
 *
 * Self-loops on `v` are left alone; the caller should handle them separately.
 *
 * Typical use: after an iterative edit that may have produced link-vertex
 * duplicates around `v`, run this to restore the one-edge-per-vertex-pair
 * invariant without the cost of a whole-mesh pass.
 *
 * @param {BinaryTopology} mesh
 * @param {number} v
 * @returns {number} Number of faces killed as a side effect.
 */
export function bt_vert_fuse_duplicate_edges(mesh, v) {
    assert.defined(mesh, 'mesh');
    assert.notNull(mesh, 'mesh');
    assert.isNonNegativeInteger(v, 'v');

    const edge_pool = mesh.edges;

    const start_e = mesh.vertex_read_edge(v);

    if (start_e === NULL_POINTER) {
        return 0;
    }

    // Snapshot v's disk cycle up front so that later fusion operations, which
    // mutate that cycle, don't invalidate our traversal cursor.
    const edges_in_disk = [];
    let curr_e = start_e;

    do {
        edges_in_disk.push(curr_e);

        if (mesh.edge_read_vertex1(curr_e) === v) {
            curr_e = mesh.edge_read_v1_disk_next(curr_e);
        } else {
            curr_e = mesh.edge_read_v2_disk_next(curr_e);
        }
    } while (curr_e !== start_e && curr_e !== NULL_POINTER);

    // Group edges by other endpoint to detect duplicates in a single pass.
    const master_by_other = new Map();
    const fusion_masters = [];
    const fusion_victims = [];
    const fusion_others = [];

    for (let i = 0; i < edges_in_disk.length; i++) {
        const e = edges_in_disk[i];

        if (!edge_pool.is_allocated(e)) {
            continue;
        }

        const v1 = mesh.edge_read_vertex1(e);
        const v2 = mesh.edge_read_vertex2(e);
        const other = v1 === v ? v2 : v1;

        if (other === v) {
            // Self-loop; not our concern here.
            continue;
        }

        if (master_by_other.has(other)) {
            fusion_masters.push(master_by_other.get(other));
            fusion_victims.push(e);
            fusion_others.push(other);
        } else {
            master_by_other.set(other, e);
        }
    }

    const face_pool = mesh.faces;

    let faces_killed = 0;

    for (let i = 0; i < fusion_masters.length; i++) {
        const master = fusion_masters[i];
        const victim = fusion_victims[i];
        const other = fusion_others[i];

        if (!edge_pool.is_allocated(victim) || !edge_pool.is_allocated(master)) {
            continue;
        }

        let l = mesh.edge_read_loop(victim);

        while (l !== NULL_POINTER) {
            bt_radial_loop_remove(mesh, l, victim);

            const face = mesh.loop_read_face(l);
            let collapse_face = false;

            if (face !== NULL_POINTER && face_pool.is_allocated(face)) {
                // If master already has a loop on the same face, transferring
                // this loop would give that face two coincident edges -- a
                // degenerate polygon that has to die.
                let mast_l = mesh.edge_read_loop(master);
                if (mast_l !== NULL_POINTER) {
                    const mast_start = mast_l;
                    do {
                        if (mesh.loop_read_face(mast_l) === face) {
                            collapse_face = true;
                            break;
                        }
                        mast_l = mesh.loop_read_radial_next(mast_l);
                    } while (mast_l !== mast_start && mast_l !== NULL_POINTER);
                }
            }

            bt_radial_loop_add(mesh, l, master);

            if (collapse_face) {
                const face_to_kill = face;
                let fl = mesh.face_read_loop(face_to_kill);
                while (fl !== NULL_POINTER) {
                    bt_loop_kill(mesh, fl);
                    fl = mesh.face_read_loop(face_to_kill);
                }
                face_pool.release(face_to_kill);
                faces_killed++;
            }

            l = mesh.edge_read_loop(victim);
        }

        bt_disk_edge_remove(mesh, victim, v);
        bt_disk_edge_remove(mesh, victim, other);
        bt_kill_only_edge(mesh, victim);
    }

    return faces_killed;
}
