/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Tiles
 */
import { Cartographic } from "@itwin/core-common";
import { MapCartoRectangle } from "../../../tile/internal";
import { expectDefined, SortedArray } from "@itwin/core-bentley";
// portions adapted from Cesium.js Copyright 2011 - 2017 Cesium Contributors
class RectangleWithLevel extends MapCartoRectangle {
    level;
    constructor(level, west, south, east, north) {
        super(west, south, east, north);
        this.level = level;
    }
}
class QuadTreeNode {
    tilingScheme;
    parent;
    level;
    x;
    y;
    swNode;
    seNode;
    nwNode;
    neNode;
    extent;
    rectangles = new SortedArray((lhs, rhs) => lhs.level - rhs.level, true);
    constructor(tilingScheme, parent, level, x, y) {
        this.tilingScheme = tilingScheme;
        this.parent = parent;
        this.level = level;
        this.x = x;
        this.y = y;
        this.extent = tilingScheme.tileXYToRectangle(x, y, level + 1);
    }
    get nw() {
        if (!this.nwNode)
            this.nwNode = new QuadTreeNode(this.tilingScheme, this, this.level + 1, this.x * 2, this.y * 2);
        return this.nwNode;
    }
    get ne() {
        if (!this.neNode)
            this.neNode = new QuadTreeNode(this.tilingScheme, this, this.level + 1, this.x * 2 + 1, this.y * 2);
        return this.neNode;
    }
    get sw() {
        if (!this.swNode)
            this.swNode = new QuadTreeNode(this.tilingScheme, this, this.level + 1, this.x * 2, this.y * 2 + 1);
        return this.swNode;
    }
    get se() {
        if (!this.seNode)
            this.seNode = new QuadTreeNode(this.tilingScheme, this, this.level + 1, this.x * 2 + 1, this.y * 2 + 1);
        return this.seNode;
    }
}
function putRectangleInQuadtree(maxDepth, node, rectangle) {
    while (node.level < maxDepth) {
        if (node.nw.extent.containsRange(rectangle)) {
            node = node.nw;
        }
        else if (node.ne.extent.containsRange(rectangle)) {
            node = node.ne;
        }
        else if (node.sw.extent.containsRange(rectangle)) {
            node = node.sw;
        }
        else if (node.se.extent.containsRange(rectangle)) {
            node = node.se;
        }
        else {
            break;
        }
    }
    node.rectangles.insert(rectangle);
}
export class TileAvailability {
    _tilingScheme;
    _maximumLevel;
    _rootNodes = new Array();
    constructor(_tilingScheme, _maximumLevel) {
        this._tilingScheme = _tilingScheme;
        this._maximumLevel = _maximumLevel;
    }
    static rectangleScratch = MapCartoRectangle.createMaximum();
    findNode(level, x, y, nodes) {
        for (const node of nodes) {
            if (node.x === x && node.y === y && node.level === level) {
                return true;
            }
        }
        return false;
    }
    /**
     * Marks a rectangular range of tiles in a particular level as being available.  For best performance,
     * add your ranges in order of increasing level.
     *
     * @param {Number} level The level.
     * @param {Number} startX The X coordinate of the first available tiles at the level.
     * @param {Number} startY The Y coordinate of the first available tiles at the level.
     * @param {Number} endX The X coordinate of the last available tiles at the level.
     * @param {Number} endY The Y coordinate of the last available tiles at the level.
     */
    addAvailableTileRange(level, startX, startY, endX, endY) {
        const tilingScheme = this._tilingScheme;
        const rootNodes = this._rootNodes;
        if (level === 0) {
            for (let y = startY; y <= endY; ++y) {
                for (let x = startX; x <= endX; ++x) {
                    if (!this.findNode(level, x, y, rootNodes)) {
                        rootNodes.push(new QuadTreeNode(tilingScheme, undefined, 0, x, y));
                    }
                }
            }
        }
        tilingScheme.tileXYToRectangle(startX, startY, level + 1, TileAvailability.rectangleScratch);
        const west = TileAvailability.rectangleScratch.west;
        const south = TileAvailability.rectangleScratch.south;
        tilingScheme.tileXYToRectangle(endX, endY, level + 1, TileAvailability.rectangleScratch);
        const east = TileAvailability.rectangleScratch.east;
        const north = TileAvailability.rectangleScratch.north;
        const rectangleWithLevel = new RectangleWithLevel(level, west, south, east, north);
        for (const rootNode of rootNodes) {
            if (rootNode.extent.intersectsRange(rectangleWithLevel)) {
                putRectangleInQuadtree(this._maximumLevel, rootNode, rectangleWithLevel);
            }
        }
    }
    computeMaximumLevelAtPosition(position) {
        // Find the root node that contains this position.
        let node;
        for (const rootNode of this._rootNodes) {
            if (rootNode.extent.containsCartographic(position)) {
                node = rootNode;
                break;
            }
        }
        if (undefined === node) {
            return -1;
        }
        return this.findMaxLevelFromNode(undefined, node, position);
    }
    _cartographicScratch = Cartographic.createZero();
    /**
     * Determines if a particular tile is available.
     * @param {Number} level The tile level to check.
     * @param {Number} x The X coordinate of the tile to check.
     * @param {Number} y The Y coordinate of the tile to check.
     * @return {Boolean} True if the tile is available; otherwise, false.
     */
    isTileAvailable(level, x, y) {
        // Get the center of the tile and find the maximum level at that position.
        // Because availability is by tile, if the level is available at that point, it
        // is sure to be available for the whole tile.  We assume that if a tile at level n exists,
        // then all its parent tiles back to level 0 exist too.  This isn't really enforced
        // anywhere, but Cesium would never load a tile for which this is not true.
        const rectangle = this._tilingScheme.tileXYToRectangle(x, y, level + 1, TileAvailability.rectangleScratch);
        rectangle.getCenter(this._cartographicScratch);
        return this.computeMaximumLevelAtPosition(this._cartographicScratch) >= level;
    }
    findMaxLevelFromNode(stopNode, node, position) {
        let maxLevel = 0;
        // Find the deepest quadtree node containing this point.
        let found = false;
        while (!found && node !== undefined) {
            const nw = node.nwNode && node.nwNode.extent.containsCartographic(position);
            const ne = node.neNode && node.neNode.extent.containsCartographic(position);
            const sw = node.swNode && node.swNode.extent.containsCartographic(position);
            const se = node.seNode && node.seNode.extent.containsCartographic(position);
            // The common scenario is that the point is in only one quadrant and we can simply
            // iterate down the tree.  But if the point is on a boundary between tiles, it is
            // in multiple tiles and we need to check all of them, so use recursion.
            if ((nw ? 1 : 0) + (ne ? 1 : 0) + (sw ? 1 : 0) + (se ? 1 : 0) > 1) {
                if (nw) {
                    maxLevel = Math.max(maxLevel, this.findMaxLevelFromNode(node, node.nwNode, position));
                }
                if (ne) {
                    maxLevel = Math.max(maxLevel, this.findMaxLevelFromNode(node, node.neNode, position));
                }
                if (sw) {
                    maxLevel = Math.max(maxLevel, this.findMaxLevelFromNode(node, node.swNode, position));
                }
                if (se) {
                    maxLevel = Math.max(maxLevel, this.findMaxLevelFromNode(node, node.seNode, position));
                }
                break;
            }
            else if (nw) {
                node = node.nwNode;
            }
            else if (ne) {
                node = node.neNode;
            }
            else if (sw) {
                node = node.swNode;
            }
            else if (se) {
                node = node.seNode;
            }
            else {
                found = true;
            }
        }
        // Work up the tree until we find a rectangle that contains this point.
        while (node !== stopNode) {
            const rectangles = expectDefined(node).rectangles;
            // Rectangles are sorted by level, lowest first.
            // We're looping from length - 1 to 0, so get will always return a defined value.
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            for (let i = rectangles.length - 1; i >= 0 && rectangles.get(i).level > maxLevel; --i) {
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                const rectangle = rectangles.get(i);
                if (rectangle.containsCartographic(position))
                    maxLevel = rectangle.level;
            }
            node = node?.parent;
        }
        return maxLevel;
    }
}
//# sourceMappingURL=MapTileAvailability.js.map