import { assert } from "../../../core/assert.js";
import { BitSet } from "../../../core/binary/BitSet.js";
import { Uint32Heap } from "../../../core/collection/heap/Uint32Heap.js";
import Vector2 from '../../../core/geom/Vector2.js';
import { sign } from "../../../core/math/sign.js";

const neighbors = new Uint32Array(4);

/**
 *
 * @param {Vector2} result
 * @param {number} index
 * @param {number} width
 */
function index2point(result, index, width) {
    const x = index % width;
    const y = (index / width) | 0;

    result.set(x, y);
}

/**
 *
 * @param {number[]|Uint32Array} result
 * @param {number} index
 * @param {number} width
 * @param {number} height
 * @returns {number}
 */
function compute_neighbors(result, index, width, height) {
    const x = index % width;
    const y = (index / width) | 0;

    let neighbour_count = 0;

    if (y > 0) {
        result[neighbour_count] = index - width;
        neighbour_count++
    }

    if (x > 0) {
        result[neighbour_count] = index - 1;
        neighbour_count++;
    }

    if (x < width - 1) {
        result[neighbour_count] = index + 1;
        neighbour_count++;
    }

    if (y < height - 1) {
        result[neighbour_count] = index + width;
        neighbour_count++
    }

    return neighbour_count;
}

/**
 *
 * @param {number} node
 * @param {number[]|Float32Array} g_score
 * @param {number} width
 * @param {number} height
 * @returns {number[]}
 */
function compute_path(node, g_score, width, height) {
    let v_previous = new Vector2(-1, -1);
    let v_current = new Vector2();

    let dx = 1,
        dy = 1,
        _dx = 0,
        _dy = 0;

    const result = [];

    let current = node;
    let prev = current;

    let last_value = g_score[current];

    while (current !== -1) {
        index2point(v_current, current, width);

        _dx = v_current.x - v_previous.x;
        _dy = v_current.y - v_previous.y;

        // normalize
        if (_dx !== 0) {
            _dx = sign(_dx);
        }

        if (_dy !== 0) {
            _dy = sign(_dy);
        }

        const direction_change = dx !== _dx || dy !== _dy;

        if (direction_change) {
            // direction changed
            dx = _dx;
            dy = _dy;

            //only record points where connection bends to save space
            result.push(prev);
        }

        prev = current;


        //swap vectors
        const t = v_previous;
        v_previous = v_current;
        v_current = t;

        // find next point
        const neighbour_count = compute_neighbors(neighbors, current, width, height);

        current = -1;

        for (let i = 0; i < neighbour_count; i++) {
            const neighbor = neighbors[i];

            const score = g_score[neighbor];

            if (score < last_value) {
                current = neighbor;
                last_value = score;
            } else if (score === last_value) {
                // same score, prefer non-bending neighbours

                index2point(v_current, neighbor, width);

                _dx = v_current.x - v_previous.x;
                _dy = v_current.y - v_previous.y;

                if (_dx === dx && _dy === dy) {
                    // same direction
                    current = neighbor;
                    last_value = score;
                }

            }
        }

    }

    if (result[result.length - 1] !== prev) {

        //check if last node needs to be added
        result.push(prev);

    }

    // so far the path is from goal to start, we need to reverse it
    result.reverse();

    return result;
}


/**
 *
 * @param {number} index0
 * @param {number} index1
 * @param {number} width
 * @returns {number}
 */
function heuristic(index0, index1, width) {
    const x1 = index0 % width;
    const y1 = (index0 / width) | 0;

    const x2 = index1 % width;
    const y2 = (index1 / width) | 0;

    // deltas
    const dx = x2 - x1;
    const dy = y2 - y1;

    // we skip expensive SQRT, but still get a good guiding heuristic which has the same monotonic order
    return dx * dx + dy * dy;
}


const open = new Uint32Heap();

const closed = new BitSet();
closed.preventShrink();

/**
 * Contains refined heuristic value
 * @type {Float32Array}
 */
let g_score = new Float32Array(1024);

/**
 *
 * @param {number[]|Uint8Array|Uint16Array|Float32Array} field
 * @param {number} width X resolution of the search grid
 * @param {number} height Y resolution of the search grid
 * @param {number} start index of the starting position
 * @param {number} goal index of the destination position
 * @param {number} block_value value in the field that signifies impassible obstacle
 * @returns {Array.<number>} array of indices representing path from start to end
 */
export function find_path_on_grid_astar(
    field,
    width, height,
    start, goal,
    block_value
) {

    assert.defined(field, 'field');
    assert.notNull(field, 'field');

    assert.isNonNegativeInteger(start, "start");
    assert.isNonNegativeInteger(goal, "goal");

    assert.isNumber(block_value, 'blockValue');

    // prepare sets
    open.clear();
    closed.reset();

    const cell_count = width * height;

    if (g_score.length < cell_count) {
        // resize scratch
        g_score = new Float32Array(cell_count);
    }

    g_score.fill(Infinity, 0, cell_count);

    g_score[start] = 0;

    open.insert(start, heuristic(start, goal, width));

    while (!open.is_empty()) {

        // Grab the lowest f(x) to process next.  Heap keeps this sorted for us.
        const currentNode = open.pop_min();

        // Lazy-push: a node may be in the heap multiple times if its g-score was improved after an earlier
        // push. The lowest-score entry is popped first; later popped duplicates land on an already-closed
        // node and must be skipped.
        if (closed.get(currentNode)) {
            continue;
        }

        if (currentNode === goal) {
            // End case - result has been found, return the traced path.
            return compute_path(currentNode, g_score, width, height);
        }

        // Normal case -- move currentNode from open to closed, process each of its neighbors.
        closed.set(currentNode, true);

        // Find all neighbors for the current node.
        const neighbour_count = compute_neighbors(neighbors, currentNode, width, height);

        for (let i = 0; i < neighbour_count; ++i) {
            const neighbor = neighbors[i];

            if (closed.get(neighbor)) {
                // Already closed, skip to next neighbor.
                continue;
            }

            // The g score is the shortest distance from start to current node.
            // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.
            const neighbor_value = field[neighbor];

            if (neighbor_value === block_value) {
                //cell is blocked, close and continue
                closed.set(neighbor, true);
                continue;
            }

            // Cost of traversing to this neighbour
            const transition_cost = neighbor_value + 1;

            // updated path cost
            const cost_so_far = g_score[currentNode] + transition_cost;

            if (cost_so_far < g_score[neighbor]) {

                // update actual cost
                g_score[neighbor] = cost_so_far;

                const remaining_heuristic = heuristic(neighbor, goal, width);

                const refined_heuristic = cost_so_far + remaining_heuristic;

                // Lazy push: always insert. If neighbor was already on the open list, the older (worse-score)
                // entry will be skipped when popped (see closed-check at the top of the loop). This avoids
                // an O(n) find_index_by_id scan per relaxation.
                open.insert(neighbor, refined_heuristic);
            }

        }
    }

    // No result was found - empty array signifies failure to find path.
    return [];
}
