"use strict";
/*---------------------------------------------------------------------------------------------
* 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
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.IModelTile = exports.SelectParent = void 0;
exports.iModelTileParamsFromJSON = iModelTileParamsFromJSON;
const core_bentley_1 = require("@itwin/core-bentley");
const core_geometry_1 = require("@itwin/core-geometry");
const core_common_1 = require("@itwin/core-common");
const IModelApp_1 = require("../../IModelApp");
const internal_1 = require("./../../tile/internal");
function iModelTileParamsFromJSON(props, parent) {
    const { contentId, maximumSize, isLeaf, sizeMultiplier } = props;
    const range = core_geometry_1.Range3d.fromJSON(props.range);
    let contentRange;
    if (undefined !== props.contentRange)
        contentRange = core_geometry_1.Range3d.fromJSON(props.contentRange);
    return { contentId, range, maximumSize, isLeaf, parent, contentRange, sizeMultiplier };
}
/**
 * Indicates whether a parent tile should be drawn in place of a child tile.
 */
var SelectParent;
(function (SelectParent) {
    SelectParent[SelectParent["No"] = 0] = "No";
    SelectParent[SelectParent["Yes"] = 1] = "Yes";
})(SelectParent || (exports.SelectParent = SelectParent = {}));
/** A tile belonging to an [[IModelTileTree].
 */
class IModelTile extends internal_1.Tile {
    _sizeMultiplier;
    _emptySubRangeMask;
    /** If an initial attempt to obtain this tile's content (e.g., from cloud storage cache) failed,
     * the next channel to try.
     */
    requestChannel;
    constructor(params, tree) {
        super(params, tree);
        this._sizeMultiplier = params.sizeMultiplier;
        if (!this.isLeaf && this.tree.is3d && !this.isReady) { // ###TODO: Want to know specifically if tree is *spatial*.
            // Do not sub-divide such that chord tolerance would be below specified minimum, if minimum defined.
            const minTolerance = IModelApp_1.IModelApp.tileAdmin.minimumSpatialTolerance;
            if (minTolerance > 0 && (0, core_common_1.computeTileChordTolerance)(this, this.tree.is3d, this.iModelTree.tileScreenSize) <= minTolerance)
                this.setLeaf();
        }
    }
    get iModelTree() { return this.tree; }
    get iModelChildren() { return this.children; }
    get emptySubRangeMask() { return this._emptySubRangeMask ?? 0; }
    get sizeMultiplier() { return this._sizeMultiplier; }
    get hasSizeMultiplier() { return undefined !== this.sizeMultiplier; }
    get maximumSize() {
        return super.maximumSize * (this.sizeMultiplier ?? 1.0);
    }
    get channel() {
        return IModelApp_1.IModelApp.tileAdmin.channels.getIModelTileChannel(this);
    }
    async requestContent() {
        return IModelApp_1.IModelApp.tileAdmin.generateTileContent(this);
    }
    async readContent(data, system, isCanceled) {
        if (undefined === isCanceled)
            isCanceled = () => !this.isLoading;
        (0, core_bentley_1.assert)(data instanceof Uint8Array);
        const streamBuffer = core_bentley_1.ByteStream.fromUint8Array(data);
        const position = streamBuffer.curPos;
        const format = streamBuffer.readUint32();
        streamBuffer.curPos = position;
        let content = { isLeaf: true };
        (0, core_bentley_1.assert)(core_common_1.TileFormat.IModel === format);
        if (format !== core_common_1.TileFormat.IModel)
            return content;
        const sizeMultiplier = this.hasSizeMultiplier ? this.sizeMultiplier : undefined;
        const ecefTransform = this.tree.iModel.isGeoLocated ? this.tree.iModel.getEcefTransform() : core_geometry_1.Transform.createIdentity();
        try {
            content = await this.iModelTree.decoder.decode({
                stream: streamBuffer,
                options: { tileId: this.contentId },
                system,
                isCanceled,
                sizeMultiplier,
                tileData: {
                    ecefTransform,
                    range: this.range,
                    layerClassifiers: this.tree.layerHandler?.layerClassifiers,
                },
            });
        }
        catch {
            //
        }
        return content;
    }
    setContent(content) {
        super.setContent(content);
        this._emptySubRangeMask = content.emptySubRangeMask;
        // NB: If this tile has no graphics, it may or may not have children - but we don't want to load the children until
        // this tile is too coarse for view based on its size in pixels.
        // That is different than an "undisplayable" tile (maximumSize=0) whose children should be loaded immediately.
        if (undefined !== content.graphic && 0 === this.maximumSize)
            this._maximumSize = this.iModelTree.tileScreenSize;
        const sizeMult = content.sizeMultiplier;
        if (undefined !== sizeMult && (undefined === this._sizeMultiplier || sizeMult > this._sizeMultiplier)) {
            this._sizeMultiplier = sizeMult;
            this._contentId = this.iModelTree.contentIdProvider.idFromParentAndMultiplier(this.contentId, sizeMult);
            if (undefined !== this.children && this.children.length > 1)
                this.disposeChildren();
        }
    }
    _loadChildren(resolve, reject) {
        try {
            const tree = this.iModelTree;
            const kids = (0, core_common_1.computeChildTileProps)(this, tree.contentIdProvider, tree);
            IModelApp_1.IModelApp.tileAdmin.onTilesElided(kids.numEmpty);
            const children = [];
            for (const props of kids.children) {
                const child = new IModelTile(iModelTileParamsFromJSON(props, this), tree);
                children.push(child);
            }
            resolve(children);
        }
        catch (err) {
            reject(err instanceof Error ? err : new Error(core_bentley_1.BentleyError.getErrorMessage(err)));
        }
    }
    get rangeGraphicColor() {
        return this.hasSizeMultiplier ? core_common_1.ColorDef.red : super.rangeGraphicColor;
    }
    addRangeGraphic(builder, type) {
        if (internal_1.TileBoundingBoxes.ChildVolumes !== type) {
            super.addRangeGraphic(builder, type);
            return;
        }
        const ranges = (0, core_common_1.computeChildTileRanges)(this, this.iModelTree);
        for (const range of ranges) {
            const color = range.isEmpty ? core_common_1.ColorDef.blue : core_common_1.ColorDef.green;
            const pixels = !range.isEmpty ? core_common_1.LinePixels.HiddenLine : core_common_1.LinePixels.Solid;
            const width = !range.isEmpty ? 2 : 1;
            builder.setSymbology(color, color, width, pixels);
            (0, internal_1.addRangeGraphic)(builder, range.range, this.tree.is2d);
        }
    }
    pruneChildren(olderThan) {
        // A tile's usage marker indicates its the most recent time its *children* were used.
        if (this.usageMarker.isExpired(olderThan)) {
            this.disposeChildren();
            return;
        }
        // this node has been used recently. Keep it, but potentially unload its grandchildren.
        const children = this.iModelChildren;
        if (undefined !== children)
            for (const child of children)
                child.pruneChildren(olderThan);
    }
    selectTiles(selected, args, numSkipped) {
        let vis = this.computeVisibility(args);
        if (internal_1.TileVisibility.OutsideFrustum === vis)
            return SelectParent.No;
        const maxDepth = this.iModelTree.debugMaxDepth;
        if (undefined !== maxDepth && this.depth >= maxDepth)
            vis = internal_1.TileVisibility.Visible;
        if (internal_1.TileVisibility.Visible === vis) {
            // This tile is of appropriate resolution to draw. If need loading or refinement, enqueue.
            if (!this.isReady)
                args.insertMissing(this);
            if (this.hasGraphics) {
                // It can be drawn - select it
                args.markReady(this);
                selected.push(this);
            }
            else if (!this.isReady) {
                // It can't be drawn. Try to draw children in its place; otherwise draw the parent.
                // Do not load/request the children for this purpose.
                const initialSize = selected.length;
                const kids = this.iModelChildren;
                if (undefined === kids)
                    return SelectParent.Yes;
                // Find any descendant to draw, until we exceed max initial tiles to skip.
                if (this.depth < this.iModelTree.maxInitialTilesToSkip) {
                    for (const kid of kids) {
                        if (SelectParent.Yes === kid.selectTiles(selected, args, numSkipped)) {
                            selected.length = initialSize;
                            return SelectParent.Yes;
                        }
                        return SelectParent.No;
                    }
                }
                // If all visible direct children can be drawn, draw them.
                for (const kid of kids) {
                    if (internal_1.TileVisibility.OutsideFrustum !== kid.computeVisibility(args)) {
                        if (!kid.hasGraphics) {
                            selected.length = initialSize;
                            return SelectParent.Yes;
                        }
                        else {
                            selected.push(kid);
                        }
                    }
                }
                args.markUsed(this);
            }
            // We're drawing either this tile, or its direct children.
            return SelectParent.No;
        }
        // This tile is too coarse to draw. Try to draw something more appropriate.
        // If it is not ready to draw, we may want to skip loading in favor of loading its descendants.
        // If we previously loaded and later unloaded content for this tile to free memory, don't force it to reload its content - proceed to children.
        let canSkipThisTile = (this._hadGraphics && !this.hasGraphics) || this.depth < this.iModelTree.maxInitialTilesToSkip;
        if (canSkipThisTile) {
            numSkipped = 1;
        }
        else {
            canSkipThisTile = this.isReady || this.isParentDisplayable || this.depth < this.iModelTree.maxInitialTilesToSkip;
            if (canSkipThisTile && this.isDisplayable) { // skipping an undisplayable tile doesn't count toward the maximum
                // Some tiles do not sub-divide - they only facet the same geometry to a higher resolution. We can skip directly to the correct resolution.
                const isNotReady = !this.isReady && !this.hasGraphics && !this.hasSizeMultiplier;
                if (isNotReady) {
                    if (numSkipped >= this.iModelTree.maxTilesToSkip)
                        canSkipThisTile = false;
                    else
                        numSkipped += 1;
                }
            }
        }
        const childrenLoadStatus = this.loadChildren(); // NB: asynchronous
        const children = canSkipThisTile ? this.iModelChildren : undefined;
        if (canSkipThisTile && internal_1.TileTreeLoadStatus.Loading === childrenLoadStatus) {
            args.markChildrenLoading();
            args.markUsed(this);
        }
        if (undefined !== children) {
            // If we are the root tile and we are not displayable, then we want to draw *any* currently available children in our place, or else we would draw nothing.
            // Otherwise, if we want to draw children in our place, we should wait for *all* of them to load, or else we would show missing chunks where not-yet-loaded children belong.
            const isUndisplayableRootTile = this.isUndisplayableRootTile;
            args.markUsed(this);
            let drawChildren = true;
            const initialSize = selected.length;
            for (const child of children) {
                // NB: We must continue iterating children so that they can be requested if missing.
                if (SelectParent.Yes === child.selectTiles(selected, args, numSkipped)) {
                    if (child.loadStatus === internal_1.TileLoadStatus.NotFound) {
                        // At least one child we want to draw failed to load. e.g., we reached max depth of map tile tree. Draw parent instead.
                        drawChildren = canSkipThisTile = false;
                    }
                    else {
                        // At least one child we want to draw is not yet loaded. Wait for it to load before drawing it and its siblings, unless we have nothing to draw in their place.
                        drawChildren = isUndisplayableRootTile;
                    }
                }
            }
            if (drawChildren)
                return SelectParent.No;
            // Some types of tiles (like maps) allow the ready children to be drawn on top of the parent while other children are not yet loaded.
            if (args.parentsAndChildrenExclusive)
                selected.length = initialSize;
        }
        if (this.isReady) {
            if (this.hasGraphics) {
                selected.push(this);
                if (!canSkipThisTile) {
                    // This tile is too coarse, but we require loading it before we can start loading higher-res children.
                    args.markReady(this);
                }
            }
            return SelectParent.No;
        }
        // This tile is not ready to be drawn. Request it *only* if we cannot skip it.
        if (!canSkipThisTile)
            args.insertMissing(this);
        return this.isParentDisplayable ? SelectParent.Yes : SelectParent.No;
    }
    clearLayers() {
        super.clearLayers();
        this.disposeChildren();
    }
}
exports.IModelTile = IModelTile;
//# sourceMappingURL=IModelTile.js.map