/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Utils
 */
import { assert, compareBooleans, compareBooleansOrUndefined, compareNumbers, comparePossiblyUndefined, compareStrings, compareStringsOrUndefined, CompressedId64Set, Id64, } from "@itwin/core-bentley";
import { Cartographic, DefaultSupportedTypes, GeoCoordStatus, PlanarClipMaskPriority, RealityDataProvider, RealityDataSourceKey, RealityModelDisplaySettings, } from "@itwin/core-common";
import { Angle, Constant, Ellipsoid, Matrix3d, Point3d, Range3d, Ray3d, Transform, Vector3d } from "@itwin/core-geometry";
import { calculateEcefToDbTransformAtLocation } from "../BackgroundMapGeometry";
import { DisplayStyleState } from "../DisplayStyleState";
import { IModelApp } from "../IModelApp";
import { PlanarClipMaskState } from "../PlanarClipMaskState";
import { RealityDataSource } from "../RealityDataSource";
import { BatchedTileIdMap, CesiumIonAssetProvider, createClassifierTileTreeReference, createDefaultViewFlagOverrides, getGcsConverterAvailable, RealityTileLoader, RealityTileTree, TileLoadPriority, TileTreeReference, } from "./internal";
function getUrl(content) {
    return content ? (content.url ? content.url : content.uri) : undefined;
}
var RealityTreeId;
(function (RealityTreeId) {
    function compareOrigins(lhs, rhs) {
        return compareNumbers(lhs.x, rhs.x) || compareNumbers(lhs.y, rhs.y) || compareNumbers(lhs.z, rhs.z);
    }
    function compareMatrices(lhs, rhs) {
        for (let i = 0; i < 9; i++) {
            const cmp = compareNumbers(lhs.coffs[i], rhs.coffs[i]);
            if (0 !== cmp)
                return cmp;
        }
        return 0;
    }
    function compareTransforms(lhs, rhs) {
        return compareOrigins(lhs.origin, rhs.origin) || compareMatrices(lhs.matrix, rhs.matrix);
    }
    function compareRealityDataSourceKeys(lhs, rhs) {
        return compareStringsOrUndefined(lhs.id, rhs.id) || compareStringsOrUndefined(lhs.format, rhs.format) || compareStringsOrUndefined(lhs.iTwinId, rhs.iTwinId);
    }
    function compareWithoutModelId(lhs, rhs) {
        return compareRealityDataSourceKeys(lhs.rdSourceKey, rhs.rdSourceKey)
            || compareBooleans(lhs.deduplicateVertices, rhs.deduplicateVertices)
            || compareBooleansOrUndefined(lhs.produceGeometry, rhs.produceGeometry)
            || compareStringsOrUndefined(lhs.maskModelIds, rhs.maskModelIds)
            || comparePossiblyUndefined((ltf, rtf) => compareTransforms(ltf, rtf), lhs.transform, rhs.transform);
    }
    RealityTreeId.compareWithoutModelId = compareWithoutModelId;
    function compare(lhs, rhs) {
        return compareStrings(lhs.modelId, rhs.modelId) || compareWithoutModelId(lhs, rhs);
    }
    RealityTreeId.compare = compare;
})(RealityTreeId || (RealityTreeId = {}));
class RealityTreeSupplier {
    constructor() {
        this.isEcefDependent = true;
    }
    getOwner(treeId, iModel) {
        return iModel.tiles.getTileTreeOwner(treeId, this);
    }
    async createTileTree(treeId, iModel) {
        if (treeId.maskModelIds)
            await iModel.models.load(CompressedId64Set.decompressSet(treeId.maskModelIds));
        const opts = { deduplicateVertices: treeId.deduplicateVertices, produceGeometry: treeId.produceGeometry };
        return RealityModelTileTree.createRealityModelTileTree(treeId.rdSourceKey, iModel, treeId.modelId, treeId.transform, opts);
    }
    compareTileTreeIds(lhs, rhs) {
        return RealityTreeId.compare(lhs, rhs);
    }
    findCompatibleContextRealityModelId(id, style) {
        const owners = style.iModel.tiles.getTreeOwnersForSupplier(this);
        for (const owner of owners) {
            // Find an existing tree with the same Id, ignoring its model Id.
            if (0 === RealityTreeId.compareWithoutModelId(id, owner.id)) {
                const modelId = owner.id.modelId;
                assert(undefined !== modelId);
                // If the model Id is unused by any other context reality model in the view and does not identify a persistent reality model, use it.
                if (Id64.isTransientId64(modelId) && !style.contextRealityModelStates.some((model) => model.modelId === modelId))
                    return modelId;
            }
        }
        return undefined;
    }
}
const realityTreeSupplier = new RealityTreeSupplier();
/** @internal */
export function createRealityTileTreeReference(props) {
    return new RealityTreeReference(props);
}
const zeroPoint = Point3d.createZero();
const earthEllipsoid = Ellipsoid.createCenterMatrixRadii(zeroPoint, undefined, Constant.earthRadiusWGS84.equator, Constant.earthRadiusWGS84.equator, Constant.earthRadiusWGS84.polar);
const scratchRay = Ray3d.createXAxis();
/** @internal */
export class RealityTileRegion {
    constructor(values) {
        this.minLongitude = values.minLongitude;
        this.minLatitude = values.minLatitude;
        this.minHeight = values.minHeight;
        this.maxLongitude = values.maxLongitude;
        this.maxLatitude = values.maxLatitude;
        this.maxHeight = values.maxHeight;
    }
    static create(region) {
        const minHeight = region[4];
        const maxHeight = region[5];
        const minLongitude = region[0];
        const maxLongitude = region[2];
        const minLatitude = Cartographic.parametricLatitudeFromGeodeticLatitude(region[1]);
        const maxLatitude = Cartographic.parametricLatitudeFromGeodeticLatitude(region[3]);
        return new RealityTileRegion({ minLongitude, minLatitude, minHeight, maxLongitude, maxLatitude, maxHeight });
    }
    static isGlobal(boundingVolume) {
        return Array.isArray(boundingVolume?.region) && (boundingVolume.region[2] - boundingVolume.region[0]) > Angle.piRadians && (boundingVolume.region[3] - boundingVolume.region[1]) > Angle.piOver2Radians;
    }
    getRange() {
        const maxAngle = Math.max(Math.abs(this.maxLatitude - this.minLatitude), Math.abs(this.maxLongitude - this.minLongitude));
        let corners;
        let range;
        if (maxAngle < Math.PI / 8) {
            corners = new Array(8);
            const chordTolerance = (1 - Math.cos(maxAngle / 2)) * Constant.earthRadiusWGS84.polar;
            const addEllipsoidCorner = ((long, lat, index) => {
                const ray = earthEllipsoid.radiansToUnitNormalRay(long, lat, scratchRay);
                corners[index] = ray.fractionToPoint(this.minHeight - chordTolerance);
                corners[index + 4] = ray.fractionToPoint(this.maxHeight + chordTolerance);
            });
            addEllipsoidCorner(this.minLongitude, this.minLatitude, 0);
            addEllipsoidCorner(this.minLongitude, this.maxLatitude, 1);
            addEllipsoidCorner(this.maxLongitude, this.minLatitude, 2);
            addEllipsoidCorner(this.maxLongitude, this.maxLatitude, 3);
            range = Range3d.createArray(corners);
        }
        else {
            const minEq = Constant.earthRadiusWGS84.equator + this.minHeight, maxEq = Constant.earthRadiusWGS84.equator + this.maxHeight;
            const minEllipsoid = Ellipsoid.createCenterMatrixRadii(zeroPoint, undefined, minEq, minEq, Constant.earthRadiusWGS84.polar + this.minHeight);
            const maxEllipsoid = Ellipsoid.createCenterMatrixRadii(zeroPoint, undefined, maxEq, maxEq, Constant.earthRadiusWGS84.polar + this.maxHeight);
            range = minEllipsoid.patchRangeStartEndRadians(this.minLongitude, this.maxLongitude, this.minLatitude, this.maxLatitude);
            range.extendRange(maxEllipsoid.patchRangeStartEndRadians(this.minLongitude, this.maxLongitude, this.minLatitude, this.maxLatitude));
        }
        return { range, corners };
    }
}
/** @internal */
export class RealityModelTileUtils {
    static rangeFromBoundingVolume(boundingVolume) {
        if (undefined === boundingVolume)
            return undefined;
        let corners;
        let range;
        if (undefined !== boundingVolume.box) {
            const box = boundingVolume.box;
            const center = Point3d.create(box[0], box[1], box[2]);
            const ux = Vector3d.create(box[3], box[4], box[5]);
            const uy = Vector3d.create(box[6], box[7], box[8]);
            const uz = Vector3d.create(box[9], box[10], box[11]);
            corners = new Array();
            for (let j = 0; j < 2; j++) {
                for (let k = 0; k < 2; k++) {
                    for (let l = 0; l < 2; l++) {
                        corners.push(center.plus3Scaled(ux, (j ? -1.0 : 1.0), uy, (k ? -1.0 : 1.0), uz, (l ? -1.0 : 1.0)));
                    }
                }
            }
            range = Range3d.createArray(corners);
        }
        else if (Array.isArray(boundingVolume.sphere)) {
            const sphere = boundingVolume.sphere;
            const center = Point3d.create(sphere[0], sphere[1], sphere[2]);
            const radius = sphere[3];
            range = Range3d.createXYZXYZ(center.x - radius, center.y - radius, center.z - radius, center.x + radius, center.y + radius, center.z + radius);
        }
        else if (Array.isArray(boundingVolume.region)) {
            const region = RealityTileRegion.create(boundingVolume.region);
            const regionRange = region.getRange();
            return { range: regionRange.range, corners: regionRange.corners, region };
        }
        return range ? { range, corners } : undefined;
    }
    static maximumSizeFromGeometricTolerance(range, geometricError) {
        const minToleranceRatio = true === IModelApp.renderSystem.isMobile ? IModelApp.tileAdmin.mobileRealityTileMinToleranceRatio : 1.0; // Nominally the error on screen size of a tile.  Increasing generally increases performance (fewer draw calls) at expense of higher load times.
        // NB: We increase the above minToleranceRatio on mobile devices in order to help avoid pruning too often based on the memory threshold for
        // pruning currently used by reality tile trees on mobile.
        return minToleranceRatio * range.diagonal().magnitude() / geometricError;
    }
    static transformFromJson(jTrans) {
        return (jTrans === undefined) ? Transform.createIdentity() : Transform.createOriginAndMatrix(Point3d.create(jTrans[12], jTrans[13], jTrans[14]), Matrix3d.createRowValues(jTrans[0], jTrans[4], jTrans[8], jTrans[1], jTrans[5], jTrans[9], jTrans[2], jTrans[6], jTrans[10]));
    }
}
/** @internal */
var SMTextureType;
(function (SMTextureType) {
    SMTextureType[SMTextureType["None"] = 0] = "None";
    SMTextureType[SMTextureType["Embedded"] = 1] = "Embedded";
    SMTextureType[SMTextureType["Streaming"] = 2] = "Streaming";
})(SMTextureType || (SMTextureType = {}));
/** @internal */
class RealityModelTileTreeProps {
    constructor(json, root, rdSource, tilesetToDbTransform, tilesetToEcef) {
        this.tilesetToEcef = tilesetToEcef;
        this.doDrapeBackgroundMap = false;
        this.yAxisUp = false;
        this.tilesetJson = root;
        this.rdSource = rdSource;
        this.location = tilesetToDbTransform;
        this.doDrapeBackgroundMap = (json.root && json.root.SMMasterHeader && SMTextureType.Streaming === json.root.SMMasterHeader.IsTextured);
        if (json.asset.gltfUpAxis === undefined || json.asset.gltfUpAxis === "y" || json.asset.gltfUpAxis === "Y")
            this.yAxisUp = true;
    }
}
class RealityModelTileTreeParams {
    get location() { return this.loader.tree.location; }
    get yAxisUp() { return this.loader.tree.yAxisUp; }
    get priority() { return this.loader.priority; }
    constructor(tileTreeId, iModel, modelId, loader, gcsConverterAvailable, rootToEcef) {
        this.gcsConverterAvailable = gcsConverterAvailable;
        this.rootToEcef = rootToEcef;
        this.is3d = true;
        this.loader = loader;
        this.id = tileTreeId;
        this.modelId = modelId;
        this.iModel = iModel;
        const refine = loader.tree.tilesetJson.refine;
        this.rootTile = new RealityModelTileProps({
            json: loader.tree.tilesetJson,
            id: "",
            // If not specified explicitly, additiveRefinement is inherited from parent tile.
            additiveRefinement: undefined !== refine ? "ADD" === refine : undefined,
            usesGeometricError: loader.tree.rdSource.usesGeometricError,
        });
    }
}
/** @internal */
class RealityModelTileProps {
    constructor(args) {
        this.contentId = args.id;
        this.parent = args.parent;
        this.transformToRoot = args.transformToRoot;
        this.additiveRefinement = args.additiveRefinement;
        const json = args.json;
        const boundingVolume = RealityModelTileUtils.rangeFromBoundingVolume(json.boundingVolume);
        if (boundingVolume) {
            this.range = boundingVolume.range;
            this.rangeCorners = boundingVolume.corners;
            this.region = boundingVolume?.region;
        }
        else {
            this.range = Range3d.createNull();
            assert(false, "Unbounded tile");
        }
        this.isLeaf = !Array.isArray(json.children) || 0 === json.children.length;
        const hasContents = undefined !== getUrl(json.content);
        if (hasContents)
            this.contentRange = RealityModelTileUtils.rangeFromBoundingVolume(json.content.boundingVolume)?.range;
        else {
            // A node without content should probably be selectable even if not additive refinement - But restrict it to that case here
            // to avoid potential problems with existing reality models, but still avoid overselection in the OSM world building set.
            if (this.additiveRefinement || args.parent?.additiveRefinement)
                this.noContentButTerminateOnSelection = true;
        }
        this.maximumSize = (this.noContentButTerminateOnSelection || hasContents) ? RealityModelTileUtils.maximumSizeFromGeometricTolerance(Range3d.fromJSON(this.range), json.geometricError) : 0;
        if (args.usesGeometricError)
            this.geometricError = json.geometricError;
    }
}
/** @internal */
class FindChildResult {
    constructor(id, json, transformToRoot) {
        this.id = id;
        this.json = json;
        this.transformToRoot = transformToRoot;
    }
}
/** @internal */
function assembleUrl(prefix, url) {
    if (url.startsWith("/")) {
        // Relative to base origin, not to parent tile
        return url.substring(1);
    }
    if (url.startsWith("./")) {
        // Relative to parent tile
        url = url.substring(2);
    }
    else {
        const prefixParts = prefix.split("/");
        prefixParts.pop();
        while (url.startsWith("../")) {
            prefixParts.pop();
            url = url.substring(3);
        }
        prefixParts.push("");
        prefix = prefixParts.join("/");
    }
    return prefix + url;
}
/** @internal */
function addUrlPrefix(subTree, prefix) {
    if (undefined === subTree)
        return;
    if (undefined !== subTree.content) {
        if (undefined !== subTree.content.url)
            subTree.content.url = assembleUrl(prefix, subTree.content.url);
        else if (undefined !== subTree.content.uri)
            subTree.content.uri = assembleUrl(prefix, subTree.content.uri);
    }
    if (undefined !== subTree.children)
        for (const child of subTree.children)
            addUrlPrefix(child, prefix);
}
/** @internal */
async function expandSubTree(root, rdsource) {
    const childUrl = getUrl(root.content);
    if (undefined === childUrl || "tileset" !== rdsource.getTileContentType(childUrl))
        return root;
    const subTree = await rdsource.getTileJson(childUrl);
    const prefixIndex = childUrl.lastIndexOf("/");
    if (prefixIndex > 0)
        addUrlPrefix(subTree.root, childUrl.substring(0, prefixIndex + 1));
    return subTree.root;
}
/** @internal */
class RealityModelTileLoader extends RealityTileLoader {
    constructor(tree, batchedIdMap, opts) {
        super(opts?.produceGeometry ?? false);
        this.tree = tree;
        this._batchedIdMap = batchedIdMap;
        this._deduplicateVertices = opts?.deduplicateVertices ?? false;
        let clipVolume;
        if (RealityTileRegion.isGlobal(tree.tilesetJson.boundingVolume))
            clipVolume = false;
        this._viewFlagOverrides = createDefaultViewFlagOverrides({ lighting: true, clipVolume });
        // Display edges if they are present (Cesium outline extension) and enabled for view.
        this._viewFlagOverrides.visibleEdges = undefined;
        this._viewFlagOverrides.hiddenEdges = undefined;
        // Allow wiremesh display.
        this._viewFlagOverrides.wiremesh = undefined;
    }
    get doDrapeBackgroundMap() { return this.tree.doDrapeBackgroundMap; }
    get wantDeduplicatedVertices() { return this._deduplicateVertices; }
    get maxDepth() { return Number.MAX_SAFE_INTEGER; }
    get minDepth() { return 0; }
    get priority() { return TileLoadPriority.Context; }
    getBatchIdMap() { return this._batchedIdMap; }
    get clipLowResolutionTiles() { return true; }
    get viewFlagOverrides() { return this._viewFlagOverrides; }
    get maximumScreenSpaceError() {
        return this.tree.rdSource.maximumScreenSpaceError;
    }
    async loadChildren(tile) {
        const props = await this.getChildrenProps(tile);
        if (undefined === props)
            return undefined;
        const children = [];
        for (const prop of props)
            children.push(tile.realityRoot.createTile(prop));
        return children;
    }
    async getChildrenProps(parent) {
        const props = [];
        const thisId = parent.contentId;
        const prefix = thisId.length ? `${thisId}_` : "";
        const findResult = await this.findTileInJson(this.tree.tilesetJson, thisId, "", undefined);
        if (undefined !== findResult && Array.isArray(findResult.json.children)) {
            for (let i = 0; i < findResult.json.children.length; i++) {
                const childId = prefix + i;
                const foundChild = await this.findTileInJson(this.tree.tilesetJson, childId, "", undefined);
                if (undefined !== foundChild) {
                    const refine = foundChild.json.refine;
                    props.push(new RealityModelTileProps({
                        json: foundChild.json,
                        parent,
                        id: foundChild.id,
                        transformToRoot: foundChild.transformToRoot,
                        // If not specified explicitly, additiveRefinement is inherited from parent tile.
                        additiveRefinement: undefined !== refine ? refine === "ADD" : undefined,
                        usesGeometricError: this.tree.rdSource.usesGeometricError,
                    }));
                }
            }
        }
        return props;
    }
    getRequestChannel(_tile) {
        // ###TODO: May want to extract the hostname from the URL.
        return IModelApp.tileAdmin.channels.getForHttp("itwinjs-reality-model");
    }
    async requestTileContent(tile, isCanceled) {
        const foundChild = await this.findTileInJson(this.tree.tilesetJson, tile.contentId, "");
        if (undefined === foundChild || undefined === foundChild.json.content || isCanceled())
            return undefined;
        return this.tree.rdSource.getTileContent(getUrl(foundChild.json.content));
    }
    async findTileInJson(tilesetJson, id, parentId, transformToRoot) {
        if (id.length === 0)
            return new FindChildResult(id, tilesetJson, transformToRoot); // Root.
        const separatorIndex = id.indexOf("_");
        const childId = (separatorIndex < 0) ? id : id.substring(0, separatorIndex);
        const childIndex = parseInt(childId, 10);
        if (isNaN(childIndex) || tilesetJson === undefined || tilesetJson.children === undefined || childIndex >= tilesetJson.children.length) {
            assert(false, "scalable mesh child not found.");
            return undefined;
        }
        const foundChild = tilesetJson.children[childIndex];
        const thisParentId = parentId.length ? (`${parentId}_${childId}`) : childId;
        if (foundChild.transform) {
            const thisTransform = RealityModelTileUtils.transformFromJson(foundChild.transform);
            transformToRoot = transformToRoot ? transformToRoot.multiplyTransformTransform(thisTransform) : thisTransform;
        }
        if (separatorIndex >= 0) {
            return this.findTileInJson(foundChild, id.substring(separatorIndex + 1), thisParentId, transformToRoot);
        }
        tilesetJson.children[childIndex] = await expandSubTree(foundChild, this.tree.rdSource);
        return new FindChildResult(thisParentId, tilesetJson.children[childIndex], transformToRoot);
    }
}
/** @internal */
export class RealityModelTileTree extends RealityTileTree {
    constructor(params) {
        super(params);
        this._isContentUnbounded = this.rootTile.contentRange.diagonal().magnitude() > 2 * Constant.earthRadiusWGS84.equator;
        if (!this.isContentUnbounded && !this.rootTile.contentRange.isNull) {
            const worldContentRange = this.iModelTransform.multiplyRange(this.rootTile.contentRange);
            /* eslint-disable-next-line deprecation/deprecation */
            this.iModel.expandDisplayedExtents(worldContentRange);
        }
    }
    get isContentUnbounded() { return this._isContentUnbounded; }
}
/** @internal */
// eslint-disable-next-line no-redeclare
(function (RealityModelTileTree) {
    class Reference extends TileTreeReference {
        // public get classifiers(): SpatialClassifiers | undefined { return undefined !== this._classifier ? this._classifier.classifiers : undefined; }
        get planarClipMask() { return this._planarClipMask; }
        set planarClipMask(planarClipMask) { this._planarClipMask = planarClipMask; }
        get planarClipMaskPriority() {
            if (this._planarClipMask?.settings.priority !== undefined)
                return this._planarClipMask.settings.priority;
            return this.isGlobal ? PlanarClipMaskPriority.GlobalRealityModel : PlanarClipMaskPriority.RealityModel;
        }
        get maskModelIds() {
            return this._planarClipMask?.settings.compressedModelIds;
        }
        constructor(props) {
            super();
            this._name = undefined !== props.name ? props.name : "";
            let transform;
            if (undefined !== props.tilesetToDbTransform) {
                const tf = Transform.fromJSON(props.tilesetToDbTransform);
                if (!tf.isIdentity)
                    transform = tf;
                this._transform = transform;
            }
            this._iModel = props.iModel;
            this._source = props.source;
            this._getDisplaySettings = () => props.getDisplaySettings();
            if (props.planarClipMask)
                this._planarClipMask = PlanarClipMaskState.create(props.planarClipMask);
            if (undefined !== props.classifiers)
                this._classifier = createClassifierTileTreeReference(props.classifiers, this, props.iModel, props.source);
        }
        get planarClassifierTreeRef() { return this._classifier && this._classifier.activeClassifier && this._classifier.isPlanar ? this._classifier : undefined; }
        unionFitRange(union) {
            const contentRange = this.computeWorldContentRange();
            if (!contentRange.isNull && contentRange.diagonal().magnitude() < Constant.earthRadiusWGS84.equator)
                union.extendRange(contentRange);
        }
        get isGlobal() {
            if (undefined === this._isGlobal) {
                const range = this.computeWorldContentRange();
                if (!range.isNull)
                    this._isGlobal = range.diagonal().magnitude() > 2 * Constant.earthRadiusWGS84.equator;
            }
            return this._isGlobal === undefined ? false : this._isGlobal;
        }
        addToScene(context) {
            // NB: The classifier must be added first, so we can find it when adding our own tiles.
            if (this._classifier && this._classifier.activeClassifier)
                this._classifier.addToScene(context);
            this.addPlanarClassifierOrMaskToScene(context);
            super.addToScene(context);
        }
        addPlanarClassifierOrMaskToScene(context) {
            // A planarClassifier is required if there is a classification tree OR planar masking is required.
            const classifierTree = this.planarClassifierTreeRef;
            const planarClipMask = this._planarClipMask ?? context.viewport.displayStyle.getPlanarClipMaskState(this.modelId);
            if (!classifierTree && !planarClipMask)
                return;
            if (classifierTree && !classifierTree.treeOwner.load())
                return;
            context.addPlanarClassifier(this.modelId, classifierTree, planarClipMask);
        }
        discloseTileTrees(trees) {
            super.discloseTileTrees(trees);
            if (undefined !== this._classifier)
                this._classifier.discloseTileTrees(trees);
            if (undefined !== this._mapDrapeTree)
                this._mapDrapeTree.discloseTileTrees(trees);
            if (undefined !== this._planarClipMask)
                this._planarClipMask.discloseTileTrees(trees);
        }
        collectStatistics(stats) {
            super.collectStatistics(stats);
            const tree = undefined !== this._classifier ? this._classifier.treeOwner.tileTree : undefined;
            if (undefined !== tree)
                tree.collectStatistics(stats);
        }
        createDrawArgs(context) {
            const args = super.createDrawArgs(context);
            if (args) {
                args.graphics.realityModelDisplaySettings = this._getDisplaySettings();
                args.graphics.realityModelRange = args.tree.rootTile.contentRange;
                if (args.tree instanceof RealityTileTree) {
                    const maxSSE = args.tree.loader.maximumScreenSpaceError;
                    if (undefined !== maxSSE)
                        args.maximumScreenSpaceError = maxSSE;
                }
            }
            return args;
        }
    }
    RealityModelTileTree.Reference = Reference;
    async function createRealityModelTileTree(rdSourceKey, iModel, modelId, tilesetToDb, opts) {
        const rdSource = await RealityDataSource.fromKey(rdSourceKey, iModel.iTwinId);
        // If we can get a valid connection from sourceKey, returns the tile tree
        if (rdSource) {
            // Serialize the reality data source key into a string to uniquely identify this tile tree
            const tileTreeId = RealityDataSourceKey.convertToString(rdSource.key);
            if (tileTreeId === undefined)
                return undefined;
            const props = await getTileTreeProps(rdSource, tilesetToDb, iModel);
            const loader = new RealityModelTileLoader(props, new BatchedTileIdMap(iModel), opts);
            const gcsConverterAvailable = await getGcsConverterAvailable(iModel);
            const params = new RealityModelTileTreeParams(tileTreeId, iModel, modelId, loader, gcsConverterAvailable, props.tilesetToEcef);
            return new RealityModelTileTree(params);
        }
        return undefined;
    }
    RealityModelTileTree.createRealityModelTileTree = createRealityModelTileTree;
    async function getTileTreeProps(rdSource, tilesetToDbJson, iModel) {
        const json = await rdSource.getRootDocument(iModel.iTwinId);
        let rootTransform = iModel.ecefLocation ? iModel.getMapEcefToDb(0) : Transform.createIdentity();
        const geoConverter = iModel.noGcsDefined ? undefined : iModel.geoServices.getConverter("WGS84");
        if (geoConverter !== undefined) {
            let realityTileRange = RealityModelTileUtils.rangeFromBoundingVolume(json.root.boundingVolume).range;
            if (json.root.transform) {
                const realityToEcef = RealityModelTileUtils.transformFromJson(json.root.transform);
                realityTileRange = realityToEcef.multiplyRange(realityTileRange);
            }
            if (iModel.ecefLocation) {
                // In initial publishing version the iModel ecef Transform was used to locate the reality model.
                // This would work well only for tilesets published from that iModel but for iModels the ecef transform is calculated
                // at the center of the project extents and the reality model location may differ greatly, and the curvature of the earth
                // could introduce significant errors.
                // The publishing was modified to calculate the ecef transform at the reality model range center and at the same time the "iModelPublishVersion"
                // member was added to the root object.  In order to continue to locate reality models published from older versions at the
                // project extents center we look for Tileset version 0.0 and no root.iModelVersion.
                const ecefOrigin = realityTileRange.localXYZToWorld(.5, .5, .5);
                const dbOrigin = rootTransform.multiplyPoint3d(ecefOrigin);
                const realityOriginToProjectDistance = iModel.projectExtents.distanceToPoint(dbOrigin);
                const maxProjectDistance = 1E5; // Only use the project GCS projection if within 100KM of the project.   Don't attempt to use GCS if global reality model or in another locale - Results will be unreliable.
                if (realityOriginToProjectDistance < maxProjectDistance && json.asset?.version !== "0.0" || undefined !== json.root?.iModelPublishVersion) {
                    const cartographicOrigin = Cartographic.fromEcef(ecefOrigin);
                    if (cartographicOrigin !== undefined) {
                        const geoOrigin = Point3d.create(cartographicOrigin.longitudeDegrees, cartographicOrigin.latitudeDegrees, cartographicOrigin.height);
                        const response = await geoConverter.getIModelCoordinatesFromGeoCoordinates([geoOrigin]);
                        if (response.iModelCoords[0].s === GeoCoordStatus.Success) {
                            const ecefToDb = await calculateEcefToDbTransformAtLocation(Point3d.fromJSON(response.iModelCoords[0].p), iModel);
                            if (ecefToDb)
                                rootTransform = ecefToDb;
                        }
                    }
                }
            }
        }
        let tilesetToEcef = Transform.createIdentity();
        if (json.root.transform) {
            tilesetToEcef = RealityModelTileUtils.transformFromJson(json.root.transform);
            rootTransform = rootTransform.multiplyTransformTransform(tilesetToEcef);
        }
        if (undefined !== tilesetToDbJson)
            rootTransform = Transform.fromJSON(tilesetToDbJson).multiplyTransformTransform(rootTransform);
        const root = await expandSubTree(json.root, rdSource);
        return new RealityModelTileTreeProps(json, root, rdSource, rootTransform, tilesetToEcef);
    }
})(RealityModelTileTree || (RealityModelTileTree = {}));
/** Supplies a reality data [[TileTree]] from a URL. May be associated with a persistent [[GeometricModelState]], or attached at run-time via a [[ContextRealityModelState]].
 * @internal
 */
export class RealityTreeReference extends RealityModelTileTree.Reference {
    constructor(props) {
        super(props);
        this._produceGeometry = props.produceGeometry;
        // Maybe we should throw if both props.rdSourceKey && props.url are undefined
        if (props.rdSourceKey)
            this._rdSourceKey = props.rdSourceKey;
        else
            this._rdSourceKey = RealityDataSource.createKeyFromUrl(props.url ?? "", RealityDataProvider.ContextShare);
        if (this._produceGeometry)
            this.collectTileGeometry = (collector) => this._collectTileGeometry(collector);
        let modelId = props.modelId;
        if (undefined === modelId && this._source instanceof DisplayStyleState) {
            const treeId = this.createTreeId(Id64.invalid);
            modelId = realityTreeSupplier.findCompatibleContextRealityModelId(treeId, this._source);
        }
        this._modelId = modelId ?? props.iModel.transientIds.getNext();
    }
    get modelId() { return this._modelId; }
    createTreeId(modelId) {
        return {
            rdSourceKey: this._rdSourceKey,
            transform: this._transform,
            modelId,
            maskModelIds: this.maskModelIds,
            deduplicateVertices: this._wantWiremesh,
            produceGeometry: this._produceGeometry,
        };
    }
    get treeOwner() {
        return realityTreeSupplier.getOwner(this.createTreeId(this.modelId), this._iModel);
    }
    _createGeometryTreeReference() {
        const ref = new RealityTreeReference({
            iModel: this._iModel,
            modelId: this.modelId,
            source: this._source,
            rdSourceKey: this._rdSourceKey,
            name: this._name,
            produceGeometry: true,
            getDisplaySettings: () => RealityModelDisplaySettings.defaults,
        });
        assert(undefined !== ref.collectTileGeometry);
        return ref;
    }
    get _wantWiremesh() {
        return this._source.viewFlags.wiremesh;
    }
    get castsShadows() {
        return true;
    }
    get _isLoadingComplete() {
        return !this._mapDrapeTree || this._mapDrapeTree.isLoadingComplete;
    }
    createDrawArgs(context) {
        // For global reality models (OSM Building layer only) - offset the reality model by the BIM elevation bias.  This would not be necessary
        // if iModels had their elevation set correctly but unfortunately many GCS erroneously report Sea (Geoid) elevation rather than
        // Geodetic.
        const tree = this.treeOwner.load();
        if (undefined === tree)
            return undefined;
        const drawArgs = super.createDrawArgs(context);
        if (drawArgs !== undefined && this._iModel.isGeoLocated && tree.isContentUnbounded) {
            const elevationBias = context.viewport.view.displayStyle.backgroundMapElevationBias;
            if (undefined !== elevationBias)
                drawArgs.location.origin.z -= elevationBias;
        }
        return drawArgs;
    }
    addToScene(context) {
        const tree = this.treeOwner.tileTree;
        if (undefined !== tree && context.viewport.iModel.isGeoLocated && tree.loader.doDrapeBackgroundMap) {
            // NB: We save this off strictly so that discloseTileTrees() can find it...better option?
            this._mapDrapeTree = context.viewport.backgroundDrapeMap;
            context.addBackgroundDrapedModel(this, undefined);
        }
        super.addToScene(context);
    }
    canSupplyToolTip(hit) {
        const classifier = this._classifier?.activeClassifier?.tileTreeReference;
        if (classifier && classifier.canSupplyToolTip(hit)) {
            return true;
        }
        const tree = this.treeOwner.tileTree;
        return tree instanceof RealityTileTree && hit.iModel === tree.iModel && undefined !== tree.batchTableProperties?.getFeatureProperties(hit.sourceId);
    }
    async getToolTip(hit) {
        const tooltip = this._getToolTip(hit);
        if (tooltip) {
            return tooltip;
        }
        const classifierTree = this._classifier?.activeClassifier?.tileTreeReference;
        if (classifierTree) {
            return classifierTree.getToolTip(hit);
        }
        return undefined;
    }
    _getToolTip(hit) {
        const tree = this.treeOwner.tileTree;
        if (!(tree instanceof RealityTileTree) || hit.iModel !== tree.iModel)
            return undefined;
        const batch = tree.batchTableProperties?.getFeatureProperties(hit.sourceId);
        if (undefined === batch && tree.modelId !== hit.sourceId)
            return undefined;
        const strings = [];
        const loader = tree.loader;
        const type = loader.tree.rdSource.realityDataType;
        // If a type is specified, display it
        if (type !== undefined) {
            // Case insensitive
            switch (type.toUpperCase()) {
                case DefaultSupportedTypes.RealityMesh3dTiles.toUpperCase():
                    strings.push(IModelApp.localization.getLocalizedString("iModelJs:RealityModelTypes.RealityMesh3DTiles"));
                    break;
                case DefaultSupportedTypes.Terrain3dTiles.toUpperCase():
                    strings.push(IModelApp.localization.getLocalizedString("iModelJs:RealityModelTypes.Terrain3DTiles"));
                    break;
                case DefaultSupportedTypes.Cesium3dTiles.toUpperCase():
                    strings.push(IModelApp.localization.getLocalizedString("iModelJs:RealityModelTypes.Cesium3DTiles"));
                    break;
            }
        }
        if (this._name) {
            strings.push(`${IModelApp.localization.getLocalizedString("iModelJs:TooltipInfo.Name")} ${this._name}`);
        }
        else {
            const cesiumAsset = this._rdSourceKey.provider === RealityDataProvider.CesiumIonAsset ? CesiumIonAssetProvider.parseCesiumUrl(this._rdSourceKey.id) : undefined;
            strings.push(cesiumAsset ? `Cesium Asset: ${cesiumAsset.id}` : this._rdSourceKey.id);
        }
        if (batch !== undefined)
            for (const key of Object.keys(batch))
                if (-1 === key.indexOf("#")) // Avoid internal cesium
                    strings.push(`${key}: ${JSON.stringify(batch[key])}`);
        const div = document.createElement("div");
        div.innerHTML = strings.join("<br>");
        return div;
    }
    addLogoCards(cards) {
        if (this._rdSourceKey.provider === RealityDataProvider.CesiumIonAsset && !cards.dataset.openStreetMapLogoCard) {
            cards.dataset.openStreetMapLogoCard = "true";
            cards.appendChild(IModelApp.makeLogoCard({ heading: "OpenStreetMap", notice: `&copy;<a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> ${IModelApp.localization.getLocalizedString("iModelJs:BackgroundMap:OpenStreetMapContributors")}` }));
        }
    }
}
//# sourceMappingURL=RealityModelTileTree.js.map