"use strict";
/*---------------------------------------------------------------------------------------------
 * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
 * See LICENSE.md in the project root for license terms and full copyright notice.
 *--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SelectionSet = exports.HiliteSet = exports.SelectionSetEventType = void 0;
/** @packageDocumentation
 * @module SelectionSet
 */
const core_bentley_1 = require("@itwin/core-bentley");
const IModelApp_1 = require("./IModelApp");
/** Identifies the type of changes made to the [[SelectionSet]] to produce a [[SelectionSetEvent]].
 * @public
 * @extensions
 */
var SelectionSetEventType;
(function (SelectionSetEventType) {
    /** Ids have been added to the set. */
    SelectionSetEventType[SelectionSetEventType["Add"] = 0] = "Add";
    /** Ids have been removed from the set. */
    SelectionSetEventType[SelectionSetEventType["Remove"] = 1] = "Remove";
    /** Some ids have been added to the set and others have been removed. */
    SelectionSetEventType[SelectionSetEventType["Replace"] = 2] = "Replace";
    /** All ids have been removed from the set. */
    SelectionSetEventType[SelectionSetEventType["Clear"] = 3] = "Clear";
})(SelectionSetEventType || (exports.SelectionSetEventType = SelectionSetEventType = {}));
/** Holds a set of hilited entities and makes any changes to the set by passing the change
 * function to given `change` callback.
 * @internal
 */
class HilitedIds extends core_bentley_1.Id64.Uint32Set {
    _change;
    constructor(_change) {
        super();
        this._change = _change;
    }
    add(low, high) {
        this._change(() => super.add(low, high));
    }
    delete(low, high) {
        this._change(() => super.delete(low, high));
    }
    clear() {
        this._change(() => super.clear());
    }
    addIds(ids) {
        this._change(() => super.addIds(ids));
    }
    deleteIds(ids) {
        this._change(() => super.deleteIds(ids));
    }
}
/** A set of *hilited* elements for an [[IModelConnection]], by element id.
 * Hilited elements are displayed with a customizable hilite effect within a [[Viewport]].
 * The set exposes 3 types of elements in 3 separate collections: [GeometricElement]($backend), [GeometricModel]($backend), and [SubCategory]($backend).
 * The [[models]] and [[subcategories]] can be hilited independently or as an intersection of the two sets, as specified by [[modelSubCategoryMode]].
 *
 * Technically, the hilite effect is applied to [Feature]($common)s, not [Element]($backend)s. An element's geometry stream can contain multiple
 * features belonging to different subcategories.
 *
 * Because Javascript lacks efficient support for 64-bit integers, the Ids are stored as pairs of 32-bit integers via [Id64.Uint32Set]($bentley).
 *
 * @note Typically, elements are hilited by virtue of their presence in the IModelConnection's [[SelectionSet]]. The HiliteSet allows additional
 * elements to be displayed with the hilite effect without adding them to the [[SelectionSet]]. If you add elements to the HiliteSet directly, you
 * are also responsible for removing them as appropriate.
 * @see [[IModelConnection.hilited]] for the HiliteSet associated with an iModel.
 * @see [Hilite.Settings]($common) for customization of the hilite effect.
 * @public
 * @extensions
 */
class HiliteSet {
    iModel;
    #mode = "union";
    #selectionChangesListener;
    #changing = false;
    /** The set of hilited elements. */
    elements;
    /** The set of hilited subcategories.
     * @see [[modelSubCategoryMode]] to control how this set interacts with the set of hilited [[models]].
     * @see [[IModelConnection.Categories]] to obtain the set of subcategories associated with one or more [Category]($backend)'s.
     */
    subcategories;
    /** The set of hilited [[GeometricModelState]]s.
     * @see [[modelSubCategoryMode]] to control how this set interacts with the set of hilited [[subcategories]].
     */
    models;
    /** Controls how the sets of hilited [[models]] and [[subcategories]] interact with one another.
     * By default they are treated as a union: a [Feature]($common) is hilited if either its model **or** its subcategory is hilited.
     * This can be changed to an intersection such that a [Feature]($common) is hilited only if both its model **and** subcategory are hilited.
     * @note The sets of hilited models and subcategories are independent of the set of hilited [[elements]] - an element whose Id is present in
     * [[elements]] is always hilited regardless of its model or subcategories.
     */
    get modelSubCategoryMode() {
        return this.#mode;
    }
    set modelSubCategoryMode(mode) {
        if (mode === this.#mode) {
            return;
        }
        this.onModelSubCategoryModeChanged.raiseEvent(mode);
        this.#mode = mode;
    }
    /** Event raised just before changing the value of [[modelSubCategoryMode]]. */
    onModelSubCategoryModeChanged = new core_bentley_1.BeEvent();
    /** Construct a HiliteSet
     * @param iModel The iModel containing the entities to be hilited.
     * @param syncWithSelectionSet If true, the hilite set contents will be synchronized with those in the `iModel`'s [[SelectionSet]].
     */
    constructor(iModel, syncWithSelectionSet = true) {
        this.iModel = iModel;
        this.elements = new HilitedIds((func) => this.#change(func));
        this.subcategories = new HilitedIds((func) => this.#change(func));
        this.models = new HilitedIds((func) => this.#change(func));
        this.wantSyncWithSelectionSet = syncWithSelectionSet;
    }
    /** Control whether the hilite set will be synchronized with the contents of the [[SelectionSet]].
     * By default they are synchronized. Applications that override this take responsibility for managing the set of hilited entities.
     * When turning synchronization off, the contents of the HiliteSet will remain unchanged.
     * When turning synchronization on, the current contents of the HiliteSet will be preserved, and the contents of the selection set will be added to them.
     */
    get wantSyncWithSelectionSet() {
        return !!this.#selectionChangesListener;
    }
    set wantSyncWithSelectionSet(want) {
        if (want === this.wantSyncWithSelectionSet) {
            return;
        }
        if (want) {
            const set = this.iModel.selectionSet;
            this.#selectionChangesListener = set.onChanged.addListener((ev) => this.#processSelectionSetEvent(ev));
            this.add(set.active);
        }
        else {
            (0, core_bentley_1.assert)(undefined !== this.#selectionChangesListener);
            this.#selectionChangesListener();
            this.#selectionChangesListener = undefined;
        }
    }
    #onChanged() {
        if (!this.#changing) {
            IModelApp_1.IModelApp.viewManager.onSelectionSetChanged(this.iModel);
        }
    }
    #change(func) {
        const changing = this.#changing;
        this.#changing = true;
        try {
            func();
        }
        finally {
            this.#changing = changing;
        }
        this.#onChanged();
    }
    #processSelectionSetEvent(ev) {
        switch (ev.type) {
            case SelectionSetEventType.Add:
                return this.add(ev.additions);
            case SelectionSetEventType.Replace:
                return this.#change(() => {
                    this.add(ev.additions);
                    this.remove(ev.removals);
                });
            case SelectionSetEventType.Remove:
            case SelectionSetEventType.Clear:
                return this.remove(ev.removals);
        }
    }
    /** Adds a collection of geometric element, model and subcategory ids to this hilite set. */
    add(additions) {
        this.#change(() => {
            additions.elements && this.elements.addIds(additions.elements);
            additions.models && this.models.addIds(additions.models);
            additions.subcategories && this.subcategories.addIds(additions.subcategories);
        });
    }
    /** Removes a collection of geometric element, model and subcategory ids from this hilite set. */
    remove(removals) {
        this.#change(() => {
            removals.elements && this.elements.deleteIds(removals.elements);
            removals.models && this.models.deleteIds(removals.models);
            removals.subcategories && this.subcategories.deleteIds(removals.subcategories);
        });
    }
    /** Replaces ids currently in the hilite set with the given collection. */
    replace(ids) {
        this.#change(() => {
            this.clear();
            this.add(ids);
        });
    }
    /** Remove all elements from the hilited set. */
    clear() {
        this.#change(() => {
            this.elements.clear();
            this.models.clear();
            this.subcategories.clear();
        });
    }
    /** Returns true if nothing is hilited. */
    get isEmpty() {
        return this.elements.isEmpty && this.subcategories.isEmpty && this.models.isEmpty;
    }
    /** Toggle the hilited state of one or more elements.
     * @param arg the ID(s) of the elements whose state is to be toggled.
     * @param onOff True to add the elements to the hilited set, false to remove them.
     * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [[add]], [[remove]], [[replace]] instead.
     */
    setHilite(arg, onOff) {
        if (onOff) {
            this.add({ elements: arg });
        }
        else {
            this.remove({ elements: arg });
        }
    }
}
exports.HiliteSet = HiliteSet;
/** A set of *currently selected* geometric elements, models and subcategories for an `IModelConnection`.
 * Generally, selected elements are displayed with a customizable hilite effect within a [[Viewport]], see [[HiliteSet]].
 * @see [Hilite.Settings]($common) for customization of the hilite effect.
 * @public
 * @extensions
 */
class SelectionSet {
    iModel;
    #selection;
    /** The IDs of the selected elements.
     * @note Do not modify this set directly. Instead, use methods like [[SelectionSet.add]].
     */
    get elements() {
        return this.#selection.elements;
    }
    /** The IDs of the selected models.
     * @note Do not modify this set directly. Instead, use methods like [[SelectionSet.add]].
     */
    get models() {
        return this.#selection.models;
    }
    /** The IDs of the selected subcategories.
     * @note Do not modify this set directly. Instead, use methods like [[SelectionSet.add]].
     */
    get subcategories() {
        return this.#selection.subcategories;
    }
    /** Get the active selection as a collection of geometric element, model and subcategory ids.
     * @note Do not modify the sets in returned collection directly. Instead, use methods like [[SelectionSet.add]].
     */
    get active() {
        return { ...this.#selection };
    }
    /** Called whenever ids are added or removed from this `SelectionSet` */
    onChanged = new core_bentley_1.BeEvent();
    constructor(iModel) {
        this.iModel = iModel;
        this.#selection = {
            elements: new Set(),
            models: new Set(),
            subcategories: new Set(),
        };
    }
    #sendChangedEvent(ev) {
        IModelApp_1.IModelApp.viewManager.onSelectionSetChanged(this.iModel);
        this.onChanged.raiseEvent(ev);
    }
    /** Get the number of entries in this selection set. */
    get size() {
        return this.elements.size + this.models.size + this.subcategories.size;
    }
    /** Check whether there are any ids in this selection set. */
    get isActive() {
        return this.elements.size > 0 || this.models.size > 0 || this.subcategories.size > 0;
    }
    /** Return true if elemId is in this `SelectionSet`.
     * @see [[isSelected]]
     * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use `SelectionSet.elements.has(elemId)` instead.
     */
    has(elemId) {
        return !!elemId && this.elements.has(elemId);
    }
    /** Query whether an Id is in the selection set.
     * @see [[has]]
     * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use `SelectionSet.elements.has(elemId)` instead.
     */
    isSelected(elemId) {
        return !!elemId && this.elements.has(elemId);
    }
    /** Clear current selection set.
     * @note raises the [[onChanged]] event with [[SelectionSetEventType.Clear]].
     */
    emptyAll() {
        if (!this.isActive) {
            return;
        }
        const removals = this.#selection;
        this.#selection = {
            elements: new Set(),
            models: new Set(),
            subcategories: new Set(),
        };
        this.#sendChangedEvent({ set: this, type: SelectionSetEventType.Clear, removals, removed: removals.elements });
    }
    /**
     * Add one or more Ids to the current selection set.
     * @param elem The set of Ids to add.
     * @returns true if any elements were added.
     */
    add(adds) {
        return !!this.#add(adds);
    }
    #add(adds, sendEvent = true) {
        const oldSize = this.size;
        const additions = {};
        forEachSelectableType({
            ids: adds,
            elements: (elementIds) => addIds({
                target: this.#selection.elements,
                ids: elementIds,
                onAdd: (id) => (additions.elements ??= []).push(id),
            }),
            models: (modelIds) => addIds({
                target: this.#selection.models,
                ids: modelIds,
                onAdd: (id) => (additions.models ??= []).push(id),
            }),
            subcategories: (subcategoryIds) => addIds({
                target: this.#selection.subcategories,
                ids: subcategoryIds,
                onAdd: (id) => (additions.subcategories ??= []).push(id),
            }),
        });
        const changed = oldSize !== this.size;
        if (!changed) {
            return undefined;
        }
        if (sendEvent) {
            this.#sendChangedEvent({
                type: SelectionSetEventType.Add,
                set: this,
                additions,
                added: additions.elements ?? [],
            });
        }
        return additions;
    }
    /**
     * Remove one or more Ids from the current selection set.
     * @param elem The set of Ids to remove.
     * @returns true if any elements were removed.
     */
    remove(removes) {
        return !!this.#remove(removes);
    }
    #remove(removes, sendEvent = true) {
        const oldSize = this.size;
        const removals = {};
        forEachSelectableType({
            ids: removes,
            elements: (elementIds) => removeIds({
                target: this.#selection.elements,
                ids: elementIds,
                onRemove: (id) => (removals.elements ??= []).push(id),
            }),
            models: (modelIds) => removeIds({
                target: this.#selection.models,
                ids: modelIds,
                onRemove: (id) => (removals.models ??= []).push(id),
            }),
            subcategories: (subcategoryIds) => removeIds({
                target: this.#selection.subcategories,
                ids: subcategoryIds,
                onRemove: (id) => (removals.subcategories ??= []).push(id),
            }),
        });
        const changed = oldSize !== this.size;
        if (!changed) {
            return undefined;
        }
        if (sendEvent) {
            this.#sendChangedEvent({
                type: SelectionSetEventType.Remove,
                set: this,
                removals,
                removed: removals.elements ?? [],
            });
        }
        return removals;
    }
    /**
     * Add one set of Ids, and remove another set of Ids. Any Ids that are in both sets are removed.
     * @returns True if any Ids were either added or removed.
     */
    addAndRemove(adds, removes) {
        const additions = this.#add(adds, false);
        const removals = this.#remove(removes, false);
        const addedElements = additions?.elements ?? [];
        const removedElements = removals?.elements ?? [];
        if (additions && removals) {
            this.#sendChangedEvent({
                type: SelectionSetEventType.Replace,
                set: this,
                additions,
                added: addedElements,
                removals,
                removed: removedElements,
            });
        }
        else if (additions) {
            this.#sendChangedEvent({
                type: SelectionSetEventType.Add,
                set: this,
                additions,
                added: addedElements,
            });
        }
        else if (removals) {
            this.#sendChangedEvent({
                type: SelectionSetEventType.Remove,
                set: this,
                removals,
                removed: removedElements,
            });
        }
        return !!additions || !!removals;
    }
    /** Invert the state of a set of Ids in the `SelectionSet` */
    invert(ids) {
        const adds = {};
        const removes = {};
        forEachSelectableType({
            ids,
            elements: (elementIds) => {
                for (const id of core_bentley_1.Id64.iterable(elementIds)) {
                    ((this.elements.has(id) ? removes : adds).elements ??= new Set()).add(id);
                }
            },
            models: (modelIds) => {
                for (const id of core_bentley_1.Id64.iterable(modelIds)) {
                    ((this.models.has(id) ? removes : adds).models ??= new Set()).add(id);
                }
            },
            subcategories: (subcategoryIds) => {
                for (const id of core_bentley_1.Id64.iterable(subcategoryIds)) {
                    ((this.subcategories.has(id) ? removes : adds).subcategories ??= new Set()).add(id);
                }
            },
        });
        return this.addAndRemove(adds, removes);
    }
    /** Change selection set to be the supplied set of Ids. */
    replace(ids) {
        if (areEqual(this.#selection, ids)) {
            return false;
        }
        const previousSelection = this.#selection;
        this.#selection = {
            elements: new Set(),
            models: new Set(),
            subcategories: new Set(),
        };
        this.#add(ids, false);
        const additions = {};
        const removals = {};
        forEachSelectableType({
            ids: this.#selection,
            elements: (elementIds) => {
                removeIds({
                    target: previousSelection.elements,
                    ids: elementIds,
                    onNotFound: (id) => (additions.elements ??= new Set()).add(id),
                });
                if (previousSelection.elements.size > 0) {
                    removals.elements = previousSelection.elements;
                }
            },
            models: (modelIds) => {
                removeIds({
                    target: previousSelection.models,
                    ids: modelIds,
                    onNotFound: (id) => (additions.models ??= new Set()).add(id),
                });
                if (previousSelection.models.size > 0) {
                    removals.models = previousSelection.models;
                }
            },
            subcategories: (subcategoryIds) => {
                removeIds({
                    target: previousSelection.subcategories,
                    ids: subcategoryIds,
                    onNotFound: (id) => (additions.subcategories ??= new Set()).add(id),
                });
                if (previousSelection.subcategories.size > 0) {
                    removals.subcategories = previousSelection.subcategories;
                }
            },
        });
        this.#sendChangedEvent({
            type: SelectionSetEventType.Replace,
            set: this,
            additions,
            added: additions.elements ?? [],
            removals,
            removed: removals.elements ?? [],
        });
        return true;
    }
}
exports.SelectionSet = SelectionSet;
function forEachSelectableType({ ids, elements, models, subcategories, }) {
    if (typeof ids === "string" || Array.isArray(ids) || ids instanceof Set) {
        elements(ids);
        return { elements: ids };
    }
    elements(ids.elements ?? []);
    models(ids.models ?? []);
    subcategories(ids.subcategories ?? []);
    return ids;
}
function areEqual(lhs, rhs) {
    let result = true;
    forEachSelectableType({
        ids: rhs,
        elements: (elementIds) => {
            if (result && !areIdsEqual(lhs.elements, elementIds)) {
                result = false;
            }
        },
        models: (modelIds) => {
            if (result && !areIdsEqual(lhs.models, modelIds)) {
                result = false;
            }
        },
        subcategories: (subcategoryIds) => {
            if (result && !areIdsEqual(lhs.subcategories, subcategoryIds)) {
                result = false;
            }
        },
    });
    return result;
}
function areIdsEqual(lhs, rhs) {
    // Size is unreliable if input can contain duplicates...
    if (Array.isArray(rhs)) {
        rhs = core_bentley_1.Id64.toIdSet(rhs);
    }
    if (lhs.size !== core_bentley_1.Id64.sizeOf(rhs)) {
        return false;
    }
    for (const id of core_bentley_1.Id64.iterable(rhs)) {
        if (!lhs.has(id)) {
            return false;
        }
    }
    return true;
}
function addIds({ target, ids, onAdd }) {
    let size = target.size;
    for (const id of core_bentley_1.Id64.iterable(ids)) {
        target.add(id);
        const newSize = target.size;
        if (newSize !== size) {
            onAdd?.(id);
        }
        size = newSize;
    }
}
function removeIds({ target, ids, onRemove, onNotFound, }) {
    let size = target.size;
    for (const id of core_bentley_1.Id64.iterable(ids)) {
        target.delete(id);
        const newSize = target.size;
        if (newSize !== size) {
            onRemove?.(id);
        }
        else {
            onNotFound?.(id);
        }
        size = newSize;
    }
}
//# sourceMappingURL=SelectionSet.js.map