"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Tiles
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.RealityTileLoader = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const core_geometry_1 = require("@itwin/core-geometry");
const core_common_1 = require("@itwin/core-common");
const IModelApp_1 = require("../IModelApp");
const GraphicBranch_1 = require("../render/GraphicBranch");
const Viewport_1 = require("../Viewport");
const GltfSchema_1 = require("../common/gltf/GltfSchema");
const internal_1 = require("./internal");
const defaultViewFlagOverrides = (0, internal_1.createDefaultViewFlagOverrides)({});
const scratchTileCenterWorld = new core_geometry_1.Point3d();
const scratchTileCenterView = new core_geometry_1.Point3d();
/** Serves as a "handler" for a specific type of [[TileTree]]. Its primary responsibilities involve loading tile content.
 * @internal
 */
class RealityTileLoader {
    constructor(_produceGeometry) {
        this._produceGeometry = _produceGeometry;
        this._containsPointClouds = false;
        this.preloadRealityParentDepth = IModelApp_1.IModelApp.tileAdmin.contextPreloadParentDepth;
        this.preloadRealityParentSkip = IModelApp_1.IModelApp.tileAdmin.contextPreloadParentSkip;
    }
    computeTilePriority(tile, viewports, _users) {
        // ###TODO: Handle case where tile tree reference(s) have a transform different from tree's (background map with ground bias).
        return RealityTileLoader.computeTileLocationPriority(tile, viewports, tile.tree.iModelTransform);
    }
    get wantDeduplicatedVertices() { return false; }
    get _batchType() { return core_common_1.BatchType.Primary; }
    get _loadEdges() { return true; }
    getBatchIdMap() { return undefined; }
    get isContentUnbounded() { return false; }
    get containsPointClouds() { return this._containsPointClouds; }
    get parentsAndChildrenExclusive() { return true; }
    forceTileLoad(_tile) { return false; }
    get maximumScreenSpaceError() { return undefined; }
    processSelectedTiles(selected, _args) { return selected; }
    // NB: The isCanceled arg is chiefly for tests...in usual case it just returns false if the tile is no longer in 'loading' state.
    async loadTileContent(tile, data, system, isCanceled) {
        (0, core_bentley_1.assert)(data instanceof Uint8Array);
        const blob = data;
        const streamBuffer = core_bentley_1.ByteStream.fromUint8Array(blob);
        const realityTile = tile;
        return this._produceGeometry ? this.loadGeometryFromStream(realityTile, streamBuffer, system) : this.loadGraphicsFromStream(realityTile, streamBuffer, system, isCanceled);
    }
    _getFormat(streamBuffer) {
        const position = streamBuffer.curPos;
        const format = streamBuffer.readUint32();
        streamBuffer.curPos = position;
        return format;
    }
    async loadGeometryFromStream(tile, streamBuffer, system) {
        const format = this._getFormat(streamBuffer);
        if (format !== core_common_1.TileFormat.B3dm)
            return {};
        const { is3d, yAxisUp, iModel, modelId } = tile.realityRoot;
        const reader = internal_1.B3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, tile.center, tile.transformToRoot, undefined, this.getBatchIdMap());
        if (reader)
            reader.defaultWrapMode = GltfSchema_1.GltfWrapMode.ClampToEdge;
        return { geometry: reader?.readGltfAndCreateGeometry(tile.tree.iModelTransform) };
    }
    async loadGraphicsFromStream(tile, streamBuffer, system, isCanceled) {
        const format = this._getFormat(streamBuffer);
        if (undefined === isCanceled)
            isCanceled = () => !tile.isLoading;
        const { is3d, yAxisUp, iModel, modelId } = tile.realityRoot;
        let reader;
        switch (format) {
            case core_common_1.TileFormat.IModel:
                reader = internal_1.ImdlReader.create({
                    stream: streamBuffer,
                    iModel,
                    modelId,
                    is3d,
                    system,
                    isCanceled,
                });
                break;
            case core_common_1.TileFormat.Pnts:
                this._containsPointClouds = true;
                const res = await (0, internal_1.readPointCloudTileContent)(streamBuffer, iModel, modelId, is3d, tile, system);
                let graphic = res.graphic;
                const rtcCenter = res.rtcCenter;
                if (graphic && (rtcCenter || tile.transformToRoot && !tile.transformToRoot.isIdentity)) {
                    const transformBranch = new GraphicBranch_1.GraphicBranch(true);
                    transformBranch.add(graphic);
                    let xform;
                    if (!tile.transformToRoot && rtcCenter)
                        xform = core_geometry_1.Transform.createTranslation(rtcCenter);
                    else {
                        if (rtcCenter)
                            xform = core_geometry_1.Transform.createOriginAndMatrix(rtcCenter.plus(tile.transformToRoot.origin), tile.transformToRoot.matrix);
                        else
                            xform = tile.transformToRoot;
                    }
                    graphic = system.createBranch(transformBranch, xform);
                }
                return { graphic };
            case core_common_1.TileFormat.B3dm:
                reader = internal_1.B3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, tile.center, tile.transformToRoot, isCanceled, this.getBatchIdMap(), this.wantDeduplicatedVertices);
                if (reader) {
                    // glTF spec defaults wrap mode to "repeat" but many reality tiles omit the wrap mode and should not repeat.
                    // The render system also currently only produces mip-maps for repeating textures, and we don't want mip-maps for reality tile textures.
                    (0, core_bentley_1.assert)(reader instanceof internal_1.GltfReader);
                    reader.defaultWrapMode = GltfSchema_1.GltfWrapMode.ClampToEdge;
                }
                break;
            case core_common_1.TileFormat.I3dm:
                reader = internal_1.I3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, isCanceled, undefined, this.wantDeduplicatedVertices);
                break;
            case core_common_1.TileFormat.Gltf:
                const props = internal_1.GltfReaderProps.create(streamBuffer.nextBytes(streamBuffer.arrayBuffer.byteLength), yAxisUp);
                if (props) {
                    reader = new internal_1.GltfGraphicsReader(props, {
                        iModel,
                        gltf: props.glTF,
                        contentRange: tile.contentRange,
                        transform: tile.transformToRoot,
                        hasChildren: !tile.isLeaf,
                        pickableOptions: { id: modelId },
                        idMap: this.getBatchIdMap(),
                    });
                }
                break;
            case core_common_1.TileFormat.Cmpt:
                const header = new core_common_1.CompositeTileHeader(streamBuffer);
                if (!header.isValid)
                    return {};
                const branch = new GraphicBranch_1.GraphicBranch(true);
                for (let i = 0; i < header.tileCount; i++) {
                    const tilePosition = streamBuffer.curPos;
                    streamBuffer.advance(8); // Skip magic and version.
                    const tileBytes = streamBuffer.readUint32();
                    streamBuffer.curPos = tilePosition;
                    const result = await this.loadGraphicsFromStream(tile, streamBuffer, system, isCanceled);
                    if (result.graphic)
                        branch.add(result.graphic);
                    streamBuffer.curPos = tilePosition + tileBytes;
                }
                return { graphic: branch.isEmpty ? undefined : system.createBranch(branch, core_geometry_1.Transform.createIdentity()), isLeaf: tile.isLeaf };
            default:
                (0, core_bentley_1.assert)(false, `unknown tile format ${format}`);
                break;
        }
        let content = {};
        if (undefined !== reader) {
            try {
                content = await reader.read();
                if (content.containsPointCloud)
                    this._containsPointClouds = true;
            }
            catch (_err) {
                // Failure to load should prevent us from trying to load children
                content.isLeaf = true;
            }
        }
        return content;
    }
    get viewFlagOverrides() { return defaultViewFlagOverrides; }
    static computeTileLocationPriority(tile, viewports, location) {
        // Compute a priority value for tiles that are:
        // * Closer to the eye;
        // * Closer to the center of attention (center of the screen or zoom target).
        // This way, we can load in priority tiles that are more likely to be important.
        let center;
        let minDistance = 1.0;
        const currentInputState = IModelApp_1.IModelApp.toolAdmin.currentInputState;
        const now = Date.now();
        const wheelEventRelevanceTimeout = 1000; // Wheel events older than this value will not be considered
        for (const viewport of viewports) {
            center = center ?? location.multiplyPoint3d(tile.center, scratchTileCenterWorld);
            const npc = viewport.worldToNpc(center, scratchTileCenterView);
            let focusPoint = new core_geometry_1.Point2d(0.5, 0.5);
            if (currentInputState.viewport === viewport && viewport instanceof Viewport_1.ScreenViewport) {
                // Try to get a better target point from the last zoom target
                const { lastWheelEvent } = currentInputState;
                if (lastWheelEvent !== undefined && now - lastWheelEvent.time < wheelEventRelevanceTimeout) {
                    const focusPointCandidate = core_geometry_1.Point2d.fromJSON(viewport.worldToNpc(lastWheelEvent.point));
                    if (focusPointCandidate.x > 0 && focusPointCandidate.x < 1 && focusPointCandidate.y > 0 && focusPointCandidate.y < 1)
                        focusPoint = focusPointCandidate;
                }
            }
            // NB: In NPC coords, 0 = far plane, 1 = near plane.
            const distanceToEye = 1.0 - npc.z;
            const distanceToCenter = Math.min(npc.distanceXY(focusPoint) / 0.707, 1.0); // Math.sqrt(0.5) = 0.707
            // Distance is a mix of the two previously computed values, still in range [0; 1]
            // We use this factor to determine how much the distance to the center of attention is important compared to distance to the eye
            const distanceToCenterWeight = 0.3;
            const distance = distanceToEye * (1.0 - distanceToCenterWeight) + distanceToCenter * distanceToCenterWeight;
            minDistance = Math.min(distance, minDistance);
        }
        return minDistance;
    }
}
exports.RealityTileLoader = RealityTileLoader;
//# sourceMappingURL=RealityTileLoader.js.map