/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module SelectionSet
 */
import { BeEvent, Id64 } from "@itwin/core-bentley";
import { IModelApp } from "./IModelApp";
/** Identifies the type of changes made to the [[SelectionSet]] to produce a [[SelectionSetEvent]].
 * @public
 * @extensions
 */
export var SelectionSetEventType;
(function (SelectionSetEventType) {
    /** Elements have been added to the set. */
    SelectionSetEventType[SelectionSetEventType["Add"] = 0] = "Add";
    /** Elements have been removed from the set. */
    SelectionSetEventType[SelectionSetEventType["Remove"] = 1] = "Remove";
    /** Some elements have been added to the set and others have been removed. */
    SelectionSetEventType[SelectionSetEventType["Replace"] = 2] = "Replace";
    /** All elements are about to be removed from the set. */
    SelectionSetEventType[SelectionSetEventType["Clear"] = 3] = "Clear";
})(SelectionSetEventType || (SelectionSetEventType = {}));
/** Tracks a set of hilited entities. When the set changes, notifies ViewManager so that symbology overrides can be updated in active Viewports.
 * @internal
 */
class HilitedIds extends Id64.Uint32Set {
    constructor(iModel) {
        super();
        this._changing = false;
        this._iModel = iModel;
    }
    add(low, high) {
        super.add(low, high);
        this.onChanged();
    }
    delete(low, high) {
        super.delete(low, high);
        this.onChanged();
    }
    clear() {
        super.clear();
        this.onChanged();
    }
    addIds(ids) {
        this.change(() => super.addIds(ids));
    }
    deleteIds(ids) {
        this.change(() => super.deleteIds(ids));
    }
    onChanged() {
        if (!this._changing)
            IModelApp.viewManager.onSelectionSetChanged(this._iModel);
    }
    change(func) {
        const changing = this._changing;
        this._changing = false;
        func();
        this._changing = changing;
        this.onChanged();
    }
}
/** Keeps the set of hilited elements in sync with the selection set.
 * @internal
 */
class HilitedElementIds extends HilitedIds {
    constructor(iModel, syncWithSelectionSet = true) {
        super(iModel);
        this.wantSyncWithSelectionSet = syncWithSelectionSet;
    }
    get wantSyncWithSelectionSet() { return undefined !== this._removeListener; }
    set wantSyncWithSelectionSet(want) {
        if (want === this.wantSyncWithSelectionSet)
            return;
        if (want) {
            const set = this._iModel.selectionSet;
            this._removeListener = set.onChanged.addListener((ev) => this.change(() => this.processSelectionSetEvent(ev)));
            this.processSelectionSetEvent({
                set,
                type: SelectionSetEventType.Add,
                added: set.elements,
            });
        }
        else {
            this._removeListener();
            this._removeListener = undefined;
        }
    }
    processSelectionSetEvent(ev) {
        if (SelectionSetEventType.Add !== ev.type)
            this.deleteIds(ev.removed);
        if (ev.type === SelectionSetEventType.Add || ev.type === SelectionSetEventType.Replace)
            this.addIds(ev.added);
    }
}
/** 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
 */
export class HiliteSet {
    /** The set of hilited elements. */
    get elements() { return this._elements; }
    /** 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;
    }
    /** Construct a HiliteSet
     * @param iModel The iModel containing the entities to be hilited.
     * @param syncWithSelectionSet If true, the contents of the `elements` set will be synchronized with those in the `iModel`'s [[SelectionSet]].
     */
    constructor(iModel, syncWithSelectionSet = true) {
        this.iModel = iModel;
        this._mode = "union";
        /** Event raised just before changing the value of [[modelSubCategoryMode]]. */
        this.onModelSubCategoryModeChanged = new BeEvent();
        this._elements = new HilitedElementIds(iModel, syncWithSelectionSet);
        this.subcategories = new HilitedIds(iModel);
        this.models = new HilitedIds(iModel);
    }
    /** Control whether the hilited elements 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._elements.wantSyncWithSelectionSet; }
    set wantSyncWithSelectionSet(want) { this._elements.wantSyncWithSelectionSet = want; }
    /** Remove all elements from the hilited set. */
    clear() {
        this.elements.clear();
        this.subcategories.clear();
        this.models.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.
     */
    setHilite(arg, onOff) {
        const oldSize = this.elements.size;
        for (const id of Id64.iterable(arg)) {
            if (onOff)
                this.elements.addId(id);
            else
                this.elements.deleteId(id);
        }
        if (oldSize !== this.elements.size)
            IModelApp.viewManager.onSelectionSetChanged(this.iModel);
    }
}
/** A set of *currently selected* elements for an IModelConnection.
 * Selected elements are displayed with a customizable hilite effect within a [[Viewport]].
 * @see [Hilite.Settings]($common) for customization of the hilite effect.
 * @public
 * @extensions
 */
export class SelectionSet {
    /** The IDs of the selected elements.
     * @note Do not modify this set directly. Instead, use methods like [[SelectionSet.add]].
     */
    get elements() { return this._elements; }
    constructor(iModel) {
        this.iModel = iModel;
        this._elements = new Set();
        /** Called whenever elements are added or removed from this SelectionSet */
        this.onChanged = new BeEvent();
    }
    sendChangedEvent(ev) {
        IModelApp.viewManager.onSelectionSetChanged(this.iModel);
        this.onChanged.raiseEvent(ev);
    }
    /** Get the number of entries in this selection set. */
    get size() { return this.elements.size; }
    /** Check whether there are any selected elements. */
    get isActive() { return this.size !== 0; }
    /** Return true if elemId is in this SelectionSet.
     * @see [[isSelected]]
     */
    has(elemId) { return !!elemId && this.elements.has(elemId); }
    /** Query whether an Id is in the selection set.
     * @see [[has]]
     */
    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 removed = this._elements;
        this._elements = new Set();
        this.sendChangedEvent({ set: this, type: SelectionSetEventType.Clear, removed });
    }
    /**
     * 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(elem) {
        return this._add(elem);
    }
    _add(elem, sendEvent = true) {
        const oldSize = this.elements.size;
        for (const id of Id64.iterable(elem))
            this.elements.add(id);
        const changed = oldSize !== this.elements.size;
        if (sendEvent && changed)
            this.sendChangedEvent({ type: SelectionSetEventType.Add, set: this, added: elem });
        return changed;
    }
    /**
     * 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(elem) {
        return this._remove(elem);
    }
    _remove(elem, sendEvent = true) {
        const oldSize = this.elements.size;
        for (const id of Id64.iterable(elem))
            this.elements.delete(id);
        const changed = oldSize !== this.elements.size;
        if (sendEvent && changed)
            this.sendChangedEvent({ type: SelectionSetEventType.Remove, set: this, removed: elem });
        return changed;
    }
    /**
     * 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 added = this._add(adds, false);
        const removed = this._remove(removes, false);
        if (added && removed)
            this.sendChangedEvent({ type: SelectionSetEventType.Replace, set: this, added: adds, removed: removes });
        else if (added)
            this.sendChangedEvent({ type: SelectionSetEventType.Add, set: this, added: adds });
        else if (removed)
            this.sendChangedEvent({ type: SelectionSetEventType.Remove, set: this, removed: removes });
        return (added || removed);
    }
    /** Invert the state of a set of Ids in the SelectionSet */
    invert(elem) {
        const elementsToAdd = new Set();
        const elementsToRemove = new Set();
        for (const id of Id64.iterable(elem)) {
            if (this.elements.has(id))
                elementsToRemove.add(id);
            else
                elementsToAdd.add(id);
        }
        return this.addAndRemove(elementsToAdd, elementsToRemove);
    }
    /** Change selection set to be the supplied set of Ids. */
    replace(elem) {
        if (areEqual(this.elements, elem))
            return;
        const removed = this._elements;
        this._elements = new Set();
        this._add(elem, false);
        if (0 < removed.size) {
            for (const id of Id64.iterable(elem)) {
                if (removed.has(id))
                    removed.delete(id);
            }
        }
        this.sendChangedEvent({ type: SelectionSetEventType.Replace, set: this, added: elem, removed });
    }
}
function areEqual(lhs, rhs) {
    // Size is unreliable if input can contain duplicates...
    if (Array.isArray(rhs))
        rhs = Id64.toIdSet(rhs);
    if (lhs.size !== Id64.sizeOf(rhs))
        return false;
    for (const id of Id64.iterable(rhs))
        if (!lhs.has(id))
            return false;
    return true;
}
//# sourceMappingURL=SelectionSet.js.map