/*---------------------------------------------------------------------------------------------
* 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, dispose } from "@itwin/core-bentley";
import { ColorByName, ColorDef, FrustumPlanes, GlobeMode, PackedFeatureTable } from "@itwin/core-common";
import { AxisOrder, BilinearPatch, ClipPlane, ClipPrimitive, ClipShape, ClipVector, Constant, ConvexClipPlaneSet, EllipsoidPatch, LongitudeLatitudeNumber, Matrix3d, Point3d, PolygonOps, Range1d, Range2d, Range3d, Ray3d, Transform, Vector2d, Vector3d } from "@itwin/core-geometry";
import { IModelApp } from "../../IModelApp";
import { RealityMeshParams } from "../../render/RealityMeshParams";
import { upsampleRealityMeshParams } from "../../render/UpsampleRealityMeshParams";
import { TerrainTexture } from "../../render/RenderSystem";
import { MapCartoRectangle, MapTileTree, QuadId, RealityTile, TileGraphicType, TileLoadStatus, TileTreeLoadStatus, } from "../internal";
/** @internal */
export class PlanarTilePatch {
    constructor(corners, normal, _chordHeight) {
        this.corners = corners;
        this.normal = normal;
        this._chordHeight = _chordHeight;
    }
    getRangeCorners(heightRange, result) {
        let index = 0;
        for (const corner of this.corners)
            corner.plusScaled(this.normal, heightRange.low - this._chordHeight, result[index++]);
        for (const corner of this.corners)
            corner.plusScaled(this.normal, heightRange.high + this._chordHeight, result[index++]);
        return result;
    }
    getClipShape() {
        return [this.corners[0], this.corners[1], this.corners[3], this.corners[2]];
    }
}
/** Projects points within the rectangular region of a [[MapTile]] into 3d space.
 * @see [[MapTile.getProjection]] to obtain the projection for a [[MapTile]].
 * @public
 */
export class MapTileProjection {
    /** @alpha */
    get ellipsoidPatch() { return undefined; }
    /** @alpha */
    getGlobalPoint(u, v, z, result) {
        const point = this.getPoint(u, v, z, result);
        return this.transformFromLocal.multiplyPoint3d(point, point);
    }
}
/** @alpha */
class EllipsoidProjection extends MapTileProjection {
    constructor(_patch, heightRange) {
        super();
        this._patch = _patch;
        this.transformFromLocal = Transform.createIdentity();
        this.localRange = _patch.range();
        this.localRange.expandInPlace(heightRange ? (heightRange.high - heightRange.low) : 0);
    }
    getPoint(u, v, height, result) {
        const angles = this._patch.uvFractionToAngles(u, v, height, EllipsoidProjection._scratchAngles);
        const ray = this._patch.anglesToUnitNormalRay(angles, EllipsoidProjection._scratchRay);
        return Point3d.createFrom(ray.origin, result);
    }
    get ellipsoidPatch() { return this._patch; }
}
EllipsoidProjection._scratchAngles = LongitudeLatitudeNumber.createZero();
EllipsoidProjection._scratchRay = Ray3d.createZero();
/** @alpha */
class PlanarProjection extends MapTileProjection {
    constructor(patch, heightRange) {
        super();
        this.transformFromLocal = Transform.createOriginAndMatrix(patch.corners[0], Matrix3d.createRigidHeadsUp(patch.normal, AxisOrder.ZYX));
        const planeCorners = this.transformFromLocal.multiplyInversePoint3dArray([patch.corners[0], patch.corners[1], patch.corners[2], patch.corners[3]]);
        this.localRange = Range3d.createArray(planeCorners);
        this.localRange.low.z += heightRange ? heightRange.low : 0;
        this.localRange.high.z += heightRange ? heightRange.high : 0;
        this._bilinearPatch = new BilinearPatch(planeCorners[0], planeCorners[1], planeCorners[2], planeCorners[3]);
    }
    getPoint(u, v, z, result) {
        result = this._bilinearPatch.uvFractionToPoint(u, v, result);
        result.z += z;
        return result;
    }
}
const scratchNormal = Vector3d.create();
const scratchViewZ = Vector3d.create();
const scratchPoint = Point3d.create();
const scratchClipPlanes = [ClipPlane.createNormalAndPoint(scratchNormal, scratchPoint), ClipPlane.createNormalAndPoint(scratchNormal, scratchPoint), ClipPlane.createNormalAndPoint(scratchNormal, scratchPoint), ClipPlane.createNormalAndPoint(scratchNormal, scratchPoint)];
const scratchCorners = [Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero()];
/** A [[Tile]] belonging to a [[MapTileTree]] representing a rectangular region of a map of the Earth.
 * @public
 */
export class MapTile extends RealityTile {
    /** @internal */
    get isReady() {
        return super.isReady && this.baseImageryIsReady;
    }
    /** @internal */
    get hasGraphics() {
        return this._renderGeometry !== undefined;
    }
    /** @internal */
    get renderGeometry() { return this._renderGeometry; }
    /** @internal */
    get mesh() { return this._mesh; }
    /** @internal */
    get loadableTerrainTile() { return this.loadableTile; }
    /** @internal */
    get isPlanar() { return this._patch instanceof PlanarTilePatch; }
    /** @internal */
    get imageryTiles() { return this._imageryTiles; }
    /** List of selected tiles but are currently in hidden state (i.e. scale range visibility)
     * @internal
     */
    get hiddenImageryTiles() { return this._hiddenTiles; }
    /** List of leafs tiles that have been selected as a replacement for missing high resolution tiles.
     * When this list is non-empty this means we are past the maximum LOD available of the tile tree.
     * By using those tiles, you are likely to get a display where tiles looks pixelated..
     * in some cases this is preferred to have no tile at all.
     * @internal
     */
    get highResolutionReplacementTiles() { return this._highResolutionReplacementTiles; }
    /** @internal */
    constructor(params, mapTree, quadId, patch, rectangle, heightRange, cornerRays) {
        super(params, mapTree);
        /** @internal */
        this.everLoaded = false; // If the tile is only required for availability metadata, load it once and then allow it to be unloaded.
        this.mapTree = mapTree;
        this.quadId = quadId;
        this._patch = patch;
        this.rectangle = rectangle;
        this._cornerRays = cornerRays;
        this._heightRange = heightRange?.clone();
    }
    /** @internal */
    getRangeCorners(result) {
        return this._patch instanceof PlanarTilePatch ? this._patch.getRangeCorners(this.heightRange, result) : this.range.corners(result);
    }
    /** @internal */
    getSizeProjectionCorners() {
        // Use only the first 4 corners -- On terrain tiles the height is initially exagerated to world height range which can cause excessive tile loading.
        const rangeCorners = this.getRangeCorners(scratchCorners);
        return rangeCorners.slice(0, 4);
    }
    /** @internal */
    markUsed(args) {
        super.markUsed(args);
        if (this._imageryTiles)
            for (const imageryTile of this._imageryTiles)
                imageryTile.markUsed(args);
    }
    /** @internal */
    get graphicType() {
        if (this.mapTree.isOverlay)
            return TileGraphicType.Overlay;
        return (this.mapTree.useDepthBuffer || this._forceDepthBuffer) ? TileGraphicType.Scene : TileGraphicType.BackgroundMap;
    }
    /** @internal */
    get mapLoader() { return this.realityRoot.loader; }
    /** @internal */
    get isUpsampled() { return false; }
    /** @internal */
    tileFromQuadId(quadId) {
        if (0 === quadId.compare(this.quadId))
            return this;
        assert(quadId.level > this.quadId.level);
        if (quadId.level <= this.quadId.level)
            return undefined;
        if (this.children) {
            const shift = quadId.level - this.quadId.level - 1;
            const childRow = quadId.row >> shift;
            const childColumn = quadId.column >> shift;
            for (const child of this.children) {
                const mapChild = child;
                const childQuadId = mapChild.quadId;
                if (childQuadId.row === childRow && childQuadId.column === childColumn)
                    return mapChild.tileFromQuadId(quadId);
            }
        }
        return undefined;
    }
    get _forceDepthBuffer() {
        // For large ellipsoidal globe tile force the depth buffer on to avoid anomalies at horizon.
        return this.mapTree.globeMode === GlobeMode.Ellipsoid && this.depth < 8;
    }
    /** @internal */
    addBoundingGraphic(builder, color) {
        if (!this.isDisplayable)
            return;
        const heightRange = (this.heightRange === undefined) ? Range1d.createXX(-1, 1) : this.heightRange;
        const lows = [], highs = [], reorder = [0, 1, 3, 2, 0];
        const cornerRays = this._cornerRays;
        if (this._patch instanceof PlanarTilePatch) {
            const normal = this._patch.normal;
            for (let i = 0; i < 5; i++) {
                const corner = this._patch.corners[reorder[i]];
                lows.push(corner.plusScaled(normal, heightRange.low));
                highs.push(corner.plusScaled(normal, heightRange.high));
            }
        }
        else {
            for (let i = 0; i < 5; i++) {
                const cornerRay = cornerRays[reorder[i]];
                lows.push(cornerRay.origin.plusScaled(cornerRay.direction, heightRange.low));
                highs.push(cornerRay.origin.plusScaled(cornerRay.direction, heightRange.high));
            }
        }
        builder.setSymbology(color, color, 1);
        builder.addLineString(lows);
        builder.addLineString(highs);
        for (let i = 0; i < 4; i++)
            builder.addLineString([lows[i], highs[i]]);
        const inColor = ColorDef.create(ColorByName.cornflowerBlue);
        const outColor = ColorDef.create(ColorByName.chartreuse);
        const transitionColor = ColorDef.create(ColorByName.aquamarine);
        const inPoints = [], outPoints = [], transitionPoints = [];
        for (const point of highs)
            if (this.mapTree.cartesianRange.containsPoint(point))
                inPoints.push(point);
            else if (this.mapTree.cartesianRange.distanceToPoint(point) < this.mapTree.cartesianTransitionDistance)
                transitionPoints.push(point);
            else
                outPoints.push(point);
        builder.setSymbology(inColor, inColor, 15);
        builder.addPointString(inPoints);
        builder.setSymbology(outColor, outColor, 15);
        builder.addPointString(outPoints);
        builder.setSymbology(transitionColor, transitionColor, 31);
        builder.addPointString(transitionPoints);
    }
    /** @internal */
    getContentClip() {
        const points = this.getClipShape();
        if (points.length < 3)
            return undefined;
        if (this.mapTree.globeMode === GlobeMode.Ellipsoid) {
            const normal = PolygonOps.areaNormal(points);
            const globeOrigin = this.mapTree.globeOrigin;
            const globeNormal = Vector3d.createStartEnd(globeOrigin, points[0]);
            const negate = normal.dotProduct(globeNormal) < 0;
            const clipPlanes = [];
            for (let i = 0; i < 4; i++) {
                const point = points[i];
                const clipNormal = globeOrigin.crossProductToPoints(point, points[(i + 1) % 4], scratchNormal);
                if (negate)
                    clipNormal.negate(clipNormal);
                const clipPlane = ClipPlane.createNormalAndPoint(clipNormal, point, false, false, scratchClipPlanes[i]);
                if (clipPlane !== undefined) // Undefined at pole tiles...
                    clipPlanes.push(clipPlane);
            }
            const planeSet = ConvexClipPlaneSet.createPlanes(clipPlanes);
            const clipPrimitive = ClipPrimitive.createCapture(planeSet);
            return ClipVector.createCapture([clipPrimitive]);
        }
        else {
            return ClipVector.createCapture([ClipShape.createShape(points)]);
        }
    }
    /** @internal */
    setNotFound() {
        super.setNotFound();
        // For map tiles assume that an unfound tile implies descendants and siblings will also be unfound.
        if (undefined !== this.parent)
            this.parent.setLeaf();
    }
    /** @internal */
    getGraphic(_system, _texture) {
        return undefined;
    }
    /** For globe tiles displaying less then depth 2 appears distorted
     * @internal
     */
    get isDisplayable() {
        return this.mapTree.globeMode === GlobeMode.Ellipsoid ? (this.depth >= MapTileTree.minDisplayableDepth) : super.isDisplayable;
    }
    /** @internal */
    isOccluded(viewingSpace) {
        if (undefined === this._cornerRays || this.mapTree.globeMode !== GlobeMode.Ellipsoid)
            return false;
        if (viewingSpace.eyePoint !== undefined) {
            if (!this.mapTree.pointAboveEllipsoid(viewingSpace.eyePoint))
                return false;
            for (const cornerNormal of this._cornerRays) {
                const eyeNormal = Vector3d.createStartEnd(viewingSpace.eyePoint, cornerNormal.origin, scratchNormal);
                eyeNormal.normalizeInPlace();
                if (eyeNormal.dotProduct(cornerNormal.direction) < .01)
                    return false;
            }
        }
        else {
            const viewZ = viewingSpace.rotation.getRow(2, scratchViewZ);
            for (const cornerNormal of this._cornerRays)
                if (cornerNormal.direction.dotProduct(viewZ) > 0)
                    return false;
        }
        return true;
    }
    /** @internal */
    _loadChildren(resolve, _reject) {
        const mapTree = this.mapTree;
        const childLevel = this.quadId.level + 1;
        const rowCount = mapTree.sourceTilingScheme.getNumberOfYChildrenAtLevel(childLevel);
        const columnCount = mapTree.sourceTilingScheme.getNumberOfXChildrenAtLevel(childLevel);
        const resolveChildren = (children) => {
            const childrenRange = Range3d.createNull();
            for (const child of children)
                childrenRange.extendRange(child.range);
            if (!this.range.containsRange(childrenRange))
                this.range.extendRange(childrenRange);
            resolve(children);
        };
        if (mapTree.doCreateGlobeChildren(this)) {
            this.createGlobeChildren(columnCount, rowCount, resolveChildren);
            return;
        }
        const resolvePlanarChildren = (childCorners) => {
            const level = this.quadId.level + 1;
            const column = this.quadId.column * 2;
            const row = this.quadId.row * 2;
            const children = [];
            const childrenAreLeaves = (this.depth + 1) === mapTree.loader.maxDepth;
            const globeMode = this.mapTree.globeMode;
            for (let j = 0; j < rowCount; j++) {
                for (let i = 0; i < columnCount; i++) {
                    const quadId = new QuadId(level, column + i, row + j);
                    const corners = childCorners[j * columnCount + i];
                    const rectangle = mapTree.getTileRectangle(quadId);
                    const normal = PolygonOps.areaNormal([corners[0], corners[1], corners[3], corners[2]]);
                    normal.normalizeInPlace();
                    const heightRange = this.mapTree.getChildHeightRange(quadId, rectangle, this);
                    const diagonal = Math.max(corners[0].distance(corners[3]), corners[1].distance(corners[2])) / 2.0;
                    const chordHeight = globeMode === GlobeMode.Ellipsoid ? Math.sqrt(diagonal * diagonal + Constant.earthRadiusWGS84.equator * Constant.earthRadiusWGS84.equator) - Constant.earthRadiusWGS84.equator : 0.0;
                    const rangeCorners = MapTile.computeRangeCorners(corners, normal, chordHeight, undefined, heightRange);
                    const range = Range3d.createArray(rangeCorners);
                    const child = this.mapTree.createPlanarChild({ contentId: quadId.contentId, maximumSize: 512, range, parent: this, isLeaf: childrenAreLeaves }, quadId, corners, normal, rectangle, chordHeight, heightRange);
                    if (child)
                        children.push(child);
                }
            }
            resolveChildren(children);
        };
        mapTree.getPlanarChildCorners(this, columnCount, rowCount, resolvePlanarChildren);
    }
    createGlobeChildren(columnCount, rowCount, resolve) {
        const level = this.quadId.level + 1;
        const column = this.quadId.column * 2;
        const row = this.quadId.row * 2;
        const mapTree = this.mapTree;
        const children = [];
        for (let j = 0; j < rowCount; j++) {
            for (let i = 0; i < columnCount; i++) {
                const quadId = new QuadId(level, column + i, row + j);
                const angleSweep = quadId.getAngleSweep(mapTree.sourceTilingScheme);
                const ellipsoidPatch = EllipsoidPatch.createCapture(this.mapTree.earthEllipsoid, angleSweep.longitude, angleSweep.latitude);
                const range = ellipsoidPatch.range();
                const rectangle = mapTree.getTileRectangle(quadId);
                const heightRange = this.mapTree.getChildHeightRange(quadId, rectangle, this);
                if (undefined !== heightRange)
                    range.expandInPlace(heightRange.high - heightRange.low);
                children.push(this.mapTree.createGlobeChild({ contentId: quadId.contentId, maximumSize: 512, range, parent: this, isLeaf: false }, quadId, range.corners(), rectangle, ellipsoidPatch, heightRange));
            }
        }
        resolve(children);
        return children;
    }
    /** @internal */
    static computeRangeCorners(corners, normal, chordHeight, result, heightRange) {
        if (result === undefined) {
            result = [];
            for (let i = 0; i < 8; i++)
                result.push(Point3d.create());
        }
        let index = 0;
        assert(corners.length === 4);
        const deltaLow = normal.scale(-chordHeight + (heightRange ? heightRange.low : 0));
        const deltaHigh = normal.scale(chordHeight + (heightRange ? heightRange.high : 0));
        for (const corner of corners)
            corner.plus(deltaLow, result[index++]);
        for (const corner of corners)
            corner.plus(deltaHigh, result[index++]);
        return result;
    }
    /** @internal */
    isRegionCulled(args) {
        return this.isContentCulled(args);
    }
    /** @internal */
    isContentCulled(args) {
        return FrustumPlanes.Containment.Outside === args.frustumPlanes.computeContainment(this.getRangeCorners(scratchCorners));
    }
    /** @internal */
    clearLayers() {
        this.clearImageryTiles();
        this._graphic = undefined;
        if (this.children)
            for (const child of this.children)
                child.clearLayers();
    }
    clearImageryTiles() {
        if (this._imageryTiles) {
            this._imageryTiles.forEach((tile) => tile.releaseMapTileUsage());
            this._imageryTiles = undefined;
        }
        if (this._hiddenTiles) {
            this._hiddenTiles = undefined;
        }
        if (this._highResolutionReplacementTiles) {
            this._highResolutionReplacementTiles = undefined;
        }
    }
    /** @internal */
    produceGraphics() {
        if (undefined !== this._graphic && this.imageryIsReady)
            return this._graphic;
        const geometry = this.renderGeometry;
        if (undefined === geometry)
            return undefined;
        const textures = this.getDrapeTextures();
        const { baseColor, baseTransparent, layerClassifiers } = this.mapTree;
        const graphic = IModelApp.renderSystem.createRealityMeshGraphic({ realityMesh: geometry, projection: this.getProjection(), tileRectangle: this.rectangle, featureTable: PackedFeatureTable.pack(this.mapLoader.featureTable), tileId: this.contentId, baseColor, baseTransparent, textures, layerClassifiers }, true);
        // If there are no layer classifiers then we can save this graphic for re-use.  If layer classifiers exist they are regenerated based on view and we must collate them with the imagery.
        if (this.imageryIsReady && 0 === this.mapTree.layerClassifiers.size)
            this._graphic = graphic;
        return graphic;
    }
    /** @internal */
    getClipShape() {
        return (this._patch instanceof PlanarTilePatch) ? this._patch.getClipShape() : [this._cornerRays[0].origin, this._cornerRays[1].origin, this._cornerRays[3].origin, this._cornerRays[2].origin];
    }
    /** @internal */
    _collectStatistics(stats) {
        super._collectStatistics(stats);
        this._renderGeometry?.collectStatistics(stats);
        if (this._mesh) {
            stats.addTerrain(this._mesh.indices.byteLength
                + this._mesh.positions.points.byteLength
                + this._mesh.uvs.points.byteLength
                + (this._mesh.normals ? this._mesh.normals.byteLength : 0));
        }
    }
    /** Height range is along with the tile corners to detect if tile intersects view frustum.
     * Range will be single value fo ron-terrain tiles -- if terrain tile is not loaded it will
     * inherit height from ancestors.
     * @internal
     */
    get heightRange() {
        if (undefined !== this._heightRange)
            return this._heightRange;
        for (let parent = this.parent; undefined !== parent; parent = parent.parent) {
            const mapParent = parent;
            if (undefined !== mapParent._heightRange)
                return mapParent._heightRange;
        }
        assert(false);
        return Range1d.createNull();
    }
    /** @internal */
    get mapTilingScheme() {
        return this.mapTree.sourceTilingScheme;
    }
    /** Adjust the minimum and maximum elevations of the terrain within this tile. */
    adjustHeights(minHeight, maxHeight) {
        if (undefined === this._heightRange)
            this._heightRange = Range1d.createXX(minHeight, maxHeight);
        else {
            this._heightRange.low = Math.max(this.heightRange.low, minHeight);
            this._heightRange.high = Math.min(this.heightRange.high, maxHeight);
        }
        if (this.rangeCorners && this._patch instanceof PlanarTilePatch)
            this._patch.getRangeCorners(this.heightRange, this.rangeCorners);
    }
    /** Obtain a [[MapTileProjection]] to project positions within this tile's area into 3d space. */
    getProjection(heightRange) {
        return this._patch instanceof PlanarTilePatch ? new PlanarProjection(this._patch, heightRange) : new EllipsoidProjection(this._patch, heightRange);
    }
    /** @internal */
    get baseImageryIsReady() {
        if (undefined !== this.mapTree.baseColor || 0 === this.mapTree.layerImageryTrees.length)
            return true;
        if (undefined === this._imageryTiles)
            return false;
        const baseTreeId = this.mapTree.layerImageryTrees[0].tree.modelId;
        return this._imageryTiles.every((imageryTile) => imageryTile.imageryTree.modelId !== baseTreeId || imageryTile.isReady);
    }
    /** @internal */
    get imageryIsReady() {
        if (undefined === this._imageryTiles)
            return 0 === this.mapTree.layerImageryTrees.length;
        return this._imageryTiles.every((tile) => tile.isReady);
    }
    /** Select secondary (imagery) tiles
     * @internal
     */
    selectSecondaryTiles(args, context) {
        if (0 === this.mapTree.layerImageryTrees.length || this.imageryIsReady)
            return;
        this.clearImageryTiles();
        this._imageryTiles = new Array();
        this._hiddenTiles = new Array();
        this._highResolutionReplacementTiles = new Array();
        for (const layerImageryTree of this.mapTree.layerImageryTrees) {
            let tmpTiles = new Array();
            const tmpLeafTiles = new Array();
            if (TileTreeLoadStatus.Loaded !== layerImageryTree.tree.selectCartoDrapeTiles(tmpTiles, tmpLeafTiles, this, args)) {
                this._imageryTiles = undefined;
                return;
            }
            // When the base layer is zoomed-in beyond it's max resolution,
            // we display leaf tiles and stretched them if needed.
            // We don't want the same behavior non-base layers, in the case,
            // the layer will simply disappear past its max resolution.
            // Note: Replacement leaf tiles are kept as a mean to determine which
            // imagery tree has reached it's maximum zoom level.
            if (layerImageryTree.baseImageryLayer) {
                tmpTiles = [...tmpTiles, ...tmpLeafTiles];
            }
            else {
                this._highResolutionReplacementTiles = [...this._highResolutionReplacementTiles, ...tmpLeafTiles];
            }
            // MapTileTree might include a non-visible imagery tree, we need to check for that.
            if (layerImageryTree.settings.visible && !layerImageryTree.settings.allSubLayersInvisible) {
                for (const imageryTile of tmpTiles) {
                    imageryTile.markMapTileUsage();
                    if (imageryTile.isReady)
                        args.markReady(imageryTile);
                    else
                        context.missing.push(imageryTile);
                    this._imageryTiles.push(imageryTile);
                }
            }
            else {
                // Even though those selected imagery tile are not visible,
                // we keep track of them for scale range reporting.
                for (const imageryTile of tmpTiles) {
                    this._hiddenTiles.push(imageryTile);
                }
            }
        }
    }
    /** The height range for terrain tiles is not known until the tiles are unloaded.  We use "ApproximateTerrainHeight" for first 6 levels but below
     * that the tiles inherit height range from parents.  This is problematic as tiles with large height range will be unnecessarily selected as
     * they apparently intersect view frustum.   To avoid this force loading of terrain tiles if they exceed "_maxParentHightDepth".
     * @internal
     */
    forceSelectRealityTile() {
        let parentHeightDepth = 0;
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        for (let parent = this; parent !== undefined && parent._heightRange === undefined; parent = parent.parent)
            parentHeightDepth++;
        return parentHeightDepth > MapTile._maxParentHeightDepth;
    }
    /** @internal */
    minimumVisibleFactor() {
        // if minimumVisibleFactor is more than 0, it stops parents from loading when children are not ready, to fill in gaps
        return 0.0;
    }
    /** @internal */
    getDrapeTextures() {
        if (undefined === this._imageryTiles)
            return undefined;
        const drapeTextures = [];
        const thisRectangle = this.loadableTerrainTile.rectangle;
        const thisDiagonal = thisRectangle.diagonal(MapTile._scratchThisDiagonal);
        const bordersNorthPole = this.quadId.bordersNorthPole(this.mapTree.sourceTilingScheme);
        const bordersSouthPole = this.quadId.bordersSouthPole(this.mapTree.sourceTilingScheme);
        for (const imageryTile of this._imageryTiles) {
            if (imageryTile.texture) {
                drapeTextures.push(this.computeDrapeTexture(thisRectangle, thisDiagonal, imageryTile, imageryTile.rectangle));
                if ((bordersNorthPole && imageryTile.quadId.bordersNorthPole(imageryTile.tilingScheme) && imageryTile.rectangle.high.y < thisRectangle.high.y) ||
                    (bordersSouthPole && imageryTile.quadId.bordersSouthPole(imageryTile.tilingScheme) && imageryTile.rectangle.low.y > thisRectangle.low.y)) {
                    // Add separate texture stretching last sliver of tile imagery to cover pole.
                    const sliverRectangle = imageryTile.rectangle.clone(MapTile._scratchRectangle1);
                    const clipRectangle = thisRectangle.clone(MapTile._scratchRectangle2);
                    const sliverHeight = sliverRectangle.high.y - sliverRectangle.low.y;
                    if (bordersSouthPole) {
                        clipRectangle.high.y = sliverRectangle.low.y;
                        sliverRectangle.low.y = thisRectangle.low.y;
                        sliverRectangle.high.y += 1 / sliverHeight;
                    }
                    else {
                        clipRectangle.low.y = sliverRectangle.high.y;
                        sliverRectangle.high.y = thisRectangle.high.y;
                        sliverRectangle.low.y -= 1 / sliverHeight;
                    }
                    drapeTextures.push(this.computeDrapeTexture(thisRectangle, thisDiagonal, imageryTile, sliverRectangle, clipRectangle));
                }
            }
            else {
                for (let parent = imageryTile.parent; undefined !== parent; parent = parent.parent) {
                    const mapTile = parent;
                    if (mapTile.texture) {
                        drapeTextures.push(this.computeDrapeTexture(thisRectangle, thisDiagonal, mapTile, mapTile.rectangle, imageryTile.rectangle));
                        break;
                    }
                }
            }
        }
        return drapeTextures.length > 0 ? drapeTextures : undefined;
    }
    computeDrapeTexture(thisRectangle, thisDiagonal, imageryTile, drapeRectangle, clipRectangle) {
        assert(imageryTile.texture !== undefined);
        // Compute transformation from the terrain tile texture coordinates (0-1) to the drape tile texture coordinates.
        const drapeDiagonal = drapeRectangle.diagonal(MapTile._scratchDrapeDiagonal);
        const translate = Vector2d.create((thisRectangle.low.x - drapeRectangle.low.x) / drapeDiagonal.x, (thisRectangle.low.y - drapeRectangle.low.y) / drapeDiagonal.y);
        const scale = Vector2d.create(thisDiagonal.x / drapeDiagonal.x, thisDiagonal.y / drapeDiagonal.y);
        const featureIndex = this.mapLoader.getFeatureIndex(imageryTile.imageryTree.modelId);
        let clipRect;
        if (undefined !== clipRectangle) {
            const intersect = clipRectangle.intersect(drapeRectangle, MapTile._scratchIntersectRange);
            assert(!intersect.isNull);
            clipRect = Range2d.createXYXY((intersect.low.x - drapeRectangle.low.x) / drapeDiagonal.x, (intersect.low.y - drapeRectangle.low.y) / drapeDiagonal.y, (intersect.high.x - drapeRectangle.low.x) / drapeDiagonal.x, (intersect.high.y - drapeRectangle.low.y) / drapeDiagonal.y);
        }
        const imageryModelId = imageryTile.tree.modelId;
        return new TerrainTexture(imageryTile.texture, featureIndex, scale, translate, drapeRectangle, this.mapTree.getLayerIndex(imageryModelId), this.mapTree.getLayerTransparency(imageryModelId), clipRect);
    }
    /** @internal */
    setContent(content) {
        if (this.quadId.level < this.maxDepth) {
            const childIds = this.quadId.getChildIds();
            for (const childId of childIds) {
                if (!this.mapLoader.isTileAvailable(childId)) {
                    this._mesh = content.terrain?.mesh; // If a child is unavailable retain mesh for upsampling.
                    break;
                }
            }
        }
        if (this.mapTree.produceGeometry) {
            const iModelTransform = this.mapTree.iModelTransform;
            const geometryTransform = content.terrain?.renderGeometry?.transform;
            const transform = geometryTransform ? iModelTransform.multiplyTransformTransform(geometryTransform) : iModelTransform;
            const polyface = content.terrain?.mesh ? RealityMeshParams.toPolyface(content.terrain.mesh, { transform }) : undefined;
            this._geometry = polyface ? { polyfaces: [polyface] } : undefined;
        }
        else {
            dispose(this._renderGeometry);
            this._renderGeometry = content.terrain?.renderGeometry;
        }
        this.everLoaded = true;
        if (undefined !== content.contentRange)
            this._contentRange = content.contentRange;
        this.setIsReady();
    }
    /** @internal */
    freeMemory() {
        // ###TODO MapTiles and ImageryMapTiles share resources and don't currently interact well with TileAdmin.freeMemory(). Opt out for now.
    }
    /** @internal */
    disposeContents() {
        super.disposeContents();
        this._renderGeometry = dispose(this._renderGeometry);
        this.clearImageryTiles();
        // Note - don't dispose of mesh - these should only ever exist on terrain leaf tile and are required by children.  Let garbage collector handle them.
    }
}
MapTile._maxParentHeightDepth = 4;
MapTile._scratchRectangle1 = MapCartoRectangle.createZero();
MapTile._scratchRectangle2 = MapCartoRectangle.createZero();
MapTile._scratchThisDiagonal = Vector2d.create();
MapTile._scratchDrapeDiagonal = Vector2d.create();
MapTile._scratchIntersectRange = Range2d.createNull();
/** A child tile that has no content of its own available. It instead produces content by up-sampling the content of an ancestor tile.
 * @internal
  */
export class UpsampledMapTile extends MapTile {
    constructor(params, mapTree, quadId, patch, rectangle, heightRange, cornerRays, loadableTile) {
        super(params, mapTree, quadId, patch, rectangle, heightRange, cornerRays);
        this._loadableTile = loadableTile;
    }
    get isUpsampled() { return true; }
    get isEmpty() { return false; }
    get loadableTile() { return this._loadableTile; }
    upsampleFromParent() {
        const parent = this.loadableTerrainTile;
        const parentMesh = parent.mesh;
        if (undefined === parentMesh) {
            return undefined;
        }
        const thisId = this.quadId, parentId = parent.quadId;
        const levelDelta = thisId.level - parentId.level;
        const thisColumn = thisId.column - (parentId.column << levelDelta);
        const thisRow = thisId.row - (parentId.row << levelDelta);
        const scale = 1.0 / (1 << levelDelta);
        const parentParameterRange = Range2d.createXYXY(scale * thisColumn, scale * thisRow, scale * (thisColumn + 1), scale * (thisRow + 1));
        const upsample = upsampleRealityMeshParams(parentMesh, parentParameterRange);
        this.adjustHeights(upsample.heightRange.low, upsample.heightRange.high);
        return upsample;
    }
    get renderGeometry() {
        if (undefined === this._renderGeometry) {
            const upsample = this.upsampleFromParent();
            const projection = this.loadableTerrainTile.getProjection(this.heightRange);
            if (upsample)
                this._renderGeometry = IModelApp.renderSystem.createTerrainMesh(upsample.mesh, projection.transformFromLocal, true);
        }
        return this._renderGeometry;
    }
    get isLoading() { return this.loadableTile.isLoading; }
    get isQueued() { return this.loadableTile.isQueued; }
    get isNotFound() { return this.loadableTile.isNotFound; }
    get isReady() { return (this._renderGeometry !== undefined || this.loadableTile.loadStatus === TileLoadStatus.Ready) && this.baseImageryIsReady; }
    markUsed(args) {
        args.markUsed(this);
        args.markUsed(this.loadableTile);
    }
}
//# sourceMappingURL=MapTile.js.map