import { NULL_POINTER } from "../BinaryTopology.js";

/**
 * Walk a pool and assign a new sequential ID to each live element.
 * Free slots map to NULL_POINTER.
 * @param {BinaryElementPool} pool
 * @returns {Uint32Array} remap[old_id] -> new_id (or NULL_POINTER if old_id was free)
 */
function build_remap(pool) {
    const size = pool.size;
    const remap = new Uint32Array(size);

    let next_id = 0;
    for (let old_id = 0; old_id < size; old_id++) {
        if (pool.is_allocated(old_id)) {
            remap[old_id] = next_id;
            next_id++;
        } else {
            remap[old_id] = NULL_POINTER;
        }
    }

    return remap;
}

/**
 * Returns NULL_POINTER unchanged, otherwise looks up the remap.
 * @param {Uint32Array} remap
 * @param {number} ptr
 * @returns {number}
 */
function patch(remap, ptr) {
    if (ptr === NULL_POINTER) {
        return NULL_POINTER;
    }
    return remap[ptr];
}

/**
 * Physically move live elements down so they occupy [0, live_count) contiguously.
 * Uses the remap only to identify free slots (where remap[i] === NULL_POINTER).
 * @param {BinaryElementPool} pool
 * @param {Uint32Array} remap remap built from this pool's pre-compaction state
 */
function compact_bytes(pool, remap) {
    const size = remap.length;

    let write = 0;
    for (let read = 0; read < size; read++) {
        if (remap[read] === NULL_POINTER) {
            continue;
        }

        if (read !== write) {
            pool.swap(read, write);
        }

        write++;
    }
}

/**
 * Count live slots from a remap built by {@link build_remap}.
 * @param {Uint32Array} remap
 * @returns {number}
 */
function live_count_from_remap(remap) {
    let count = 0;
    for (let i = 0; i < remap.length; i++) {
        if (remap[i] !== NULL_POINTER) {
            count++;
        }
    }
    return count;
}

/**
 * Remove allocation holes from a mesh's pools.
 * After compaction, every base pool's live elements occupy a contiguous
 * range [0, N) and the free set is empty. All cross-references between
 * pools are patched to the new IDs. Optional per-element data pools
 * (vertex_data, edge_data, loop_data, face_data) are compacted in
 * lockstep with their base pools.
 *
 * The mesh's topological meaning is preserved; only IDs may change.
 *
 * @param {BinaryTopology} mesh
 */
export function bt_mesh_compact(mesh) {
    const vertex_pool = mesh.vertices;
    const edge_pool = mesh.edges;
    const loop_pool = mesh.loops;
    const face_pool = mesh.faces;

    // 1. Build remap tables for all four base pools
    const vertex_remap = build_remap(vertex_pool);
    const edge_remap = build_remap(edge_pool);
    const loop_remap = build_remap(loop_pool);
    const face_remap = build_remap(face_pool);

    // 2. Patch pointer fields in-place at OLD positions, live elements only.
    //    NULL_POINTER passes through unchanged.

    // Vertices: edge_pointer -> edge
    const vertex_size = vertex_remap.length;
    for (let v = 0; v < vertex_size; v++) {
        if (vertex_remap[v] === NULL_POINTER) continue;

        mesh.vertex_write_edge(v, patch(edge_remap, mesh.vertex_read_edge(v)));
    }

    // Edges: v1, v2 -> vertex; l -> loop; disk pointers -> edge
    const edge_size = edge_remap.length;
    for (let e = 0; e < edge_size; e++) {
        if (edge_remap[e] === NULL_POINTER) continue;

        mesh.edge_write_vertex1(e, patch(vertex_remap, mesh.edge_read_vertex1(e)));
        mesh.edge_write_vertex2(e, patch(vertex_remap, mesh.edge_read_vertex2(e)));
        mesh.edge_write_loop(e, patch(loop_remap, mesh.edge_read_loop(e)));

        mesh.edge_write_v1_disk_next(e, patch(edge_remap, mesh.edge_read_v1_disk_next(e)));
        mesh.edge_write_v1_disk_prev(e, patch(edge_remap, mesh.edge_read_v1_disk_prev(e)));
        mesh.edge_write_v2_disk_next(e, patch(edge_remap, mesh.edge_read_v2_disk_next(e)));
        mesh.edge_write_v2_disk_prev(e, patch(edge_remap, mesh.edge_read_v2_disk_prev(e)));
    }

    // Loops: v -> vertex; e -> edge; f -> face; radial/face cycle -> loop
    const loop_size = loop_remap.length;
    for (let l = 0; l < loop_size; l++) {
        if (loop_remap[l] === NULL_POINTER) continue;

        mesh.loop_write_vertex(l, patch(vertex_remap, mesh.loop_read_vertex(l)));
        mesh.loop_write_edge(l, patch(edge_remap, mesh.loop_read_edge(l)));
        mesh.loop_write_face(l, patch(face_remap, mesh.loop_read_face(l)));

        mesh.loop_write_radial_next(l, patch(loop_remap, mesh.loop_read_radial_next(l)));
        mesh.loop_write_radial_prev(l, patch(loop_remap, mesh.loop_read_radial_prev(l)));
        mesh.loop_write_next(l, patch(loop_remap, mesh.loop_read_next(l)));
        mesh.loop_write_prev(l, patch(loop_remap, mesh.loop_read_prev(l)));
    }

    // Faces: l_first -> loop
    const face_size = face_remap.length;
    for (let f = 0; f < face_size; f++) {
        if (face_remap[f] === NULL_POINTER) continue;

        mesh.face_write_loop(f, patch(loop_remap, mesh.face_read_loop(f)));
    }

    // 3. Physically compact bytes. Base pools first, then any optional data
    //    pools using the same remaps.
    compact_bytes(vertex_pool, vertex_remap);
    compact_bytes(edge_pool, edge_remap);
    compact_bytes(loop_pool, loop_remap);
    compact_bytes(face_pool, face_remap);

    if (mesh.vertex_data !== null) compact_bytes(mesh.vertex_data, vertex_remap);
    if (mesh.edge_data !== null) compact_bytes(mesh.edge_data, edge_remap);
    if (mesh.loop_data !== null) compact_bytes(mesh.loop_data, loop_remap);
    if (mesh.face_data !== null) compact_bytes(mesh.face_data, face_remap);

    // 4. Truncate sizes on every pool (base + data).
    const vertex_live = live_count_from_remap(vertex_remap);
    const edge_live = live_count_from_remap(edge_remap);
    const loop_live = live_count_from_remap(loop_remap);
    const face_live = live_count_from_remap(face_remap);

    vertex_pool.truncate(vertex_live);
    edge_pool.truncate(edge_live);
    loop_pool.truncate(loop_live);
    face_pool.truncate(face_live);

    if (mesh.vertex_data !== null) mesh.vertex_data.truncate(vertex_live);
    if (mesh.edge_data !== null) mesh.edge_data.truncate(edge_live);
    if (mesh.loop_data !== null) mesh.loop_data.truncate(loop_live);
    if (mesh.face_data !== null) mesh.face_data.truncate(face_live);
}
