/*---------------------------------------------------------------------------------------------
* 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 { assert, BeTimePoint, expectDefined, ProcessDetector } from "@itwin/core-bentley";
import { Matrix3d, Point3d, Range3d, Transform, Vector3d, } from "@itwin/core-geometry";
import { Cartographic, ColorDef, GeoCoordStatus } from "@itwin/core-common";
import { BackgroundMapGeometry } from "../BackgroundMapGeometry";
import { IModelApp } from "../IModelApp";
import { GraphicBranch } from "../render/GraphicBranch";
import { MapTile, RealityTile, TileTree, } from "./internal";
/** @internal */
export class TraversalDetails {
    queuedChildren = new Array();
    childrenSelected = false;
    shouldSelectParent = false;
    initialize() {
        this.queuedChildren.length = 0;
        this.childrenSelected = false;
        this.shouldSelectParent = false;
    }
}
/** @internal */
export class TraversalChildrenDetails {
    _childDetails = [];
    initialize() {
        for (const child of this._childDetails)
            child.initialize();
    }
    getChildDetail(index) {
        while (this._childDetails.length <= index)
            this._childDetails.push(new TraversalDetails());
        return this._childDetails[index];
    }
    combine(parentDetails) {
        parentDetails.queuedChildren.length = 0;
        parentDetails.childrenSelected = false;
        parentDetails.shouldSelectParent = false;
        for (const child of this._childDetails) {
            parentDetails.childrenSelected = parentDetails.childrenSelected || child.childrenSelected;
            parentDetails.shouldSelectParent = parentDetails.shouldSelectParent || child.shouldSelectParent;
            for (const queuedChild of child.queuedChildren)
                parentDetails.queuedChildren.push(queuedChild);
        }
    }
}
/** @internal */
export class TraversalSelectionContext {
    selected;
    displayedDescendants;
    preloadDebugBuilder;
    _maxSelectionCount;
    preloaded = new Set();
    missing = new Array();
    get selectionCountExceeded() { return this._maxSelectionCount === undefined ? false : (this.missing.length + this.selected.length) > this._maxSelectionCount; } // Avoid selecting excessive number of tiles.
    constructor(selected, displayedDescendants, preloadDebugBuilder, _maxSelectionCount) {
        this.selected = selected;
        this.displayedDescendants = displayedDescendants;
        this.preloadDebugBuilder = preloadDebugBuilder;
        this._maxSelectionCount = _maxSelectionCount;
    }
    selectOrQueue(tile, args, traversalDetails) {
        tile.selectSecondaryTiles(args, this);
        tile.markUsed(args);
        traversalDetails.shouldSelectParent = true;
        if (tile.isReady) {
            args.markReady(tile);
            this.selected.push(tile);
            tile.markDisplayed();
            this.displayedDescendants.push((traversalDetails.childrenSelected) ? traversalDetails.queuedChildren.slice() : []);
            traversalDetails.queuedChildren.length = 0;
            traversalDetails.childrenSelected = true;
            traversalDetails.shouldSelectParent = false;
        }
        else if (!tile.isNotFound) {
            traversalDetails.queuedChildren.push(tile);
            if (!tile.isLoaded)
                this.missing.push(tile);
        }
    }
    preload(tile, args) {
        if (!this.preloaded.has(tile)) {
            if (this.preloadDebugBuilder)
                tile.addBoundingGraphic(this.preloadDebugBuilder, ColorDef.red);
            tile.markUsed(args);
            tile.selectSecondaryTiles(args, this);
            this.preloaded.add(tile);
            if (!tile.isNotFound && !tile.isLoaded)
                this.missing.push(tile);
        }
    }
    select(tiles, args) {
        for (const tile of tiles) {
            tile.markUsed(args);
            this.selected.push(tile);
            this.displayedDescendants.push([]);
        }
    }
}
const scratchCarto = Cartographic.createZero();
const scratchPoint = Point3d.createZero(), scratchOrigin = Point3d.createZero();
const scratchRange = Range3d.createNull();
const scratchX = Vector3d.createZero(), scratchY = Vector3d.createZero(), scratchZ = Vector3d.createZero();
const scratchMatrix = Matrix3d.createZero(), scratchTransform = Transform.createZero();
/** Base class for a [[TileTree]] representing a reality model (e.g., a point cloud or photogrammetry mesh) or 3d terrain with map imagery.
 * The tiles within the tree are instances of [[RealityTile]]s.
 * @public
 */
export class RealityTileTree extends TileTree {
    /** @internal */
    traversalChildrenByDepth = [];
    /** @internal */
    loader;
    /** @internal */
    yAxisUp;
    /** @internal */
    cartesianRange;
    /** @internal */
    cartesianTransitionDistance;
    /** @internal */
    _gcsConverter;
    /** @internal */
    _rootTile;
    /** @internal */
    _rootToEcef;
    /** @internal */
    _ecefToDb;
    /** @internal */
    baseUrl;
    /** If set to true, tile geometry will be reprojected using the tile's reprojection transform when geometry is collected.
     * @internal */
    reprojectGeometry;
    /** @internal */
    constructor(params) {
        super(params);
        this.loader = params.loader;
        this.yAxisUp = true === params.yAxisUp;
        this._rootTile = this.createTile(params.rootTile);
        this.cartesianRange = BackgroundMapGeometry.getCartesianRange(this.iModel);
        this.cartesianTransitionDistance = this.cartesianRange.diagonal().magnitudeXY() * .25; // Transition distance from elliptical to cartesian.
        this._gcsConverter = params.gcsConverterAvailable ? params.iModel.geoServices.getConverter("WGS84") : undefined;
        if (params.rootToEcef) {
            this._rootToEcef = params.rootToEcef;
            const dbToRoot = this.iModelTransform.inverse();
            if (dbToRoot) {
                const dbToEcef = this._rootToEcef.multiplyTransformTransform(dbToRoot);
                this._ecefToDb = dbToEcef.inverse();
            }
        }
        this.baseUrl = params.baseUrl;
        this.reprojectGeometry = params.reprojectGeometry;
    }
    /** The mapping of per-feature JSON properties from this tile tree's batch table, if one is defined.
     * @beta
     */
    get batchTableProperties() {
        return this.loader.getBatchIdMap();
    }
    /** @internal */
    get rootTile() { return this._rootTile; }
    /** @internal */
    get is3d() { return true; }
    /** @internal */
    get maxDepth() { return this.loader.maxDepth; }
    /** @internal */
    get minDepth() { return this.loader.minDepth; }
    /** @internal */
    get isContentUnbounded() { return this.loader.isContentUnbounded; }
    /** @internal */
    get isTransparent() { return false; }
    /** @internal */
    _selectTiles(args) { return this.selectRealityTiles(args, []); }
    /** @internal */
    get viewFlagOverrides() { return this.loader.viewFlagOverrides; }
    /** @internal */
    get parentsAndChildrenExclusive() { return this.loader.parentsAndChildrenExclusive; }
    /** @internal */
    createTile(props) { return new RealityTile(props, this); }
    /** Collect tiles from this tile tree based on the criteria implemented by `collector`.
     * @internal
     */
    collectTileGeometry(collector) {
        this.rootTile.collectTileGeometry(collector);
    }
    /** @internal */
    prune() {
        const olderThan = BeTimePoint.now().minus(this.expirationTime);
        this.rootTile.purgeContents(olderThan, !ProcessDetector.isMobileBrowser);
    }
    /** @internal */
    draw(args) {
        const displayedTileDescendants = new Array();
        const debugControl = args.context.target.debugControl;
        const selectBuilder = (debugControl && debugControl.displayRealityTileRanges) ? args.context.createSceneGraphicBuilder() : undefined;
        const preloadDebugBuilder = (debugControl && debugControl.displayRealityTilePreload) ? args.context.createSceneGraphicBuilder() : undefined;
        const graphicTypeBranches = new Map();
        const selectedTiles = this.selectRealityTiles(args, displayedTileDescendants, preloadDebugBuilder);
        args.processSelectedTiles(selectedTiles);
        let sortIndices;
        if (!this.parentsAndChildrenExclusive) {
            sortIndices = selectedTiles.map((_x, i) => i);
            sortIndices.sort((a, b) => selectedTiles[a].depth - selectedTiles[b].depth);
        }
        if (args.shouldCollectClassifierGraphics)
            this.collectClassifierGraphics(args, selectedTiles);
        assert(selectedTiles.length === displayedTileDescendants.length);
        for (let i = 0; i < selectedTiles.length; i++) {
            const index = sortIndices ? sortIndices[i] : i;
            const selectedTile = selectedTiles[index];
            const graphics = args.getTileGraphics(selectedTile);
            const tileGraphicType = selectedTile.graphicType;
            let targetBranch;
            if (undefined !== tileGraphicType && tileGraphicType !== args.context.graphicType) {
                if (!(targetBranch = graphicTypeBranches.get(tileGraphicType))) {
                    graphicTypeBranches.set(tileGraphicType, targetBranch = new GraphicBranch(false));
                    targetBranch.setViewFlagOverrides(args.graphics.viewFlagOverrides);
                    targetBranch.symbologyOverrides = args.graphics.symbologyOverrides;
                }
            }
            if (!targetBranch)
                targetBranch = args.graphics;
            if (undefined !== graphics) {
                const displayedDescendants = displayedTileDescendants[index];
                if (0 === displayedDescendants.length || !this.loader.parentsAndChildrenExclusive || selectedTile.allChildrenIncluded(displayedDescendants)) {
                    targetBranch.add(graphics);
                    if (selectBuilder)
                        selectedTile.addBoundingGraphic(selectBuilder, ColorDef.green);
                }
                else {
                    if (selectBuilder)
                        selectedTile.addBoundingGraphic(selectBuilder, ColorDef.red);
                    for (const displayedDescendant of displayedDescendants) {
                        const clipVector = displayedDescendant.getContentClip();
                        if (selectBuilder)
                            displayedDescendant.addBoundingGraphic(selectBuilder, ColorDef.blue);
                        if (undefined === clipVector) {
                            targetBranch.add(graphics);
                        }
                        else {
                            clipVector.transformInPlace(args.location);
                            if (!this.isTransparent)
                                for (const primitive of clipVector.clips)
                                    for (const clipPlanes of expectDefined(primitive.fetchClipPlanesRef()).convexSets)
                                        for (const plane of clipPlanes.planes)
                                            plane.offsetDistance(-displayedDescendant.radius * .05); // Overlap with existing (high resolution) tile slightly to avoid cracks.
                            const branch = new GraphicBranch(false);
                            branch.add(graphics);
                            const clipVolume = args.context.target.renderSystem.createClipVolume(clipVector);
                            targetBranch.add(args.context.createGraphicBranch(branch, Transform.createIdentity(), { clipVolume }));
                        }
                    }
                }
                if (preloadDebugBuilder)
                    targetBranch.add(preloadDebugBuilder.finish());
                if (selectBuilder)
                    targetBranch.add(selectBuilder.finish());
                const rangeGraphic = selectedTile.getRangeGraphic(args.context);
                if (undefined !== rangeGraphic)
                    targetBranch.add(rangeGraphic);
            }
        }
        args.drawGraphics();
        for (const graphicTypeBranch of graphicTypeBranches) {
            args.drawGraphicsWithType(graphicTypeBranch[0], graphicTypeBranch[1]);
        }
    }
    /** @internal */
    collectClassifierGraphics(args, selectedTiles) {
        const classifier = args.context.planarClassifiers.get(this.modelId);
        if (classifier)
            classifier.collectGraphics(args.context, { modelId: this.modelId, tiles: selectedTiles, location: args.location, isPointCloud: this.isPointCloud });
    }
    /** @internal */
    getTraversalChildren(depth) {
        while (this.traversalChildrenByDepth.length <= depth)
            this.traversalChildrenByDepth.push(new TraversalChildrenDetails());
        return this.traversalChildrenByDepth[depth];
    }
    /** @internal */
    doReprojectChildren(tile) {
        if (!(tile instanceof RealityTile) || this._gcsConverter === undefined || this._rootToEcef === undefined || undefined === this._ecefToDb)
            return false;
        const tileRange = this.iModelTransform.isIdentity ? tile.range : this.iModelTransform.multiplyRange(tile.range, scratchRange);
        return this.cartesianRange.intersectsRange(tileRange);
    }
    /** @internal */
    reprojectAndResolveChildren(parent, children, resolve) {
        if (!this.doReprojectChildren(parent)) {
            resolve(children);
            return;
        }
        const ecefToDb = expectDefined(this._ecefToDb); // Tested for undefined in doReprojectChildren
        const rootToDb = this.iModelTransform;
        const dbToEcef = expectDefined(ecefToDb.inverse());
        const reprojectChildren = new Array();
        for (const child of children) {
            const realityChild = child;
            const childRange = rootToDb.multiplyRange(realityChild.contentRange, scratchRange);
            const dbCenter = childRange.center;
            const ecefCenter = dbToEcef.multiplyPoint3d(dbCenter);
            const dbPoints = [dbCenter, dbCenter.plusXYZ(1), dbCenter.plusXYZ(0, 1), dbCenter.plusXYZ(0, 0, 1)];
            reprojectChildren.push({ child: realityChild, ecefCenter, dbPoints });
        }
        if (reprojectChildren.length === 0)
            resolve(children);
        else {
            const requestProps = new Array();
            for (const reprojection of reprojectChildren) {
                for (const dbPoint of reprojection.dbPoints) {
                    const ecefPoint = dbToEcef.multiplyPoint3d(dbPoint);
                    const carto = Cartographic.fromEcef(ecefPoint, scratchCarto);
                    if (carto)
                        requestProps.push({ x: carto.longitudeDegrees, y: carto.latitudeDegrees, z: carto.height });
                }
            }
            if (requestProps.length !== 4 * reprojectChildren.length)
                resolve(children);
            else {
                expectDefined(this._gcsConverter).getIModelCoordinatesFromGeoCoordinates(requestProps).then((response) => {
                    const reprojectedCoords = response.iModelCoords;
                    const dbToRoot = expectDefined(rootToDb.inverse());
                    // Interpolate between the original and reprojected points
                    const getReprojectedPoint = (original, reprojectedXYZ) => {
                        scratchPoint.setFromJSON(reprojectedXYZ);
                        const cartesianDistance = this.cartesianRange.distanceToPoint(scratchPoint);
                        if (cartesianDistance < this.cartesianTransitionDistance)
                            return scratchPoint.interpolate(cartesianDistance / this.cartesianTransitionDistance, original, scratchPoint);
                        else
                            return original;
                    };
                    let responseIndex = 0;
                    for (const reprojection of reprojectChildren) {
                        if (reprojectedCoords.every((coord) => coord.s === GeoCoordStatus.Success)) {
                            const reprojectedOrigin = getReprojectedPoint(reprojection.dbPoints[0], reprojectedCoords[responseIndex++].p).clone(scratchOrigin);
                            const xVector = Vector3d.createStartEnd(reprojectedOrigin, getReprojectedPoint(reprojection.dbPoints[1], reprojectedCoords[responseIndex++].p), scratchX);
                            const yVector = Vector3d.createStartEnd(reprojectedOrigin, getReprojectedPoint(reprojection.dbPoints[2], reprojectedCoords[responseIndex++].p), scratchY);
                            const zVector = Vector3d.createStartEnd(reprojectedOrigin, getReprojectedPoint(reprojection.dbPoints[3], reprojectedCoords[responseIndex++].p), scratchZ);
                            const matrix = Matrix3d.createColumns(xVector, yVector, zVector, scratchMatrix);
                            if (matrix !== undefined) {
                                const dbReprojection = Transform.createMatrixPickupPutdown(matrix, reprojection.dbPoints[0], reprojectedOrigin, scratchTransform);
                                if (dbReprojection) {
                                    const rootReprojection = dbToRoot.multiplyTransformTransform(dbReprojection).multiplyTransformTransform(rootToDb);
                                    reprojection.child.reproject(rootReprojection);
                                }
                            }
                        }
                    }
                    resolve(children);
                }).catch(() => {
                    resolve(children); // Error occured in reprojection - just resolve with unprojected corners.
                });
            }
        }
    }
    /** @internal */
    getBaseRealityDepth(_sceneContext) { return -1; }
    /** Scan the list of currently selected reality tiles, and fire the viewport's 'onMapLayerScaleRangeVisibilityChanged ' event
     * if any scale range visibility change is detected for one more map-layer definition.
     * @internal
     */
    reportTileVisibility(_args, _selected) { }
    /** @internal */
    selectRealityTiles(args, displayedDescendants, preloadDebugBuilder) {
        this._lastSelected = BeTimePoint.now();
        const selected = [];
        const context = new TraversalSelectionContext(selected, displayedDescendants, preloadDebugBuilder, args.maxRealityTreeSelectionCount);
        const rootTile = this.rootTile;
        const debugControl = args.context.target.debugControl;
        const freezeTiles = debugControl && debugControl.freezeRealityTiles;
        rootTile.selectRealityTiles(context, args, new TraversalDetails());
        const baseDepth = this.getBaseRealityDepth(args.context);
        if (IModelApp.tileAdmin.isPreloadingAllowed && 0 === context.missing.length) {
            if (baseDepth > 0) // Maps may force loading of low level globe tiles.
                rootTile.preloadRealityTilesAtDepth(baseDepth, context, args);
            if (!freezeTiles)
                rootTile.preloadProtectedTiles(args, context);
        }
        if (!freezeTiles)
            for (const tile of context.missing) {
                const loadableTile = tile.loadableTile;
                loadableTile.markUsed(args);
                args.insertMissing(loadableTile);
            }
        if (debugControl && debugControl.logRealityTiles) {
            this.logTiles("Selected: ", selected.values());
            const preloaded = [];
            for (const tile of context.preloaded)
                preloaded.push(tile);
            this.logTiles("Preloaded: ", preloaded.values());
            this.logTiles("Missing: ", context.missing.values());
            const imageryTiles = [];
            for (const selectedTile of selected) {
                if (selectedTile instanceof MapTile) {
                    const selectedImageryTiles = (selectedTile).imageryTiles;
                    if (selectedImageryTiles)
                        selectedImageryTiles.forEach((tile) => imageryTiles.push(tile));
                }
            }
            if (imageryTiles.length)
                this.logTiles("Imagery:", imageryTiles.values());
        }
        this.reportTileVisibility(args, selected);
        IModelApp.tileAdmin.addTilesForUser(args.context.viewport, selected, args.readyTiles, args.touchedTiles);
        return selected;
    }
    /** @internal */
    logTiles(label, tiles) {
        let depthString = "";
        let min = 10000, max = -10000;
        let count = 0;
        const depthMap = new Map();
        for (const tile of tiles) {
            count++;
            const depth = tile.depth;
            min = Math.min(min, tile.depth);
            max = Math.max(max, tile.depth);
            const found = depthMap.get(depth);
            depthMap.set(depth, found === undefined ? 1 : found + 1);
        }
        depthMap.forEach((value, key) => depthString += `${key}(x${value}), `);
        // eslint-disable-next-line no-console
        console.log(`${label}: ${count} Min: ${min} Max: ${max} Depths: ${depthString}`);
    }
}
//# sourceMappingURL=RealityTileTree.js.map