import { assert } from "../../../assert.js";
import { max2 } from "../../../math/max2.js";
import { min2 } from "../../../math/min2.js";
import AABB2 from "../../2d/aabb/AABB2.js";
import { qt_collect_by_box } from "./qt_collect_by_box.js";
import { qt_collect_by_point } from "./qt_collect_by_point.js";
import { QuadTreeDatum } from "./QuadTreeDatum.js";


/**
 * Must have at least this many items before splitting makes sense.
 * @type {number}
 */
const THRESHOLD_SPLIT = 16;

/**
 * Must have at least this many items before merging makes sense.
 * @type {number}
 */
const THRESHOLD_MERGE = 8;

/**
 * @template D
 * @extends AABB2
 */
export class QuadTreeNode extends AABB2 {
    /**
     *
     * @type {QuadTreeNode|null}
     */
    topLeft = null;
    /**
     *
     * @type {QuadTreeNode|null}
     */
    topRight = null;
    /**
     *
     * @type {QuadTreeNode|null}
     */
    bottomLeft = null;
    /**
     *
     * @type {QuadTreeNode|null}
     */
    bottomRight = null;

    /**
     *
     * @type {QuadTreeNode|null}
     */
    parentNode = null;

    /**
     *
     * @type {number}
     */
    treeDataCount = 0;


    /**
     *
     * @type {QuadTreeDatum<D>[]}
     */
    data = [];

    /**
     * Internal method. Assesses whether splitting this node would be beneficial.
     * @returns {boolean}
     */
    isSplitMeaningful() {

        const data = this.data;
        const data_size = data.length;

        if (data_size < THRESHOLD_SPLIT) {
            return false;
        }

        const x0 = this.x0;
        const x1 = this.x1;
        const y0 = this.y0;
        const y1 = this.y1;

        if (x0 === x1 && y0 === y1) {
            // zero area, splitting will do nothing useful
            return false;
        }

        const xm = (x0 + x1) * 0.5;
        const ym = (y0 + y1) * 0.5;

        // simulate split and count how many items are in each quadrant
        let count_tl = 0;
        let count_tr = 0;
        let count_bl = 0;
        let count_br = 0;
        for (let i = 0; i < data_size; i++) {
            const datum = data[i];

            if (datum.y1 < ym) {
                //top
                if (datum.x1 < xm) {
                    //left
                    count_tl++;
                } else if (datum.x0 >= xm) {
                    //right
                    count_tr++;
                }
            } else if (datum.y0 >= ym) {
                //top
                if (datum.x1 < xm) {
                    //left
                    count_bl++;
                } else if (datum.x0 >= xm) {
                    //right
                    count_br++;
                }
            }
        }

        const refined_sum = count_tl + count_tr + count_bl + count_br;

        if (refined_sum === 0) {
            return false;
        }

        const refined_max = Math.max(count_tl, count_tr, count_bl, count_br);

        if (refined_max === data_size) {
            // all children would go quadrant, this is pointless recursion
            return false;
        }

        return true;
    }

    balance() {
        const is_split = this.isSplit();

        if (!is_split && this.isSplitMeaningful()) {
            this.split();

            return 1;
        } else if (this.treeDataCount < THRESHOLD_MERGE && is_split) {
            this.merge();

            return 2;
        }

        return 0;
    }

    balanceBubbleUp() {
        if (this.balance() === 2) {
            let node = this.parentNode;

            while (node !== null) {
                if (node.balance() !== 2) {
                    break;
                }

                node = node.parentNode;
            }
        }
    }

    /**
     *
     * @param {number} x0
     * @param {number} y0
     * @param {number} x1
     * @param {number} y1
     */
    resize(x0, y0, x1, y1) {
        if (this.x0 === x0 && this.y0 === y0 && this.x1 === x1 && this.y1 === y1) {
            //no change, do nothing
            return;
        }

        if (this.isSplit()) {
            this.merge();
        }

        this.x0 = x0;
        this.y0 = y0;
        this.x1 = x1;
        this.y1 = y1;

        this.balance();
    }

    /**
     * @template T
     * @param {T} data
     * @param {number} x0
     * @param {number} y0
     * @param {number} x1
     * @param {number} y1
     * @returns {QuadTreeDatum}
     */
    add(data, x0, y0, x1, y1) {
        const datum = new QuadTreeDatum(x0, y0, x1, y1);

        datum.data = data;

        this.insertDatum(datum);

        return datum;
    }

    /**
     *
     * @param {QuadTreeDatum} datum
     */
    insertDatum(datum) {
        assert.ok(datum.isQuadTreeDatum, 'datum is not a QuadTreeDatum');

        //check bound invariants
        assert.ok(datum.x0 <= datum.x1, `Datum's x0(=${datum.x0}) > x1(=${datum.x1})`);
        assert.ok(datum.y0 <= datum.y1, `Datum's y0(=${datum.y0}) > y1(=${datum.y1})`);

        const x0 = this.x0;
        const y0 = this.y0;

        const x1 = this.x1;
        const y1 = this.y1;


        if (datum.x0 < x0 || datum.x1 > x1 || datum.y0 < y0 || datum.y1 > y1) {
            //search up, bounds of this node have been violated
            if (this.parentNode === null) {
                //resize this node
                this.resize(
                    min2(x0, datum.x0),
                    min2(y0, datum.y0),
                    max2(x1, datum.x1),
                    max2(y1, datum.y1)
                );
                this.addDatum(datum);
            } else {
                //insert into parent
                this.parentNode.insertDatum(datum);
            }
        } else {

            if (!this.isSplit()) {
                if (this.treeDataCount >= THRESHOLD_SPLIT && this.isSplitMeaningful()) {
                    //node is too large and should be split
                    this.split();
                } else {
                    this.addDatum(datum);
                    //we're done
                    return;
                }
            }

            const xm = (x0 + x1) * 0.5;
            const ym = (y0 + y1) * 0.5;

            if (datum.y1 < ym) {
                //top
                if (datum.x1 < xm) {
                    //left
                    this.topLeft.insertDatum(datum);
                } else if (datum.x0 >= xm) {
                    //right
                    this.topRight.insertDatum(datum);
                } else {
                    this.addDatum(datum);
                }
            } else if (datum.y0 >= ym) {
                //bottom
                if (datum.x1 < xm) {
                    //left
                    this.bottomLeft.insertDatum(datum);
                } else if (datum.x0 >= xm) {
                    //right
                    this.bottomRight.insertDatum(datum);
                } else {
                    this.addDatum(datum);
                }
            } else {
                this.addDatum(datum);
            }
        }
    }

    /**
     * @private
     * @param {QuadTreeDatum} datum
     */
    addDatum(datum) {
        this.treeDataCount++;
        this.data.push(datum);

        datum.parentNode = this;

        let node = this.parentNode;

        while (node !== null) {
            node.treeDataCount++;

            node = node.parentNode;
        }
    }

    /**
     *
     * @return {boolean}
     */
    isSplit() {
        return this.topLeft !== null;
    }

    split() {
        // assert.notOk(this.isSplit(), 'Node is already split');

        const x0 = this.x0;
        const y0 = this.y0;

        const x1 = this.x1;
        const y1 = this.y1;

        const xm = (x0 + x1) * 0.5;
        const ym = (y0 + y1) * 0.5;

        this.topLeft = new QuadTreeNode(x0, y0, xm, ym);
        this.topLeft.parentNode = this;

        this.topRight = new QuadTreeNode(xm, y0, x1, ym);
        this.topRight.parentNode = this;

        this.bottomLeft = new QuadTreeNode(x0, ym, xm, y1);
        this.bottomLeft.parentNode = this;

        this.bottomRight = new QuadTreeNode(xm, ym, x1, y1);
        this.bottomRight.parentNode = this;

        this.pushDataDown();

        this.topLeft.balance();
        this.topRight.balance();
        this.bottomLeft.balance();
        this.bottomRight.balance();
    }

    /**
     * Pull all data from descendants to this node
     */
    pullDataUp() {
        this.topLeft.traversePreOrder(this.absorbDataFrom, this);
        this.topRight.traversePreOrder(this.absorbDataFrom, this);
        this.bottomLeft.traversePreOrder(this.absorbDataFrom, this);
        this.bottomRight.traversePreOrder(this.absorbDataFrom, this);
    }

    /**
     * Move data from supplied node to this one
     * @param {QuadTreeNode} node
     */
    absorbDataFrom(node) {
        const this_data = this.data;

        const nodeData = node.data;
        const nodeDataSize = nodeData.length;

        for (let i = 0; i < nodeDataSize; i++) {
            const nodeDatum = nodeData[i];

            nodeDatum.parentNode = this;

            this_data.push(nodeDatum);
        }

        // reset data in the input node
        node.treeDataCount = 0;
        node.data = [];
    }

    /**
     * Push data down to descendants as far as possible
     */
    pushDataDown() {
        assert.ok(this.isSplit(), 'Node is not split');

        const data = this.data;
        let dataLength = data.length;

        let i = 0;

        const x0 = this.x0;
        const y0 = this.y0;

        const x1 = this.x1;
        const y1 = this.y1;

        // get center point
        const xm = (x0 + x1) * 0.5;
        const ym = (y0 + y1) * 0.5;

        for (; i < dataLength; i++) {
            const datum = data[i];

            let child;
            if (datum.y1 < ym) {
                //top
                if (datum.x1 < xm) {
                    //left
                    child = this.topLeft;

                } else if (datum.x0 >= xm) {
                    //right
                    child = this.topRight;
                } else {
                    continue;
                }
            } else if (datum.y0 >= ym) {
                //top
                if (datum.x1 < xm) {
                    //left
                    child = this.bottomLeft;
                } else if (datum.x0 >= xm) {
                    //right
                    child = this.bottomRight;
                } else {
                    continue;
                }
            } else {
                continue;
            }

            child.data.push(datum);
            child.treeDataCount++;

            data.splice(i, 1);

            datum.parentNode = child;

            i--;
            dataLength--;
        }
    }

    merge() {
        // assert.ok(this.isSplit(), 'Node is not split');

        this.pullDataUp();

        this.topLeft = null;
        this.topRight = null;
        this.bottomLeft = null;
        this.bottomRight = null;
    }

    clear() {
        this.data = [];
        this.treeDataCount = 0;

        this.topLeft = null;
        this.topRight = null;
        this.bottomLeft = null;
        this.bottomRight = null;
    }

    /**
     * @param {D[]} result
     */
    getRawData(result) {

        this.traverseData(n => {
            const data = n.data;

            result.push(data);
        });

    }

    /**
     *
     * @param {function(QuadTreeDatum<D>)} visitor
     * @param {*} [thisArg]
     */
    traverseData(visitor, thisArg) {
        this.traversePreOrder((node) => {

            const data = node.data;
            const n = data.length;

            for (let i = 0; i < n; i++) {
                const treeDatum = data[i];

                visitor.call(thisArg, treeDatum);
            }

        });
    }

    /**
     *
     * @param {function(QuadTreeNode<D>):boolean} visitor
     * @param {*} [thisArg]
     */
    traversePreOrder(visitor, thisArg) {
        const continueFlag = visitor.call(thisArg, this);

        if (continueFlag === false) {
            return;
        }

        if (this.isSplit()) {
            this.topLeft.traversePreOrder(visitor, thisArg);
            this.topRight.traversePreOrder(visitor, thisArg);
            this.bottomLeft.traversePreOrder(visitor, thisArg);
            this.bottomRight.traversePreOrder(visitor, thisArg);
        }

    }

    /**
     * @deprecated use {@link qt_collect_by_point} directly
     * @param {Array<QuadTreeDatum<D>>} result
     * @param {number} x
     * @param {number} y
     * @returns {number} number of intersecting objects added to the result
     */
    requestDatumIntersectionsPoint(result, x, y) {
        return qt_collect_by_point(result, 0, this, x, y);
    }

    /**
     * @deprecated use {@link qt_collect_by_box} instead
     * @param {Array<QuadTreeDatum<D>>} result
     * @param {number} x0
     * @param {number} y0
     * @param {number} x1
     * @param {number} y1
     * @returns {number} number of intersecting objects added to the result
     */
    requestDatumIntersectionsRectangle(result, x0, y0, x1, y1) {
        return qt_collect_by_box(result, 0, this, x0, y0, x1, y1);
    }

    /**
     * NOTE: touching is not counted as intersection
     * @deprecated use {@link qt_collect_by_box} instead
     * @param {number} x0
     * @param {number} y0
     * @param {number} x1
     * @param {number} y1
     * @param {function(QuadTreeDatum<D>)} visitor
     * @param {*} [thisArg]
     */
    traverseRectangleIntersections(x0, y0, x1, y1, visitor, thisArg) {
        const data = this.data;
        const dataCount = data.length;

        for (let i = 0; i < dataCount; i++) {
            const datum = data[i];

            if (datum.x0 < x1 && datum.x1 > x0 && datum.y0 < y1 && datum.y1 > y0) {
                const continueTraversal = visitor.call(thisArg, datum, x0, y0, x1, y1);

                if (continueTraversal === false) {
                    return;
                }
            }
        }

        if (this.isSplit()) {

            const hx = (this.x0 + this.x1) / 2;
            const hy = (this.y0 + this.y1) / 2;

            if (hx >= x0) {
                if (hy >= y0) {
                    this.topLeft.traverseRectangleIntersections(x0, y0, x1, y1, visitor, thisArg);
                }
                if (hy <= y1) {
                    this.bottomLeft.traverseRectangleIntersections(x0, y0, x1, y1, visitor, thisArg);
                }
            }

            if (hx <= x1) {
                if (hy >= y0) {
                    this.topRight.traverseRectangleIntersections(x0, y0, x1, y1, visitor, thisArg);
                }
                if (hy <= y1) {
                    this.bottomRight.traverseRectangleIntersections(x0, y0, x1, y1, visitor, thisArg);
                }
            }

        }
    }

}

/**
 * @readonly
 * @type {boolean}
 */
QuadTreeNode.prototype.isQuadTreeNode = true;
