/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module DisplayStyles
 */
import { assert, BeEvent } from "@itwin/core-bentley";
import { FeatureAppearance } from "./FeatureSymbology";
import { PlanarClipMaskMode, PlanarClipMaskSettings } from "./PlanarClipMask";
import { SpatialClassifiers } from "./SpatialClassification";
import { RealityModelDisplaySettings } from "./RealityModelDisplaySettings";
/** Identify the Reality Data service provider
 * @beta
 */
export var RealityDataProvider;
(function (RealityDataProvider) {
    /**
     * This is the legacy mode where the access to the 3d tiles is harcoded in ContextRealityModelProps.tilesetUrl property.
     * It was use to support RealityMesh3DTiles, Terrain3DTiles, Cesium3DTiles
     * You should use other mode when possible
     * @see [[RealityDataSource.createKeyFromUrl]] that will try to detect provider from an URL
     */
    RealityDataProvider["TilesetUrl"] = "TilesetUrl";
    /**
     * This is the legacy mode where the access to the 3d tiles is harcoded in ContextRealityModelProps.OrbitGtBlob property.
     * It was use to support OrbitPointCloud (OPC) from other server than ContextShare
     * You should use other mode when possible
     * @see [[RealityDataSource.createKeyFromOrbitGtBlobProps]] that will try to detect provider from an URL
     */
    RealityDataProvider["OrbitGtBlob"] = "OrbitGtBlob";
    /**
     * Will provide access url from realityDataId and iTwinId on contextShare for 3dTile storage format or  OPC storage format
     * This provider support all type of 3dTile storage fomat and OrbitPointCloud: RealityMesh3DTiles, Terrain3DTiles, Cesium3DTiles, OPC
     * @see [[RealityDataFormat]].
     */
    RealityDataProvider["ContextShare"] = "ContextShare";
    /**
     * Will provide Open Street Map Building (OSM) from Cesium Ion (in 3dTile format)
     */
    RealityDataProvider["CesiumIonAsset"] = "CesiumIonAsset";
})(RealityDataProvider || (RealityDataProvider = {}));
/** Identify the Reality Data storage format
 * @beta
 */
export var RealityDataFormat;
(function (RealityDataFormat) {
    /**
     * 3dTile supported formats; RealityMesh3DTiles, Terrain3DTiles, Cesium3DTiles
     * */
    RealityDataFormat["ThreeDTile"] = "ThreeDTile";
    /**
     * Orbit Point Cloud (OPC) storage format (RealityDataType.OPC)
    */
    RealityDataFormat["OPC"] = "OPC";
})(RealityDataFormat || (RealityDataFormat = {}));
/** Utility function for RealityDataFormat
 * @beta
 */
(function (RealityDataFormat) {
    /**
     * Try to extract the RealityDataFormat from the url
     * @param tilesetUrl the reality data attachment url
     * @returns the extracted RealityDataFormat or ThreeDTile by default if not found
     */
    function fromUrl(tilesetUrl) {
        let format = RealityDataFormat.ThreeDTile;
        if (tilesetUrl.includes(".opc"))
            format = RealityDataFormat.OPC;
        return format;
    }
    RealityDataFormat.fromUrl = fromUrl;
})(RealityDataFormat || (RealityDataFormat = {}));
/**
 * RealityDataSourceKey utility functions
 * @beta */
export var RealityDataSourceKey;
(function (RealityDataSourceKey) {
    /** Utility function to convert a RealityDataSourceKey into its string representation */
    function convertToString(rdSourceKey) {
        return `${rdSourceKey.provider}:${rdSourceKey.format}:${rdSourceKey.id}:${rdSourceKey?.iTwinId}`;
    }
    RealityDataSourceKey.convertToString = convertToString;
    /** Utility function to compare two RealityDataSourceKey, we consider it equal even if itwinId is different */
    function isEqual(key1, key2) {
        if ((key1.provider === RealityDataProvider.CesiumIonAsset) && key2.provider === RealityDataProvider.CesiumIonAsset)
            return true; // ignore other properties for CesiumIonAsset, id is hidden
        if ((key1.provider === key2.provider) && (key1.format === key2.format) && (key1.id === key2.id)) {
            // && (key1?.iTwinId === key2?.iTwinId)) -> ignore iTwinId, consider it is the same reality data
            return true;
        }
        return false;
    }
    RealityDataSourceKey.isEqual = isEqual;
})(RealityDataSourceKey || (RealityDataSourceKey = {}));
/** @public */
export var ContextRealityModelProps;
(function (ContextRealityModelProps) {
    /** Produce a deep copy of `input`. */
    function clone(input) {
        // Spread operator is shallow, and includes `undefined` properties and empty strings.
        // We want to make deep copies, omit undefined properties and empty strings, and require tilesetUrl to be defined.
        const output = { tilesetUrl: input.tilesetUrl ?? "" };
        if (input.rdSourceKey)
            output.rdSourceKey = { ...input.rdSourceKey };
        if (input.name)
            output.name = input.name;
        if (input.realityDataId)
            output.realityDataId = input.realityDataId;
        if (input.description)
            output.description = input.description;
        if (input.orbitGtBlob)
            output.orbitGtBlob = { ...input.orbitGtBlob };
        if (input.appearanceOverrides) {
            output.appearanceOverrides = { ...input.appearanceOverrides };
            if (input.appearanceOverrides.rgb)
                output.appearanceOverrides.rgb = { ...input.appearanceOverrides.rgb };
        }
        if (input.displaySettings) {
            output.displaySettings = { ...input.displaySettings };
            if (input.displaySettings.pointCloud)
                output.displaySettings.pointCloud = { ...output.displaySettings.pointCloud };
        }
        if (input.planarClipMask)
            output.planarClipMask = { ...input.planarClipMask };
        if (input.classifiers)
            output.classifiers = input.classifiers.map((x) => { return { ...x, flags: { ...x.flags } }; });
        if (input.invisible)
            output.invisible = input.invisible;
        return output;
    }
    ContextRealityModelProps.clone = clone;
})(ContextRealityModelProps || (ContextRealityModelProps = {}));
/** A reality model not associated with a [GeometricModel]($backend) but instead defined in a [DisplayStyle]($backend) or [DisplayStyleState]($frontend).
 * Such reality models are displayed to provide context to the view and can be freely attached and detached at display time.
 * @see [this interactive example](https://www.itwinjs.org/sample-showcase/?group=Viewer&sample=reality-data-sample)
 * @see [[DisplayStyleSettings.contextRealityModels]] to define context reality models for a display style.
 * @public
 */
export class ContextRealityModel {
    /** Construct a new context reality model.
     * @param props JSON representation of the reality model, which will be kept in sync with changes made via the ContextRealityModel's methods.
     * @param options Options to customize how the reality model is created.
     */
    constructor(props, options) {
        /** Event dispatched just before assignment to [[planarClipMaskSettings]]. */
        this.onPlanarClipMaskChanged = new BeEvent();
        /** Event dispatched just before assignment to [[appearanceOverrides]]. */
        this.onAppearanceOverridesChanged = new BeEvent();
        /** Event dispatched just before assignment to [[displaySettings]].
         * @beta
         */
        this.onDisplaySettingsChanged = new BeEvent();
        /** Event dispatched just before a model become invisible
         * @beta
         */
        this.onInvisibleChanged = new BeEvent();
        this._props = props;
        this.rdSourceKey = props.rdSourceKey;
        this.name = props.name ?? "";
        this.url = props.tilesetUrl ?? "";
        this.orbitGtBlob = props.orbitGtBlob;
        this.realityDataId = props.realityDataId;
        this.description = props.description ?? "";
        this._invisible = props.invisible ?? false;
        this._appearanceOverrides = props.appearanceOverrides ? FeatureAppearance.fromJSON(props.appearanceOverrides) : undefined;
        this._displaySettings = RealityModelDisplaySettings.fromJSON(props.displaySettings);
        if (props.planarClipMask && props.planarClipMask.mode !== PlanarClipMaskMode.None)
            this._planarClipMask = PlanarClipMaskSettings.fromJSON(props.planarClipMask);
        if (options?.createClassifiers) {
            this._classifiers = options.createClassifiers(props);
        }
        else {
            this._classifiers = new SpatialClassifiers(props);
        }
    }
    /** A set of [[SpatialClassifier]]s, of which one at any given time can be used to classify the reality model. */
    get classifiers() {
        return this._classifiers;
    }
    /** Optionally describes how the geometry of the reality model can be masked by other models. */
    get planarClipMaskSettings() {
        return this._planarClipMask;
    }
    set planarClipMaskSettings(settings) {
        this.onPlanarClipMaskChanged.raiseEvent(settings, this);
        if (!settings)
            delete this._props.planarClipMask;
        else
            this._props.planarClipMask = settings.toJSON();
        this._planarClipMask = settings;
    }
    /** Overrides applied to the appearance of the reality model. Only the rgb, transparency, nonLocatable, and emphasized properties are applicable - the rest are ignored. */
    get appearanceOverrides() {
        return this._appearanceOverrides;
    }
    set appearanceOverrides(overrides) {
        this.onAppearanceOverridesChanged.raiseEvent(overrides, this);
        if (!overrides)
            delete this._props.appearanceOverrides;
        else
            this._props.appearanceOverrides = overrides.toJSON();
        this._appearanceOverrides = overrides;
    }
    /** Settings controlling how this reality model is displayed in a [Viewport]($frontend).
     * @beta
     */
    get displaySettings() {
        return this._displaySettings;
    }
    set displaySettings(settings) {
        this.onDisplaySettingsChanged.raiseEvent(settings, this);
        this._props.displaySettings = settings.toJSON();
        this._displaySettings = settings;
    }
    /** If true, reality model is not drawn.
     * @beta
    */
    get invisible() {
        return this._invisible;
    }
    set invisible(invisible) {
        if (invisible !== this.invisible) {
            this.onInvisibleChanged.raiseEvent(invisible, this);
        }
        this._props.invisible = invisible;
        this._invisible = invisible;
    }
    /** Convert this model to its JSON representation. */
    toJSON() {
        return ContextRealityModelProps.clone(this._props);
    }
    /** Returns true if [[name]] and [[url]] match the specified name and url. */
    matchesNameAndUrl(name, url) {
        return this.name === name && this.url === url;
    }
}
/** A list of [[ContextRealityModel]]s attached to a [[DisplayStyleSettings]]. The list may be presented to the user with the name and description of each model.
 * The list is automatically synchronized with the underlying JSON representation provided by the input [[ContextRealityModelsContainer]].
 * @see [this interactive example](https://www.itwinjs.org/sample-showcase/?group=Viewer&sample=reality-data-sample)
 * @see [[DisplayStyleSettings.contextRealityModels]].
 * @public
 */
export class ContextRealityModels {
    /** @internal */
    constructor(arg0, createContextRealityModel) {
        this._models = [];
        /** Event dispatched just before [[ContextRealityModel.planarClipMaskSettings]] is modified for one of the reality models. */
        this.onPlanarClipMaskChanged = new BeEvent();
        /** Event dispatched just before [[ContextRealityModel.appearanceOverrides]] is modified for one of the reality models. */
        this.onAppearanceOverridesChanged = new BeEvent();
        /** Event dispatched just before [[ContextRealityModel.displaySettings]] is modified for one of the reality models.
         * @beta
         */
        this.onDisplaySettingsChanged = new BeEvent();
        /** Event dispatched just before [[ContextRealityModel.invisible]] is modified for one of the reality models.
         * @beta
         */
        this.onInvisibleChanged = new BeEvent();
        /** Event dispatched when a model is [[add]]ed, [[delete]]d, [[replace]]d, or [[update]]d. */
        this.onChanged = new BeEvent();
        let container;
        let defer = false;
        if (arg0.container) {
            container = arg0.container;
            createContextRealityModel = arg0.createContextRealityModel;
            defer = true === arg0.deferPopulating;
        }
        else {
            container = arg0;
        }
        this._container = container;
        this._createModel = createContextRealityModel ?? ((props) => new ContextRealityModel(props));
        if (!defer)
            this.populate();
    }
    /** @internal needs to be invoked after DisplayStyleSettings constructor by DisplayStyleState constructor.*/
    /** Populate the list of [[models]] from the container that was supplied to the constructor.
     * This should only be invoked once, and only if [[ContextRealityModelsArgs.deferPopulating]] was specified as `true` when calling the constructor.
     * @public
     */
    populate() {
        assert(this._models.length === 0, "do not call ContextRealityModels.populate more than once");
        const models = this._container.contextRealityModels;
        if (models)
            for (const model of models)
                this._models.push(this.createModel(model));
    }
    /** The read-only list of reality models. */
    get models() {
        return this._models;
    }
    /** Append a new reality model to the list.
     * @param The JSON representation of the reality model.
     * @returns the newly-added reality model.
     */
    add(props) {
        if (!this._container.contextRealityModels)
            this._container.contextRealityModels = [];
        props = ContextRealityModelProps.clone(props);
        const model = this.createModel(props);
        this.onChanged.raiseEvent(undefined, model);
        this._models.push(model);
        this._container.contextRealityModels.push(props);
        return model;
    }
    /** Remove the specified reality model from the list.
     * @param model The reality model to remove.
     * @returns true if the model was removed, or false if the model was not present in the list.
     */
    delete(model) {
        const index = this._models.indexOf(model);
        if (-1 === index)
            return false;
        assert(undefined !== this._container.contextRealityModels);
        assert(index < this._container.contextRealityModels.length);
        this.dropEventListeners(model);
        this.onChanged.raiseEvent(model, undefined);
        this._models.splice(index, 1);
        if (this.models.length === 0)
            this._container.contextRealityModels = undefined;
        else
            this._container.contextRealityModels.splice(index, 1);
        return true;
    }
    /** Remove all reality models from the list. */
    clear() {
        for (const model of this.models) {
            this.dropEventListeners(model);
            this.onChanged.raiseEvent(model, undefined);
        }
        this._container.contextRealityModels = undefined;
        this._models.length = 0;
    }
    /** Replace a reality model in the list.
     * @param toReplace The reality model to be replaced.
     * @param replaceWith The JSON representation of the replacement reality model.
     * @returns the newly-created reality model that replaced `toReplace`.
     * @throws Error if `toReplace` is not present in the list
     * @note The replacement occupies the same index in the list as `toReplace` did.
     */
    replace(toReplace, replaceWith) {
        const index = this._models.indexOf(toReplace);
        if (-1 === index)
            throw new Error("ContextRealityModel not present in list.");
        assert(undefined !== this._container.contextRealityModels);
        assert(index < this._container.contextRealityModels.length);
        replaceWith = ContextRealityModelProps.clone(replaceWith);
        const model = this.createModel(replaceWith);
        this.onChanged.raiseEvent(toReplace, model);
        this.dropEventListeners(toReplace);
        this._models[index] = model;
        this._container.contextRealityModels[index] = replaceWith;
        return model;
    }
    /** Change selected properties of a reality model.
     * @param toUpdate The reality model whose properties are to be modified.
     * @param updateProps The properties to change.
     * @returns The updated reality model, identical to `toUpdate` except for properties explicitly supplied by `updateProps`.
     * @throws Error if `toUpdate` is not present in the list.
     */
    update(toUpdate, updateProps) {
        const props = {
            ...toUpdate.toJSON(),
            ...updateProps,
        };
        // Partial<> makes it possible to pass `undefined` for tilesetUrl...preserve previous URL in that case.
        if (undefined === props.tilesetUrl)
            props.tilesetUrl = toUpdate.url;
        return this.replace(toUpdate, props);
    }
    createModel(props) {
        const model = this._createModel(props);
        // eslint-disable-next-line @typescript-eslint/unbound-method
        model.onPlanarClipMaskChanged.addListener(this.handlePlanarClipMaskChanged, this);
        // eslint-disable-next-line @typescript-eslint/unbound-method
        model.onAppearanceOverridesChanged.addListener(this.handleAppearanceOverridesChanged, this);
        // eslint-disable-next-line @typescript-eslint/unbound-method
        model.onDisplaySettingsChanged.addListener(this.handleDisplaySettingsChanged, this);
        // eslint-disable-next-line @typescript-eslint/unbound-method
        model.onInvisibleChanged.addListener(this.handleInvisibleChanged, this);
        return model;
    }
    dropEventListeners(model) {
        // eslint-disable-next-line @typescript-eslint/unbound-method
        model.onPlanarClipMaskChanged.removeListener(this.handlePlanarClipMaskChanged, this);
        // eslint-disable-next-line @typescript-eslint/unbound-method
        model.onAppearanceOverridesChanged.removeListener(this.handleAppearanceOverridesChanged, this);
        // eslint-disable-next-line @typescript-eslint/unbound-method
        model.onDisplaySettingsChanged.removeListener(this.handleDisplaySettingsChanged, this);
        // eslint-disable-next-line @typescript-eslint/unbound-method
        model.onInvisibleChanged.removeListener(this.handleInvisibleChanged, this);
    }
    handlePlanarClipMaskChanged(mask, model) {
        this.onPlanarClipMaskChanged.raiseEvent(model, mask);
    }
    handleAppearanceOverridesChanged(app, model) {
        this.onAppearanceOverridesChanged.raiseEvent(model, app);
    }
    handleDisplaySettingsChanged(settings, model) {
        this.onDisplaySettingsChanged.raiseEvent(model, settings);
    }
    handleInvisibleChanged(invisible, model) {
        this.onInvisibleChanged.raiseEvent(model, invisible);
    }
}
//# sourceMappingURL=ContextRealityModel.js.map