import { Point } from "@devexpress/utils/lib/geometry/point";
import { Rectangle } from "@devexpress/utils/lib/geometry/rectangle";


export class RTree<T> {
    private maxWidth: number;
    private minWidth: number;
    private root: RTreeNodeBranch;

    constructor(maxWidth: number = 6) {
        this.maxWidth = maxWidth;
        this.minWidth = Math.floor(maxWidth / 2);
        this.root = new RTreeNodeBranch(new Rectangle(0, 0, 0, 0), []);
    }

    search(point: Point): T[] {
        const result: T[] = [];
        if(!this.root.rect.containsPoint(point))
            return result;
        const hitsStack: RTreeNode[][] = [];
        hitsStack.push(this.root.nodes);
        while(hitsStack.length) {
            const nodes = hitsStack.pop();
            for(let i = nodes.length - 1, node: RTreeNode; node = nodes[i]; i--)
                if(node.rect.containsPoint(point))
                    if(isBranch(node))
                        hitsStack.push(node.nodes);
                    else
                        result.push((<RTreeNodeLeaf<T>>node).obj);
        }
        return result;
    }

    insert(rect: Rectangle, obj: T): void {
        const newLeaf = new RTreeNodeLeaf(rect.clone(), obj);
        if(!this.root.nodes.length) {
            this.root.rect = rect.clone();
            this.root.nodes.push(newLeaf);
            return;
        }
        const treeStack = this.chooseLeafSubtree(newLeaf);
        let retObj : RTreeNode | RTreeNode[] | Rectangle = newLeaf;
        let current: RTreeNodeBranch;
        let previous: RTreeNodeBranch;
        while(treeStack.length) {
            if(current && isBranch(current) && !current.nodes.length) {
                previous = current;
                current = treeStack.pop();
                for(let i = 0, node: RTreeNode; node = current.nodes[i]; i++) {
                    if(isBranch(node) && (node === previous || !node.nodes.length)) {
                        node.nodes.splice(i, 1);
                        break;
                    }
                }
            }
            else
                current = treeStack.pop();

            if(retObj instanceof RTreeNode || Array.isArray(retObj)) {
                if(Array.isArray(retObj)) {
                    for(let i = 0, retObjChild: RTreeNode; retObjChild = retObj[i]; i++) {
                        expandRect(current.rect, retObjChild.rect);
                    }
                    current.nodes = current.nodes.concat(retObj);
                }
                else {
                    expandRect(current.rect, retObj.rect);
                    current.nodes.push(retObj);
                }

                if(current.nodes.length <= this.maxWidth) {
                    retObj = current.rect.clone();
                }
                else {
                    const a = this.linearSplit(current.nodes);
                    retObj = a;
                    if(!treeStack.length) {
                        current.nodes.push(a[0]);
                        treeStack.push(current);
                        retObj = a[1];
                    }
                }
            }
            else {
                expandRect(current.rect, retObj);
                retObj = current.rect.clone();
            }
        }
    }

    private chooseLeafSubtree(newLeaf: RTreeNodeLeaf<T>): RTreeNodeBranch[] {
        const result: RTreeNodeBranch[] = [];
        let bestChoiceIndex = -1;
        let bestChoiceArea;
        let first = true;
        result.push(this.root);
        let nodes = this.root.nodes;
        while(first || bestChoiceIndex !== -1) {
            if(first)
                first = false;
            else {
                result.push(<RTreeNodeBranch>nodes[bestChoiceIndex]);
                nodes = (<RTreeNodeBranch>nodes[bestChoiceIndex]).nodes;
                bestChoiceIndex = -1;
            }

            for(let i = nodes.length - 1, ltree: RTreeNode; ltree = nodes[i]; i--) {
                if(!isBranch(ltree)) {
                    bestChoiceIndex = -1;
                    break;
                }
                const oldLRatio = squarifiedRatio(ltree.rect.width, ltree.rect.height, ltree.nodes.length + 1);
                const nw = Math.max(ltree.rect.x + ltree.rect.width, newLeaf.rect.x + newLeaf.rect.width) - Math.min(ltree.rect.x, newLeaf.rect.x);
                const nh = Math.max(ltree.rect.y + ltree.rect.height, newLeaf.rect.y + newLeaf.rect.height) - Math.min(ltree.rect.y, newLeaf.rect.y);
                const lratio = squarifiedRatio(nw, nh, ltree.nodes.length + 2);
                if(bestChoiceIndex < 0 || Math.abs(lratio - oldLRatio) < bestChoiceArea) {
                    bestChoiceArea = Math.abs(lratio - oldLRatio);
                    bestChoiceIndex = i;
                }
            }
        }
        return result;
    }

    private linearSplit(nodes): RTreeNodeBranch[] {
        const n = this.pickLinear(nodes);
        while(nodes.length > 0) {
            this.pickNext(nodes, n[0], n[1]);
        }
        return n;
    }

    private pickLinear(nodes: RTreeNode[]): RTreeNodeBranch[] {
        let lowestHighX = nodes.length - 1;
        let highestLowX = 0;
        let lowestHighY = nodes.length - 1;
        let highestLowY = 0;
        let t1: RTreeNode;
        let t2: RTreeNode;

        for(let i = nodes.length - 2, l: RTreeNode; l = nodes[i]; i--) {
            if(l.rect.x > nodes[highestLowX].rect.x)
                highestLowX = i;
            else if(l.rect.right < nodes[lowestHighX].rect.right)
                lowestHighX = i;
            if(l.rect.y > nodes[highestLowY].rect.y)
                highestLowY = i;
            else if(l.rect.bottom < nodes[lowestHighY].rect.bottom)
                lowestHighY = i;
        }
        const dx = Math.abs((nodes[lowestHighX].rect.right) - nodes[highestLowX].rect.x);
        const dy = Math.abs((nodes[lowestHighY].rect.bottom) - nodes[highestLowY].rect.y);
        if(dx > dy) {
            if(lowestHighX > highestLowX) {
                t1 = nodes.splice(lowestHighX, 1)[0];
                t2 = nodes.splice(highestLowX, 1)[0];
            }
            else {
                t2 = nodes.splice(highestLowX, 1)[0];
                t1 = nodes.splice(lowestHighX, 1)[0];
            }
        }
        else {
            if(lowestHighY > highestLowY) {
                t1 = nodes.splice(lowestHighY, 1)[0];
                t2 = nodes.splice(highestLowY, 1)[0];
            }
            else {
                t2 = nodes.splice(highestLowY, 1)[0];
                t1 = nodes.splice(lowestHighY, 1)[0];
            }
        }
        return [
            new RTreeNodeBranch(t1.rect.clone(), [t1]),
            new RTreeNodeBranch(t2.rect.clone(), [t2])
        ];
    }

    private pickNext(nodes: RTreeNode[], a: RTreeNodeBranch, b: RTreeNodeBranch): void {
        const areaA = squarifiedRatio(a.rect.width, a.rect.height, a.nodes.length + 1);
        const areaB = squarifiedRatio(b.rect.width, b.rect.height, b.nodes.length + 1);
        let highAreaDelta: number;
        let highAreaNode: number;
        let lowestGrowthGroup: RTreeNodeBranch;

        for(let i = nodes.length - 1, l: RTreeNode; l = nodes[i]; i--) {
            const newAreaA = new Rectangle(
                Math.min(a.rect.x, l.rect.x),
                Math.min(a.rect.y, l.rect.y),
                0, 0
            );
            newAreaA.width = Math.max(a.rect.right, l.rect.right) - newAreaA.x;
            newAreaA.height = Math.max(a.rect.bottom, l.rect.bottom) - newAreaA.y;
            const changeNewAreaA = Math.abs(squarifiedRatio(newAreaA.width, newAreaA.height, a.nodes.length + 2) - areaA);

            const newAreaB = new Rectangle(
                Math.min(b.rect.x, l.rect.x),
                Math.min(b.rect.y, l.rect.y),
                0, 0
            );
            newAreaB.width = Math.max(b.rect.right, l.rect.right) - newAreaB.x;
            newAreaB.height = Math.max(b.rect.bottom, l.rect.bottom) - newAreaB.y;
            const changeNewAreaB = Math.abs(squarifiedRatio(newAreaB.width, newAreaB.height, b.nodes.length + 2) - areaB);

            if(!highAreaNode || !highAreaDelta || Math.abs(changeNewAreaB - changeNewAreaA) < highAreaDelta) {
                highAreaNode = i;
                highAreaDelta = Math.abs(changeNewAreaB - changeNewAreaA);
                lowestGrowthGroup = changeNewAreaB < changeNewAreaA ? b : a;
            }
        }
        const tmp = nodes.splice(highAreaNode, 1)[0];
        if(a.nodes.length + nodes.length + 1 <= this.minWidth) {
            a.nodes.push(tmp);
            expandRect(a.rect, tmp.rect);
        }
        else if(b.nodes.length + nodes.length + 1 <= this.minWidth) {
            b.nodes.push(tmp);
            expandRect(b.rect, tmp.rect);
        }
        else {
            lowestGrowthGroup.nodes.push(tmp);
            expandRect(lowestGrowthGroup.rect, tmp.rect);
        }
    }
}

class RTreeNode {
    constructor(public rect: Rectangle) { }
}
class RTreeNodeLeaf<T> extends RTreeNode {
    constructor(rect: Rectangle, public obj: T) {
        super(rect);
    }
}
class RTreeNodeBranch extends RTreeNode {
    constructor(rect: Rectangle, public nodes: RTreeNode[]) {
        super(rect);
    }
}

function isBranch(node: RTreeNode): node is RTreeNodeBranch {
    return "nodes" in node;
}

function squarifiedRatio(width: number, height: number, fill: number): number {
    const lperi = (width + height) / 2;
    const larea = width * height;
    const lgeo = larea / (lperi * lperi);
    return larea * fill / lgeo;
}

function expandRect(rect: Rectangle, other: Rectangle): void {
    const rx = Math.max(rect.right, other.right);
    const ry = Math.max(rect.bottom, other.bottom);
    rect.x = Math.min(rect.x, other.x);
    rect.y = Math.min(rect.y, other.y);
    rect.width = rx - rect.x;
    rect.height = ry - rect.y;
}
