import { Box3D } from './box3d.js';
import { Coord3D, CoordIsEqual3D } from './coord3d.js';
import { IsGreaterOrEqual, IsLowerOrEqual } from './geometry.js';

export class OctreeNode
{
    constructor (boundingBox, level)
    {
        this.boundingBox = boundingBox;
        this.level = level;
        this.pointItems = [];
        this.childNodes = [];
    }

    AddPoint (point, data, options)
    {
        let node = this.FindNodeForPoint (point);
        if (node === null) {
            return false;
        }

        if (node.FindPointDirectly (point) !== null) {
            return false;
        }

        if (node.pointItems.length < options.maxPointsPerNode || node.level >= options.maxTreeDepth) {
            node.AddPointDirectly (point, data);
            return true;
        } else {
            node.CreateChildNodes ();
            let oldPointItems = node.pointItems;
            node.pointItems = [];
            for (let i = 0; i < oldPointItems.length; i++) {
                let pointItem = oldPointItems[i];
                if (!node.AddPoint (pointItem.point, pointItem.data, options)) {
                    return false;
                }
            }
            return node.AddPoint (point, data, options);
        }
    }

    FindPoint (point)
    {
        let node = this.FindNodeForPoint (point);
        if (node === null) {
            return null;
        }
        return node.FindPointDirectly (point);
    }

    AddPointDirectly (point, data)
    {
        this.pointItems.push ({
            point : point,
            data : data
        });
    }

    FindPointDirectly (point)
    {
        for (let i = 0; i < this.pointItems.length; i++) {
            let pointItem = this.pointItems[i];
            if (CoordIsEqual3D (point, pointItem.point)) {
                return pointItem.data;
            }
        }
        return null;
    }

    FindNodeForPoint (point)
    {
        if (!this.IsPointInBounds (point)) {
            return null;
        }

        if (this.childNodes.length === 0) {
            return this;
        }

        for (let i = 0; i < this.childNodes.length; i++) {
            let childNode = this.childNodes[i];
            let foundNode = childNode.FindNodeForPoint (point);
            if (foundNode !== null) {
                return foundNode;
            }
        }

        return null;
    }

    CreateChildNodes ()
    {
        function AddChildNode (node, minX, minY, minZ, sizeX, sizeY, sizeZ)
        {
            let box = new Box3D (
                new Coord3D (minX, minY, minZ),
                new Coord3D (minX + sizeX, minY + sizeY, minZ + sizeZ)
            );
            node.childNodes.push (new OctreeNode (box, node.level + 1));
        }

        let min = this.boundingBox.min;
        let center = this.boundingBox.GetCenter ();
        let sizeX = (this.boundingBox.max.x - this.boundingBox.min.x) / 2.0;
        let sizeY = (this.boundingBox.max.y - this.boundingBox.min.y) / 2.0;
        let sizeZ = (this.boundingBox.max.z - this.boundingBox.min.z) / 2.0;

        AddChildNode (this, min.x, min.y, min.z, sizeX, sizeY, sizeZ);
        AddChildNode (this, center.x, min.y, min.z, sizeX, sizeY, sizeZ);
        AddChildNode (this, min.x, center.y, min.z, sizeX, sizeY, sizeZ);
        AddChildNode (this, center.x, center.y, min.z, sizeX, sizeY, sizeZ);
        AddChildNode (this, min.x, min.y, center.z, sizeX, sizeY, sizeZ);
        AddChildNode (this, center.x, min.y, center.z, sizeX, sizeY, sizeZ);
        AddChildNode (this, min.x, center.y, center.z, sizeX, sizeY, sizeZ);
        AddChildNode (this, center.x, center.y, center.z, sizeX, sizeY, sizeZ);
    }

    IsPointInBounds (point)
    {
        let isEqual =
            IsGreaterOrEqual (point.x, this.boundingBox.min.x) &&
            IsGreaterOrEqual (point.y, this.boundingBox.min.y) &&
            IsGreaterOrEqual (point.z, this.boundingBox.min.z) &&
            IsLowerOrEqual (point.x, this.boundingBox.max.x) &&
            IsLowerOrEqual (point.y, this.boundingBox.max.y) &&
            IsLowerOrEqual (point.z, this.boundingBox.max.z);
        return isEqual;
    }
}

export class Octree
{
    constructor (boundingBox, options)
    {
        this.options = {
            maxPointsPerNode : 10,
            maxTreeDepth : 10
        };
        if (options !== undefined) {
            if (options.maxPointsPerNode !== undefined) {
                this.options.maxPointsPerNode = options.maxPointsPerNode;
            }
            if (options.maxTreeDepth !== undefined) {
                this.options.maxTreeDepth = options.maxTreeDepth;
            }
        }
        this.rootNode = new OctreeNode (boundingBox, 0);
    }

    AddPoint (point, data)
    {
        return this.rootNode.AddPoint (point, data, this.options);
    }

    FindPoint (point)
    {
        return this.rootNode.FindPoint (point);
    }
}
