/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module IModelConnection
 */
import { assert, BeEvent, compareStrings, DbOpcode, DuplicatePolicy, SortedArray } from "@itwin/core-bentley";
import { ipcAppChannels, ModelGeometryChanges, } from "@itwin/core-common";
import { BriefcaseNotificationHandler } from "./BriefcaseTxns";
import { IpcApp } from "./IpcApp";
class ModelChanges extends SortedArray {
    geometryGuid;
    range;
    constructor(geometryGuid, range) {
        super((lhs, rhs) => compareStrings(lhs.id, rhs.id), DuplicatePolicy.Replace);
        this.geometryGuid = geometryGuid;
        this.range = range;
    }
}
/** Represents a period of time within an [interactive editing]($docs/learning/InteractiveEditing.md) session during which the
 * geometry of elements being displayed in one or more [[Viewport]]s is being modified. Outside of such a scope, whenever the
 * geometry within a [GeometricModel]($backend) changes new [[Tile]]s must be generated to reflect those changes in a viewport.
 * Regenerating entire tiles each time individual elements change can be time-consuming, which may introduce an unacceptable delay
 * between making a modification and seeing its result on the screen.
 *
 * Within the context of a graphical editing scope, no new tiles are generated. Instead, the geometry for any deleted or modified elements
 * is hidden in the tile graphics, and additional temporary graphics are displayed for any newly-inserted or modified elements. Only when the
 * scope exits are new tiles produced.
 *
 * The application decides when to enter and exit a graphical editing scope. A single interactive editing session may involve any number of
 * editing scopes. Typically, applications will enter a new editing scope (after first exiting a previous scope, if one exists):
 *  - When switching from a non-graphical workflow to one that involves editing geometry; or
 *  - When changing which geometric model is being edited; or
 *  - After performing an operation that creates or modifies a "large" number (perhaps hundreds?) of elements.
 *
 * An application should typically exit any graphical editing scope before:
 *  - Pulling changesets; or
 *  - Switching from a graphical editing workflow to some non-graphical workflow.
 *
 * Graphical editing scopes are only supported for [[BriefcaseConnection]]s opened in read-write mode that contain version 1.0.11 or newer of the BisCore schema.
 * @see [[BriefcaseConnection.enterEditingScope]] to create a scope for a briefcase.
 * @see [[BriefcaseConnection.editingScope]] to obtain a briefcase's current scope.
 * @see [[exit]] to terminate a scope.
 * @public
 */
export class GraphicalEditingScope extends BriefcaseNotificationHandler {
    get briefcaseChannelName() { return ipcAppChannels.editingScope; }
    /** Maps model Id to accumulated changes to geometric elements within the associated model. */
    _geometryChanges = new Map();
    _disposed = false;
    _cleanup;
    /** The connection to the iModel being edited. */
    iModel;
    /** Event raised when a new scope is created for any [[BriefcaseConnection]].
     * @see [[onExiting]] and [[onExited]] for complementary events.
     */
    static onEnter = new BeEvent();
    /** Event raised when this scope is about to exit.
     * @see [[onEnter]] for the complementary event.
     * @see [[onExited]] for an event raised after the scope exits.
     */
    onExiting = new BeEvent();
    /** Event raised when this scope has exited.
     * @see [[onEnter]] for the complementary event.
     * @see [[onExiting]] for an event raised just before the scope is exited.
     */
    onExited = new BeEvent();
    /** Event raised after geometric changes are written to the iModel. */
    onGeometryChanges = new BeEvent();
    /** Don't call this directly - use BriefcaseConnection.enterEditingScope.
     * @internal
     */
    static async enter(imodel) {
        if (imodel.editingScope)
            throw new Error("Cannot create an editing scope for an iModel that already has one");
        // Register the scope synchronously, in case enter() is called again for same iModel while awaiting asynchronous initialization.
        const scope = new GraphicalEditingScope(imodel);
        try {
            const scopeStarted = await IpcApp.appFunctionIpc.toggleGraphicalEditingScope(imodel.key, true);
            assert(scopeStarted); // If it didn't, the backend threw an error.
        }
        catch (e) {
            scope[Symbol.dispose]();
            throw e;
        }
        this.onEnter.raiseEvent(scope);
        return scope;
    }
    /** Exits this editing scope. The associated [[BriefcaseConnection]]'s `editingScope` will be reset to `undefined`.
     * @throws Error if the scope could not be exited, e.g., if it has already been exited.
     * @see [[BriefcaseConnection.enterEditingScope]] to enter an editing scope.
     */
    async exit() {
        if (this._disposed || this.iModel.editingScope !== this)
            throw new Error("Cannot exit editing scope after it is disconnected from the iModel");
        this._disposed = true;
        try {
            this.onExiting.raiseEvent(this);
        }
        finally {
            const scopeExited = await IpcApp.appFunctionIpc.toggleGraphicalEditingScope(this.iModel.key, false);
            assert(!scopeExited);
            try {
                this.onExited.raiseEvent(this);
            }
            finally {
                this[Symbol.dispose]();
            }
        }
    }
    /** Obtain all geometric changes to elements within the specified model accumulated within this scope. */
    getGeometryChangesForModel(modelId) {
        return this._geometryChanges.get(modelId);
    }
    /** Obtain all geometric changes to models accumulated within this scope. */
    getGeometryChanges() {
        return { [Symbol.iterator]: () => this.geometryChangeIterator() };
    }
    /** @internal */
    get isDisposed() {
        return this._disposed;
    }
    *geometryChangeIterator() {
        for (const [key, value] of this._geometryChanges) {
            yield {
                id: key,
                geometryGuid: value.geometryGuid,
                range: value.range,
                elements: value,
            };
        }
    }
    constructor(iModel) {
        super(iModel.key);
        this.iModel = iModel;
        this._cleanup = this.registerImpl();
    }
    [Symbol.dispose]() {
        this._disposed = true;
        this.onExiting.clear();
        this.onGeometryChanges.clear();
        this.onExited.clear();
        this._geometryChanges.clear();
        if (this._cleanup) {
            this._cleanup();
            this._cleanup = undefined;
        }
    }
    /** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [Symbol.dispose] instead. */
    dispose() {
        this[Symbol.dispose]();
    }
    /** @internal */
    notifyGeometryChanged(props) {
        const changes = ModelGeometryChanges.iterable(props);
        const modelIds = [];
        let deletedIds;
        for (const modelChanges of changes) {
            // ###TODO do we care about the model range?
            let list = this._geometryChanges.get(modelChanges.id);
            modelIds.push(modelChanges.id);
            for (const elementChange of modelChanges.elements) {
                if (!list) {
                    this._geometryChanges.set(modelChanges.id, list = new ModelChanges(modelChanges.geometryGuid, modelChanges.range));
                }
                else {
                    list.geometryGuid = modelChanges.geometryGuid;
                    modelChanges.range.clone(list.range);
                }
                list.insert(elementChange);
                if (DbOpcode.Delete === elementChange.type) {
                    if (undefined === deletedIds)
                        deletedIds = new Set();
                    deletedIds.add(elementChange.id);
                }
            }
        }
        if (deletedIds) {
            this.iModel.selectionSet.remove(deletedIds);
            this.iModel.hilited.remove({ elements: deletedIds });
        }
        this.onGeometryChanges.raiseEvent(changes, this);
    }
}
//# sourceMappingURL=GraphicalEditingScope.js.map