import { array_copy_unique } from "../../../../collection/array/array_copy_unique.js";
import BinaryHeap from "../../../../collection/heap/BinaryHeap.js";
import { update_topo_face_normals } from "../update_topo_face_normals.js";
import { build_edge_collapse_candidates } from "./build_edge_collapse_candidates.js";
import { collapse_all_degenerate_edges } from "./collapse_all_degenerate_edges.js";
import { collapse_degenerate_edge } from "./collapse_degenerate_edge.js";
import { EdgeCollapseCandidate } from "./EdgeCollapseCandidate.js";
import { build_vertex_quadratics } from "./quadratic/build_vertex_quadratics.js";


const scratch_array_1 = [];
const scratch_array_2 = [];


/**
 *
 * @param {EdgeCollapseCandidate} edge
 */
export function extract_edge_cost(edge) {
    return edge.cost;
}


/**
 * @param {number} number_faces_to_remove minimum number of faces to removed
 * @param {BinaryHeap<EdgeCollapseCandidate>} heap
 * @param {TopoMesh} mesh
 * @param {Map<TopoEdge, EdgeCollapseCandidate>} edge_to_collapse_map
 * @param {Map<number, Quadric3>} vertex_quadratics
 * @param {Set<number>} restricted_vertices
 */
export function collapse_edges(
    number_faces_to_remove,
    heap,
    mesh,
    edge_to_collapse_map,
    vertex_quadratics,
    restricted_vertices
) {

    const mesh_faces = mesh.getFaces();

    const initial_face_count = mesh_faces.size;
    const target_face_count = initial_face_count - number_faces_to_remove;

    while (mesh_faces.size > target_face_count && !heap.isEmpty()) {

        const collapse_candidate = heap.pop();

        if (!collapse_candidate.edge.isLinked()) {
            // no longer connected
            continue;
        }

        // if (!collapse_candidate.validate()) {
        //     console.log('!');
        //
        //     // edge is out of date, update it
        //     collapse_candidate.update();
        //
        //     // push back onto the heap
        //     open_set.push(collapse_candidate);
        //     continue;
        // }

        // find edges that need to be updated
        const victim_vertex = collapse_candidate.victim_vertex;

        // record edges who's cost that might have been affected by collapse
        const neighbour_count = victim_vertex.computeNeighbourVertices(scratch_array_2, 0);

        // debugValidateMesh(mesh);


        // perform edge collapse
        collapse_candidate.collapse(mesh, vertex_quadratics);


        // debugValidateMesh(mesh);

        const victim_vertex_attached_faces = victim_vertex.faces;
        // const victim_vertex_attached_faces = collapse_candidate.target_vertex.faces;
        const victim_vertex_attached_face_count = victim_vertex_attached_faces.length;

        // update face normals around collapsed vertex
        update_topo_face_normals(victim_vertex_attached_faces, victim_vertex_attached_face_count);

        let update_edge_count = 0;
        // for (let i = 0; i < victim_vertex_attached_face_count; i++) {
        //     const victim_face = victim_vertex_attached_faces[i];
        //
        //     if (!victim_face.isLinked()) {
        //         // disconnected face
        //         continue;
        //     }
        //
        //     const edges = victim_face.edges;
        //
        //     const addition_count = array_copy_unique(edges, 0, scratch_array_1, update_edge_count, edges.length);
        //
        //     update_edge_count += addition_count;
        // }

        update_edge_count += array_copy_unique(collapse_candidate.target_vertex.edges, 0, scratch_array_1, update_edge_count, victim_vertex.edges.length);

        // const copy_of_update_edges = scratch_array_1.slice(0, update_edge_count);

        //
        for (let i = 0; i < neighbour_count; i++) {
            /**
             *
             * @type {TopoVertex}
             */
            const v = scratch_array_2[i];

            const edges = v.edges;

            const addition_count = array_copy_unique(edges, 0, scratch_array_1, update_edge_count, edges.length);

            update_edge_count += addition_count;
        }

        // update edge costs
        for (let i = 0; i < update_edge_count; i++) {
            /**
             *
             * @type {TopoEdge}
             */
            const edge = scratch_array_1[i];

            const edge_faces = edge.faces;
            const edge_face_count = edge_faces.length;

            if (edge_face_count <= 0) {
                //disconnected edge, skip
                continue;
            }


            const collapseEdge = edge_to_collapse_map.get(edge);

            const heap_index = heap.data.indexOf(collapseEdge);

            if (heap_index === -1) {
                // edge is not in the heap, meaning it was already processed
                continue;
            }

            if (edge.isDegenerateEdge()) {

                if (edge.isLinked()) {
                    // edge is degenerate, it can be safely removed
                    collapse_degenerate_edge(edge, mesh);
                }

                // removed_edges.push(collapseEdge);

                // exclude edge
                heap.deleteByIndex(heap_index);
                continue;
            }

            // update length
            edge.computeSquaredLength();

            // recompute cost
            const is_valid = collapseEdge.update(vertex_quadratics, restricted_vertices);

            if (is_valid) {
                // update the heap, re-scoring element
                heap.bubbleDown(heap_index);
            } else {
                // edge is no longer valid for collapse, remove from heap
                heap.deleteByIndex(heap_index);
            }

        }


        // DEBUG validate cost
        // for (let i = 0; i < open_set.length; i++) {
        //     const datum = open_set.data[i];
        //
        //     if (!datum.edge.isLinked()) {
        //         continue;
        //     }
        //
        //     if (!datum.validate()) {
        //         debugger;
        //     }
        //
        //     const delta = datum.validateUpdate();
        //     if (delta !== 0) {
        //         debugger;
        //     }
        // }

    }
}

/**
 * Simplifies a given topology mesh by reducing number of faces. Preserves original vertices
 * NOTE: preserves outline of the mesh, that is the open edge
 * NOTE: assumes that face normals are set
 * @param {TopoMesh} mesh
 * @param {number} num_faces_to_remove desired number of faces to remove
 * @param {Set<number>} [restricted_vertices] vertices that are not allowed to be removed
 */
export function simplifyTopoMesh(mesh, num_faces_to_remove, restricted_vertices = new Set()) {

    const faces = mesh.getFaces();
    const face_count = faces.size;

    if (num_faces_to_remove <= 0 || face_count <= 0) {
        // we're already at the target
        return;
    }


    /**
     *
     * @type {BinaryHeap<EdgeCollapseCandidate>}
     */
    const open_set = new BinaryHeap(extract_edge_cost);

    /**
     *
     * @type {Map<TopoEdge, EdgeCollapseCandidate>}
     */
    const edge_to_collapse_map = new Map();

    /**
     *
     * @type {Map<number, Quadric3>}
     */
    const vertex_quadratics = new Map();

    build_vertex_quadratics({ mesh: mesh, quadratics: vertex_quadratics });

    // compute collapse cost of each vertex/edge pair
    build_edge_collapse_candidates(
        mesh,
        open_set,
        edge_to_collapse_map,
        vertex_quadratics,
        restricted_vertices
    );

    collapse_edges(
        num_faces_to_remove,
        open_set,
        mesh,
        edge_to_collapse_map,
        vertex_quadratics,
        restricted_vertices
    );
    // debugValidateMesh(mesh);

    // get rid of degenerate edges
    collapse_all_degenerate_edges(mesh.getEdges(), mesh);

    // debugValidateMesh(mesh);
}
