"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TileAdmin = void 0;
exports.overrideRequestTileTreeProps = overrideRequestTileTreeProps;
/*---------------------------------------------------------------------------------------------
* 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
 */
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const IModelApp_1 = require("../IModelApp");
const IpcApp_1 = require("../IpcApp");
const IModelConnection_1 = require("../IModelConnection");
const Viewport_1 = require("../Viewport");
const internal_1 = require("./internal");
/** Manages [[Tile]]s and [[TileTree]]s on behalf of [[IModelApp]]. Its responsibilities include scheduling requests for tile content via a priority queue;
 * keeping track of and imposing limits upon the amount of GPU memory consumed by tiles; and notifying listeners of tile-related events.
 * @see [[IModelApp.tileAdmin]] to access the instance of the TileAdmin.
 * @see [[TileAdmin.Props]] to configure the TileAdmin at startup.
 * @public
 * @extensions
 */
class TileAdmin {
    /** Create a TileAdmin suitable for passing to [[IModelApp.startup]] via [[IModelAppOptions.tileAdmin]] to customize aspects of
     * its behavior.
     * @param props Options for customizing the behavior of the TileAdmin.
     * @returns the TileAdmin
     */
    static async create(props) {
        const rpcConcurrency = IpcApp_1.IpcApp.isValid ? (await IpcApp_1.IpcApp.appFunctionIpc.queryConcurrency("cpu")) : undefined;
        const isMobile = core_bentley_1.ProcessDetector.isMobileBrowser;
        return new TileAdmin(isMobile, rpcConcurrency, props);
    }
    /** @internal */
    get emptyTileUserSet() { return internal_1.UniqueTileUserSets.emptySet; }
    /** Returns basic statistics about the TileAdmin's current state. */
    get statistics() {
        let numActiveTileTreePropsRequests = 0;
        for (const req of this._tileTreePropsRequests) {
            if (!req.isDispatched)
                break;
            ++numActiveTileTreePropsRequests;
        }
        return {
            ...this.channels.statistics,
            totalElidedTiles: this._totalElided,
            numActiveTileTreePropsRequests,
            numPendingTileTreePropsRequests: this._tileTreePropsRequests.length - numActiveTileTreePropsRequests,
        };
    }
    /** Resets the cumulative (per-session) statistics like totalCompletedRequests, totalEmptyTiles, etc. */
    resetStatistics() {
        this.channels.resetStatistics();
        this._totalElided = 0;
    }
    /** Exposed as public strictly for tests.
     * @internal
     */
    constructor(isMobile, rpcConcurrency, options) {
        this._users = new Set();
        this._requestsPerUser = new Map();
        this._tileUsagePerUser = new Map();
        this._selectedAndReady = new Map();
        this._tileUserSetsForRequests = new internal_1.UniqueTileUserSets();
        this._totalElided = 0;
        this._rpcInitialized = false;
        this._tileTreePropsRequests = [];
        this._lruList = new internal_1.LRUTileList();
        this._gpuMemoryLimit = "none";
        /** Event raised when a request to load a tile's content completes. */
        this.onTileLoad = new core_bentley_1.BeEvent();
        /** Event raised when a request to load a tile tree completes. */
        this.onTileTreeLoad = new core_bentley_1.BeEvent();
        /** Event raised when a request to load a tile's child tiles completes. */
        this.onTileChildrenLoad = new core_bentley_1.BeEvent();
        this._isMobile = isMobile;
        if (undefined === options)
            options = {};
        this.channels = new internal_1.TileRequestChannels(rpcConcurrency, true === options.cacheTileMetadata);
        this._maxActiveTileTreePropsRequests = options.maxActiveTileTreePropsRequests ?? 10;
        this._defaultTileSizeModifier = (undefined !== options.defaultTileSizeModifier && options.defaultTileSizeModifier > 0) ? options.defaultTileSizeModifier : 1.0;
        this._retryInterval = undefined !== options.retryInterval ? options.retryInterval : 1000;
        this._enableInstancing = options.enableInstancing ?? core_common_1.defaultTileOptions.enableInstancing;
        this.edgeOptions = {
            type: false === options.enableIndexedEdges ? "non-indexed" : "compact",
            smooth: options.generateAllPolyfaceEdges ?? true,
        };
        this.enableImprovedElision = options.enableImprovedElision ?? core_common_1.defaultTileOptions.enableImprovedElision;
        this.enableFrontendScheduleScripts = options.enableFrontendScheduleScripts ?? false;
        this.decodeImdlInWorker = options.decodeImdlInWorker ?? true;
        this.ignoreAreaPatterns = options.ignoreAreaPatterns ?? core_common_1.defaultTileOptions.ignoreAreaPatterns;
        this.enableExternalTextures = options.enableExternalTextures ?? core_common_1.defaultTileOptions.enableExternalTextures;
        this.disableMagnification = options.disableMagnification ?? core_common_1.defaultTileOptions.disableMagnification;
        this.percentGPUMemDisablePreload = Math.max(0, Math.min((options.percentGPUMemDisablePreload === undefined ? 80 : options.percentGPUMemDisablePreload), 80));
        this.alwaysRequestEdges = true === options.alwaysRequestEdges;
        this.alwaysSubdivideIncompleteTiles = options.alwaysSubdivideIncompleteTiles ?? core_common_1.defaultTileOptions.alwaysSubdivideIncompleteTiles;
        this.maximumMajorTileFormatVersion = options.maximumMajorTileFormatVersion ?? core_common_1.defaultTileOptions.maximumMajorTileFormatVersion;
        this.useProjectExtents = options.useProjectExtents ?? core_common_1.defaultTileOptions.useProjectExtents;
        this.expandProjectExtents = options.expandProjectExtents ?? core_common_1.defaultTileOptions.expandProjectExtents;
        this.optimizeBRepProcessing = options.optimizeBRepProcessing ?? core_common_1.defaultTileOptions.optimizeBRepProcessing;
        this.useLargerTiles = options.useLargerTiles ?? core_common_1.defaultTileOptions.useLargerTiles;
        this.mobileRealityTileMinToleranceRatio = Math.max(options.mobileRealityTileMinToleranceRatio ?? 3.0, 1.0);
        this.cesiumIonKey = options.cesiumIonKey;
        this._cloudStorage = options.tileStorage;
        const gpuMemoryLimits = options.gpuMemoryLimits;
        let gpuMemoryLimit;
        if (typeof gpuMemoryLimits === "object")
            gpuMemoryLimit = isMobile ? gpuMemoryLimits.mobile : gpuMemoryLimits.nonMobile;
        else
            gpuMemoryLimit = gpuMemoryLimits;
        if (undefined === gpuMemoryLimit)
            gpuMemoryLimit = isMobile ? "default" : TileAdmin.nonMobileUndefinedGpuMemoryLimit;
        this.gpuMemoryLimit = gpuMemoryLimit;
        if (undefined !== options.maximumLevelsToSkip)
            this.maximumLevelsToSkip = Math.floor(Math.max(0, options.maximumLevelsToSkip));
        else
            this.maximumLevelsToSkip = 1;
        const minSpatialTol = options.minimumSpatialTolerance;
        this.minimumSpatialTolerance = undefined !== minSpatialTol ? Math.max(minSpatialTol, 0) : 0.001;
        const clamp = (seconds, min, max) => {
            seconds = Math.min(seconds, max);
            seconds = Math.max(seconds, min);
            return core_bentley_1.BeDuration.fromSeconds(seconds);
        };
        const ignoreMinimums = true === options.ignoreMinimumExpirationTimes;
        const minTileTime = ignoreMinimums ? 0.1 : 5;
        const minTreeTime = ignoreMinimums ? 0.1 : 10;
        // If unspecified, tile expiration time defaults to 20 seconds.
        this.tileExpirationTime = clamp((options.tileExpirationTime ?? 20), minTileTime, 60);
        // If unspecified, trees never expire (will change this to use a default later).
        this.tileTreeExpirationTime = clamp(options.tileTreeExpirationTime ?? 300, minTreeTime, 3600);
        const now = core_bentley_1.BeTimePoint.now();
        this._nextPruneTime = now.plus(this.tileExpirationTime);
        this._nextPurgeTime = now.plus(this.tileTreeExpirationTime);
        this._removeIModelConnectionOnCloseListener = IModelConnection_1.IModelConnection.onClose.addListener((iModel) => this.onIModelClosed(iModel));
        // If unspecified preload 2 levels of parents for context tiles.
        this.contextPreloadParentDepth = Math.max(0, Math.min((options.contextPreloadParentDepth === undefined ? 2 : options.contextPreloadParentDepth), 8));
        // If unspecified skip one level before preloading  of parents of context tiles.
        this.contextPreloadParentSkip = Math.max(0, Math.min((options.contextPreloadParentSkip === undefined ? 1 : options.contextPreloadParentSkip), 5));
        const removals = [
            this.onTileLoad.addListener(() => this.invalidateAllScenes()),
            this.onTileChildrenLoad.addListener(() => this.invalidateAllScenes()),
            this.onTileTreeLoad.addListener(() => {
                // A reality model tile tree's range may extend outside of the project extents - we'll want to recompute the extents
                // of any spatial view's that may be displaying the reality model.
                for (const user of this.tileUsers)
                    if (user instanceof Viewport_1.Viewport && user.view.isSpatialView())
                        user.invalidateController();
            }),
        ];
        this._cleanup = () => {
            removals.forEach((removal) => removal());
        };
    }
    async getTileStorage() {
        if (this._tileStorage !== undefined)
            return this._tileStorage;
        // if custom implementation is provided, construct a new TileStorage instance and return it.
        if (this._cloudStorage !== undefined) {
            this._tileStorage = new internal_1.TileStorage(this._cloudStorage);
            return this._tileStorage;
        }
        const fetchStorage = new internal_1.FetchCloudStorage();
        this._tileStorage = new internal_1.TileStorage(fetchStorage);
        return this._tileStorage;
    }
    /** @internal */
    get enableInstancing() { return this._enableInstancing; }
    /** Given a numeric combined major+minor tile format version (typically obtained from a request to the backend to query the maximum tile format version it supports),
     * return the maximum *major* format version to be used to request tile content from the backend.
     * @see [[TileAdmin.Props.maximumMajorTileFormatVersion]]
     * @see [[CurrentImdlVersion]]
     */
    getMaximumMajorTileFormatVersion(formatVersion) {
        return (0, core_common_1.getMaximumMajorTileFormatVersion)(this.maximumMajorTileFormatVersion, formatVersion);
    }
    /** A default multiplier applied to the size in pixels of a [[Tile]] during tile selection for any [[Viewport]].
     * Individual Viewports can override this multiplier if desired.
     * A value greater than 1.0 causes lower-resolution tiles to be selected; a value < 1.0 selects higher-resolution tiles.
     * This can allow an application to sacrifice quality for performance or vice-versa.
     * This property is initialized from the value supplied by the [[TileAdmin.Props.defaultTileSizeModifier]] used to initialize the TileAdmin at startup.
     * Changing it after startup will change it for all Viewports that do not explicitly override it with their own multiplier.
     * This value must be greater than zero.
     */
    get defaultTileSizeModifier() { return this._defaultTileSizeModifier; }
    set defaultTileSizeModifier(modifier) {
        if (modifier !== this._defaultTileSizeModifier && modifier > 0 && !Number.isNaN(modifier)) {
            this._defaultTileSizeModifier = modifier;
            IModelApp_1.IModelApp.viewManager.invalidateScenes();
        }
    }
    /** The total number of bytes of GPU memory allocated to [[Tile]] contents.
     * @see [[gpuMemoryLimit]] to impose limits on how high this can grow.
     */
    get totalTileContentBytes() {
        return this._lruList.totalBytesUsed;
    }
    /** The maximum number of bytes of GPU memory that can be allocated to the contents of [[Tile]]s. When this limit is exceeded, the contents of the least-recently-drawn
     * tiles are discarded until the total is below this limit or all undisplayed tiles' contents have been discarded.
     * @see [[totalTileContentBytes]] for the current GPU memory usage.
     * @see [[gpuMemoryLimit]] to adjust this maximum.
     */
    get maxTotalTileContentBytes() {
        return this._maxTotalTileContentBytes;
    }
    /** The strategy for limiting the amount of GPU memory allocated to [[Tile]] graphics.
     * @see [[TileAdmin.Props.gpuMemoryLimits]] to configure this at startup.
     * @see [[maxTotalTileContentBytes]] for the limit as a maximum number of bytes.
     */
    get gpuMemoryLimit() {
        return this._gpuMemoryLimit;
    }
    set gpuMemoryLimit(limit) {
        if (limit === this.gpuMemoryLimit)
            return;
        let maxBytes;
        if (typeof limit === "number") {
            limit = Math.max(0, limit);
            maxBytes = limit;
        }
        else {
            switch (limit) {
                case "default":
                case "aggressive":
                case "relaxed":
                    const spec = this._isMobile ? TileAdmin.mobileGpuMemoryLimits : TileAdmin.nonMobileGpuMemoryLimits;
                    maxBytes = spec[limit];
                    break;
                default:
                    limit = "none";
                // eslint-disable-next-line no-fallthrough
                case "none":
                    maxBytes = undefined;
                    break;
            }
        }
        this._gpuMemoryLimit = limit;
        this._maxTotalTileContentBytes = maxBytes;
    }
    /** Returns whether or not preloading for context (reality and map tiles) is currently allowed.
     * It is not allowed on mobile devices or if [[TileAdmin.Props.percentGPUMemDisablePreload]] is 0.
     * Otherwise it is always allowed if [[GpuMemoryLimit]] is "none".
     * Otherwise it is only allowed if current GPU memory utilization is less than [[TileAdmin.Props.percentGPUMemDisablePreload]] of GpuMemoryLimit.
     * @internal
     */
    get isPreloadingAllowed() {
        return !this._isMobile && this.percentGPUMemDisablePreload > 0 && (this._maxTotalTileContentBytes === undefined || this._lruList.totalBytesUsed / this._maxTotalTileContentBytes * 100 < this.percentGPUMemDisablePreload);
    }
    /** Invoked from the [[ToolAdmin]] event loop to process any pending or active requests for tiles.
     * @internal
     */
    process() {
        this.processQueue();
        // Prune expired tiles and purge expired tile trees. This may free up some memory.
        this.pruneAndPurge();
        // Free up any additional memory as required to keep within our limit.
        this.freeMemory();
    }
    /** Iterate over the tiles that have content loaded but are not in use by any [[TileUser]].
     * @alpha
     */
    get unselectedLoadedTiles() {
        return this._lruList.unselectedTiles;
    }
    /** Iterate over the tiles that have content loaded and are in use by any [[TileUser]].
     * @alpha
     */
    get selectedLoadedTiles() {
        return this._lruList.selectedTiles;
    }
    /** Returns the number of pending and active requests associated with the specified viewport. */
    getNumRequestsForViewport(vp) {
        return this.getNumRequestsForUser(vp);
    }
    /** Returns the number of pending and active requests associated with the specified user. */
    getNumRequestsForUser(user) {
        const requests = this.getRequestsForUser(user);
        let count = requests?.size ?? 0;
        const tiles = this.getTilesForUser(user);
        if (tiles)
            count += tiles.external.requested;
        return count;
    }
    /** Returns the current set of Tiles requested by the specified TileUser.
     * Do not modify the set or the Tiles.
     * @internal
     */
    getRequestsForUser(user) {
        return this._requestsPerUser.get(user);
    }
    /** Specifies the set of tiles currently requested for use by a TileUser. This set replaces any previously specified for the same user.
     * The requests are not actually processed until the next call to [[TileAdmin.process].
     * This is typically invoked when a viewport recreates its scene, e.g. in response to camera movement.
     * @internal
     */
    requestTiles(user, tiles) {
        this._requestsPerUser.set(user, tiles);
    }
    /** Returns two sets of tiles associated with the specified user - typically, a viewport's current scene.
     * Do not modify the returned sets.
     * @internal
     */
    getTilesForUser(user) {
        return this._selectedAndReady.get(user);
    }
    /** Adds the specified tiles to the sets of selected and ready tiles for the specified TileUser.
     * The TileAdmin takes ownership of the `ready` set - do not modify it after passing it in.
     * @internal
     */
    addTilesForUser(user, selected, ready, touched) {
        // "selected" are tiles we are drawing.
        this._lruList.markUsed(user.tileUserId, selected);
        // "ready" are tiles we want to draw but can't yet because, for example, their siblings are not yet ready to be drawn.
        this._lruList.markUsed(user.tileUserId, ready);
        // "touched" are tiles whose contents we want to keep in memory regardless of whether they are "selected" or "ready".
        this._lruList.markUsed(user.tileUserId, touched);
        const entry = this.getTilesForUser(user);
        if (undefined === entry) {
            this._selectedAndReady.set(user, { ready, selected: new Set(selected), external: { selected: 0, requested: 0, ready: 0 } });
            return;
        }
        for (const tile of selected)
            entry.selected.add(tile);
        for (const tile of ready)
            entry.ready.add(tile);
    }
    /** Disclose statistics about tiles that are handled externally from TileAdmin. At this time, that means OrbitGT point cloud tiles.
     * These statistics are included in the return value of [[getTilesForUser]].
     * @internal
     */
    addExternalTilesForUser(user, statistics) {
        const entry = this.getTilesForUser(user);
        if (!entry) {
            this._selectedAndReady.set(user, { ready: new Set(), selected: new Set(), external: { ...statistics } });
            return;
        }
        entry.external.requested += statistics.requested;
        entry.external.selected += statistics.selected;
        entry.external.ready += statistics.ready;
    }
    /** Clears the sets of tiles associated with a TileUser. */
    clearTilesForUser(user) {
        this._selectedAndReady.delete(user);
        this._lruList.clearUsed(user.tileUserId);
    }
    /** Indicates that the TileAdmin should cease tracking the specified TileUser, e.g. because it is about to be destroyed.
     * Any requests which are of interest only to the specified user will be canceled.
     */
    forgetUser(user) {
        this.onUserIModelClosed(user);
        this._users.delete(user);
    }
    /** Indicates that the TileAdmin should track tile requests for the specified TileUser.
     * This is invoked by the Viewport constructor and should be invoked manually for any non-Viewport TileUser.
     * [[forgetUser]] must be later invoked to unregister the user.
     */
    registerUser(user) {
        this._users.add(user);
    }
    /** Iterable over all TileUsers registered with TileAdmin. This may include [[OffScreenViewport]]s.
     * @alpha
     */
    get tileUsers() {
        return this._users;
    }
    /** @internal */
    invalidateAllScenes() {
        for (const user of this.tileUsers)
            if (user instanceof Viewport_1.Viewport)
                user.invalidateScene();
    }
    /** @internal */
    onShutDown() {
        if (this._cleanup) {
            this._cleanup();
            this._cleanup = undefined;
        }
        this._removeIModelConnectionOnCloseListener();
        this.channels.onShutDown();
        for (const req of this._tileTreePropsRequests)
            req.abandon();
        this._requestsPerUser.clear();
        this._tileUserSetsForRequests.clear();
        this._tileUsagePerUser.clear();
        this._tileTreePropsRequests.length = 0;
        this._lruList.dispose();
    }
    /** Returns the union of the input set and the input TileUser, to be associated with a [[TileRequest]].
     * @internal
     */
    getTileUserSetForRequest(user, users) {
        return this._tileUserSetsForRequests.getTileUserSet(user, users);
    }
    /** Marks the Tile as "in use" by the specified TileUser, where the tile defines what "in use" means.
     * A tile will not be discarded while it is in use by any TileUser.
     * @see [[TileTree.prune]]
     * @internal
     */
    markTileUsed(marker, user) {
        let set = this._tileUsagePerUser.get(user);
        if (!set)
            this._tileUsagePerUser.set(user, set = new Set());
        set.add(marker);
    }
    /** Returns true if the Tile is currently in use by any TileUser.
     * @see [[markTileUsed]].
     * @internal
     */
    isTileInUse(marker) {
        for (const [_user, markers] of this._tileUsagePerUser)
            if (markers.has(marker))
                return true;
        return false;
    }
    /** Indicates that the TileAdmin should reset usage tracking for the specified TileUser, e.g. because the user is a Viewport about
     * to recreate its scene. Any tiles currently marked as "in use" by this user no longer will be.
     * @internal
     */
    clearUsageForUser(user) {
        this._tileUsagePerUser.delete(user);
    }
    /** @internal */
    async requestTileTreeProps(iModel, treeId) {
        this.initializeRpc();
        const requests = this._tileTreePropsRequests;
        return new Promise((resolve, reject) => {
            const request = new TileTreePropsRequest(iModel, treeId, resolve, reject);
            requests.push(request);
            if (this._tileTreePropsRequests.length <= this._maxActiveTileTreePropsRequests)
                request.dispatch();
        });
    }
    /** Temporary workaround for authoring applications. Usage:
     * ```ts
     *  async function handleModelChanged(modelId: Id64String, iModel: IModelConnection): Promise<void> {
     *    await iModel.tiles.purgeTileTrees([modelId]);
     *    IModelApp.viewManager.refreshForModifiedModels(modelId);
     *  }
     * ```
     * @internal
     */
    async purgeTileTrees(iModel, modelIds) {
        this.initializeRpc();
        return core_common_1.IModelTileRpcInterface.getClient().purgeTileTrees(iModel.getRpcProps(), modelIds);
    }
    /** @internal */
    async requestCachedTileContent(tile) {
        if (tile.iModelTree.iModel.iModelId === undefined)
            throw new Error("Provided iModel has no iModelId");
        const { guid, tokenProps, treeId } = this.getTileRequestProps(tile);
        const content = await (await this.getTileStorage()).downloadTile(tokenProps, tile.iModelTree.iModel.iModelId, tile.iModelTree.iModel.changeset.id, treeId, tile.contentId, guid);
        return content;
    }
    /** @internal */
    async generateTileContent(tile) {
        this.initializeRpc();
        const props = this.getTileRequestProps(tile);
        const retrieveMethod = await core_common_1.IModelTileRpcInterface.getClient().generateTileContent(props.tokenProps, props.treeId, props.contentId, props.guid);
        if (tile.request?.isCanceled) {
            // the content is no longer needed, return an empty array.
            return new Uint8Array();
        }
        if (retrieveMethod === core_common_1.TileContentSource.ExternalCache) {
            const tileContent = await this.requestCachedTileContent(tile);
            if (tileContent === undefined)
                throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NoContent, "Failed to fetch generated tile from external cache");
            return tileContent;
        }
        else if (retrieveMethod === core_common_1.TileContentSource.Backend) {
            return core_common_1.IModelTileRpcInterface.getClient().retrieveTileContent(props.tokenProps, this.getTileRequestProps(tile));
        }
        throw new core_common_1.BackendError(core_bentley_1.BentleyStatus.ERROR, "", "Invalid response from RPC backend");
    }
    /** @internal */
    getTileRequestProps(tile) {
        const tree = tile.iModelTree;
        const tokenProps = tree.iModel.getRpcProps();
        let guid = tree.geometryGuid || tokenProps.changeset?.id || "first";
        if (tree.contentIdQualifier)
            guid = `${guid}_${tree.contentIdQualifier}`;
        const contentId = tile.contentId;
        const treeId = tree.id;
        return { tokenProps, treeId, contentId, guid };
    }
    /** Request graphics for a single element or geometry stream.
     * @see [[readElementGraphics]] to convert the result into a [[RenderGraphic]] for display.
     * @public
     */
    async requestElementGraphics(iModel, requestProps) {
        if (true !== requestProps.omitEdges && undefined === requestProps.edgeType)
            requestProps = { ...requestProps, edgeType: "non-indexed" !== this.edgeOptions.type ? 2 : 1 };
        // For backwards compatibility, these options default to true in the backend. Explicitly set them to false in (newer) frontends if not supplied.
        if (undefined === requestProps.quantizePositions || undefined === requestProps.useAbsolutePositions) {
            requestProps = {
                ...requestProps,
                quantizePositions: requestProps.quantizePositions ?? false,
                useAbsolutePositions: requestProps.useAbsolutePositions ?? false,
            };
        }
        this.initializeRpc();
        const intfc = core_common_1.IModelTileRpcInterface.getClient();
        return intfc.requestElementGraphics(iModel.getRpcProps(), requestProps);
    }
    /** Obtain information about the version/format of the tiles supplied by the backend. */
    async queryVersionInfo() {
        if (!this._versionInfo) {
            this.initializeRpc();
            this._versionInfo = await core_common_1.IModelTileRpcInterface.getClient().queryVersionInfo();
        }
        return this._versionInfo;
    }
    /** @internal */
    onTilesElided(numElided) {
        this._totalElided += numElided;
    }
    /** Invoked when a Tile marks itself as "ready" - i.e., its content is loaded (or determined not to exist, or not to be needed).
     * If the tile has content, it is added to the LRU list of tiles with content.
     * The `onTileLoad` event will also be raised.
     * @internal
     */
    onTileContentLoaded(tile) {
        // It may already be present if it previously had content - perhaps we're replacing its content.
        this._lruList.drop(tile);
        this._lruList.add(tile);
        this.onTileLoad.raiseEvent(tile);
    }
    /** Invoked when a Tile's content is disposed of. It will be removed from the LRU list of tiles with content.
     * @internal
     */
    onTileContentDisposed(tile) {
        this._lruList.drop(tile);
    }
    /** @internal */
    terminateTileTreePropsRequest(request) {
        const index = this._tileTreePropsRequests.indexOf(request);
        if (index >= 0) {
            this._tileTreePropsRequests.splice(index, 1);
            this.dispatchTileTreePropsRequests();
        }
    }
    /** Subscribe to [[onTileLoad]], [[onTileTreeLoad]], and [[onTileChildrenLoad]]. */
    addLoadListener(callback) {
        const tileLoad = this.onTileLoad.addListener((tile) => callback(tile.tree.iModel));
        const treeLoad = this.onTileTreeLoad.addListener((tree) => callback(tree.iModel));
        const childLoad = this.onTileChildrenLoad.addListener((tile) => callback(tile.tree.iModel));
        return () => {
            tileLoad();
            treeLoad();
            childLoad();
        };
    }
    /** Determine what information about the schedule script is needed to produce tiles.
     * If no script, or the script doesn't require batching, then no information is needed - normal tiles can be used.
     * If possible and enabled, normal tiles can be requested and then processed on the frontend based on the ModelTimeline.
     * Otherwise, special tiles must be requested based on the script's sourceId (RenderTimeline or DisplayStyle element).
     * @internal
     */
    getScriptInfoForTreeId(modelId, script) {
        if (!script || !script.script.requiresBatching)
            return undefined;
        const timeline = script.script.modelTimelines.find((x) => x.modelId === modelId);
        if (!timeline || (!timeline.requiresBatching && !timeline.containsTransform))
            return undefined;
        // Frontend schedule scripts require the element Ids to be included in the script - previously saved views may have omitted them.
        if (!core_bentley_1.Id64.isValidId64(script.sourceId) || (this.enableFrontendScheduleScripts && !timeline.omitsElementIds))
            return { timeline };
        return { animationId: script.sourceId };
    }
    dispatchTileTreePropsRequests() {
        for (let i = 0; i < this._maxActiveTileTreePropsRequests && i < this._tileTreePropsRequests.length; i++)
            this._tileTreePropsRequests[i].dispatch();
    }
    processQueue() {
        // Mark all requests as being associated with no users, indicating they are no longer needed.
        this._tileUserSetsForRequests.clearAll();
        // Notify channels that we are enqueuing new requests.
        this.channels.swapPending();
        // Repopulate pending requests queue from each user. We do NOT sort by priority while doing so.
        this._requestsPerUser.forEach((value, key) => this.processRequests(key, value));
        // Ask channels to update their queues and dispatch requests.
        this.channels.process();
    }
    /** Exported strictly for tests. @internal */
    freeMemory() {
        if (undefined !== this._maxTotalTileContentBytes)
            this._lruList.freeMemory(this._maxTotalTileContentBytes);
    }
    pruneAndPurge() {
        const now = core_bentley_1.BeTimePoint.now();
        const needPrune = this._nextPruneTime.before(now);
        const needPurge = this._nextPurgeTime.before(now);
        if (!needPrune && !needPurge)
            return;
        // Identify all of the TileTrees in use by all of the TileUsers known to the TileAdmin.
        // NOTE: A single viewport can display tiles from more than one IModelConnection.
        // NOTE: A viewport may be displaying no trees - but we need to record its IModel so we can purge those which are NOT being displayed
        //  NOTE: That won't catch external tile trees previously used by that viewport.
        const trees = new internal_1.DisclosedTileTreeSet();
        const treesByIModel = needPurge ? new Map() : undefined;
        for (const user of this._users) {
            if (!user.iModel.isOpen) // case of closing an IModelConnection while keeping the Viewport open, possibly for reuse with a different IModelConnection.
                continue;
            user.discloseTileTrees(trees);
            if (treesByIModel && undefined === treesByIModel.get(user.iModel))
                treesByIModel.set(user.iModel, new Set());
        }
        if (needPrune) {
            // Request that each displayed tile tree discard any tiles and/or tile content that is no longer needed.
            for (const tree of trees)
                tree.prune();
            this._nextPruneTime = now.plus(this.tileExpirationTime);
        }
        if (treesByIModel) {
            for (const tree of trees) {
                let set = treesByIModel.get(tree.iModel);
                if (undefined === set)
                    treesByIModel.set(tree.iModel, set = new Set());
                set.add(tree);
            }
            // Discard any tile trees that are no longer in use by any user.
            const olderThan = now.minus(this.tileTreeExpirationTime);
            for (const entry of treesByIModel)
                entry[0].tiles.purge(olderThan, entry[1]);
            this._nextPurgeTime = now.plus(this.tileTreeExpirationTime);
        }
    }
    processRequests(user, tiles) {
        for (const tile of tiles) {
            if (undefined === tile.request) {
                // ###TODO: This assertion triggers for AttachmentViewports used for rendering 3d sheet attachments.
                // Determine why and fix.
                // assert(tile.loadStatus === Tile.LoadStatus.NotLoaded);
                if (internal_1.TileLoadStatus.NotLoaded === tile.loadStatus) {
                    const request = new internal_1.TileRequest(tile, user);
                    tile.request = request;
                    (0, core_bentley_1.assert)(this.channels.has(request.channel));
                    request.channel.append(request);
                }
            }
            else {
                const req = tile.request;
                (0, core_bentley_1.assert)(undefined !== req);
                if (undefined !== req) {
                    // Request may already be dispatched (in channel's active requests) - if so do not re-enqueue!
                    if (req.isQueued && 0 === req.users.length)
                        req.channel.append(req);
                    req.addUser(user);
                    (0, core_bentley_1.assert)(0 < req.users.length);
                }
            }
        }
    }
    // NB: This does *not* remove from this._users - a viewport could later be reused with a different IModelConnection.
    onUserIModelClosed(user) {
        this.clearUsageForUser(user);
        this.clearTilesForUser(user);
        // NB: user will be removed from TileUserSets in process() - but if we can establish that only this user wants a given tile, cancel its request immediately.
        const tiles = this._requestsPerUser.get(user);
        if (undefined !== tiles) {
            for (const tile of tiles) {
                const request = tile.request;
                if (undefined !== request && 1 === request.users.length)
                    request.cancel();
            }
            this._requestsPerUser.delete(user);
        }
    }
    onIModelClosed(iModel) {
        this._requestsPerUser.forEach((_req, user) => {
            if (user.iModel === iModel)
                this.onUserIModelClosed(user);
        });
        // Remove any TileTreeProps requests associated with this iModel.
        this._tileTreePropsRequests = this._tileTreePropsRequests.filter((req) => {
            if (req.iModel !== iModel)
                return true;
            req.abandon();
            return false;
        });
        // Dispatch TileTreeProps requests not associated with this iModel.
        this.dispatchTileTreePropsRequests();
        this.channels.onIModelClosed(iModel);
    }
    initializeRpc() {
        // Would prefer to do this in constructor - but nothing enforces that the app initializes the rpc interfaces before it creates the TileAdmin (via IModelApp.startup()) - so do it on first request instead.
        if (this._rpcInitialized)
            return;
        this._rpcInitialized = true;
        const retryInterval = this._retryInterval;
        core_common_1.RpcOperation.lookup(core_common_1.IModelTileRpcInterface, "requestTileTreeProps").policy.retryInterval = () => retryInterval;
        const policy = core_common_1.RpcOperation.lookup(core_common_1.IModelTileRpcInterface, "generateTileContent").policy;
        policy.retryInterval = () => retryInterval;
        policy.allowResponseCaching = () => core_common_1.RpcResponseCacheControl.Immutable;
    }
}
exports.TileAdmin = TileAdmin;
/** @public */
(function (TileAdmin) {
    /** The number of bytes of GPU memory associated with the various [[GpuMemoryLimit]]s for non-mobile devices.
     * @see [[TileAdmin.Props.gpuMemoryLimits]] to specify the limit at startup.
     * @see [[TileAdmin.gpuMemoryLimit]] to adjust the actual limit after startup.
     * @see [[TileAdmin.mobileMemoryLimits]] for mobile devices.
     */
    TileAdmin.nonMobileGpuMemoryLimits = {
        default: 1024 * 1024 * 1024, // 1 GB
        aggressive: 500 * 1024 * 1024, // 500 MB
        relaxed: 2.5 * 1024 * 1024 * 1024, // 2.5 GB
    };
    /** @internal exported for tests */
    TileAdmin.nonMobileUndefinedGpuMemoryLimit = 6000 * 1024 * 1024; // 6,000 MB - used when nonMobile limit is undefined
    /** The number of bytes of GPU memory associated with the various [[GpuMemoryLimit]]s for mobile devices.
     * @see [[TileAdmin.Props.gpuMemoryLimits]] to specify the limit at startup.
     * @see [[TileAdmin.gpuMemoryLimit]] to adjust the actual limit after startup.
     * @see [[TileAdmin.nonMobileMemoryLimits]] for non-mobile devices.
     */
    TileAdmin.mobileGpuMemoryLimits = {
        default: 200 * 1024 * 1024, // 200 MB
        aggressive: 75 * 1024 * 1024, // 75 MB
        relaxed: 500 * 1024 * 1024, // 500 MB
    };
})(TileAdmin || (exports.TileAdmin = TileAdmin = {}));
/** Some views contain thousands of models. When we open such a view, the first thing we do is request the IModelTileTreeProps for each model. This involves a http request per model,
 * which can exceed the maximum number of simultaneous requests permitted by the browser.
 * Similar to how we throttle requests for tile *content*, we throttle requests for IModelTileTreeProps based on `TileAdmin.Props.maxActiveTileTreePropsRequests`, heretofore referred to as `N`.
 * TileAdmin maintains a FIFO queue of requests for IModelTileTreeProps. The first N of those requests have been dispatched; the remainder are waiting for their turn.
 * When `TileAdmin.requestTileTreeProps` is called, it appends a new request to the queue, and if the queue length < N, dispatches it immediately.
 * When a request completes, throws an error, or is canceled, it is removed from the queue, and any not-yet-dispatched requests are dispatched (not exceeding N total in flight).
 * When an IModelConnection is closed, any requests associated with that iModel are canceled.
 * NOTE: This request queue currently does not interact at all with the tile content request queue.
 * NOTE: We rely on TreeOwner to not request the same IModelTileTreeProps multiple times - we do not check the queue for presence of a requested tree before enqeueing it.
 */
class TileTreePropsRequest {
    constructor(iModel, _treeId, _resolve, _reject) {
        this.iModel = iModel;
        this._treeId = _treeId;
        this._resolve = _resolve;
        this._reject = _reject;
        this._isDispatched = false;
    }
    get isDispatched() { return this._isDispatched; }
    dispatch() {
        if (this.isDispatched)
            return;
        this._isDispatched = true;
        requestTileTreeProps(this.iModel, this._treeId).then((props) => {
            this.terminate();
            this._resolve(props);
        }).catch((err) => {
            this.terminate();
            this._reject(err);
        });
    }
    /** The IModelConnection was closed, or IModelApp was shut down. Don't call terminate(), because we don't want to dispatch pending requests as a result.
     * Just reject if not yet dispatched.
     */
    abandon() {
        if (!this.isDispatched) {
            // A little white lie that tells the TileTreeOwner it can try to load again later if needed, rather than treating rejection as failure to load.
            this._reject(new core_common_1.ServerTimeoutError("requestTileTreeProps cancelled"));
        }
    }
    terminate() {
        IModelApp_1.IModelApp.tileAdmin.terminateTileTreePropsRequest(this);
    }
}
let requestTileTreePropsOverride;
async function requestTileTreeProps(iModel, treeId) {
    if (requestTileTreePropsOverride)
        return requestTileTreePropsOverride(iModel, treeId);
    return core_common_1.IModelTileRpcInterface.getClient().requestTileTreeProps(iModel.getRpcProps(), treeId);
}
/** Strictly for tests - overrides the call to IModelTileRpcInterface.requestTileTreeProps with a custom function, or clears the override.
 * @internal
 */
function overrideRequestTileTreeProps(func) {
    requestTileTreePropsOverride = func;
}
//# sourceMappingURL=TileAdmin.js.map