import { vec3 } from "gl-matrix";
import { array_get_index_in_range } from "../../../../collection/array/array_get_index_in_range.js";
import { EPSILON } from "../../../../math/EPSILON.js";
import { min2 } from "../../../../math/min2.js";
import { query_edge_is_boundary } from "../query/query_edge_is_boundary.js";
import { query_edge_other_vertex } from "../query/query_edge_other_vertex.js";
import { tm_edge_kill } from "../tm_edge_kill.js";
import { tm_vertex_compute_normal } from "../tm_vertex_compute_normal.js";
import { tm_edge_collapse } from "./edge_collapse_quadratic.js";
import { compute_edge_collapse_cost_quadratic } from "./quadratic/compute_edge_collapse_cost_quadratic.js";
import { tm_edge_collapse_is_degenerate_flip } from "./tm_edge_collapse_is_degenerate_flip.js";

/**
 *
 * @type {TopoEdge[]}
 */
const scratch_edges = [];


const scratch_v3_0 = [];
const scratch_v3_1 = [];

/**
 *
 * @param {TopoVertex} v
 * @returns {number}
 */
function edge_collapse_vertex_removal_cost(v) {
    let result = 0;

    const edges = v.edges;
    const edge_count = edges.length;

    for (let i = 0; i < edge_count; i++) {
        const edge = edges[i];

        if (query_edge_is_boundary(edge)) {
            // prefer to preserve boundary
            result += 100;
        } else {
            // prefer not to remove vertices that have lots of faces attached. This is to prevent cases where more and more faces are aggregated around a single vertex
            result += 1;
        }
    }

    return result;
}

/**
 *
 * @param {TopoEdge} edge
 * @returns {TopoVertex}
 */
export function pick_lowest_cost_edge_collapse_vertex(edge) {
    const v0 = edge.v0;
    const v1 = edge.v1;

    if (edge_collapse_vertex_removal_cost(v0) > edge_collapse_vertex_removal_cost(v1)) {
        return v0;
    } else {
        return v1;
    }
}

/**
 *
 * @param {TopoEdge} edge
 * @param {Set<number>} restricted_vertices locked vertices
 * @returns {TopoVertex|undefined}
 */
export function edge_collapse_pick_target_vertex(edge, restricted_vertices) {

    const v0 = edge.v0;
    const v1 = edge.v1;

    const v0_restricted = restricted_vertices.has(v0.index);
    const v1_restricted = restricted_vertices.has(v1.index);

    let expected_target;

    if (v0_restricted && v1_restricted) {
        return;
    } else if (v0_restricted) {
        if (tm_edge_collapse_is_degenerate_flip(edge, v0)) {
            return;
        } else {
            expected_target = v0;
        }
    } else if (v1_restricted) {

        if (tm_edge_collapse_is_degenerate_flip(edge, v1)) {
            return;
        } else {
            expected_target = v1;
        }
    } else {
        const v0_degenerate = tm_edge_collapse_is_degenerate_flip(edge, v0);
        const v1_degenerate = tm_edge_collapse_is_degenerate_flip(edge, v1);
        if (v0_degenerate && v1_degenerate) {
            return;
        } else if (v1_degenerate) {
            expected_target = v0;
        } else if (v0_degenerate) {
            expected_target = v1;
        } else {
            expected_target = pick_lowest_cost_edge_collapse_vertex(edge);
        }
    }

    return expected_target;
}

/**
 *
 * @param {TopoEdge} edge
 * @returns {number}
 */
function build_edge_cost_squared_topology(edge) {
    tm_vertex_compute_normal(scratch_v3_0, 0, edge.v0);
    tm_vertex_compute_normal(scratch_v3_1, 0, edge.v1);

    const distance_sqr = vec3.squaredDistance(edge.v0, edge.v1);

    let cost = Math.abs(vec3.dot(scratch_v3_0, scratch_v3_1))
        / min2(-distance_sqr, -EPSILON);

    return cost;

}


/**
 *
 * @param {TopoEdge} edge
 * @param {Map<number,Quadric3>} quadratics
 * @param {Set<number>} restricted_vertices
 */
export function compute_edge_collapse_cost(edge, quadratics, restricted_vertices) {
    const expected_target = edge_collapse_pick_target_vertex(edge, restricted_vertices);

    if (expected_target === undefined) {
        return Number.POSITIVE_INFINITY;
    }


    const v0 = edge.v0;
    const v1 = edge.v1;

    let cost = compute_edge_collapse_cost_quadratic(v0, v1, expected_target, quadratics);

    // fallback to topology if quadratics are noise
    if (cost < 1e-5) {
        // subtracting cost to introduce minor possible variation in cost, to produce stable order
        cost = build_edge_cost_squared_topology(edge) - cost;
    }

    return cost;
}

/**
 *
 * @param {TopoEdge} edge
 * @param {Map<number,Quadric3>} quadratics
 * @param {Uint32Heap} heap
 * @param {Set<number>} restricted_vertices
 */
function tm_build_cost(edge, quadratics, heap, restricted_vertices) {
    const index_in_heap = heap.find_index_by_id(edge.index);
    if (index_in_heap === -1) {
        // not in heap
        return;
    }

    let cost = compute_edge_collapse_cost(edge, quadratics, restricted_vertices);

    heap.__update_score_by_index(index_in_heap, cost);

}

/**
 *
 * @param {TopoMesh} mesh
 * @param {TopoEdge}edge
 * @param {TopoVertex} target
 * @param {Map<number,Quadric3>} quadratics
 * @param {Uint32Heap} heap
 * @param {Set<number>} restricted_vertices
 * @returns {boolean}
 */
export function decimate_edge_collapse_snap(
    mesh,
    edge,
    target,
    quadratics,
    heap,
    restricted_vertices
) {

    if (!edge.isLinked()) {
        // edge is not part of topology, this should never happen in practice
        return false;
    }

    if (edge.v0 === edge.v1) {
        // degenerate edge, don't touch
        return false;
    }

    if (tm_edge_collapse_is_degenerate_flip(edge, target)) {
        // re-insert into heap with very high cost
        heap.insert(edge.index, Number.POSITIVE_INFINITY);
        return false;
    }

    // update quadratics
    const victim_vertex = query_edge_other_vertex(edge, target);

    if (!tm_edge_collapse(mesh, edge, target)) {
        // failed collapse
        // re-insert into heap with very high cost
        heap.insert(edge.index, Number.POSITIVE_INFINITY);
        return false;
    }


    const q_a = quadratics.get(victim_vertex.index);
    const q_b = quadratics.get(target.index);
    q_b.add(q_a);

    // TODO we can probably discount all original faces around the target vertex
    // record affected edges
    const attached_faces = target.faces;
    const attached_face_count = attached_faces.length;

    let dirty_edge_count = 0;

    for (let i = 0; i < attached_face_count; i++) {
        const face = attached_faces[i];

        const edges = face.edges;
        const edge_count = edges.length;

        for (let j = 0; j < edge_count; j++) {

            const topoEdge = edges[j];

            if (array_get_index_in_range(scratch_edges, topoEdge, 0, dirty_edge_count) === -1) {
                scratch_edges[dirty_edge_count++] = topoEdge;
            }
        }

    }

    // update costs
    for (let i = 0; i < dirty_edge_count; i++) {
        const dirty_edge = scratch_edges[i];

        if (dirty_edge.isDegenerateEdge()) {
            heap.remove(dirty_edge.index);

            tm_edge_kill(mesh, dirty_edge);

        } else {

            tm_build_cost(dirty_edge, quadratics, heap, restricted_vertices);

        }
    }

    return true;
}
