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

export const QT_NULL_POINTER = UINT32_MAX;

/**
 * Numeric data supplied by the user
 * @type {number}
 */
const COLUMN_ELEMENT_USER_DATA = 0;

/**
 * Singly-linked list, nest element
 * can be NULL_POINTER to represent end of the list
 * @type {number}
 */
const COLUMN_ELEMENT_NEXT = 1;

/**
 * Tree node that contains this element
 * @type {number}
 */
const COLUMN_ELEMENT_PARENT_NODE = 2;

const COLUMN_ELEMENT_X0 = 3;
const COLUMN_ELEMENT_Y0 = 4;
const COLUMN_ELEMENT_X1 = 5;
const COLUMN_ELEMENT_Y1 = 6;


const COLUMN_TREE_NODE_FIRST_ELEMENT_NODE_POINTER = 0;
/**
 * Number of elements stored in the node's element list
 * @type {number}
 */
const COLUMN_TREE_NODE_ELEMENT_COUNT = 1
const COLUMN_TREE_NODE_PARENT = 2;
const COLUMN_TREE_NODE_CHILDREN_POINTER_TL = 3;
const COLUMN_TREE_NODE_CHILDREN_POINTER_TR = 4;
const COLUMN_TREE_NODE_CHILDREN_POINTER_BL = 5;
const COLUMN_TREE_NODE_CHILDREN_POINTER_BR = 6;

const THRESHOLD_SPLIT = 16;
const THRESHOLD_MERGE = 8;

const temp_array_element = new Uint32Array(4096);

/**
 *
 *  NOTE: THIS CODE IS UNFINISHED, IT IS ONLY A SKETCH
 *  TODO finish implementation
 */
export class QuadTree {

    #node_pool = new BinaryElementPool(28);
    #element_pool = new BinaryElementPool(28);

    #root = QT_NULL_POINTER

    /**
     * AABB of the entire tree
     * @type {Float32Array}
     */
    #dimensions = new Float32Array([0, 0, 0, 0]);

    /**
     * Parameters allow us to set initial bounds to prevent early resizing
     * @param {number} x0
     * @param {number} y0
     * @param {number} x1
     * @param {number} y1
     */
    constructor(x0 = 0, y0 = 0, x1 = 0, y1 = 0) {
        this.#setDimensions(x0, y0, x1, y1);
    }

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

    /**
     * This method is unsafe as it does not re-build the tree
     * Make sure to rebuild the tree as necessary after calling this
     * @param {number} x0
     * @param {number} y0
     * @param {number} x1
     * @param {number} y1
     */
    #setDimensions(x0, y0, x1, y1) {

        assert.isFinite(x0, 'x0');
        assert.notNaN(x0, 'x0');

        assert.isFinite(y0, 'y0');
        assert.notNaN(y0, 'y0');

        assert.isFinite(x1, 'x1');
        assert.notNaN(x1, 'x1');

        assert.isFinite(y1, 'y1');
        assert.notNaN(y1, 'y1');

        const dimensions = this.#dimensions;

        dimensions[0] = x0;
        dimensions[1] = y0;
        dimensions[2] = x1;
        dimensions[3] = y1;

    }

    /**
     * Resize dimensions of the tree to tightly fit all inserted elements
     */
    shrink() {
        if (this.#root === QT_NULL_POINTER) {
            // tree is empty
            this.#setDimensions(0, 0, 0, 0);
        }

        const bounds = [0, 0, 0, 0];

        this.compute_tight_bounds(bounds);

        this.#setDimensions(...bounds);

        this.rebuild();
    }

    /**
     *
     * @param {number[]|Float32Array} output
     */
    compute_tight_bounds(output) {
        const pool = this.#element_pool;
        const data_pool_size = pool.size;
        const float32 = pool.data_float32;

        let bounds_x0 = Infinity;
        let bounds_y0 = Infinity;
        let bounds_x1 = -Infinity;
        let bounds_y1 = -Infinity;

        for (let id = 0; id < data_pool_size; id++) {
            if (pool.is_allocated(id)) {

                const word = pool.element_word(id);

                const x0 = float32[word + COLUMN_ELEMENT_X0];
                const y0 = float32[word + COLUMN_ELEMENT_Y0];

                const x1 = float32[word + COLUMN_ELEMENT_X1];
                const y1 = float32[word + COLUMN_ELEMENT_Y1];

                bounds_x0 = min2(x0, bounds_x0);
                bounds_y0 = min2(y0, bounds_y0);

                bounds_x1 = max2(x1, bounds_x1);
                bounds_y1 = max2(y1, bounds_y1);
            }
        }

        output[0] = bounds_x0;
        output[1] = bounds_y0;
        output[2] = bounds_x1;
        output[3] = bounds_y1;
    }

    /**
     * Rebuild tree but keep all the data
     */
    rebuild() {
        // drop existing structure
        this.#root = QT_NULL_POINTER;

        this.#node_pool.clear();

        // re-insert data elements
        const pool = this.#element_pool;
        const data_pool_size = pool.size;
        for (let i = 0; i < data_pool_size; i++) {
            if (pool.is_allocated(i)) {
                this.element_insert(i);
            }
        }
    }

    /**
     *
     * @returns {number} ID of data in the tree
     */
    element_allocate() {
        return this.#element_pool.allocate();
    }

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

        const pool = this.#element_pool;

        const word = pool.element_word(element);

        pool.data_uint32[word + COLUMN_ELEMENT_USER_DATA] = user_data;
    }

    /**
     *
     * @param {number} element
     * @return {number}
     */
    element_get_user_data(element) {
        const pool = this.#element_pool;
        const word = pool.element_word(element);

        return pool.data_uint32[word + COLUMN_ELEMENT_USER_DATA];
    }

    /**
     *
     * @param {number} element
     * @param {number} x0
     * @param {number} y0
     * @param {number} x1
     * @param {number} y1
     */
    element_set_bounds_primitive(
        element,
        x0, y0,
        x1, y1
    ) {

        assert.isFinite(x0, 'x0');
        assert.notNaN(x0, 'x0');

        assert.isFinite(y0, 'y0');
        assert.notNaN(y0, 'y0');

        assert.isFinite(x1, 'x1');
        assert.notNaN(x1, 'x1');

        assert.isFinite(y1, 'y1');
        assert.notNaN(y1, 'y1');

        const pool = this.#element_pool;

        const word = pool.element_word(element);

        const float32 = pool.data_float32;

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

    }

    /**
     *
     * @param {number} element
     * @return {number} next element in the node
     */
    element_get_next(element) {
        const pool = this.#element_pool;
        const word = pool.element_word(element);

        return pool.data_uint32[word + COLUMN_ELEMENT_NEXT];
    }

    /**
     *
     * @param {number} x0
     * @param {number} y0
     * @param {number} x1
     * @param {number} y1
     * @return {number}
     */
    #find_parent_for_box(x0, y0, x1, y1) {

        if (this.#root === QT_NULL_POINTER) {
            // special case
            return QT_NULL_POINTER;
        }

        let node = this.#root;

        let bounds_x0 = this.#dimensions[0];
        let bounds_y0 = this.#dimensions[1];
        let bounds_x1 = this.#dimensions[2];
        let bounds_y1 = this.#dimensions[3];

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

        for (; ;) {
            const node_address = node_pool.element_word(node);

            const bounds_mid_x = (bounds_x0 + bounds_x1) * 0.5;
            const bounds_mid_y = (bounds_y0 + bounds_y1) * 0.5;

            if (y1 < bounds_mid_y) {
                //top
                bounds_y1 = bounds_mid_y;
                if (x1 < bounds_mid_x) {
                    //left
                    const child_tl = node_uint32[node_address + COLUMN_TREE_NODE_CHILDREN_POINTER_TL];
                    if (child_tl === QT_NULL_POINTER) {
                        break;
                    }
                    node = child_tl;
                    bounds_x1 = bounds_mid_x;
                } else if (x0 >= bounds_mid_x) {
                    //right
                    const child_tr = node_uint32[node_address + COLUMN_TREE_NODE_CHILDREN_POINTER_TR];
                    if (child_tr === QT_NULL_POINTER) {
                        break;
                    }
                    node = child_tr;
                    bounds_x0 = bounds_mid_x;
                } else {
                    break;
                }
            } else if (y0 >= bounds_mid_y) {
                //bottom
                bounds_y0 = bounds_mid_y;
                if (x1 < bounds_mid_x) {
                    //left
                    const child_bl = node_uint32[node_address + COLUMN_TREE_NODE_CHILDREN_POINTER_BL];
                    if (child_bl === QT_NULL_POINTER) {
                        break;
                    }
                    node = child_bl;
                    bounds_x1 = bounds_mid_x;
                } else if (x0 >= bounds_mid_x) {
                    //right
                    const child_br = node_uint32[node_address + COLUMN_TREE_NODE_CHILDREN_POINTER_BR];
                    if (child_br === QT_NULL_POINTER) {
                        break;
                    }
                    node = child_br;
                    bounds_x0 = bounds_mid_x;
                } else {
                    break;
                }
            } else {
                // violates child bounds
                break;
            }
        }

        return node;
    }

    /**
     *
     * @param {number} x0
     * @param {number} y0
     * @param {number} x1
     * @param {number} y1
     */
    ensure_bounds(x0, y0, x1, y1) {

        const dimensions = this.#dimensions;

        const bounds_x0 = dimensions[0];
        const bounds_y0 = dimensions[1];
        const bounds_x1 = dimensions[2];
        const bounds_y1 = dimensions[3];

        if (
            x0 >= bounds_x0
            && y0 >= bounds_y0
            && x1 < bounds_x1
            && y1 < bounds_y1
        ) {
            // bounds are satisfied
            return;
        }

        // dimensions violated
        this.#setDimensions(
            min2(x0, bounds_x0),
            min2(y0, bounds_y0),
            max2(x1, bounds_x1),
            max2(y1, bounds_y1),
        );

    }

    /**
     * Assumes element is allocated and is not present in the tree yet
     * @param {number} element
     */
    element_insert(element) {
        const element_pool = this.#element_pool;
        const element_word = element_pool.element_word(element);

        // clear out element pointers
        element_pool.data_uint32[element_word + COLUMN_ELEMENT_NEXT] = QT_NULL_POINTER;

        const x0 = element_pool.data_float32[element_word + COLUMN_ELEMENT_X0];
        const y0 = element_pool.data_float32[element_word + COLUMN_ELEMENT_Y0];
        const x1 = element_pool.data_float32[element_word + COLUMN_ELEMENT_X1];
        const y1 = element_pool.data_float32[element_word + COLUMN_ELEMENT_Y1];

        this.ensure_bounds(x0, y0, x1, y1);

        let parent_node = this.#find_parent_for_box(x0, y0, x1, y1);

        if (parent_node === QT_NULL_POINTER) {
            this.#root = this.#node_allocate();

            parent_node = this.#root;
        }


        this.#insert_element_into(parent_node, element);

        this.#node_balance(parent_node);
    }

    #node_allocate() {
        const pool = this.#node_pool;

        const node = pool.allocate();

        const address = pool.element_word(node);

        const uint32 = pool.data_uint32;

        // initialize pointers
        uint32[address + COLUMN_TREE_NODE_PARENT] = QT_NULL_POINTER;
        uint32[address + COLUMN_TREE_NODE_FIRST_ELEMENT_NODE_POINTER] = QT_NULL_POINTER;
        uint32[address + COLUMN_TREE_NODE_CHILDREN_POINTER_TL] = QT_NULL_POINTER;
        uint32[address + COLUMN_TREE_NODE_CHILDREN_POINTER_TR] = QT_NULL_POINTER;
        uint32[address + COLUMN_TREE_NODE_CHILDREN_POINTER_BL] = QT_NULL_POINTER;
        uint32[address + COLUMN_TREE_NODE_CHILDREN_POINTER_BR] = QT_NULL_POINTER;

        return node;
    }

    /**
     * This operation may create new nodes
     * @param {number} node
     */
    #node_balance(node) {
        // count owned elements
        const address = this.#node_pool.element_word(node);

        const element_count = this.#node_pool.data_uint32[address + COLUMN_TREE_NODE_ELEMENT_COUNT];

        if (element_count > THRESHOLD_SPLIT) {

            this.#node_push_data_down(node);
        } else if (element_count < THRESHOLD_MERGE) {
            this.#node_pull_data();
        }
    }

    /**
     *
     * @param {number} node
     * @param {number[]|Uint32Array} destination
     * @param {number} destination_offset
     * @return {number} number of elements read
     */
    node_read_elements(node, destination, destination_offset) {
        const node_address = this.#node_pool.element_word(node);

        let element = this.#node_pool.data_uint32[node_address + COLUMN_TREE_NODE_FIRST_ELEMENT_NODE_POINTER];

        let count = 0;

        while (element !== QT_NULL_POINTER) {
            destination[destination_offset + count] = element;

            count++;

            const element_address = this.#element_pool.element_word(element);
            element = this.#element_pool.data_uint32[element_address + COLUMN_ELEMENT_NEXT];
        }

        return count;
    }

    /**
     * Take elements owned by the element and attempt to push them as deep as possible
     * This operation may create new nodes
     * @param {number} node
     */
    #node_push_data_down(node) {

    }

    #node_pull_data() {

    }

    /**
     *
     * @param {number} node
     * @param {number} element
     */
    #insert_element_into(node, element) {

        const node_pool = this.#node_pool;
        const node_address = node_pool.element_word(node);

        // increment node count
        node_pool[node_address + COLUMN_TREE_NODE_ELEMENT_COUNT]++;

        let _element = node_pool.data_uint32[node_address + COLUMN_TREE_NODE_FIRST_ELEMENT_NODE_POINTER];

        const element_pool = this.#element_pool;

        if (_element === QT_NULL_POINTER) {
            // first element
            node_pool.data_uint32[node_address + COLUMN_TREE_NODE_FIRST_ELEMENT_NODE_POINTER] = element;
        } else {
            // scan to the last element
            let _link = _element;

            for (; ;) {
                const address = element_pool.element_word(_link);
                const next = element_pool.data_uint32[address + COLUMN_ELEMENT_NEXT];
                if (next === QT_NULL_POINTER) {
                    // end of the list is found
                    element_pool.data_uint32[address + COLUMN_ELEMENT_NEXT] = element;
                    break;
                }
                _link = next;
            }

        }


        // patch links on the element
        const element_address = element_pool.element_word(element);
        element_pool.data_uint32[element_address + COLUMN_ELEMENT_PARENT_NODE] = node;
    }

    /**
     *
     * @param {number} datum_id ID of data in tree
     */
    element_remove(datum_id) {

        const element_pool = this.#element_pool;
        const element_word = element_pool.element_word(datum_id);

        const tree_node_id = element_pool.data_uint32[element_word + COLUMN_ELEMENT_PARENT_NODE];

        this.#tree_node_remove_element(tree_node_id, datum_id);
    }

    /**
     *
     * @param {number} node
     * @return {boolean}
     */
    #is_node_empty(node) {
        const pool = this.#node_pool;
        const word = pool.element_word(node);

        const uint32 = pool.data_uint32;

        const first_element_node = uint32[word + COLUMN_TREE_NODE_FIRST_ELEMENT_NODE_POINTER]

        if (first_element_node !== QT_NULL_POINTER) {
            return false;
        }

        const has_allocated_children = uint32[word + COLUMN_TREE_NODE_CHILDREN_POINTER_TL] !== QT_NULL_POINTER
            || uint32[word + COLUMN_TREE_NODE_CHILDREN_POINTER_TR] !== QT_NULL_POINTER
            || uint32[word + COLUMN_TREE_NODE_CHILDREN_POINTER_BL] !== QT_NULL_POINTER
            || uint32[word + COLUMN_TREE_NODE_CHILDREN_POINTER_BR] !== QT_NULL_POINTER;

        return !has_allocated_children;
    }

    /**
     * Assumes the node is empty
     * Does not perform any checks
     * Only updates parent node
     * @param {number} node
     * @returns {number} parent of this node
     */
    #remove_node(node) {

        const pool = this.#node_pool;
        const word = pool.element_word(node);

        const uint32 = pool.data_uint32;

        // let's remove reference from the parent to this node
        const parent = uint32[word + COLUMN_TREE_NODE_PARENT];

        if (parent !== QT_NULL_POINTER) {
            // not root
            const parent_word = pool.element_word(parent);

            if (uint32[parent_word + COLUMN_TREE_NODE_CHILDREN_POINTER_TL] === node) {
                uint32[parent_word + COLUMN_TREE_NODE_CHILDREN_POINTER_TL] = QT_NULL_POINTER;
            } else if (uint32[parent_word + COLUMN_TREE_NODE_CHILDREN_POINTER_TR] === node) {
                uint32[parent_word + COLUMN_TREE_NODE_CHILDREN_POINTER_TR] = QT_NULL_POINTER;
            } else if (uint32[parent_word + COLUMN_TREE_NODE_CHILDREN_POINTER_BL] === node) {
                uint32[parent_word + COLUMN_TREE_NODE_CHILDREN_POINTER_BL] = QT_NULL_POINTER;
            } else if (uint32[parent_word + COLUMN_TREE_NODE_CHILDREN_POINTER_BR] === node) {
                uint32[parent_word + COLUMN_TREE_NODE_CHILDREN_POINTER_BR] = QT_NULL_POINTER;
            } else {
                throw new Error(`specified 'parent' node(${parent}) does not point to this node(${node})`);
            }

        } else {
            // we are root
            this.#root = QT_NULL_POINTER;
        }

        // de-allocate self
        pool.release(node);

        return parent;
    }

    /**
     * Remove node if it's empty, propagates up the parent chain if possible
     * @param {number} node
     * @returns {boolean} true if collapsed
     */
    #try_remove_node(node) {

        let n = node;

        while (n !== QT_NULL_POINTER && this.#is_node_empty(n)) {
            // at this point we store no data and have no children, node is useless and can be reclaimed
            n = this.#remove_node(n);
        }

        return true;
    }

    /**
     * NOTE: This method does NOT do de-allocation
     * @param {number} node ID of tree node
     * @param {number} element ID of inserted element
     * @returns {boolean} if element was found and cut
     */
    #tree_node_remove_element(
        node,
        element
    ) {

        assert.isNonNegativeInteger(node, 'tree_node_id');
        assert.isNonNegativeInteger(element, 'element_id');

        const node_pool = this.#node_pool;

        const node_address = node_pool.element_word(node);

        let element_node_pointer = node_pool.data_uint32[node_address + COLUMN_TREE_NODE_FIRST_ELEMENT_NODE_POINTER];
        let previous_node_pointer = QT_NULL_POINTER;

        const element_pool = this.#element_pool;

        while (element_node_pointer !== QT_NULL_POINTER) {
            const element_node_word = element_pool.element_word(element_node_pointer);

            const next_element_pointer = element_pool.data_uint32[element_node_word + COLUMN_ELEMENT_NEXT];

            if (element_node_pointer === element) {
                // found the right element

                node_pool.data_uint32[node_address + COLUMN_TREE_NODE_ELEMENT_COUNT]--;

                if (previous_node_pointer === QT_NULL_POINTER) {
                    // this was the first element in the list
                    node_pool.data_uint32[node_address + COLUMN_TREE_NODE_FIRST_ELEMENT_NODE_POINTER] = next_element_pointer;
                    // this might be the last element in the list, try to clean-up node if possible
                    this.#try_remove_node(node);
                } else {
                    // patch previous element to point to the next element
                    const previous_node_word = element_pool.element_word(previous_node_pointer);

                    element_pool.data_uint32[previous_node_word + COLUMN_ELEMENT_NEXT] = next_element_pointer;
                }


                return true;
            }

            previous_node_pointer = element_node_pointer;

            element_node_pointer = next_element_pointer;
        }

        // element not found
        return false;
    }

    /**
     * Remove all data
     */
    release_all() {
        this.#root = QT_NULL_POINTER;
        this.#element_pool.clear();
        this.#node_pool.clear();
    }
}