import { assert } from "../../../assert.js";
import { UINT32_MAX } from "../../../binary/UINT32_MAX.js";
import { clamp } from "../../../math/clamp.js";
import { BinaryElementPool } from "../../3d/topology/struct/binary/BinaryElementPool.js";


export const NULL_POINTER = UINT32_MAX;
/**
 * Single-linked list
 * Pointer to next NODE in the same grid cell
 * @type {number}
 */
const COLUMN_NODE_SAME_NODE_NEXT = 0;
/**
 * Single-linked list
 * Pointer to next NODE containing the same element
 * @type {number}
 */
const COLUMN_NODE_SAME_ELEMENT_NEXT = 1;
/**
 * Pointer to ELEMENT
 * @type {number}
 */
const COLUMN_NODE_ELEMENT = 2;

/**
 * Points to grid cell that stores this node
 * @type {number}
 */
const COLUMN_NODE_GRID_INDEX = 3;

const COLUMN_ELEMENT_DATA = 0;
const COLUMN_ELEMENT_NODE_FIRST = 1;
export const COLUMN_ELEMENT_X0 = 2;
export const COLUMN_ELEMENT_Y0 = 3;
export const COLUMN_ELEMENT_X1 = 4;
export const COLUMN_ELEMENT_Y1 = 5;

export class SpatialHashGrid {
    #element_pool = new BinaryElementPool(24)
    #node_pool = new BinaryElementPool(16);
    #grid_data = new Uint32Array(0);

    #size_x = 0;
    #size_y = 0;

    #scale = 1;
    #scale_inverse = 1;

    get size_x() {
        return this.#size_x;
    }

    get size_y() {
        return this.#size_y;
    }

    get node_pool() {
        return this.#node_pool;
    }

    /**
     *
     * @return {BinaryElementPool}
     */
    get element_pool() {
        return this.#element_pool;
    }

    get scale() {
        return this.#scale;
    }

    get scale_inverse() {
        return this.#scale_inverse;
    }

    constructor(size_x = 1, size_y = 1, scale = 8) {

        assert.notNaN(scale, "scale");
        assert.isFinite(scale, 'scale');
        assert.greaterThan(scale, 0, 'scale must be greater than 0');

        this.#size_x = size_x;
        this.#size_y = size_y;
        this.#scale = scale;
        this.#scale_inverse = 1 / scale;

        this.#build();
    }

    #build() {
        this.#grid_data = new Uint32Array(this.#size_x * this.#size_y);

        this.#grid_data.fill(NULL_POINTER);
    }

    element_allocate() {
        const element = this.#element_pool.allocate();

        return element;
    }

    element_set_user_data(element, user_data) {
        const pool = this.#element_pool;
        const address = pool.element_word(element);

        pool.data_uint32[address + COLUMN_ELEMENT_DATA] = user_data;
    }

    element_get_user_data(element) {
        const pool = this.#element_pool;
        const address = pool.element_word(element);

        return pool.data_uint32[address + COLUMN_ELEMENT_DATA];
    }

    /**
     * You have to remove the element from the grid before changing its bounds
     * @param {number} element
     * @param {number} x0
     * @param {number} y0
     * @param {number} x1
     * @param {number} y1
     */
    element_set_bounds_primitive(element, x0, y0, x1, y1) {
        const pool = this.#element_pool;
        const address = pool.element_word(element);

        const float32 = pool.data_float32;

        float32[address + COLUMN_ELEMENT_X0] = x0;
        float32[address + COLUMN_ELEMENT_Y0] = y0;
        float32[address + COLUMN_ELEMENT_X1] = x1;
        float32[address + COLUMN_ELEMENT_Y1] = y1;
    }

    /**
     *
     * @param {number} element
     */
    element_release(element) {
        this.#element_pool.release(element);
    }

    /**
     *
     * @param {number} element
     */
    element_insert(element) {
        const pool = this.#element_pool;
        const address = pool.element_word(element);

        const uint32 = pool.data_uint32;

        // make sure that we don't point at anything yet
        uint32[address + COLUMN_ELEMENT_NODE_FIRST] = NULL_POINTER;

        const float32 = pool.data_float32;

        const x0 = float32[address + COLUMN_ELEMENT_X0];
        const y0 = float32[address + COLUMN_ELEMENT_Y0];
        const x1 = float32[address + COLUMN_ELEMENT_X1];
        const y1 = float32[address + COLUMN_ELEMENT_Y1];

        // convert to grid coordinates
        const grid_x0 = Math.floor(x0 * this.#scale_inverse);
        const grid_y0 = Math.floor(y0 * this.#scale_inverse);

        const grid_x1 = Math.floor(x1 * this.#scale_inverse);
        const grid_y1 = Math.floor(y1 * this.#scale_inverse);

        for (let y = grid_y0; y <= grid_y1; y++) {
            for (let x = grid_x0; x <= grid_x1; x++) {
                this.#element_insert_into_cell(element, x, y);
            }
        }
    }

    /**
     *
     * @param {number} node
     * @return {number}
     */
    node_get_element(node) {
        const address = this.#node_pool.element_word(node);
        return this.#node_pool.data_uint32[address + COLUMN_NODE_ELEMENT];
    }

    /**
     * Next element in the linked list of cell's nodes
     * @param {number} node
     * @return {number}
     */
    node_get_same_cell_next_node(node) {
        const address = this.#node_pool.element_word(node);

        return this.#node_pool.data_uint32[address + COLUMN_NODE_SAME_NODE_NEXT];
    }

    /**
     *
     * @param {number} cell_index
     * @return {number}
     */
    cell_get_first_node(cell_index) {
        assert.lessThan(cell_index, this.#size_x * this.#size_y);

        return this.#grid_data[cell_index]
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @return {number}
     */
    world_position_to_cell_index(x, y) {
        const grid_x = clamp(Math.floor(x * this.#scale_inverse), 0, this.#size_x - 1);
        const grid_y = clamp(Math.floor(y * this.#scale_inverse), 0, this.#size_y - 1);

        return this.cell_position_to_index(grid_x, grid_y);
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @returns {number}
     */
    cell_position_to_index(x, y) {
        assert.isNonNegativeInteger(x, 'x');
        assert.isNonNegativeInteger(y, 'y');

        return y * this.#size_x + x;

        // const cell_count = this.#size_x * this.#size_y;
        //
        // const _x = split_by_2(x);
        // const _y = split_by_2(y);
        //
        // const hash = _x | (_y << 1);
        //
        // return hash % cell_count;
    }

    /**
     *
     * @param {number} cell_x
     * @param {number} cell_y
     * @return {boolean}
     */
    is_cell_empty(cell_x, cell_y) {
        assert.isNonNegativeInteger(cell_x, 'cell_x');
        assert.lessThan(cell_x, this.#size_x, 'cell_x overflow');

        assert.isNonNegativeInteger(cell_y, 'cell_y');
        assert.lessThan(cell_y, this.#size_y, 'cell_y overflow');

        const index = this.cell_position_to_index(cell_x, cell_y);

        const grid = this.#grid_data;

        return grid[index] === NULL_POINTER;
    }

    /**
     *
     * @param {number} element
     * @param {number} grid_x
     * @param {number} grid_y
     */
    #element_insert_into_cell(element, grid_x, grid_y) {

        // create a node
        const node_pool = this.#node_pool;
        const node = node_pool.allocate();

        const node_address = node_pool.element_word(node);
        const node_data_uint32 = node_pool.data_uint32;

        node_data_uint32[node_address + COLUMN_NODE_ELEMENT] = element;

        // link node to the element
        const element_pool = this.#element_pool;
        const element_address = element_pool.element_word(element);

        const element_data_uint32 = element_pool.data_uint32;

        const element_first_node = element_data_uint32[element_address + COLUMN_ELEMENT_NODE_FIRST];

        // point to previous first
        node_data_uint32[node_address + COLUMN_NODE_SAME_ELEMENT_NEXT] = element_first_node;


        element_data_uint32[element_address + COLUMN_ELEMENT_NODE_FIRST] = node;

        // link node to the cell
        const cell_index = this.cell_position_to_index(grid_x, grid_y);

        node_data_uint32[node_address + COLUMN_NODE_GRID_INDEX] = cell_index;

        const grid_data = this.#grid_data;

        const cell_first_node = grid_data[cell_index];

        node_data_uint32[node_address + COLUMN_NODE_SAME_NODE_NEXT] = cell_first_node;

        grid_data[cell_index] = node;
    }

    /**
     *
     * @param {number} cell_index
     * @param {number} node
     * @returns {boolean}
     */
    #grid_cell_remove_node(cell_index, node) {
        assert.isNonNegativeInteger(cell_index, 'cell_index');
        assert.isNonNegativeInteger(node, 'node');

        const pool = this.#node_pool;
        const node_data_uint32 = pool.data_uint32;

        const grid_data = this.#grid_data;
        let n = grid_data[cell_index]
        let previous = NULL_POINTER;

        while (n !== NULL_POINTER) {
            const address = pool.element_word(n);
            const next = node_data_uint32[address + COLUMN_NODE_SAME_NODE_NEXT];

            if (n === node) {
                // found the node to cut

                if (previous === NULL_POINTER) {
                    // first element in the list
                    grid_data[cell_index] = next;
                } else {
                    const previous_address = pool.element_word(previous);

                    node_data_uint32[previous_address + COLUMN_NODE_SAME_NODE_NEXT] = next;
                }

                return true;
            }

            previous = n;
            n = next;
        }

        return false;
    }

    /**
     *
     * @param {number} element
     */
    element_remove(element) {
        assert.isNonNegativeInteger(element, 'element');

        const element_pool = this.#element_pool;
        const address = element_pool.element_word(element);

        let node = element_pool.data_uint32[address + COLUMN_ELEMENT_NODE_FIRST];

        const node_pool = this.#node_pool;
        const node_data_uint32 = node_pool.data_uint32;

        do {

            const node_address = node_pool.element_word(node);

            const next = node_data_uint32[node_address + COLUMN_NODE_SAME_ELEMENT_NEXT];

            const grid_index = node_data_uint32[node_address + COLUMN_NODE_GRID_INDEX];

            // cut this node from CELL list
            this.#grid_cell_remove_node(grid_index, node);


            // advance onto next
            node = next;
        } while (node !== NULL_POINTER)

    }
}