/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Views
 */
import { assert, BeEvent, Id64 } from "@itwin/core-bentley";
import { Range1d } from "@itwin/core-geometry";
import { BackgroundMapProvider, BaseMapLayerSettings, ColorDef, DisplayStyle3dSettings, DisplayStyleSettings, GlobeMode, ImageMapLayerSettings, MapLayerSettings, RenderSchedule, SubCategoryOverride, TerrainHeightOriginMode, ThematicDisplay, ThematicDisplayMode, ThematicGradientMode, } from "@itwin/core-common";
import { ApproximateTerrainHeights } from "./ApproximateTerrainHeights";
import { BackgroundMapGeometry } from "./BackgroundMapGeometry";
import { ContextRealityModelState } from "./ContextRealityModelState";
import { ElementState } from "./EntityState";
import { IModelApp } from "./IModelApp";
import { PlanarClipMaskState } from "./PlanarClipMaskState";
import { getCesiumOSMBuildingsUrl } from "./tile/internal";
/** @internal */
export class TerrainDisplayOverrides {
}
/** A DisplayStyle defines the parameters for 'styling' the contents of a [[ViewState]].
 * @public
 * @extensions
 */
export class DisplayStyleState extends ElementState {
    static get className() { return "DisplayStyle"; }
    /** Construct a new DisplayStyleState from its JSON representation.
     * @param props JSON representation of the display style.
     * @param iModel IModelConnection containing the display style.
     * @param source If the constructor is being invoked from [[EntityState.clone]], the display style that is being cloned.
     */
    constructor(props, iModel, source) {
        super(props, iModel);
        this._attachedRealityModelPlanarClipMasks = new Map();
        this._assigningScript = false;
        /** Event raised just before the [[scheduleScriptReference]] property is changed.
         * @deprecated in 3.x. use [[onScheduleScriptChanged]].
         */
        this.onScheduleScriptReferenceChanged = new BeEvent();
        /** Event raised just before the [[scheduleScript]] property is changed. */
        this.onScheduleScriptChanged = new BeEvent();
        /** Event raised just after [[setOSMBuildingDisplay]] changes the enabled state of the OSM buildings. */
        this.onOSMBuildingDisplayChanged = new BeEvent();
        const styles = this.jsonProperties.styles;
        if (source)
            this._scriptReference = source._scriptReference;
        if (styles) {
            // ###TODO Use DisplayStyleSettings.planarClipMasks
            if (styles.planarClipOvr)
                for (const planarClipOvr of styles.planarClipOvr)
                    if (Id64.isValid(planarClipOvr.modelId))
                        this._attachedRealityModelPlanarClipMasks.set(planarClipOvr.modelId, PlanarClipMaskState.fromJSON(planarClipOvr));
        }
    }
    /** Ensures all of the data required by the display style is loaded. This method is invoked for you by [[ViewState.load]], but if
     * you obtain a display style by some other means you should `await` this method before using the display style.
     */
    async load() {
        // If we were cloned, we may already have a valid schedule state, and our display style Id may be invalid / different.
        // Preserve it if still usable.
        if (this._scriptReference) {
            if (this.settings.renderTimeline === this._scriptReference.sourceId) {
                // The script came from the same RenderTimeline element. Keep it.
                return;
            }
            if (undefined === this.settings.renderTimeline) {
                // The script came from a display style's JSON properties. Keep it if (1) this style is not persistent or (2) this style has the same Id
                if (this.id === this._scriptReference.sourceId || !Id64.isValidId64(this.id))
                    return;
            }
        }
        // The schedule script stored in JSON properties takes precedence over the RenderTimeline if both are defined.
        if (this.settings.scheduleScriptProps)
            this.loadScriptReferenceFromScript(this.settings.scheduleScriptProps);
        else
            await this.loadScriptReferenceFromTimeline(this.settings.renderTimeline);
    }
    loadScriptReferenceFromScript(scriptProps) {
        let newState;
        try {
            const script = RenderSchedule.Script.fromJSON(scriptProps);
            if (script)
                newState = new RenderSchedule.ScriptReference(this.id, script);
        }
        catch {
            // schedule state is undefined.
        }
        if (newState !== this._scriptReference) {
            this.onScheduleScriptReferenceChanged.raiseEvent(newState); // eslint-disable-line @typescript-eslint/no-deprecated
            this.onScheduleScriptChanged.raiseEvent(newState?.script);
            this._scriptReference = newState;
        }
    }
    async loadScriptReferenceFromTimeline(timelineId) {
        let newState;
        if (timelineId && Id64.isValidId64(timelineId)) {
            try {
                // If a subsequent call to loadScriptReferenceFromTimeline is made while we're awaiting this one, we'll abort this one.
                const promise = this._queryRenderTimelinePropsPromise = this.queryRenderTimelineProps(timelineId);
                const timeline = await promise;
                if (promise !== this._queryRenderTimelinePropsPromise)
                    return;
                if (timeline) {
                    const scriptProps = JSON.parse(timeline.script);
                    const script = RenderSchedule.Script.fromJSON(scriptProps);
                    if (script)
                        newState = new RenderSchedule.ScriptReference(timelineId, script);
                }
            }
            catch {
                // schedule state is undefined.
            }
        }
        this._queryRenderTimelinePropsPromise = undefined;
        if (newState !== this._scriptReference) {
            this.onScheduleScriptReferenceChanged.raiseEvent(newState); // eslint-disable-line @typescript-eslint/no-deprecated
            this.onScheduleScriptChanged.raiseEvent(newState?.script);
            this._scriptReference = newState;
        }
    }
    /** @internal */
    async queryRenderTimelineProps(timelineId) {
        try {
            const omitScriptElementIds = !IModelApp.tileAdmin.enableFrontendScheduleScripts;
            return await this.iModel.elements.loadProps(timelineId, { renderTimeline: { omitScriptElementIds } });
        }
        catch {
            return undefined;
        }
    }
    /** @internal */
    get displayTerrain() {
        return this.viewFlags.backgroundMap && this.settings.backgroundMap.applyTerrain;
    }
    /** @internal */
    get globeMode() { return this.settings.backgroundMap.globeMode; }
    /** Settings controlling how the base map is displayed within a view.
     *  The base map can be provided by any map imagery source or set to be a single color.
     */
    get backgroundMapBase() {
        return this.settings.mapImagery.backgroundBase;
    }
    set backgroundMapBase(base) {
        this.settings.mapImagery.backgroundBase = base;
        this._synchBackgroundMapImagery();
    }
    /** The settings controlling how a background map is displayed within a view.
     * @see [[ViewFlags.backgroundMap]] for toggling display of the map on or off.
     */
    get backgroundMapSettings() { return this.settings.backgroundMap; }
    set backgroundMapSettings(settings) {
        this.settings.backgroundMap = settings;
    }
    /** Modify a subset of the background map display settings.
     * @param name props JSON representation of the properties to change. Any properties not present will retain their current values in `this.backgroundMapSettings`.
     * @see [[ViewFlags.backgroundMap]] for toggling display of the map.
     * @see [[changeBackgroundMapProvider]] to change the type of map imagery displayed.
     *
     * Example that changes only the elevation, leaving the provider and type unchanged:
     * ``` ts
     *  style.changeBackgroundMapProps({ groundBias: 16.2 });
     * ```
     * @public
     */
    changeBackgroundMapProps(props) {
        const newSettings = this.backgroundMapSettings.clone(props);
        this.backgroundMapSettings = newSettings;
    }
    /** Change aspects of the [BackgroundMapProvider]($common) from which background map imagery is obtained.
     * Any properties not explicitly specified by `props` will retain their current values.
     * @public
     */
    changeBackgroundMapProvider(props) {
        const base = this.settings.mapImagery.backgroundBase;
        if (base instanceof ColorDef) {
            this.settings.mapImagery.backgroundBase = BaseMapLayerSettings.fromProvider(BackgroundMapProvider.fromJSON(props));
        }
        else {
            const provider = base.provider ? base.provider.clone(props) : BackgroundMapProvider.fromJSON(props);
            this.settings.mapImagery.backgroundBase = base.cloneWithProvider(provider);
        }
        this._synchBackgroundMapImagery();
    }
    /** Call a function for each reality model attached to this display style.
     * @see [DisplayStyleSettings.contextRealityModels]($common).
     */
    forEachRealityModel(func) {
        for (const model of this.settings.contextRealityModels.models) {
            assert(model instanceof ContextRealityModelState);
            func(model);
        }
    }
    *getRealityModels() {
        for (const model of this.settings.contextRealityModels.models) {
            assert(model instanceof ContextRealityModelState);
            yield model;
        }
    }
    /** Iterate over the reality models attached to this display style. */
    get realityModels() {
        return this.getRealityModels();
    }
    /** @internal */
    forEachRealityTileTreeRef(func) {
        this.forEachRealityModel((model) => {
            if (!model.invisible) {
                func(model.treeRef);
            }
        });
    }
    /** @internal */
    forEachTileTreeRef(func) {
        this.forEachRealityTileTreeRef(func);
    }
    /** Performs logical comparison against another display style. Two display styles are logically equivalent if they have the same name, Id, and settings.
     * @param other The display style to which to compare.
     * @returns true if the specified display style is logically equivalent to this display style - i.e., both styles have the same values for all of their settings.
     */
    equalState(other) {
        if (this.name !== other.name || this.id !== other.id)
            return false;
        else
            return JSON.stringify(this.settings) === JSON.stringify(other.settings);
    }
    /** The name of this DisplayStyle */
    get name() { return this.code.value; }
    /** Change the Id of the [RenderTimeline]($backend) element that hosts the [RenderSchedule.Script]($common) to be applied by this display style for
     * animating the contents of the view, and update [[scheduleScript]] using the script associated with the [RenderTimeline]($backend) element.
     * @see [DisplayStyleSettings.renderTimeline]($common).
     */
    async changeRenderTimeline(timelineId) {
        // Potentially trigger async loading of new schedule state.
        this.settings.renderTimeline = timelineId;
        // Await async loading if necessary.
        // Note the `await` in loadScriptReferenceFromTimeline will resolve before this one [per the spec](https://262.ecma-international.org/6.0/#sec-triggerpromisereactions).
        if (this._queryRenderTimelinePropsPromise)
            await this._queryRenderTimelinePropsPromise;
    }
    /** The [RenderSchedule.Script]($common) that animates the contents of the view, if any.
     * @see [[changeRenderTimeline]] to change the script.
     */
    get scheduleScript() {
        return this._scriptReference?.script;
    }
    set scheduleScript(script) {
        if (script === this.scheduleScript)
            return;
        try {
            const scriptRef = script ? new RenderSchedule.ScriptReference(script) : undefined;
            this.onScheduleScriptReferenceChanged.raiseEvent(scriptRef); // eslint-disable-line @typescript-eslint/no-deprecated
            this.onScheduleScriptChanged.raiseEvent(script);
            this._scriptReference = scriptRef;
            this._assigningScript = true;
            this.settings.scheduleScriptProps = script?.toJSON();
            if (!script)
                this.loadScriptReferenceFromTimeline(this.settings.renderTimeline); // eslint-disable-line @typescript-eslint/no-floating-promises
        }
        finally {
            this._assigningScript = false;
        }
    }
    /** The [RenderSchedule.Script]($common) that animates the contents of the view, if any, along with the Id of the element that hosts the script.
     * @note The host element may be a [RenderTimeline]($backend) or a [DisplayStyle]($backend).
     * @deprecated in 3.x. Use [[scheduleScript]].
     */
    get scheduleScriptReference() {
        return this._scriptReference;
    }
    /** Attach a [ContextRealityModel]($common) to this display style.
     * @see [DisplayStyleSettings.contextRealityModels]($common).
     * @see [ContextRealityModels.add]($common)
     */
    attachRealityModel(props) {
        const model = this.settings.contextRealityModels.add(props);
        assert(model instanceof ContextRealityModelState);
        return model;
    }
    /** Detach the first [ContextRealityModel]($common) that matches the specified name and url.
     * @see [DisplayStyleSettings.contextRealityModels]($common)
     * @see [ContextRealityModels.delete]($common)
     */
    detachRealityModelByNameAndUrl(name, url) {
        const model = this.settings.contextRealityModels.models.find((x) => x.matchesNameAndUrl(name, url));
        return undefined !== model && this.settings.contextRealityModels.delete(model);
    }
    /** Get the [[ContextRealityModelState]] that displays the OpenStreetMap worldwide building layer, if enabled.
     * @see [[setOSMBuildingDisplay]]
     */
    getOSMBuildingRealityModel() {
        if (!this.iModel.isGeoLocated || this.globeMode !== GlobeMode.Ellipsoid) // The OSM tile tree is ellipsoidal.
            return undefined;
        const url = getCesiumOSMBuildingsUrl();
        if (undefined === url)
            return undefined;
        return this.contextRealityModelStates.find((x) => x.url === url);
    }
    /** Set the display of the OpenStreetMap worldwide building layer in this display style by attaching or detaching the reality model displaying the buildings.
     * The OSM buildings are displayed from a reality model aggregated and served from Cesium ion.<(https://cesium.com/content/cesium-osm-buildings/>
     * The options [[OsmBuildingDisplayOptions]] control the display and appearance overrides.
     */
    setOSMBuildingDisplay(options) {
        if (!this.iModel.isGeoLocated || this.globeMode !== GlobeMode.Ellipsoid) // The OSM tile tree is ellipsoidal.
            return false;
        const url = getCesiumOSMBuildingsUrl();
        if (undefined === url)
            return false;
        let model = this.settings.contextRealityModels.models.find((x) => x.url === url);
        if (options.onOff === false) {
            const turnedOff = undefined !== model && this.settings.contextRealityModels.delete(model);
            if (turnedOff)
                this.onOSMBuildingDisplayChanged.raiseEvent(false);
            return turnedOff;
        }
        if (!model) {
            const name = IModelApp.localization.getLocalizedString("iModelJs:RealityModelNames.OSMBuildings");
            model = this.attachRealityModel({ tilesetUrl: url, name });
            this.onOSMBuildingDisplayChanged.raiseEvent(true);
        }
        if (options.appearanceOverrides)
            model.appearanceOverrides = options.appearanceOverrides;
        return true;
    }
    /** Return if a context reality model is attached.
     * @see [[ContextRealityModelProps]].
     */
    hasAttachedRealityModel(name, url) {
        return undefined !== this.settings.contextRealityModels.models.find((x) => x.matchesNameAndUrl(name, url));
    }
    /** @internal */
    getMapLayers(isOverlay) { return isOverlay ? this.settings.mapImagery.overlayLayers : this.settings.mapImagery.backgroundLayers; }
    /** Attach a map layer to display style.
     * @param Settings representing the map layer.
     * @param mapLayerIndex the [[MapLayerIndex]] where the map layer should be attached.
     * @public
     */
    attachMapLayer(options) {
        const layerSettings = options.settings.clone({});
        if (undefined === layerSettings)
            return;
        const isOverlay = options.mapLayerIndex.isOverlay;
        const insertIndex = options.mapLayerIndex.index;
        const layers = this.getMapLayers(isOverlay);
        if (insertIndex < 0 || insertIndex > (layers.length - 1)) {
            this.getMapLayers(isOverlay).push(layerSettings);
        }
        else {
            layers.splice(insertIndex, 0, layerSettings);
        }
        this._synchBackgroundMapImagery();
    }
    /**
     * @param mapLayerIndex the [[MapLayerIndex]] where the map layer should be attached.
     * @internal
     */
    attachMapLayerProps(options) {
        const settings = MapLayerSettings.fromJSON(options.props);
        if (undefined === settings)
            return;
        this.attachMapLayer({ settings, mapLayerIndex: options.mapLayerIndex });
    }
    /** @internal */
    hasAttachedMapLayer(name, source, isOverlay) {
        return -1 !== this.findMapLayerIndexByNameAndSource(name, source, isOverlay);
    }
    /** @internal */
    detachMapLayerByNameAndSource(name, source, isOverlay) {
        const index = this.findMapLayerIndexByNameAndSource(name, source, isOverlay);
        if (-1 !== index)
            this.detachMapLayerByIndex({ index, isOverlay });
    }
    /** Detach map layer at index (-1 to remove all layers)
     * @param mapLayerIndex the [[MapLayerIndex]] of the map layer to detach.
     * @public
     */
    detachMapLayerByIndex(mapLayerIndex) {
        const layers = this.getMapLayers(mapLayerIndex.isOverlay);
        const index = mapLayerIndex.index;
        if (index < 0)
            layers.length = 0;
        else
            layers.splice(index, 1);
        this._synchBackgroundMapImagery();
    }
    /**
     * Lookup a maplayer index by name and source.
     * @param name Name of of the layer.
     * @param source Unique string identifying the layer.
     * @param isOverlay true if layer is overlay, otherwise layer is background. Defaults to false.
     * @public
     *
     */
    findMapLayerIndexByNameAndSource(name, source, isOverlay) {
        return this.getMapLayers(isOverlay).findIndex((layer) => layer.matchesNameAndSource(name, source));
    }
    /** Return the map layer settings for a map layer at the provided index.
     * @param mapLayerIndex the [[MapLayerIndex]] of the map layer.
     * @public
     */
    mapLayerAtIndex(mapLayerIndex) {
        const layers = this.getMapLayers(mapLayerIndex.isOverlay);
        const index = mapLayerIndex.index;
        return (index < 0 || index >= layers.length) ? undefined : layers[index];
    }
    /** Return map base transparency as a number between 0 and 1.
     * @public
     */
    get baseMapTransparency() {
        return this.settings.mapImagery.baseTransparency;
    }
    /** Change the map base transparency as a number between 0 and 1.
     * @public
     */
    changeBaseMapTransparency(transparency) {
        if (this.settings.mapImagery.backgroundBase instanceof ColorDef) {
            this.settings.mapImagery.backgroundBase = this.settings.mapImagery.backgroundBase.withTransparency(transparency * 255);
        }
        else {
            this.settings.mapImagery.backgroundBase = this.settings.mapImagery.backgroundBase.clone({ transparency });
        }
        this._synchBackgroundMapImagery();
    }
    /** Modify a subset of a map layer settings.
     * @param props props JSON representation of the properties to change. Any properties not present will retain their current values.
     * @param mapLayerIndex the [[MapLayerIndex]] where the map layer should be inserted.
     *
     * Example that changes only the visibility of the first overlay map layer.
     * ``` ts
     *  style.changeMapLayerProps({ visible: false }, 0, false);
     * ```
     * @public
     */
    changeMapLayerProps(props, mapLayerIndex) {
        const index = mapLayerIndex.index;
        const layers = this.getMapLayers(mapLayerIndex.isOverlay);
        if (index < 0 || index >= layers.length)
            return;
        layers[index] = layers[index].clone(props);
        this._synchBackgroundMapImagery();
    }
    /** Change the credentials for a map layer.
     * @param mapLayerIndex the [[MapLayerIndex]] of the map layer to change the credentials of.
     * @public
     */
    changeMapLayerCredentials(mapLayerIndex, userName, password) {
        const layers = this.getMapLayers(mapLayerIndex.isOverlay);
        const index = mapLayerIndex.index;
        if (index < 0 || index >= layers.length)
            return;
        const layer = layers[index];
        if (layer instanceof ImageMapLayerSettings) {
            layer.setCredentials(userName, password);
            this._synchBackgroundMapImagery();
        }
    }
    /** Modify a subset of a sub-layer settings.
     * @param props props JSON representation of the properties to change. Any properties not present will retain their current values.
     * @param subLayerId Id of the sub-layer that should be modified.
     * @param mapLayerIndex the [[MapLayerIndex]] of the map layer that contains the sub-layer to be modified.
     *
     * @public
     */
    changeMapSubLayerProps(props, subLayerId, mapLayerIndex) {
        const mapLayerSettings = this.mapLayerAtIndex(mapLayerIndex);
        if (undefined === mapLayerSettings)
            return;
        if (!(mapLayerSettings instanceof ImageMapLayerSettings)) {
            assert(false);
            return;
        }
        const subLayers = new Array();
        for (const subLayer of mapLayerSettings.subLayers) {
            subLayers.push((subLayerId === -1 || subLayer.id === subLayerId) ? subLayer.clone(props).toJSON() : subLayer.toJSON());
        }
        this.changeMapLayerProps({ subLayers }, mapLayerIndex);
    }
    /* @internal */
    _synchBackgroundMapImagery() {
        this.settings.synchMapImagery();
    }
    /** Move map layer to top.
     * @param mapLayerIndex the [[MapLayerIndex]] of the map layer to move.
     * @public
     *
     */
    moveMapLayerToTop(mapLayerIndex) {
        const layers = this.getMapLayers(mapLayerIndex.isOverlay);
        const index = mapLayerIndex.index;
        if (index >= 0 && index < layers.length - 1) {
            const layer = layers.splice(index, 1);
            layers.push(layer[0]);
            this._synchBackgroundMapImagery();
        }
    }
    /** Move map layer to bottom.
     * @param mapLayerIndex the [[MapLayerIndex]] of the map layer to move.
     * @public
     */
    moveMapLayerToBottom(mapLayerIndex) {
        const layers = this.getMapLayers(mapLayerIndex.isOverlay);
        const index = mapLayerIndex.index;
        if (index > 0 && index < layers.length) {
            const layer = layers.splice(index, 1);
            layers.unshift(layer[0]);
            this._synchBackgroundMapImagery();
        }
    }
    /** Reorder map layers
     * @param fromIndex index of map layer to move
     * @param toIndex insert index. If equal to length of map array the map layer is moved to end of array.
     * @param isOverlay true if map-layer is part of the overlay map, otherwise it is part of the background map.
     * @public
     */
    moveMapLayerToIndex(fromIndex, toIndex, isOverlay) {
        const layers = this.getMapLayers(isOverlay);
        if (fromIndex === toIndex)
            return;
        if (fromIndex < 0 || fromIndex >= layers.length || toIndex > layers.length)
            return;
        const layer = layers.splice(fromIndex, 1);
        layers.splice(toIndex, 0, layer[0]); // note: if toIndex === settings.mapImagery.backgroundLayers.length item is appended
        this._synchBackgroundMapImagery();
    }
    /** Flags controlling various aspects of the display style.
     * @see [DisplayStyleSettings.viewFlags]($common)
     */
    get viewFlags() { return this.settings.viewFlags; }
    set viewFlags(flags) { this.settings.viewFlags = flags; }
    /** The background color for this DisplayStyle */
    get backgroundColor() { return this.settings.backgroundColor; }
    set backgroundColor(val) { this.settings.backgroundColor = val; }
    /** The color used to draw geometry in monochrome mode.
     * @see [ViewFlags.monochrome]($common) for enabling monochrome mode.
     */
    get monochromeColor() { return this.settings.monochromeColor; }
    set monochromeColor(val) { this.settings.monochromeColor = val; }
    /** @internal */
    anyMapLayersVisible(overlay) {
        const layers = this.getMapLayers(overlay);
        for (const mapLayer of layers)
            if (mapLayer.visible)
                return true;
        return false;
    }
    /** @internal */
    getIsBackgroundMapVisible() {
        return undefined !== this.iModel.ecefLocation && (this.viewFlags.backgroundMap || this.anyMapLayersVisible(false));
    }
    /** @internal */
    get backgroundMapElevationBias() {
        if (this.backgroundMapSettings.applyTerrain) {
            const terrainSettings = this.backgroundMapSettings.terrainSettings;
            switch (terrainSettings.heightOriginMode) {
                case TerrainHeightOriginMode.Ground:
                    return (undefined === this.iModel.projectCenterAltitude) ? undefined : terrainSettings.heightOrigin + terrainSettings.exaggeration * this.iModel.projectCenterAltitude;
                case TerrainHeightOriginMode.Geodetic:
                    return terrainSettings.heightOrigin;
                case TerrainHeightOriginMode.Geoid:
                    return (undefined === this.iModel.geodeticToSeaLevel) ? undefined : terrainSettings.heightOrigin + this.iModel.geodeticToSeaLevel;
            }
        }
        else {
            return this.backgroundMapSettings.groundBias;
        }
    }
    /** @internal */
    getBackgroundMapGeometry() {
        if (undefined === this.iModel.ecefLocation)
            return undefined;
        const bimElevationBias = this.backgroundMapElevationBias;
        if (undefined === bimElevationBias)
            return undefined;
        const globeMode = this.globeMode;
        if (undefined === this._backgroundMapGeometry || this._backgroundMapGeometry.globeMode !== globeMode || this._backgroundMapGeometry.bimElevationBias !== bimElevationBias) {
            const geometry = new BackgroundMapGeometry(bimElevationBias, globeMode, this.iModel);
            this._backgroundMapGeometry = { bimElevationBias, geometry, globeMode };
        }
        return this._backgroundMapGeometry.geometry;
    }
    /** [[ContextRealityModelState]]s attached to this display style.
     * @see [DisplayStyleSettings.contextRealityModels]($common).
     */
    get contextRealityModelStates() {
        return this.settings.contextRealityModels.models;
    }
    /** @internal */
    getGlobalGeometryAndHeightRange() {
        let geometry = this.getIsBackgroundMapVisible() ? this.getBackgroundMapGeometry() : undefined;
        const terrainRange = ApproximateTerrainHeights.instance.globalHeightRange;
        let heightRange = this.displayTerrain ? terrainRange : Range1d.createXX(-1, 1);
        if (this.globeMode === GlobeMode.Ellipsoid && this.contextRealityModelStates.find((model) => model.isGlobal)) {
            if (!geometry) {
                if (!this._ellipsoidMapGeometry)
                    this._ellipsoidMapGeometry = new BackgroundMapGeometry(0, GlobeMode.Ellipsoid, this.iModel);
                geometry = this._ellipsoidMapGeometry;
            }
            heightRange = terrainRange;
        }
        return geometry ? { geometry, heightRange } : undefined;
    }
    /** Returns true if this is a 3d display style. */
    is3d() { return this instanceof DisplayStyle3dState; }
    /** Customize the way geometry belonging to a [[SubCategory]] is drawn by this display style.
     * @param id The ID of the SubCategory whose appearance is to be overridden.
     * @param ovr The overrides to apply to the [[SubCategoryAppearance]].
     * @see [[dropSubCategoryOverride]]
     */
    overrideSubCategory(id, ovr) { this.settings.overrideSubCategory(id, ovr); }
    /** Remove any [[SubCategoryOverride]] applied to a [[SubCategoryAppearance]] by this style.
     * @param id The ID of the [[SubCategory]].
     * @see [[overrideSubCategory]]
     */
    dropSubCategoryOverride(id) { this.settings.dropSubCategoryOverride(id); }
    /** Returns true if an [[SubCategoryOverride]]s are defined by this style. */
    get hasSubCategoryOverride() { return this.settings.hasSubCategoryOverride; }
    /** Obtain the overrides applied to a [[SubCategoryAppearance]] by this style.
     * @param id The ID of the [[SubCategory]].
     * @returns The corresponding SubCategoryOverride, or undefined if the SubCategory's appearance is not overridden.
     * @see [[overrideSubCategory]]
     */
    getSubCategoryOverride(id) { return this.settings.getSubCategoryOverride(id); }
    /** For each subcategory belonging to any of the specified categories, make it visible by turning off the "invisible" flag in its subcategory appearance.
     * This requires that the categories and subcategories have been previously loaded by, e.g., a call to IModelConnection.querySubCategories.
     * @returns true if the visibility of any subcategory was modified.
     * @see Viewport.changeCategoryDisplay
     * @see ViewCreator3dOptions.allSubCategoriesVisible
     * @internal
     */
    enableAllLoadedSubCategories(categoryIds) {
        let anyChanged = false;
        for (const categoryId of Id64.iterable(categoryIds)) {
            const subCategoryIds = this.iModel.subcategories.getSubCategories(categoryId);
            if (undefined !== subCategoryIds)
                for (const subCategoryId of subCategoryIds)
                    if (this.setSubCategoryVisible(subCategoryId, true))
                        anyChanged = true;
        }
        return anyChanged;
    }
    /** Change the "invisible" flag for the given subcategory's appearance.
     * This requires that the subcategory appearance has been previously loaded by, e.g., a call to IModelConnection.Categories.getSubCategoryInfo.
     * @returns true if the visibility of any subcategory was modified.
     * @see [[enableAllLoadedSubCategories]]
     * @internal
     */
    setSubCategoryVisible(subCategoryId, visible) {
        const app = this.iModel.subcategories.getSubCategoryAppearance(subCategoryId);
        if (undefined === app)
            return false; // category not enabled or subcategory not found
        const curOvr = this.getSubCategoryOverride(subCategoryId);
        const isAlreadyVisible = undefined !== curOvr && undefined !== curOvr.invisible ? !curOvr.invisible : !app.invisible;
        if (isAlreadyVisible === visible)
            return false;
        // Preserve existing overrides - just flip the visibility flag.
        const json = undefined !== curOvr ? curOvr.toJSON() : {};
        json.invisible = !visible;
        this.overrideSubCategory(subCategoryId, SubCategoryOverride.fromJSON(json));
        return true;
    }
    /** Returns true if solar shadow display is enabled by this display style. */
    get wantShadows() {
        return this.is3d() && this.viewFlags.shadows && false !== IModelApp.renderSystem.options.displaySolarShadows;
    }
    /** @internal */
    registerSettingsEventListeners() {
        this.settings.onScheduleScriptPropsChanged.addListener((scriptProps) => {
            if (this._assigningScript)
                return;
            try {
                this._assigningScript = true;
                if (scriptProps)
                    this.loadScriptReferenceFromScript(scriptProps);
                else
                    this.loadScriptReferenceFromTimeline(this.settings.renderTimeline); // eslint-disable-line @typescript-eslint/no-floating-promises
            }
            finally {
                this._assigningScript = false;
            }
        });
        this.settings.onRenderTimelineChanged.addListener((newTimeline) => {
            // Cancel any in-progress loading of script from timeline.
            this._queryRenderTimelinePropsPromise = undefined;
            if (!this.settings.scheduleScriptProps)
                this.loadScriptReferenceFromTimeline(newTimeline); // eslint-disable-line @typescript-eslint/no-floating-promises
        });
        this.settings.onPlanarClipMaskChanged.addListener((id, newSettings) => {
            if (newSettings)
                this._attachedRealityModelPlanarClipMasks.set(id, PlanarClipMaskState.create(newSettings));
            else
                this._attachedRealityModelPlanarClipMasks.delete(id);
        });
    }
    /** @internal */
    createRealityModel(props) {
        return new ContextRealityModelState(props, this.iModel, this);
    }
    /** @internal */
    getPlanarClipMaskState(modelId) {
        const model = this.iModel.models.getLoaded(modelId)?.asSpatialModel;
        return (model && model.isRealityModel) ? this._attachedRealityModelPlanarClipMasks.get(modelId) : undefined;
    }
}
/** A display style that can be applied to 2d views.
 * @public
 * @extensions
 */
export class DisplayStyle2dState extends DisplayStyleState {
    static get className() { return "DisplayStyle2d"; }
    get settings() { return this._settings; }
    /** @internal */
    overrideTerrainDisplay() { return undefined; }
    constructor(props, iModel) {
        super(props, iModel);
        this._settings = new DisplayStyleSettings(this.jsonProperties, {
            createContextRealityModel: (modelProps) => this.createRealityModel(modelProps),
            deferContextRealityModels: true,
        });
        this._settings.contextRealityModels.populate();
        this.registerSettingsEventListeners();
    }
}
/** A [[DisplayStyleState]] that can be applied to spatial views.
 * @public
 * @extensions
 */
export class DisplayStyle3dState extends DisplayStyleState {
    static get className() { return "DisplayStyle3d"; }
    get settings() { return this._settings; }
    constructor(props, iModel, source) {
        super(props, iModel, source);
        this._settings = new DisplayStyle3dSettings(this.jsonProperties, {
            createContextRealityModel: (modelProps) => this.createRealityModel(modelProps),
            deferContextRealityModels: true,
        });
        this._settings.contextRealityModels.populate();
        this.registerSettingsEventListeners();
    }
    get environment() {
        return this.settings.environment;
    }
    set environment(env) {
        this.settings.environment = env;
    }
    get lights() { return this.settings.lights; }
    set lights(lights) { this.settings.lights = lights; }
    /** The direction of the solar light. */
    get sunDirection() {
        return this.settings.lights.solar.direction;
    }
    /** Set the solar light direction based on time value
     * @param time The time in unix time milliseconds.
     * @see [DisplayStyle3dSettings.sunTime]($common) to obtain the current sun time.
     * @see [DisplayStyle3dSettings.setSunTime]($common).
     */
    setSunTime(time) {
        this.settings.setSunTime(time, this.iModel);
    }
    /** Settings controlling shadow display. */
    get solarShadows() {
        return this.settings.solarShadows;
    }
    set solarShadows(settings) {
        this.settings.solarShadows = settings;
    }
    /** @internal */
    registerSettingsEventListeners() {
        super.registerSettingsEventListeners();
        this.settings.onOverridesApplied.addListener((overrides) => {
            if (overrides.thematic && this.settings.thematic.displayMode === ThematicDisplayMode.Height && undefined === overrides.thematic.range) {
                // Use the project extents as reasonable default height range.
                // NB: assumes using Z axis...
                const extents = this.iModel.projectExtents;
                const props = { ...overrides.thematic };
                props.range = { low: extents.zLow, high: extents.zHigh };
                this.settings.thematic = ThematicDisplay.fromJSON(props);
            }
        });
    }
    /** @internal */
    overrideTerrainDisplay() {
        if (undefined !== this.settings.thematic) {
            const ovr = new TerrainDisplayOverrides();
            if (this.viewFlags.thematicDisplay && ThematicGradientMode.IsoLines === this.settings.thematic.gradientSettings.mode)
                ovr.wantSkirts = false;
            if (this.viewFlags.thematicDisplay && (ThematicDisplayMode.Slope === this.settings.thematic.displayMode || ThematicDisplayMode.HillShade === this.settings.thematic.displayMode))
                ovr.wantNormals = true;
            return ovr;
        }
        return undefined;
    }
}
//# sourceMappingURL=DisplayStyleState.js.map