import { array_copy } from "../../../collection/array/array_copy.js";
import { Uint32Heap } from "../../../collection/heap/Uint32Heap.js";
import { intersect_ray_2d } from "../intersect_ray_2d.js";
import { triangle2d_compute_area } from "../triangle2d_compute_area.js";


const v2_temp = new Float32Array(2);

class Edge {
    /**
     * Unique index of this edge
     * @type {number}
     */
    index;
    /**
     * Index into position buffer
     * @type {number}
     */
    v0;
    /**
     * Index into position buffer
     * @type {number}
     */
    v1;

    /**
     * @type {Edge}
     */
    next;
    /**
     * @type {Edge}
     */
    prev;
}

/**
 *
 * @param {number[]|Float32Array|Float64Array} output
 * @param {number} output_offset
 * @param {Edge} edge
 * @param {number[]} vertex_buffer
 * @returns {boolean}
 */
function compute_collapse_target(output, output_offset, edge, vertex_buffer) {
    const a0 = edge.prev.v0;
    const a1 = edge.prev.v1;

    const b0 = edge.next.v0;
    const b1 = edge.next.v1;

    const a0x = vertex_buffer[a0 * 2];
    const a0y = vertex_buffer[a0 * 2 + 1];

    const a1x = vertex_buffer[a1 * 2];
    const a1y = vertex_buffer[a1 * 2 + 1];

    const b0x = vertex_buffer[b0 * 2];
    const b0y = vertex_buffer[b0 * 2 + 1];

    const b1x = vertex_buffer[b1 * 2];
    const b1y = vertex_buffer[b1 * 2 + 1];

    const a_dir_x = a1x - a0x;
    const a_dir_y = a1y - a0y;

    const b_dir_x = b1x - b0x;
    const b_dir_y = b1y - b0y;

    return intersect_ray_2d(
        output, output_offset,
        a0x, a0y, a_dir_x, a_dir_y,
        b0x, b0y, b_dir_x, b_dir_y
    );
}

/**
 *
 * @param {number} point_offset
 * @param {number} point_count
 * @returns {Edge[]}
 */
function build_edges(point_offset, point_count) {
    /**
     *
     * @type {Edge[]}
     */
    const edges = [];

    for (let i = 0; i < point_count; i++) {
        const edge = new Edge();

        edge.index = i;

        edge.v0 = i + point_offset;
        edge.v1 = (i + 1) % point_count + point_offset;

        edges[i] = edge;
    }

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

        edge.next = edges[(i + 1) % point_count];
        edge.prev = edges[(point_count + i - 1) % point_count];
    }

    return edges;
}

/**
 *
 * @param {Edge} edge
 * @param {number[]|Float32Array|Float64Array} vertex_buffer
 * @param {number} new_vertex_index
 */
function collapse_edge(edge, vertex_buffer, new_vertex_index) {
    // this edge will be removed, previous and next edges will receive a new vertex where they connected to this edge
    // adjacent edges will become neighbours

    const valid = compute_collapse_target(v2_temp, 0, edge, vertex_buffer);

    if (!valid) {
        return false;
    }

    const next = edge.next;
    const prev = edge.prev;

    // connect edges together
    next.prev = prev;
    prev.next = next;

    // inject new vertex

    next.v0 = new_vertex_index;
    prev.v1 = new_vertex_index;

    //write out new vertex
    array_copy(v2_temp, 0, vertex_buffer, new_vertex_index * 2, 2);

    return true;
}

/**
 *
 * @param {Edge} edge
 * @param {number[]|Float64Array|Float32Array} vertex_buffer
 */
function compute_collapse_cost(edge, vertex_buffer) {
    if (!compute_collapse_target(v2_temp, 0, edge, vertex_buffer)) {
        return Infinity;
    }

    const v0x = vertex_buffer[edge.v0 * 2];
    const v0y = vertex_buffer[edge.v0 * 2 + 1];

    const v1x = vertex_buffer[edge.v1 * 2];
    const v1y = vertex_buffer[edge.v1 * 2 + 1];

    const area = triangle2d_compute_area(
        v0x, v0y, v2_temp[0], v2_temp[1], v1x, v1y
    );

    //
    // if (area < 0 || Number.isNaN(area)) {
    //     debugger;
    // }

    return area;
}

/**
 * Will throw an error after not being able to collapse an edge for this many cycles
 * @readonly
 * @type {number}
 */
const RETRY_MAX_COUNT = 1000;

/**
 * Relaxes existing convex hull by removing edges until desired cardinality of the hull is reached
 * The algorithm reaches the similar solution as the one proposed by humus, but arrives at a solution much faster as it doesn't need to consider every combination of edges
 * The algorithm is greedy, and thus it can lead to suboptimal cuts, especially on lower number of output vertices
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 *
 * @param {number[]|Float32Array} output result will be written here
 * @param {number} output_offset offset into output array where to write the result
 * @param {number} output_point_count number of points to comprise bounding polygon
 * @param {number[]} input_points Points to be bounded, must be an ordered loop
 * @param {number} input_point_count number of points in the input to consider
 * @returns {number} actual number of points, this can be lower because input polygon has too few points
 */
export function fixed_convex_hull_relaxation(
    output, output_offset, output_point_count,
    input_points, input_point_count
) {

    if (output_point_count >= input_point_count) {
        // special case, input polygon is smaller or equal to in cardinality to what's desired
        array_copy(input_points, 0, output, output_offset, input_point_count * 2);
        return input_point_count;
    }


    /**
     * Point buffer will be twice as large as the input to have enough space for collapsing all the edges and recording
     * new unique vertices produced
     * @type {Float64Array}
     */
    const point_buffer = new Float32Array(input_point_count * 4);

    array_copy(input_points, 0, point_buffer, 0, input_point_count * 2);

    /**
     * Offset into point buffer
     * @type {number}
     */
    let point_buffer_cursor = input_point_count;

    const edges = build_edges(0, input_point_count);
    const live_edge_set = new Set(edges);

    const edge_heap = new Uint32Heap(input_point_count);


    // build costs for edges
    for (let i = 0; i < input_point_count; i++) {
        const cost = compute_collapse_cost(edges[i], point_buffer);
        edge_heap.insert(i, cost);
    }

    let retry = 0;

    while (live_edge_set.size > output_point_count) {
        const victim = edge_heap.pop_min();

        const edge = edges[victim];

        if (!collapse_edge(edge, point_buffer, point_buffer_cursor)) {
            // couldn't collapse, re-insert with very high score
            edge_heap.insert(victim, Infinity);

            if (retry >= RETRY_MAX_COUNT) {
                console.warn('Failed to collapse an edge after maximum number of retries has been reached');
                break;
            }

            retry++;
        }

        retry = 0;

        point_buffer_cursor++;

        live_edge_set.delete(edge);

        // update costs for surviving neighbours
        const next_index = edge.next.index;
        const prev_index = edge.prev.index;
        edge_heap.update_score(next_index, compute_collapse_cost(edges[next_index], point_buffer));
        edge_heap.update_score(prev_index, compute_collapse_cost(edges[prev_index], point_buffer));
    }

    // pick first edge to start walking
    const first_live_edge = live_edge_set.values().next().value;
    let e = first_live_edge;

    let output_cursor = output_offset;

    do {
        // walk the loop, recording first vertex of each edge we visit

        array_copy(point_buffer, e.v0 * 2, output, output_cursor, 2);
        e = e.next;

        output_cursor += 2;

    } while (e !== first_live_edge);

    // number of output vertices is derived from how many we recorded
    return (output_cursor - output_offset) / 2;
}
