/*---------------------------------------------------------------------------------------------
* 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, comparePossiblyUndefined, compareStrings, } from "@itwin/core-bentley";
import { BatchType, compareIModelTileTreeIds, FeatureAppearanceProvider, iModelTileTreeIdToString, RenderMode, SpatialClassifier, } from "@itwin/core-common";
import { Range3d, StringifiedClipVector, Transform } from "@itwin/core-geometry";
import { IModelApp } from "../IModelApp";
import { formatAnimationBranchId } from "../render/GraphicBranch";
import { AnimationNodeId } from "../common/internal/render/AnimationNodeId";
import { IModelTileTree, iModelTileTreeParamsFromJSON, MapLayerTileTreeReference, TileGraphicType, TileTreeReference, } from "./internal";
class PlanProjectionTileTree extends IModelTileTree {
    constructor(params, treeId, baseElevation) {
        super(params, treeId);
        this.baseElevation = baseElevation;
    }
}
class PrimaryTreeSupplier {
    constructor() {
    }
    compareTileTreeIds(lhs, rhs) {
        // NB: we don't compare isPlanProjection or is3d - they should always have the same value for a given modelId.
        return compareStrings(lhs.modelId, rhs.modelId) || compareIModelTileTreeIds(lhs.treeId, rhs.treeId)
            || comparePossiblyUndefined((x, y) => x.compareTo(y), lhs.timeline, rhs.timeline);
    }
    async createTileTree(id, iModel) {
        const treeId = id.treeId;
        const idStr = iModelTileTreeIdToString(id.modelId, treeId, IModelApp.tileAdmin);
        const props = await IModelApp.tileAdmin.requestTileTreeProps(iModel, idStr);
        // ###TODO remove restriction that animated tile trees can't contained instanced geometry.
        const isAnimated = undefined !== treeId.animationId || undefined !== id.timeline;
        const allowInstancing = !isAnimated && !treeId.enforceDisplayPriority && !treeId.sectionCut;
        const options = {
            edges: treeId.edges,
            allowInstancing,
            is3d: id.is3d,
            batchType: BatchType.Primary,
            timeline: id.timeline,
        };
        const params = iModelTileTreeParamsFromJSON(props, iModel, id.modelId, options);
        if (!id.isPlanProjection)
            return new IModelTileTree(params, id.treeId);
        let elevation = 0;
        try {
            const ranges = await iModel.models.queryExtents(id.modelId);
            if (1 === ranges.length) {
                const range = Range3d.fromJSON(ranges[0].extents);
                const lo = range.low.z;
                const hi = range.high.z;
                if (lo <= hi)
                    elevation = (lo + hi) / 2;
            }
        }
        catch {
            //
        }
        return new PlanProjectionTileTree(params, id.treeId, elevation);
    }
    getOwner(id, iModel) {
        return iModel.tiles.getTileTreeOwner(id, this);
    }
    addModelsAnimatedByScript(modelIds, scriptSourceId, trees) {
        // Note: This is invoked when an element hosting a schedule script is updated - it doesn't care about frontend schedule scripts.
        for (const tree of trees)
            if (scriptSourceId === tree.id.treeId.animationId)
                modelIds.add(tree.id.modelId);
    }
    addSpatialModels(modelIds, trees) {
        for (const tree of trees)
            if (tree.id.is3d)
                modelIds.add(tree.id.modelId);
    }
}
const primaryTreeSupplier = new PrimaryTreeSupplier();
/** Find all extant tile trees associated with the specified model Ids and dispose of them.
 * This is used by BriefcaseConnection when a GraphicalEditingScope is exited or after a change to the models' geometry guids
 * is committed, undone, redone, or merged.
 * @internal
 */
export function disposeTileTreesForGeometricModels(modelIds, iModel) {
    const trees = iModel.tiles.getTreeOwnersForSupplier(primaryTreeSupplier);
    for (const kvp of trees) {
        const id = kvp.id;
        assert(undefined !== id.modelId);
        if (modelIds.has(id.modelId))
            kvp.owner.dispose();
    }
}
class PrimaryTreeReference extends TileTreeReference {
    constructor(view, model, planProjection, transformNodeId, sectionClip) {
        super();
        /** Chiefly for debugging - disables iteration of this reference in SpatialModelRefs to e.g. omit the reference from the scene. */
        this.deactivated = false;
        this.view = view;
        this.model = model;
        this._animationTransformNodeId = transformNodeId;
        this._sectionClip = sectionClip;
        this._viewFlagOverrides = { ...model.jsonProperties.viewFlagOverrides };
        if (sectionClip) {
            // Clipping will be applied on backend; don't clip out cut geometry.
            this._viewFlagOverrides.clipVolume = false;
            this._sectionCutAppearanceProvider = FeatureAppearanceProvider.supplement((app) => {
                const cutApp = this.view.displayStyle.settings.clipStyle.cutStyle.appearance;
                return cutApp ? app.extendAppearance(cutApp) : app;
            });
        }
        const scriptInfo = IModelApp.tileAdmin.getScriptInfoForTreeId(model.id, view.displayStyle.scheduleScriptReference); // eslint-disable-line @typescript-eslint/no-deprecated
        this._id = {
            modelId: model.id,
            is3d: model.is3d,
            treeId: this.createTreeId(view, model.id),
            isPlanProjection: planProjection,
            timeline: scriptInfo?.timeline,
        };
        this._owner = primaryTreeSupplier.getOwner(this._id, model.iModel);
    }
    getAnimationTransformNodeId() {
        return this._animationTransformNodeId ?? AnimationNodeId.Untransformed;
    }
    getViewFlagOverrides(_tree) {
        return this._viewFlagOverrides;
    }
    getAppearanceProvider(_tree) {
        if (this._sectionCutAppearanceProvider && this.view.displayStyle.settings.clipStyle.cutStyle.appearance)
            return this._sectionCutAppearanceProvider;
        return undefined;
    }
    getHiddenLineSettings(_tree) {
        return this._sectionClip ? this.view.displayStyle.settings.clipStyle.cutStyle.hiddenLine : undefined;
    }
    get castsShadows() {
        return true;
    }
    get isPlanProjection() {
        return false;
    }
    getClipVolume(_tree) {
        // ###TODO: reduce frequency with which getModelClip() is called
        return this.view.is3d() && !this._sectionClip ? this.view.getModelClip(this.model.id) : undefined;
    }
    canSupplyToolTip() {
        return false;
    }
    createDrawArgs(context) {
        const args = super.createDrawArgs(context);
        if (args)
            args.intersectionClip = this._sectionClip;
        return args;
    }
    get treeOwner() {
        const newId = this.createTreeId(this.view, this._id.modelId);
        const timeline = IModelApp.tileAdmin.getScriptInfoForTreeId(this._id.modelId, this.view.displayStyle.scheduleScriptReference)?.timeline; // eslint-disable-line @typescript-eslint/no-deprecated
        if (0 !== compareIModelTileTreeIds(newId, this._id.treeId) || timeline !== this._id.timeline) {
            this._id = {
                modelId: this._id.modelId,
                is3d: this._id.is3d,
                treeId: newId,
                isPlanProjection: this._id.isPlanProjection,
                timeline,
            };
            this._owner = primaryTreeSupplier.getOwner(this._id, this.model.iModel);
        }
        return this._owner;
    }
    createTreeId(view, modelId) {
        if (this._sectionClip) {
            // We do this each time in case the ClipStyle's overrides are modified.
            // ###TODO: can we avoid that? Event listeners maybe?
            this._viewFlagOverrides = {
                ...this.view.displayStyle.settings.clipStyle.cutStyle.viewflags,
                // Do not clip out the cut geometry intersecting the clip planes.
                clipVolume: false,
                // The cut geometry is planar - it should win a z-fight.
                // Also we need to preserve this flag if this is a plan projection tile tree reference.
                forceSurfaceDiscard: true,
            };
        }
        const animationId = IModelApp.tileAdmin.getScriptInfoForTreeId(modelId, view.displayStyle.scheduleScriptReference)?.animationId; // eslint-disable-line @typescript-eslint/no-deprecated
        const renderMode = this._viewFlagOverrides.renderMode ?? view.viewFlags.renderMode;
        const visibleEdges = this._viewFlagOverrides.visibleEdges ?? view.viewFlags.visibleEdges;
        const edgesRequired = visibleEdges || RenderMode.SmoothShade !== renderMode || IModelApp.tileAdmin.alwaysRequestEdges;
        const edges = edgesRequired ? IModelApp.tileAdmin.edgeOptions : false;
        const sectionCut = this._sectionClip?.clipString;
        return { type: BatchType.Primary, edges, animationId, sectionCut };
    }
    computeBaseTransform(tree) {
        return super.computeTransform(tree);
    }
    computeTransform(tree) {
        const baseTf = this.computeBaseTransform(tree);
        const displayTf = this.view.modelDisplayTransformProvider?.getModelDisplayTransform(this.model.id);
        if (!displayTf)
            return baseTf;
        return displayTf.premultiply ? displayTf.transform.multiplyTransformTransform(baseTf) : baseTf.multiplyTransformTransform(displayTf.transform);
    }
}
/** @internal */
export class AnimatedTreeReference extends PrimaryTreeReference {
    constructor(view, model, transformNodeId) {
        super(view, model, false, transformNodeId);
        this._branchId = formatAnimationBranchId(model.id, transformNodeId);
    }
    computeBaseTransform(tree) {
        const tf = super.computeBaseTransform(tree);
        const style = this.view.displayStyle;
        const script = style.scheduleScript;
        if (undefined === script || undefined === this._animationTransformNodeId)
            return tf;
        const timePoint = style.settings.timePoint ?? script.duration.low;
        const animTf = script.getTransform(this._id.modelId, this._animationTransformNodeId, timePoint);
        if (animTf)
            animTf.multiplyTransformTransform(tf, tf);
        return tf;
    }
    createDrawArgs(context) {
        const animBranch = context.viewport.target.animationBranches?.branchStates.get(this._branchId);
        if (animBranch && animBranch.omit)
            return undefined;
        const args = super.createDrawArgs(context);
        if (args?.tree && undefined !== this._animationTransformNodeId) {
            assert(args.tree instanceof IModelTileTree);
            args.boundingRange = args.tree.getTransformNodeRange(this._animationTransformNodeId);
        }
        return args;
    }
}
class PlanProjectionTreeReference extends PrimaryTreeReference {
    get _view3d() { return this.view; }
    constructor(view, model, sectionCut) {
        super(view, model, true, undefined, sectionCut);
        this._baseTransform = Transform.createIdentity();
        this._viewFlagOverrides.forceSurfaceDiscard = true;
    }
    get castsShadows() {
        return false;
    }
    get isPlanProjection() {
        return true;
    }
    createDrawArgs(context) {
        const args = super.createDrawArgs(context);
        if (undefined !== args && this._id.treeId.enforceDisplayPriority) {
            args.drawGraphics = () => {
                const graphics = args.produceGraphics();
                if (undefined !== graphics) {
                    const settings = this.getSettings();
                    const asOverlay = undefined !== settings && settings.overlay;
                    const transparency = settings?.transparency || 0;
                    let elevation = settings?.elevation;
                    if (undefined === elevation) {
                        const tree = this.treeOwner.tileTree;
                        if (tree) {
                            assert(tree instanceof PlanProjectionTileTree);
                            elevation = tree.baseElevation;
                        }
                        else {
                            elevation = 0;
                        }
                    }
                    context.outputGraphic(context.target.renderSystem.createGraphicLayerContainer(graphics, asOverlay, transparency, elevation));
                }
            };
        }
        return args;
    }
    computeBaseTransform(tree) {
        assert(tree instanceof PlanProjectionTileTree);
        const transform = tree.iModelTransform.clone(this._baseTransform);
        const elevation = this.getSettings()?.elevation;
        if (undefined !== elevation)
            transform.origin.z = elevation;
        return transform;
    }
    draw(args) {
        const settings = this.getSettings();
        if (undefined === settings || settings.enforceDisplayPriority || !settings.overlay)
            super.draw(args);
        else
            args.context.withGraphicType(TileGraphicType.Overlay, () => args.tree.draw(args));
    }
    getSettings() {
        return this._view3d.getDisplayStyle3d().settings.getPlanProjectionSettings(this.model.id);
    }
    createTreeId(view, modelId) {
        const id = super.createTreeId(view, modelId);
        const settings = this.getSettings();
        if (undefined !== settings && settings.enforceDisplayPriority)
            id.enforceDisplayPriority = true;
        return id;
    }
}
function isPlanProjection(view, model) {
    const model3d = view.is3d() ? model.asGeometricModel3d : undefined;
    return undefined !== model3d && model3d.isPlanProjection;
}
function createTreeRef(view, model, sectionCut) {
    if (false !== IModelApp.renderSystem.options.planProjections && isPlanProjection(view, model))
        return new PlanProjectionTreeReference(view, model, sectionCut);
    return new PrimaryTreeReference(view, model, false, undefined, sectionCut);
}
/** @internal */
export function createPrimaryTileTreeReference(view, model) {
    return createTreeRef(view, model, undefined);
}
class MaskTreeReference extends TileTreeReference {
    get castsShadows() { return false; }
    constructor(view, model) {
        super();
        this.model = model;
        this._id = {
            modelId: model.id,
            is3d: model.is3d,
            treeId: this.createTreeId(),
            isPlanProjection: isPlanProjection(view, model),
        };
        this._owner = primaryTreeSupplier.getOwner(this._id, model.iModel);
    }
    get treeOwner() {
        const newId = this.createTreeId();
        if (0 !== compareIModelTileTreeIds(newId, this._id.treeId)) {
            this._id = { modelId: this._id.modelId, is3d: this._id.is3d, treeId: newId, isPlanProjection: false };
            this._owner = primaryTreeSupplier.getOwner(this._id, this.model.iModel);
        }
        return this._owner;
    }
    createTreeId() {
        return { type: BatchType.Primary, edges: false };
    }
}
/** @internal */
export function createMaskTreeReference(view, model) {
    return new MaskTreeReference(view, model);
}
/** @internal */
export class ModelMapLayerTileTreeReference extends MapLayerTileTreeReference {
    get isPlanar() { return true; }
    get activeClassifier() { return this._classifier; }
    constructor(layerSettings, _classifier, layerIndex, iModel, _source) {
        super(layerSettings, layerIndex, iModel);
        this._classifier = _classifier;
        this._source = _source;
        this._id = {
            modelId: _classifier.modelId,
            is3d: true, // model.is3d,
            treeId: this.createTreeId(),
            isPlanProjection: false, // isPlanProjection(view, model),
        };
        this._owner = primaryTreeSupplier.getOwner(this._id, this.iModel);
    }
    createTreeId() {
        return { type: BatchType.Primary, edges: false };
    }
    get treeOwner() {
        const newId = this.createTreeId();
        if (0 !== compareIModelTileTreeIds(newId, this._id.treeId)) {
            this._id = { modelId: this._id.modelId, is3d: this._id.is3d, treeId: newId, isPlanProjection: false };
            this._owner = primaryTreeSupplier.getOwner(this._id, this.iModel);
        }
        return this._owner;
    }
    get viewFlags() {
        return {
            renderMode: RenderMode.SmoothShade,
            transparency: true, // Igored for point clouds as they don't support transparency.
            textures: true,
            lighting: false,
            shadows: false,
            monochrome: false,
            materials: false,
            ambientOcclusion: false,
            visibleEdges: true,
            hiddenEdges: false,
            fill: true,
        };
    }
}
/** @internal */
export function createModelMapLayerTileTreeReference(layerSettings, layerIndex, iModel) {
    const classifier = SpatialClassifier.fromModelMapLayer(layerSettings);
    return classifier ? new ModelMapLayerTileTreeReference(layerSettings, classifier, layerIndex, iModel) : undefined;
}
/** Provides [[TileTreeReference]]s for the loaded models present in a [[SpatialViewState]]'s [[ModelSelectorState]] and
 * not present in the optionally-supplied exclusion list.
 * @internal
 */
export function createSpatialTileTreeReferences(view, excludedModels) {
    return new SpatialRefs(view, excludedModels);
}
/** Provides [[TileTreeReference]]s for the loaded models present in a [[SpatialViewState]]'s [[ModelSelectorState]].
 * @internal
 */
export var SpatialTileTreeReferences;
(function (SpatialTileTreeReferences) {
    /** Create a SpatialTileTreeReferences object reflecting the contents of the specified view. */
    function create(view) {
        return createSpatialTileTreeReferences(view);
    }
    SpatialTileTreeReferences.create = create;
})(SpatialTileTreeReferences || (SpatialTileTreeReferences = {}));
/** Represents the [[TileTreeReference]]s associated with one model in a [[SpatialTileTreeReferences]]. */
class SpatialModelRefs {
    constructor(model, view, excluded) {
        /** TileTreeReferences representing nodes transformed by the view's schedule script. */
        this._animatedRefs = [];
        this._modelRef = model.createTileTreeReference(view);
        this._isPrimaryRef = this._modelRef instanceof PrimaryTreeReference;
        this._isExcluded = excluded;
    }
    *[Symbol.iterator]() {
        if ((!this._primaryRef || !this._primaryRef.deactivated) && !this._isExcluded)
            yield this._modelRef;
        for (const animated of this._animatedRefs)
            if (!animated.deactivated)
                yield animated;
        if (this._sectionCutRef && !this._sectionCutRef.deactivated)
            yield this._sectionCutRef;
    }
    updateAnimated(script) {
        const ref = this._primaryRef;
        if (!ref || this._isExcluded)
            return;
        this._animatedRefs.length = 0;
        const nodeIds = script?.script.getTransformBatchIds(ref.model.id);
        if (nodeIds)
            for (const nodeId of nodeIds)
                this._animatedRefs.push(new AnimatedTreeReference(ref.view, ref.model, nodeId));
    }
    updateSectionCut(clip) {
        const ref = this._primaryRef;
        if (!ref) {
            assert(undefined === this._sectionCutRef);
            return;
        }
        // If the clip isn't supposed to apply to this model, don't produce cut geometry.
        const vfJson = clip ? ref.model.jsonProperties.viewFlagOverrides : undefined;
        const vfOvrs = vfJson ? { ...vfJson } : undefined;
        if (vfOvrs && !vfOvrs.clipVolume)
            clip = undefined;
        this._sectionCutRef = clip ? createTreeRef(ref.view, ref.model, clip) : undefined;
    }
    setDeactivated(deactivated, which) {
        if (typeof which !== "string") {
            for (const index of which)
                if (this._animatedRefs[index])
                    this._animatedRefs[index].deactivated = deactivated ?? !this._animatedRefs[index].deactivated;
            return;
        }
        if (("all" === which || "primary" === which) && this._primaryRef)
            this._primaryRef.deactivated = deactivated ?? !this._primaryRef.deactivated;
        if (("all" === which || "section" === which) && this._sectionCutRef)
            this._sectionCutRef.deactivated = deactivated ?? !this._sectionCutRef.deactivated;
        if (("all" === which || "animated" === which))
            for (const ref of this._animatedRefs)
                ref.deactivated = deactivated ?? !ref.deactivated;
    }
    get _primaryRef() {
        if (!this._isPrimaryRef)
            return undefined;
        assert(this._modelRef instanceof PrimaryTreeReference);
        return this._modelRef;
    }
}
/** Provides [[TileTreeReference]]s for the loaded models present in a [[SpatialViewState]]'s [[ModelSelectorState]]. */
class SpatialRefs {
    constructor(view, excludedModels) {
        this._allLoaded = false;
        this._refs = new Map();
        this._swapRefs = new Map();
        this._sectionCutOnlyRefs = new Map();
        this._swapSectionCutOnlyRefs = new Map();
        this._view = view;
        this._scheduleScript = view.displayStyle.scheduleScriptReference; // eslint-disable-line @typescript-eslint/no-deprecated
        this._sectionCut = this.getSectionCutFromView();
        if (excludedModels)
            this._excludedModels = new Set(excludedModels);
    }
    update() {
        this._allLoaded = false;
    }
    attachToViewport() { }
    detachFromViewport() { }
    *[Symbol.iterator]() {
        this.load();
        for (const modelRef of this._refs.values())
            for (const ref of modelRef)
                yield ref;
        if (this._sectionCut) {
            for (const modelRef of this._sectionCutOnlyRefs.values())
                for (const ref of modelRef)
                    yield ref;
        }
    }
    setDeactivated(modelIds, deactivated, refs) {
        if (undefined === modelIds) {
            for (const model of this._refs.values())
                model.setDeactivated(deactivated, refs);
            return;
        }
        if (typeof modelIds === "string")
            modelIds = [modelIds];
        for (const modelId of modelIds)
            this._refs.get(modelId)?.setDeactivated(deactivated, refs);
    }
    /** For getting the [TileTreeReference]s that are in the modelIds, for planar classification.
     * @param modelIds modelIds for which to get the TileTreeReferences
     * @param maskTreeRefs where to store the TileTreeReferences
     * @param maskRange range to extend for the maskRefs
     * @internal
     */
    collectMaskRefs(modelIds, maskTreeRefs, maskRange) {
        for (const modelId of modelIds) {
            if (!this._excludedModels?.has(modelId)) {
                const model = this._view.iModel.models.getLoaded(modelId);
                assert(model !== undefined); // Models should be loaded by RealityModelTileTree
                if (model?.asGeometricModel) {
                    const treeRef = createMaskTreeReference(this._view, model.asGeometricModel);
                    maskTreeRefs.push(treeRef);
                    const range = treeRef.computeWorldContentRange();
                    maskRange.extendRange(range);
                }
            }
        }
    }
    /** For getting a list of modelIds which do not participate in masking, for planar classification.
     * For non-batched tile trees this is not needed, so just return undefined.
     * @internal
     */
    getModelsNotInMask(_maskModels, _useVisible) { return undefined; }
    load() {
        if (!this._allLoaded) {
            this._allLoaded = true;
            this.updateModels();
        }
        const curScript = this._view.displayStyle.scheduleScriptReference; // eslint-disable-line @typescript-eslint/no-deprecated
        const prevScript = this._scheduleScript;
        if (curScript !== prevScript) {
            this._scheduleScript = curScript;
            if (!curScript || !prevScript || !curScript.script.equals(prevScript.script))
                for (const ref of this._refs.values())
                    ref.updateAnimated(curScript);
        }
        const sectionCut = this.getSectionCutFromView();
        if (sectionCut?.clipString !== this._sectionCut?.clipString) {
            this._sectionCut = sectionCut;
            for (const ref of this._refs.values())
                ref.updateSectionCut(sectionCut);
            for (const ref of this._sectionCutOnlyRefs.values())
                ref.updateSectionCut(sectionCut);
        }
    }
    getSectionCutFromView() {
        const wantCut = this._view.viewFlags.clipVolume && this._view.displayStyle.settings.clipStyle.produceCutGeometry;
        const clip = wantCut ? this._view.getViewClip() : undefined;
        return StringifiedClipVector.fromClipVector(clip);
    }
    /** Ensure this._refs contains a SpatialModelRefs for all loaded models in the model selector. */
    updateModels() {
        let prev = this._refs;
        let cur = this._swapRefs;
        this._refs = cur;
        this._swapRefs = prev;
        cur.clear();
        prev = this._sectionCutOnlyRefs;
        cur = this._swapSectionCutOnlyRefs;
        this._sectionCutOnlyRefs = cur;
        this._swapSectionCutOnlyRefs = prev;
        cur.clear();
        for (const modelId of this._view.modelSelector.models) {
            let excluded = false;
            if (undefined !== this._excludedModels && this._excludedModels.has(modelId)) {
                excluded = true;
                cur = this._sectionCutOnlyRefs;
                prev = this._swapSectionCutOnlyRefs;
            }
            else {
                cur = this._refs;
                prev = this._swapRefs;
            }
            let modelRefs = prev.get(modelId);
            if (!modelRefs) {
                const model = this._view.iModel.models.getLoaded(modelId)?.asGeometricModel3d;
                if (model) {
                    modelRefs = new SpatialModelRefs(model, this._view, excluded);
                    modelRefs.updateAnimated(this._scheduleScript);
                    modelRefs.updateSectionCut(this._sectionCut);
                }
            }
            if (modelRefs)
                cur.set(modelId, modelRefs);
        }
    }
}
//# sourceMappingURL=PrimaryTileTree.js.map