/*---------------------------------------------------------------------------------------------
* 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, compareBooleans, compareNumbers, compareSimpleArrays, compareSimpleTypes, compareStrings, compareStringsOrUndefined, dispose, Logger, } from "@itwin/core-bentley";
import { Angle, Range3d, Transform } from "@itwin/core-geometry";
import { ImageSource, RenderTexture } from "@itwin/core-common";
import { IModelApp } from "../../IModelApp";
import { MapCartoRectangle, MapLayerTileTreeReference, MapTileTreeScaleRangeVisibility, QuadId, RealityTile, RealityTileLoader, RealityTileTree, TileLoadPriority, TileTreeLoadStatus, } from "../internal";
const loggerCategory = "ImageryMapTileTree";
/** @internal */
export class ImageryMapTile extends RealityTile {
    imageryTree;
    quadId;
    rectangle;
    _texture;
    _mapTileUsageCount = 0;
    _outOfLodRange;
    constructor(params, imageryTree, quadId, rectangle) {
        super(params, imageryTree);
        this.imageryTree = imageryTree;
        this.quadId = quadId;
        this.rectangle = rectangle;
        this._outOfLodRange = this.depth < imageryTree.minDepth;
    }
    get texture() { return this._texture; }
    get tilingScheme() { return this.imageryTree.tilingScheme; }
    get isDisplayable() { return (this.depth > 1) && super.isDisplayable; }
    get isOutOfLodRange() { return this._outOfLodRange; }
    setContent(content) {
        this._texture = content.imageryTexture; // No dispose - textures may be shared by terrain tiles so let garbage collector dispose them.
        if (undefined === content.imageryTexture)
            this.setLeaf(); // No imagery here — don't traverse deeper, but leave siblings unaffected.
        this.setIsReady();
    }
    selectCartoDrapeTiles(drapeTiles, highResolutionReplacementTiles, rectangleToDrape, drapePixelSize, args) {
        // Base draping overlap on width rather than height so that tiling schemes with multiple root nodes overlay correctly.
        const isSmallerThanDrape = (this.rectangle.xLength() / this.maximumSize) < drapePixelSize;
        if ((this.isLeaf && !this.isNotFound) // Include leaves so tiles get stretched past max LOD levels. (Only for base imagery layer)
            || isSmallerThanDrape) {
            if (this.isOutOfLodRange) {
                drapeTiles.push(this);
                this.setIsReady();
            }
            else if (this.isLeaf && !isSmallerThanDrape) {
                // These tiles are selected because we are beyond the max LOD of the tile tree,
                // might be used to display "stretched" tiles instead of having blank.
                highResolutionReplacementTiles.push(this);
            }
            else {
                drapeTiles.push(this);
            }
            return TileTreeLoadStatus.Loaded;
        }
        let status = this.loadChildren();
        if (TileTreeLoadStatus.Loading === status) {
            args.markChildrenLoading();
        }
        else if (TileTreeLoadStatus.Loaded === status) {
            if (undefined !== this.children) {
                for (const child of this.children) {
                    const mapChild = child;
                    if (!mapChild.rectangle.intersectsRange(rectangleToDrape))
                        continue;
                    status = mapChild.selectCartoDrapeTiles(drapeTiles, highResolutionReplacementTiles, rectangleToDrape, drapePixelSize, args);
                    if (TileTreeLoadStatus.Loaded !== status)
                        break;
                }
            }
        }
        return status;
    }
    markMapTileUsage() {
        this._mapTileUsageCount++;
    }
    releaseMapTileUsage() {
        assert(!this._texture || this._mapTileUsageCount > 0);
        if (this._mapTileUsageCount)
            this._mapTileUsageCount--;
    }
    /** @internal */
    setLeaf() {
        // Don't potentially re-request the children later.
        this.disposeChildren();
        this._isLeaf = true;
        this._childrenLoadStatus = TileTreeLoadStatus.Loaded;
    }
    _loadChildren(resolve, _reject) {
        const imageryTree = this.imageryTree;
        const resolveChildren = (childIds) => {
            const children = new Array();
            const childrenAreLeaves = (this.depth + 1) === imageryTree.maxDepth;
            // If children depth is lower than min LOD, mark them as disabled.
            // This is important: if those tiles are requested and the server refuse to serve them,
            // they will be marked as not found and their descendant will never be displayed.
            childIds.forEach((quadId) => {
                const rectangle = imageryTree.tilingScheme.tileXYToRectangle(quadId.column, quadId.row, quadId.level);
                const range = Range3d.createXYZXYZ(rectangle.low.x, rectangle.low.x, 0, rectangle.high.x, rectangle.high.y, 0);
                const maximumSize = imageryTree.imageryLoader.maximumScreenSize;
                const tile = new ImageryMapTile({ parent: this, isLeaf: childrenAreLeaves, contentId: quadId.contentId, range, maximumSize }, imageryTree, quadId, rectangle);
                children.push(tile);
            });
            resolve(children);
        };
        imageryTree.imageryLoader.generateChildIds(this, resolveChildren);
    }
    _collectStatistics(stats) {
        super._collectStatistics(stats);
        if (this._texture)
            stats.addTexture(this._texture.bytesUsed);
    }
    freeMemory() {
        // ###TODO MapTiles and ImageryMapTiles share resources and don't currently interact well with TileAdmin.freeMemory(). Opt out for now.
    }
    disposeContents() {
        if (0 === this._mapTileUsageCount) {
            super.disposeContents();
            this.disposeTexture();
        }
    }
    disposeTexture() {
        this._texture = dispose(this._texture);
    }
    [Symbol.dispose]() {
        this._mapTileUsageCount = 0;
        super[Symbol.dispose]();
    }
}
/** Object that holds various state values for an ImageryTileTree
 * @internal */
export class ImageryTileTreeState {
    _scaleRangeVis;
    constructor() {
        this._scaleRangeVis = MapTileTreeScaleRangeVisibility.Unknown;
    }
    /** Get the scale range visibility of the imagery tile tree.
     * @returns the scale range visibility of the imagery tile tree.
     */
    getScaleRangeVisibility() { return this._scaleRangeVis; }
    /** Makes a deep copy of the current object.
     */
    clone() {
        const clone = new ImageryTileTreeState();
        clone._scaleRangeVis = this._scaleRangeVis;
        return clone;
    }
    /** Reset the scale range visibility of imagery tile tree (i.e. unknown)
     */
    reset() {
        this._scaleRangeVis = MapTileTreeScaleRangeVisibility.Unknown;
    }
    /** Sets the scale range visibility of the current imagery tile tree.
     * The state will be derived based on the previous visibility values:
     * Initial state: 'Unknown'
     * The first call will set the state to either: 'Visible' or 'Hidden'.
     * If subsequent visibility values are not consistent with the first visibility state, the state become 'Partial',
     * meaning the imagery tree currently contains a mixed of tiles being in range and out of range.
     */
    setScaleRangeVisibility(visible) {
        if (this._scaleRangeVis === MapTileTreeScaleRangeVisibility.Unknown) {
            this._scaleRangeVis = (visible ? MapTileTreeScaleRangeVisibility.Visible : MapTileTreeScaleRangeVisibility.Hidden);
        }
        else if ((visible && this._scaleRangeVis === MapTileTreeScaleRangeVisibility.Hidden) || (!visible && this._scaleRangeVis === MapTileTreeScaleRangeVisibility.Visible)) {
            this._scaleRangeVis = MapTileTreeScaleRangeVisibility.Partial;
        }
    }
}
/** @internal */
export class ImageryMapTileTree extends RealityTileTree {
    _imageryLoader;
    constructor(params, _imageryLoader) {
        super(params);
        this._imageryLoader = _imageryLoader;
        const rootQuadId = new QuadId(_imageryLoader.imageryProvider.tilingScheme.rootLevel, 0, 0);
        this._rootTile = new ImageryMapTile(params.rootTile, this, rootQuadId, this.getTileRectangle(rootQuadId));
    }
    get tilingScheme() { return this._imageryLoader.imageryProvider.tilingScheme; }
    /** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [addAttributions] instead. */
    addLogoCards(cards, vp) {
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        this._imageryLoader.addLogoCards(cards, vp);
    }
    async addAttributions(cards, vp) {
        return this._imageryLoader.addAttributions(cards, vp);
    }
    getTileRectangle(quadId) {
        return this.tilingScheme.tileXYToRectangle(quadId.column, quadId.row, quadId.level);
    }
    get imageryLoader() { return this._imageryLoader; }
    get is3d() {
        assert(false);
        return false;
    }
    get viewFlagOverrides() {
        assert(false);
        return {};
    }
    get isContentUnbounded() {
        assert(false);
        return true;
    }
    _selectTiles(_args) {
        assert(false);
        return [];
    }
    draw(_args) { assert(false); }
    static _scratchDrapeRectangle = MapCartoRectangle.createZero();
    static _drapeIntersectionScale = 1.0 - 1.0E-5;
    selectCartoDrapeTiles(drapeTiles, highResolutionReplacementTiles, tileToDrape, args) {
        const drapeRectangle = tileToDrape.rectangle.clone(ImageryMapTileTree._scratchDrapeRectangle);
        // Base draping overlap on width rather than height so that tiling schemes with multiple root nodes overlay correctly.
        const drapePixelSize = 1.05 * tileToDrape.rectangle.xLength() / tileToDrape.maximumSize;
        drapeRectangle.scaleAboutCenterInPlace(ImageryMapTileTree._drapeIntersectionScale); // Contract slightly to avoid draping adjacent or slivers.
        return this.rootTile.selectCartoDrapeTiles(drapeTiles, highResolutionReplacementTiles, drapeRectangle, drapePixelSize, args);
    }
    cartoRectangleFromQuadId(quadId) { return this.tilingScheme.tileXYToRectangle(quadId.column, quadId.row, quadId.level); }
}
class ImageryTileLoader extends RealityTileLoader {
    _imageryProvider;
    _iModel;
    constructor(_imageryProvider, _iModel) {
        super();
        this._imageryProvider = _imageryProvider;
        this._iModel = _iModel;
    }
    computeTilePriority(tile) {
        return 25 * (this._imageryProvider.usesCachedTiles ? 2 : 1) - tile.depth; // Always cached first then descending by depth (high resolution/front first)
    } // Prioritized fast, cached tiles first.
    get maxDepth() { return this._imageryProvider.maximumZoomLevel; }
    get minDepth() { return this._imageryProvider.minimumZoomLevel; }
    get priority() { return TileLoadPriority.Map; }
    /** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [addAttributions] instead. */
    addLogoCards(cards, vp) {
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        this._imageryProvider.addLogoCards(cards, vp);
    }
    async addAttributions(cards, vp) {
        await this._imageryProvider.addAttributions(cards, vp);
    }
    get maximumScreenSize() { return this._imageryProvider.maximumScreenSize; }
    get imageryProvider() { return this._imageryProvider; }
    async getToolTip(strings, quadId, carto, tree) { await this._imageryProvider.getToolTip(strings, quadId, carto, tree); }
    async getMapFeatureInfo(featureInfos, quadId, carto, tree, hit, options) {
        await this._imageryProvider.getFeatureInfo(featureInfos, quadId, carto, tree, hit, options);
    }
    generateChildIds(tile, resolveChildren) { return this._imageryProvider.generateChildIds(tile, resolveChildren); }
    /** Load this tile's children, possibly asynchronously. Pass them to `resolve`, or an error to `reject`. */
    async loadChildren(_tile) {
        assert(false);
        return undefined;
    }
    async requestTileContent(tile, _isCanceled) {
        const quadId = QuadId.createFromContentId(tile.contentId);
        return this._imageryProvider.loadTile(quadId.row, quadId.column, quadId.level);
    }
    getRequestChannel(_tile) {
        // ###TODO use hostname from url - but so many layers to go through to get that...
        return IModelApp.tileAdmin.channels.getForHttp("itwinjs-imagery");
    }
    async loadTileContent(tile, data, system) {
        assert(data instanceof ImageSource);
        assert(tile instanceof ImageryMapTile);
        const content = {};
        const texture = await this.loadTextureImage(data, system);
        if (undefined === texture)
            return content;
        content.imageryTexture = texture;
        return content;
    }
    async loadTextureImage(source, system) {
        try {
            return await system.createTextureFromSource({
                type: RenderTexture.Type.FilteredTileSection,
                source,
            });
        }
        catch {
            return undefined;
        }
    }
}
/** Supplies a TileTree that can load and draw tiles based on our imagery provider.
 * The TileTree is uniquely identified by its imagery type.
 */
class ImageryMapLayerTreeSupplier {
    /** Return a numeric value indicating how two tree IDs are ordered relative to one another.
     * This allows the ID to serve as a lookup key to find the corresponding TileTree.
     */
    compareTileTreeIds(lhs, rhs) {
        let cmp = compareStrings(lhs.settings.formatId, rhs.settings.formatId);
        if (0 === cmp) {
            cmp = compareStrings(lhs.settings.url, rhs.settings.url);
            if (0 === cmp) {
                cmp = compareStringsOrUndefined(lhs.settings.userName, rhs.settings.userName);
                if (0 === cmp) {
                    cmp = compareStringsOrUndefined(lhs.settings.password, rhs.settings.password);
                    if (0 === cmp) {
                        cmp = compareBooleans(lhs.settings.transparentBackground, rhs.settings.transparentBackground);
                        if (0 === cmp) {
                            if (lhs.settings.properties || rhs.settings.properties) {
                                if (lhs.settings.properties && rhs.settings.properties) {
                                    const lhsKeysLength = Object.keys(lhs.settings.properties).length;
                                    const rhsKeysLength = Object.keys(rhs.settings.properties).length;
                                    if (lhsKeysLength !== rhsKeysLength) {
                                        cmp = lhsKeysLength - rhsKeysLength;
                                    }
                                    else {
                                        for (const key of Object.keys(lhs.settings.properties)) {
                                            const lhsProp = lhs.settings.properties[key];
                                            const rhsProp = rhs.settings.properties[key];
                                            cmp = compareStrings(typeof lhsProp, typeof rhsProp);
                                            if (0 !== cmp)
                                                break;
                                            if (Array.isArray(lhsProp) || Array.isArray(rhsProp)) {
                                                cmp = compareSimpleArrays(lhsProp, rhsProp);
                                                if (0 !== cmp)
                                                    break;
                                            }
                                            else {
                                                cmp = compareSimpleTypes(lhsProp, rhsProp);
                                                if (0 !== cmp)
                                                    break;
                                            }
                                        }
                                    }
                                }
                                else if (!lhs.settings.properties) {
                                    cmp = 1;
                                }
                                else {
                                    cmp = -1;
                                }
                            }
                            if (0 === cmp) {
                                cmp = compareNumbers(lhs.settings.subLayers.length, rhs.settings.subLayers.length);
                                if (0 === cmp) {
                                    for (let i = 0; i < lhs.settings.subLayers.length && 0 === cmp; i++) {
                                        cmp = compareStrings(lhs.settings.subLayers[i].name, rhs.settings.subLayers[i].name);
                                        if (0 === cmp) {
                                            cmp = compareBooleans(lhs.settings.subLayers[i].visible, rhs.settings.subLayers[i].visible);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return cmp;
    }
    /** The first time a tree of a particular imagery type is requested, this function creates it. */
    async createTileTree(id, iModel) {
        const imageryProvider = IModelApp.mapLayerFormatRegistry.createImageryProvider(id.settings);
        if (undefined === imageryProvider) {
            Logger.logError(loggerCategory, `Failed to create imagery provider for format '${id.settings.formatId}'`);
            return undefined;
        }
        try {
            await imageryProvider.initialize();
        }
        catch (e) {
            Logger.logError(loggerCategory, `Could not initialize imagery provider for map layer '${id.settings.name}' : ${e}`);
            throw e;
        }
        const modelId = iModel.transientIds.getNext();
        const tilingScheme = imageryProvider.tilingScheme;
        const rootLevel = (1 === tilingScheme.numberOfLevelZeroTilesX && 1 === tilingScheme.numberOfLevelZeroTilesY) ? 0 : -1;
        const rootTileId = new QuadId(rootLevel, 0, 0).contentId;
        const rootRange = Range3d.createXYZXYZ(-Angle.piRadians, -Angle.piOver2Radians, 0, Angle.piRadians, Angle.piOver2Radians, 0);
        const rootTileProps = { contentId: rootTileId, range: rootRange, maximumSize: 0 };
        const loader = new ImageryTileLoader(imageryProvider, iModel);
        const treeProps = { rootTile: rootTileProps, id: modelId, modelId, iModel, location: Transform.createIdentity(), priority: TileLoadPriority.Map, loader, gcsConverterAvailable: false };
        return new ImageryMapTileTree(treeProps, loader);
    }
}
const imageryTreeSupplier = new ImageryMapLayerTreeSupplier();
/** A reference to one of our tile trees. The specific TileTree drawn may change when the desired imagery type or target iModel changes.
 * @beta
 */
export class ImageryMapLayerTreeReference extends MapLayerTileTreeReference {
    /**
     * Constructor for an ImageryMapLayerTreeReference.
     * @param layerSettings Map layer settings that are applied to the ImageryMapLayerTreeReference.
     * @param layerIndex The index of the associated map layer. Usually passed in through [[createMapLayerTreeReference]] in [[MapTileTree]]'s constructor.
     * @param iModel The iModel containing the ImageryMapLayerTreeReference.
     */
    constructor(args) {
        super(args.layerSettings, args.layerIndex, args.iModel);
    }
    get castsShadows() { return false; }
    /** Return the owner of the TileTree to draw. */
    get treeOwner() {
        return this.iModel.tiles.getTileTreeOwner({ settings: this._layerSettings }, imageryTreeSupplier);
    }
    /* @internal */
    resetTreeOwner() {
        return this.iModel.tiles.resetTileTreeOwner({ settings: this._layerSettings }, imageryTreeSupplier);
    }
    get imageryProvider() {
        const tree = this.treeOwner.load();
        if (!tree || !(tree instanceof ImageryMapTileTree))
            return undefined;
        return tree.imageryLoader.imageryProvider;
    }
}
//# sourceMappingURL=ImageryTileTree.js.map