import { assert } from "../../../../../../assert.js";
import { NULL_POINTER } from "../BinaryTopology.js";
import { bt_disk_edge_add } from "./bt_disk_edge_add.js";
import { bt_disk_edge_remove } from "./bt_disk_edge_remove.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_edge_create } from "./edge/bt_edge_create.js";

/**
 * Breaks up shared edges and vertices between two faces.
 * As a result duplicate vertices and edges may be created.
 *
 * Primary isolate faces so they can be edited independently.
 *
 * @param {BinaryTopology} mesh
 * @param {number} face0_id
 * @param {number} face1_id
 * @see bt_merge_verts_by_distance
 */
export function bt_mesh_face_decouple(
    mesh,
    face0_id,
    face1_id,
) {
    assert.isNonNegativeInteger(face0_id, 'face0_id');
    assert.isNonNegativeInteger(face1_id, 'face1_id');
    assert.defined(mesh, 'mesh');
    assert.notNull(mesh, 'mesh');

    assert.equal(mesh.isBinaryTopology, true, 'mesh.isBinaryTopology !== true');

    let l0 = mesh.face_read_loop(face0_id);
    let l1 = mesh.face_read_loop(face1_id);

    if (l0 === NULL_POINTER || l1 === NULL_POINTER) {
        return;
    }

    // 1. Gather components of face0
    const v0_set = new Set();
    const e0_set = new Set();
    let start0 = l0;
    do {
        v0_set.add(mesh.loop_read_vertex(l0));
        e0_set.add(mesh.loop_read_edge(l0));
        l0 = mesh.loop_read_next(l0);
    } while (l0 !== start0);

    // 2. Map intersection components from face1
    const v1_loops = [];
    const shared_v = new Set();
    const shared_e = new Set();
    const unshared_e_set = new Set();

    let start1 = l1;
    do {
        v1_loops.push(l1);
        const v = mesh.loop_read_vertex(l1);
        const e = mesh.loop_read_edge(l1);

        if (v0_set.has(v)) {
            shared_v.add(v);
        }
        if (e0_set.has(e)) {
            shared_e.add(e);
        } else {
            unshared_e_set.add(e);
        }

        l1 = mesh.loop_read_next(l1);
    } while (l1 !== start1);

    // If nothing is shared, they are already decoupled.
    if (shared_v.size === 0 && shared_e.size === 0){
        return;
    }

    // Pre-allocate arrays for fast coordinate/normal copying
    const temp_coords = new Float32Array(3);
    const temp_normal = new Float32Array(3);

    // 3. Duplicate shared vertices
    const v_map = new Map();
    for (const v of shared_v) {
        const v_new = mesh.vertices.allocate();

        mesh.vertex_read_coordinate(temp_coords, 0, v);
        mesh.vertex_write_coordinate(v_new, temp_coords, 0);

        mesh.vertex_read_normal(temp_normal, 0, v);
        mesh.vertex_write_normal(v_new, temp_normal, 0);

        mesh.vertex_write_edge(v_new, NULL_POINTER);
        v_map.set(v, v_new);
    }

    // 4. Duplicate shared edges
    const e_map = new Map();
    for (const e of shared_e) {
        const v1 = mesh.edge_read_vertex1(e);
        const v2 = mesh.edge_read_vertex2(e);

        const v1_new = v_map.has(v1) ? v_map.get(v1) : v1;
        const v2_new = v_map.has(v2) ? v_map.get(v2) : v2;

        const e_new = bt_edge_create(mesh, v1_new, v2_new);
        e_map.set(e, e_new);
    }

    // 5. Shift unshared edges that are attached to duplicated vertices
    for (const e of unshared_e_set) {
        const v1 = mesh.edge_read_vertex1(e);
        const v2 = mesh.edge_read_vertex2(e);

        const v1_new = v_map.has(v1) ? v_map.get(v1) : v1;
        const v2_new = v_map.has(v2) ? v_map.get(v2) : v2;

        if (v1_new === v2_new && v1_new !== v1) {
            // Both endpoints remap to the same vertex: this edge becomes a
            // self-loop. Per the disk-cycle convention (see BinaryTopology.js
            // __edge_pool comment) a self-loop occupies the v1 slot only.
            // Detach from both old disks, update both vertex fields, then add
            // once -- a second add would land on the v2 slot and corrupt v's
            // disk cycle.
            bt_disk_edge_remove(mesh, e, v1);
            bt_disk_edge_remove(mesh, e, v2);
            mesh.edge_write_vertex1(e, v1_new);
            mesh.edge_write_vertex2(e, v2_new);
            bt_disk_edge_add(mesh, e, v1_new);
            continue;
        }

        if (v1_new !== v1) {
            bt_disk_edge_remove(mesh, e, v1);
            mesh.edge_write_vertex1(e, v1_new);
            bt_disk_edge_add(mesh, e, v1_new);
        }
        if (v2_new !== v2) {
            bt_disk_edge_remove(mesh, e, v2);
            mesh.edge_write_vertex2(e, v2_new);
            bt_disk_edge_add(mesh, e, v2_new);
        }
    }

    // 6. Redirect loops on face1 to the new duplicated edges and vertices
    for (const loop of v1_loops) {
        const v = mesh.loop_read_vertex(loop);
        const e = mesh.loop_read_edge(loop);

        if (v_map.has(v)) {
            mesh.loop_write_vertex(loop, v_map.get(v));
        }

        if (e_map.has(e)) {
            const e_new = e_map.get(e);

            bt_radial_loop_remove(mesh, loop, e);
            bt_radial_loop_add(mesh, loop, e_new);
        }
    }
}
