/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Tools
 */
import { assert } from "@itwin/core-bentley";
import { IModelError } from "@itwin/core-common";
import { Point2d, Point3d, PolygonOps } from "@itwin/core-geometry";
import { LocateFilterStatus } from "../ElementLocateManager";
import { FuzzySearch } from "../FuzzySearch";
import { IModelApp } from "../IModelApp";
/**
 * @public
 * @extensions
 */
export var BeButton;
(function (BeButton) {
    BeButton[BeButton["Data"] = 0] = "Data";
    BeButton[BeButton["Reset"] = 1] = "Reset";
    BeButton[BeButton["Middle"] = 2] = "Middle";
})(BeButton || (BeButton = {}));
/**
 * @public
 * @extensions
 */
export var CoordinateLockOverrides;
(function (CoordinateLockOverrides) {
    CoordinateLockOverrides[CoordinateLockOverrides["None"] = 0] = "None";
    CoordinateLockOverrides[CoordinateLockOverrides["ACS"] = 2] = "ACS";
    CoordinateLockOverrides[CoordinateLockOverrides["Grid"] = 4] = "Grid";
    CoordinateLockOverrides[CoordinateLockOverrides["All"] = 65535] = "All";
})(CoordinateLockOverrides || (CoordinateLockOverrides = {}));
/** The *source* that generated an event.
 * @public
 * @extensions
 */
export var InputSource;
(function (InputSource) {
    /** Source not defined */
    InputSource[InputSource["Unknown"] = 0] = "Unknown";
    /** From a mouse or other pointing device */
    InputSource[InputSource["Mouse"] = 1] = "Mouse";
    /** From a touch screen */
    InputSource[InputSource["Touch"] = 2] = "Touch";
})(InputSource || (InputSource = {}));
/** The *source* that generated a coordinate.
 * @public
 * @extensions
 */
export var CoordSource;
(function (CoordSource) {
    /** Event was created by an action from the user */
    CoordSource[CoordSource["User"] = 0] = "User";
    /** Event was created by a program or by a precision keyin */
    CoordSource[CoordSource["Precision"] = 1] = "Precision";
    /** Event was created by a tentative point */
    CoordSource[CoordSource["TentativePoint"] = 2] = "TentativePoint";
    /** Event was created by snapping to an element */
    CoordSource[CoordSource["ElemSnap"] = 3] = "ElemSnap";
})(CoordSource || (CoordSource = {}));
/** Numeric mask for a set of modifier keys (control, shift, and alt).
 * @public
 * @extensions
 */
export var BeModifierKeys;
(function (BeModifierKeys) {
    BeModifierKeys[BeModifierKeys["None"] = 0] = "None";
    BeModifierKeys[BeModifierKeys["Control"] = 1] = "Control";
    BeModifierKeys[BeModifierKeys["Shift"] = 2] = "Shift";
    BeModifierKeys[BeModifierKeys["Alt"] = 4] = "Alt";
})(BeModifierKeys || (BeModifierKeys = {}));
/**
 * @public
 * @extensions
 */
export class BeButtonState {
    constructor() {
        this._downUorPt = new Point3d();
        this._downRawPt = new Point3d();
        this.downTime = 0;
        this.isDown = false;
        this.isDoubleClick = false;
        this.isDragging = false;
        this.inputSource = InputSource.Unknown;
    }
    get downRawPt() { return this._downRawPt; }
    set downRawPt(pt) { this._downRawPt.setFrom(pt); }
    get downUorPt() { return this._downUorPt; }
    set downUorPt(pt) { this._downUorPt.setFrom(pt); }
    init(downUorPt, downRawPt, downTime, isDown, isDoubleClick, isDragging, source) {
        this.downUorPt = downUorPt;
        this.downRawPt = downRawPt;
        this.downTime = downTime;
        this.isDown = isDown;
        this.isDoubleClick = isDoubleClick;
        this.isDragging = isDragging;
        this.inputSource = source;
    }
}
/** Object sent to Tools that holds information about button/touch/wheel events.
 * @public
 * @extensions
 */
export class BeButtonEvent {
    constructor(props) {
        this._point = new Point3d();
        this._rawPoint = new Point3d();
        this._viewPoint = new Point3d();
        /** How the coordinate values were generated (either from an action by the user or from a program.) */
        this.coordsFrom = CoordSource.User;
        /** The keyboard modifiers that were pressed when the event was generated. */
        this.keyModifiers = BeModifierKeys.None;
        /** If true, this event was generated from a mouse-down transition, false from a button-up transition. */
        this.isDown = false;
        /** If true, this is the second down in a rapid double-click of the same button. */
        this.isDoubleClick = false;
        /** If true, this event was created by pressing, holding, and then moving a mouse button. */
        this.isDragging = false;
        /** The mouse button that created this event. */
        this.button = BeButton.Data;
        /** Whether this event came from a pointing device (e.g. mouse) or a touch device. */
        this.inputSource = InputSource.Unknown;
        if (props)
            this.init(props);
    }
    /** Determine whether this BeButtonEvent has valid data.
     * @note BeButtonEvents may be constructed as "blank", and are not considered to hold valid data unless the [[viewport]] member is defined.
     */
    get isValid() { return this.viewport !== undefined; }
    /** The point for this event, in world coordinates.
     * @note these coordinates may have been *adjusted* for some reason (e.g. snapping, locks, etc.) from the [[rawPoint]].
     */
    get point() { return this._point; }
    set point(pt) { this._point.setFrom(pt); }
    /** The *raw* (unadjusted) point for this event, in world coordinates. */
    get rawPoint() { return this._rawPoint; }
    set rawPoint(pt) { this._rawPoint.setFrom(pt); }
    /** The point, in screen coordinates for this event.
     * @note generally the z value is not useful, but some 3d pointing devices do supply it.
     */
    get viewPoint() { return this._viewPoint; }
    set viewPoint(pt) { this._viewPoint.setFrom(pt); }
    /** The difference in screen coordinates from previous motion event
     * @internal
     */
    get movement() { return this._movement; }
    set movement(mov) { this._movement = mov; }
    /** Mark this BeButtonEvent as invalid. Can only become valid again by calling [[init]] */
    invalidate() { this.viewport = undefined; }
    /** Initialize the values of this BeButtonEvent. */
    init(props) {
        if (undefined !== props.point)
            this.point = props.point;
        if (undefined !== props.rawPoint)
            this.rawPoint = props.rawPoint;
        if (undefined !== props.viewPoint)
            this.viewPoint = props.viewPoint;
        if (undefined !== props.viewport)
            this.viewport = props.viewport;
        if (undefined !== props.coordsFrom)
            this.coordsFrom = props.coordsFrom;
        if (undefined !== props.keyModifiers)
            this.keyModifiers = props.keyModifiers;
        if (undefined !== props.isDown)
            this.isDown = props.isDown;
        if (undefined !== props.isDoubleClick)
            this.isDoubleClick = props.isDoubleClick;
        if (undefined !== props.isDragging)
            this.isDragging = props.isDragging;
        if (undefined !== props.button)
            this.button = props.button;
        if (undefined !== props.inputSource)
            this.inputSource = props.inputSource;
    }
    /** Determine whether the control key was pressed  */
    get isControlKey() { return 0 !== (this.keyModifiers & BeModifierKeys.Control); }
    /** Determine whether the shift key was pressed  */
    get isShiftKey() { return 0 !== (this.keyModifiers & BeModifierKeys.Shift); }
    /** Determine whether the alt key was pressed  */
    get isAltKey() { return 0 !== (this.keyModifiers & BeModifierKeys.Alt); }
    /** Copy the values from another BeButtonEvent into this BeButtonEvent */
    setFrom(src) {
        this.point = src.point;
        this.rawPoint = src.rawPoint;
        this.viewPoint = src.viewPoint;
        this.viewport = src.viewport;
        this.coordsFrom = src.coordsFrom;
        this.keyModifiers = src.keyModifiers;
        this.isDown = src.isDown;
        this.isDoubleClick = src.isDoubleClick;
        this.isDragging = src.isDragging;
        this.button = src.button;
        this.inputSource = src.inputSource;
        return this;
    }
    /** Make a copy of this BeButtonEvent. */
    clone() { return new this.constructor(this); }
}
/** A ButtonEvent generated by touch input.
 * @public
 * @extensions
 */
export class BeTouchEvent extends BeButtonEvent {
    get touchCount() { return this.touchEvent.targetTouches.length; }
    get isSingleTouch() { return 1 === this.touchCount; }
    get isTwoFingerTouch() { return 2 === this.touchCount; }
    get isSingleTap() { return 1 === this.tapCount && 1 === this.touchCount; }
    get isDoubleTap() { return 2 === this.tapCount && 1 === this.touchCount; }
    get isTwoFingerTap() { return 1 === this.tapCount && 2 === this.touchCount; }
    constructor(props) {
        super(props);
        this.tapCount = 0;
        this.touchEvent = props.touchEvent;
    }
    setFrom(src) {
        super.setFrom(src);
        this.touchEvent = src.touchEvent;
        this.tapCount = src.tapCount;
        return this;
    }
    static getTouchPosition(touch, vp) {
        const rect = vp.getClientRect();
        return Point2d.createFrom({ x: touch.clientX - rect.left, y: touch.clientY - rect.top });
    }
    static getTouchListCentroid(list, vp) {
        switch (list.length) {
            case 0: {
                return undefined;
            }
            case 1: {
                return this.getTouchPosition(list[0], vp);
            }
            case 2: {
                return this.getTouchPosition(list[0], vp).interpolate(0.5, this.getTouchPosition(list[1], vp));
            }
            default: {
                const points = [];
                // eslint-disable-next-line @typescript-eslint/prefer-for-of
                for (let i = 0; i < list.length; i++) {
                    points.push(this.getTouchPosition(list[i], vp));
                }
                const centroid = Point2d.createZero();
                PolygonOps.centroidAndAreaXY(points, centroid);
                return centroid;
            }
        }
    }
    static findTouchById(list, id) {
        // eslint-disable-next-line @typescript-eslint/prefer-for-of
        for (let i = 0; i < list.length; i++) {
            if (id === list[i].identifier)
                return list[i];
        }
        return undefined;
    }
}
/** A BeButtonEvent generated by movement of a mouse wheel.
 * @note wheel events include mouse location.
 * @public
 * @extensions
 */
export class BeWheelEvent extends BeButtonEvent {
    constructor(props) {
        super(props);
        this.wheelDelta = (props && props.wheelDelta !== undefined) ? props.wheelDelta : 0;
        this.time = (props && props.time) ? props.time : Date.now();
    }
    setFrom(src) {
        super.setFrom(src);
        this.wheelDelta = src.wheelDelta;
        this.time = src.time;
        return this;
    }
}
/** A Tool that performs an action. It has a *toolId* that uniquely identifies it, so it can be found via a lookup in the [[ToolRegistry]].
 * Every time a tools run, a new instance of (a subclass of) this class is created and its [[run]] method is invoked.
 * @see [[InteractiveTool]] for a base Tool class to handle user input events from a Viewport.
 * @see [Tools]($docs/learning/frontend/tools.md)
 * @public
 * @extensions
 */
export class Tool {
    /** @internal */
    get ctor() { return this.constructor; }
    constructor(..._args) { }
    /** The minimum number of arguments allowed by [[parseAndRun]]. If subclasses override [[parseAndRun]], they should also
     * override this method to indicate the minimum number of arguments their implementation expects. UI controls can use
     * this information to ensure the tool has enough information to execute.
     */
    static get minArgs() { return 0; }
    /** The maximum number of arguments allowed by [[parseAndRun]], or undefined if there is no maximum.
     * If subclasses override [[parseAndRun]], they should also override this method to indicate the maximum
     * number of arguments their implementation expects.
     */
    static get maxArgs() { return 0; }
    /**
     * Register this Tool class with the [[ToolRegistry]].
     * @param namespace optional namespace to supply to [[ToolRegistry.register]]. If undefined, use namespace from superclass.
     */
    static register(namespace) {
        IModelApp.tools.register(this, namespace);
    }
    static getLocalizedKey(name) {
        const key = `tools.${this.toolId}.${name}`;
        const val = IModelApp.localization.getLocalizedString(key, { ns: this.namespace });
        return key === val ? undefined : val; // if translation for key doesn't exist, `translate` returns the key as the result
    }
    /**
     * Get the localized keyin string for this Tool class. This returns the value of "tools." + this.toolId + ".keyin" from
     * its registered Namespace (e.g. "en/MyApp.json").
     */
    static get keyin() {
        const keyin = this.getLocalizedKey("keyin");
        return (undefined !== keyin) ? keyin : ""; // default to empty string
    }
    /**
     * Get the English keyin string for this Tool class. This returns the value of "tools." + this.toolId + ".keyin" from
     * its registered Namespace (e.g. "en/MyApp.json").
     */
    static get englishKeyin() {
        const key = `tools.${this.toolId}.keyin`;
        const val = IModelApp.localization.getEnglishString(this.namespace, key);
        return val !== key ? val : ""; // default to empty string
    }
    /**
     * Get the localized flyover for this Tool class. This returns the value of "tools." + this.toolId + ".flyover" from
     * its registered Namespace (e.g. "en/MyApp.json"). If that key is not in the localization namespace,
     * [[keyin]] is returned.
     */
    static get flyover() {
        const flyover = this.getLocalizedKey("flyover");
        return (undefined !== flyover) ? flyover : this.keyin; // default to keyin
    }
    /**
     * Get the localized description for this Tool class. This returns the value of "tools." + this.toolId + ".description" from
     * its registered Namespace (e.g. "en/MyApp.json"). If that key is not in the localization namespace,
     * [[flyover]] is returned.
     */
    static get description() {
        const description = this.getLocalizedKey("description");
        return (undefined !== description) ? description : this.flyover; // default to flyover
    }
    /**
     * Get the toolId string for this Tool class. This string is used to identify the Tool in the ToolRegistry and is used to localize
     * the keyin, description, etc. from the current locale.
     */
    get toolId() { return this.ctor.toolId; }
    /** Get the localized keyin string from this Tool's class
     * @see `static get keyin()`
     */
    get keyin() { return this.ctor.keyin; }
    /** Get the localized flyover string from this Tool's class
     * @see `static get flyover()`
     */
    get flyover() { return this.ctor.flyover; }
    /** Get the localized description string from this Tool's class
     * @see `static get description()`
     */
    get description() { return this.ctor.description; }
    /** Get the iconSpec from this Tool's class.
     * @see `static iconSpec`
     */
    get iconSpec() { return this.ctor.iconSpec; }
    /**
     * Run this instance of a Tool. Subclasses should override to perform some action.
     * @returns `true` if the tool executed successfully.
     */
    async run(..._args) { return true; }
    /** Run this instance of a tool using a series of string arguments. Override this method to parse the arguments, and if they're
     * acceptable, execute your [[run]] method. If the arguments aren't valid, return `false`.
     * @note if you override this method, you must also override the static [[minArgs]] and [[maxArgs]] getters.
     * @note Generally, implementers of this method are **not** expected to call `super.parseAndRun(...)`. Instead, call your
     * [[run]] method with the appropriate (parsed) arguments directly.
     */
    async parseAndRun(..._args) {
        return this.run();
    }
}
/** If true, this Tool will not appear in the list from [[ToolRegistry.getToolList]]. This should be overridden in subclasses to hide them. */
Tool.hidden = false;
/** The unique string that identifies this tool. This must be overridden in every subclass. */
Tool.toolId = "";
/** The icon for this Tool. This may be overridden in subclasses to provide a tool icon.
 * The value is the name of an icon WebFont entry, or if specifying an SVG symbol, use `svg:` prefix.
 */
Tool.iconSpec = "";
/**
 * @public
 * @extensions
 */
export var EventHandled;
(function (EventHandled) {
    EventHandled[EventHandled["No"] = 0] = "No";
    EventHandled[EventHandled["Yes"] = 1] = "Yes";
})(EventHandled || (EventHandled = {}));
/** A Tool that may be installed, via [[ToolAdmin]], to handle user input. The ToolAdmin manages the currently installed ViewingTool, PrimitiveTool,
 * InputCollector, and IdleTool. Each must derive from this class and there may only be one of each type installed at a time.
 * @public
 * @extensions
 */
export class InteractiveTool extends Tool {
    constructor() {
        super(...arguments);
        /** Used to avoid sending tools up events for which they did not receive the down event. */
        this.receivedDownEvent = false;
    }
    /** Override to execute additional logic when tool is installed. Return false to prevent this tool from becoming active */
    async onInstall() { return true; }
    /** Override to execute additional logic after tool becomes active */
    async onPostInstall() { }
    /** Override Call to reset tool to initial state */
    async onReinitialize() { }
    /** Invoked when the tool becomes no longer active, to perform additional cleanup logic */
    async onCleanup() { }
    /** Notification of a ViewTool or InputCollector starting and this tool is being suspended.
     * @note Applies only to PrimitiveTool and InputCollector, a ViewTool can't be suspended.
     */
    async onSuspend() { }
    /** Notification of a ViewTool or InputCollector exiting and this tool is being unsuspended.
     *  @note Applies only to PrimitiveTool and InputCollector, a ViewTool can't be suspended.
     */
    async onUnsuspend() { }
    /** Called to support operations on pickable decorations, like snapping. */
    testDecorationHit(_id) { return false; }
    /** Called to allow snapping to pickable decoration geometry.
     * @note Snap geometry can be different from decoration geometry (ex. center point of a + symbol). Valid decoration geometry for snapping should be "stable" and not change based on the current cursor location.
     */
    getDecorationGeometry(_hit) { return undefined; }
    /**
     * Called to allow an active tool to display non-element decorations in overlay mode.
     * This method is NOT called while the tool is suspended by a viewing tool or input collector.
     */
    decorate(_context) { }
    /**
     * Called to allow a suspended tool to display non-element decorations in overlay mode.
     * This method is ONLY called when the tool is suspended by a viewing tool or input collector.
     * @note Applies only to PrimitiveTool and InputCollector, a ViewTool can't be suspended.
     */
    decorateSuspended(_context) { }
    /** Invoked when the reset button is pressed.
     * @return No by default. Sub-classes may ascribe special meaning to this status.
     * @note To support right-press menus, a tool should put its reset event processing in onResetButtonUp instead of onResetButtonDown.
     */
    async onResetButtonDown(_ev) { return EventHandled.No; }
    /** Invoked when the reset button is released.
     * @return No by default. Sub-classes may ascribe special meaning to this status.
     */
    async onResetButtonUp(_ev) { return EventHandled.No; }
    /** Invoked when the data button is pressed.
     * @return No by default. Sub-classes may ascribe special meaning to this status.
     */
    async onDataButtonDown(_ev) { return EventHandled.No; }
    /** Invoked when the data button is released.
     * @return No by default. Sub-classes may ascribe special meaning to this status.
     */
    async onDataButtonUp(_ev) { return EventHandled.No; }
    /** Invoked when the middle mouse button is pressed.
     * @return Yes if event completely handled by tool and event should not be passed on to the IdleTool.
     */
    async onMiddleButtonDown(_ev) { return EventHandled.No; }
    /** Invoked when the middle mouse button is released.
     * @return Yes if event completely handled by tool and event should not be passed on to the IdleTool.
     */
    async onMiddleButtonUp(_ev) { return EventHandled.No; }
    /** Invoked when the cursor is moving */
    async onMouseMotion(_ev) { }
    /** Invoked when the cursor begins moving while a button is depressed.
     * @return Yes if event completely handled by tool and event should not be passed on to the IdleTool.
     */
    async onMouseStartDrag(_ev) { return EventHandled.No; }
    /** Invoked when the button is released after onMouseStartDrag.
     * @note default placement tool behavior is to treat press, drag, and release of data button the same as click, click by calling onDataButtonDown.
     * @return Yes if event completely handled by tool and event should not be passed on to the IdleTool.
     */
    async onMouseEndDrag(ev) {
        if (BeButton.Data !== ev.button)
            return EventHandled.No;
        if (ev.isDown)
            return this.onDataButtonDown(ev);
        const downEv = ev.clone();
        downEv.isDown = true;
        return this.onDataButtonDown(downEv);
    }
    /** Invoked when the mouse wheel moves.
     * @return Yes if event completely handled by tool and event should not be passed on to the IdleTool.
     */
    async onMouseWheel(_ev) { return EventHandled.No; }
    /** Called when Control, Shift, or Alt modifier keys are pressed or released.
     * @param _wentDown up or down key event
     * @param _modifier The modifier key mask
     * @param _event The event that caused this call
     * @return Yes to refresh view decorations or update dynamics.
     */
    async onModifierKeyTransition(_wentDown, _modifier, _event) { return EventHandled.No; }
    /** Called when any key is pressed or released.
     * @param _wentDown up or down key event
     * @param _keyEvent The KeyboardEvent
     * @return Yes to prevent further processing of this event
     * @see [[onModifierKeyTransition]]
     */
    async onKeyTransition(_wentDown, _keyEvent) { return EventHandled.No; }
    /** Called when user adds a touch point by placing a finger or stylus on the surface. */
    async onTouchStart(_ev) { }
    /** Called when user removes a touch point by lifting a finger or stylus from the surface. */
    async onTouchEnd(_ev) { }
    /** Called when the last touch point is removed from the surface completing the current gesture. This is a convenience event sent following onTouchEnd when no target touch points remain on the surface. */
    async onTouchComplete(_ev) { }
    /** Called when a touch point is interrupted in some way and needs to be dropped from the list of target touches. */
    async onTouchCancel(_ev) { }
    /** Called when a touch point moves along the surface. */
    async onTouchMove(_ev) { }
    /** Called after at least one touch point has moved for an appreciable time and distance along the surface to not be considered a tap.
     * @param _ev The event that caused this call
     * @param _startEv The event from the last call to onTouchStart
     * @return Yes if event completely handled by tool and event should not be passed on to the IdleTool.
     */
    async onTouchMoveStart(_ev, _startEv) { return EventHandled.No; }
    /** Called when touch point(s) are added and removed from a surface within a small time window without any touch point moving.
     * @param _ev The event that caused this call
     * @return Yes if event completely handled by tool and event should not be passed on to the IdleTool.
     * @note A double or triple tap event will not be preceded by a single tap event.
     */
    async onTouchTap(_ev) { return EventHandled.No; }
    isCompatibleViewport(_vp, _isSelectedViewChange) { return true; }
    isValidLocation(_ev, _isButtonEvent) { return true; }
    /**
     * Called when active view changes. Tool may choose to restart or exit based on current view type.
     * @param previous The previously active view.
     * @param current The new active view.
     */
    onSelectedViewportChanged(_previous, _current) { }
    /**
     * Invoked before the locate tooltip is displayed to retrieve the information about the located element. Allows the tool to override the toolTip.
     * @param hit The HitDetail whose info is needed.
     * @return A Promise for the HTMLElement or string to describe the hit.
     * @note If you override this method, you may decide whether to call your superclass' implementation or not (it is not required).
     */
    async getToolTip(_hit) { return _hit.getToolTip(); }
    /** Convenience method to check whether control key is currently down without needing a button event. */
    get isControlDown() { return IModelApp.toolAdmin.currentInputState.isControlDown; }
    /** Fill the supplied button event from the current cursor location.   */
    getCurrentButtonEvent(ev) { IModelApp.toolAdmin.fillEventFromCursorLocation(ev); }
    /** Call to find out if dynamics are currently active. */
    get isDynamicsStarted() { return IModelApp.viewManager.inDynamicsMode; }
    /** Call to initialize dynamics mode. While dynamics are active onDynamicFrame will be called. Dynamics are typically only used by a PrimitiveTool that creates or modifies geometric elements. */
    beginDynamics() { IModelApp.toolAdmin.beginDynamics(); }
    /** Call to terminate dynamics mode. */
    endDynamics() { IModelApp.toolAdmin.endDynamics(); }
    /** Called to allow Tool to display dynamic elements. */
    onDynamicFrame(_ev, _context) { }
    /** Invoked to allow tools to filter which elements can be located.
     * @return Reject if hit is unacceptable for this tool (fill out response with explanation, if it is defined)
     */
    async filterHit(_hit, _out) { return LocateFilterStatus.Accept; }
    /** Helper method to keep the view cursor, display of locate circle, and coordinate lock overrides consistent with [[AccuSnap.isLocateEnabled]] and [[AccuSnap.isSnapEnabled]].
     * @param enableLocate Value to pass to [[AccuSnap.enableLocate]]. Tools that locate elements should always pass true to give the user feedback regarding the element at the current cursor location.
     * @param enableSnap Optional value to pass to [[AccuSnap.enableSnap]]. Tools that don't care about the element pick location should not pass true. Default is false.
     * @note User must also have snapping enabled [[AccuSnap.isSnapEnabledByUser]], otherwise [[TentativePoint]] is used to snap.
     * @param cursor Optional tool specific cursor override. Default is either cross or dynamics cursor depending on whether dynamics are currently active.
     * @param coordLockOvr Optional tool specific coordinate lock overrides. A tool that only identifies elements and does not use [[BeButtonEvent.point]] can set ToolState.coordLockOvr to CoordinateLockOverrides.ACS
     * or CoordinateLockOverrides.All, otherwise locate is affected by the input point being first projected to the ACS plane. A tool that will use [[BeButtonEvent.point]], especially those that call [[AccuSnap.enableSnap]]
     * should honor all locks and leave ToolState.coordLockOvr set to CoordinateLockOverrides.None, the default for ViewTool and PrimitiveTool.
     */
    changeLocateState(enableLocate, enableSnap, cursor, coordLockOvr) {
        const { toolAdmin, viewManager, accuSnap } = IModelApp;
        if (undefined !== cursor) {
            toolAdmin.setCursor(cursor);
            toolAdmin.setLocateCircleOn(enableLocate);
            viewManager.invalidateDecorationsAllViews();
        }
        else {
            toolAdmin.setLocateCursor(enableLocate);
        }
        // Always set the one that is true first, otherwise AccuSnap will clear the TouchCursor.
        if (enableLocate) {
            accuSnap.enableLocate(true);
            accuSnap.enableSnap(true === enableSnap);
        }
        else {
            accuSnap.enableSnap(true === enableSnap);
            accuSnap.enableLocate(false);
        }
        if (undefined !== coordLockOvr) {
            toolAdmin.toolState.coordLockOvr = coordLockOvr;
        }
        else {
            if (enableLocate && !accuSnap.isSnapEnabled)
                toolAdmin.toolState.coordLockOvr |= CoordinateLockOverrides.ACS;
            else
                toolAdmin.toolState.coordLockOvr &= ~CoordinateLockOverrides.ACS;
        }
    }
    /** Helper method for tools that need to locate existing elements.
     * Initializes [[ElementLocateManager]], changes the view cursor to locate, enables display of the locate circle, and sets the appropriate coordinate lock overrides.
     * @see [[changeLocateState]]
     */
    initLocateElements(enableLocate = true, enableSnap, cursor, coordLockOvr) {
        IModelApp.locateManager.initToolLocate();
        this.changeLocateState(enableLocate, enableSnap, cursor, coordLockOvr);
    }
    /** @internal */
    restoreToolSettingPropertyValue(property) {
        const itemValue = IModelApp.toolAdmin.toolSettingsState.getInitialToolSettingValue(this.toolId, property.name);
        if (undefined === itemValue?.value)
            return false;
        property.dialogItemValue = itemValue;
        return true;
    }
    /** @internal */
    saveToolSettingPropertyValue(property, itemValue) {
        if (undefined === itemValue.value)
            return false;
        property.value = itemValue.value;
        IModelApp.toolAdmin.toolSettingsState.saveToolSettingProperty(this.toolId, property.item);
        return true;
    }
    /** @internal */
    syncToolSettingPropertyValue(property, isDisabled) {
        if (undefined !== isDisabled)
            property.isDisabled = isDisabled;
        this.syncToolSettingsProperties([property.syncItem]);
    }
    /** @internal */
    getToolSettingPropertyByName(propertyName) {
        const foundProperty = this.toolSettingProperties?.get(propertyName);
        if (foundProperty)
            return foundProperty;
        throw new Error(`property not found: ${propertyName}`);
    }
    /** Override to return the property that is disabled/enabled if the supplied property is a lock property.
     * @see [[changeToolSettingPropertyValue]]
     * @public
     */
    getToolSettingPropertyLocked(_property) {
        return undefined;
    }
    /** Helper method for responding to a tool setting property value change by updating saved settings.
     * @see [[applyToolSettingPropertyChange]]
     * @see [[getToolSettingPropertyLocked]] to return the corresponding locked property, if any.
     * @public
     */
    changeToolSettingPropertyValue(syncItem) {
        const property = this.getToolSettingPropertyByName(syncItem.propertyName);
        if (!this.saveToolSettingPropertyValue(property, syncItem.value))
            return false;
        const lockedProperty = this.getToolSettingPropertyLocked(property);
        if (undefined !== lockedProperty)
            this.syncToolSettingPropertyValue(lockedProperty, !property.value);
        return true;
    }
    /** Helper method to establish initial values for tool setting properties from saved settings.
     * @see [[supplyToolSettingsProperties]]
     * @public
     */
    initializeToolSettingPropertyValues(properties) {
        if (undefined !== this.toolSettingProperties)
            return;
        this.toolSettingProperties = new Map();
        for (const property of properties) {
            this.toolSettingProperties.set(property.name, property);
            this.restoreToolSettingPropertyValue(property);
        }
    }
    /** Used to supply list of properties that can be used to generate ToolSettings. If undefined is returned then no ToolSettings will be displayed.
     * @see [[initializeToolSettingPropertyValues]]
     * @public
     */
    supplyToolSettingsProperties() { return undefined; }
    /** Used to receive property changes from UI. Return false if there was an error applying updatedValue.
     * @see [[changeToolSettingPropertyValue]]
     * @public
     */
    async applyToolSettingPropertyChange(_updatedValue) { return true; }
    /** Called by tool to synchronize the UI with property changes made by tool. This is typically used to provide user feedback during tool dynamics.
     * If the syncData contains a quantity value and if the displayValue is not defined, the displayValue will be generated in the UI layer before displaying the value.
     * @public
     */
    syncToolSettingsProperties(syncData) {
        IModelApp.toolAdmin.syncToolSettingsProperties(this.toolId, syncData);
    }
    /** Called by tool to inform UI to reload ToolSettings with new set of properties. This allows properties to be added or removed from ToolSetting
     * component as tool processing progresses.
     * @public
     */
    reloadToolSettingsProperties() {
        IModelApp.toolAdmin.reloadToolSettingsProperties();
    }
    /** Used to "bump" the value of a tool setting. To "bump" a setting means to toggle a boolean value or cycle through enum values.
     * If no `settingIndex` param is specified, the first setting is bumped.
     * Return true if the setting was successfully bumped.
     * @public
     */
    async bumpToolSetting(_settingIndex) { return false; }
}
/** The InputCollector class can be used to implement a command for gathering input
 * (ex. get a distance by snapping to 2 points) without affecting the state of the active primitive tool.
 * An InputCollector will suspend the active PrimitiveTool and can be suspended by a ViewTool.
 * @public
 * @extensions
 */
export class InputCollector extends InteractiveTool {
    async run(..._args) {
        const toolAdmin = IModelApp.toolAdmin;
        // An input collector can only suspend a primitive tool, don't install if a viewing tool is active...
        if (undefined !== toolAdmin.viewTool || !await toolAdmin.onInstallTool(this))
            return false;
        await toolAdmin.startInputCollector(this);
        await toolAdmin.onPostInstallTool(this);
        return true;
    }
    async exitTool() {
        return IModelApp.toolAdmin.exitInputCollector();
    }
    async onResetButtonUp(_ev) {
        await this.exitTool();
        return EventHandled.Yes;
    }
}
/** The result type of [[ToolRegistry.parseAndRun]].
 * @public
 * @extensions
 */
export var ParseAndRunResult;
(function (ParseAndRunResult) {
    /** The tool's `parseAndRun` method was invoked and returned `true`. */
    ParseAndRunResult[ParseAndRunResult["Success"] = 0] = "Success";
    /** No tool matching the toolId in the keyin is registered. */
    ParseAndRunResult[ParseAndRunResult["ToolNotFound"] = 1] = "ToolNotFound";
    /** The number of arguments supplied does not meet the constraints of the Tool. @see [[Tool.minArgs]] and [[Tool.maxArgs]]. */
    ParseAndRunResult[ParseAndRunResult["BadArgumentCount"] = 2] = "BadArgumentCount";
    /** The tool's `parseAndRun` method returned `false`. */
    ParseAndRunResult[ParseAndRunResult["FailedToRun"] = 3] = "FailedToRun";
    /** An opening double-quote character was not paired with a closing double-quote character. */
    ParseAndRunResult[ParseAndRunResult["MismatchedQuotes"] = 4] = "MismatchedQuotes";
})(ParseAndRunResult || (ParseAndRunResult = {}));
/** Possible errors resulting from [[ToolRegistry.parseKeyin]].
 * @public
 * @extensions
 */
export var KeyinParseError;
(function (KeyinParseError) {
    /** No registered tool matching the keyin was found. */
    KeyinParseError[KeyinParseError["ToolNotFound"] = 1] = "ToolNotFound";
    /** The opening double-quote of an argument was not terminated with a closing double-quote. */
    KeyinParseError[KeyinParseError["MismatchedQuotes"] = 4] = "MismatchedQuotes";
})(KeyinParseError || (KeyinParseError = {}));
/** The ToolRegistry holds a mapping between toolIds and their corresponding [[Tool]] class. This provides the mechanism to
 * find Tools by their toolId, and also a way to iterate over the set of Tools available.
 * @public
 */
export class ToolRegistry {
    constructor() {
        this.tools = new Map();
    }
    shutdown() {
        this.tools.clear();
        this._keyinList = undefined;
    }
    /**
     * Un-register a previously registered Tool class.
     * @param toolId the toolId of a previously registered tool to unRegister.
     */
    unRegister(toolId) {
        this.tools.delete(toolId);
        this._keyinList = undefined;
    }
    /**
     * Register a Tool class. This establishes a connection between the toolId of the class and the class itself.
     * @param toolClass the subclass of Tool to register.
     * @param namespace the namespace for the localized strings for this tool. If undefined, use namespace from superclass.
     */
    register(toolClass, namespace) {
        if (namespace) // namespace is optional because it can come from superclass
            toolClass.namespace = namespace;
        if (toolClass.toolId.length === 0)
            return; // must be an abstract class, ignore it
        if (!toolClass.namespace)
            throw new IModelError(-1, "Tools must have a namespace");
        this.tools.set(toolClass.toolId, toolClass);
        this._keyinList = undefined; // throw away the current keyinList so we'll produce a new one next time we're asked.
    }
    /**
     * Register all the Tool classes found in a module.
     * @param modelObj the module to search for subclasses of Tool.
     */
    registerModule(moduleObj, namespace) {
        for (const thisMember in moduleObj) { // eslint-disable-line guard-for-in
            const thisTool = moduleObj[thisMember];
            if (thisTool.prototype instanceof Tool) {
                this.register(thisTool, namespace);
            }
        }
    }
    /** Look up a tool by toolId */
    find(toolId) {
        return this.tools.get(toolId);
    }
    /**
     * Look up a tool by toolId and, if found, create an instance with the supplied arguments.
     * @param toolId the toolId of the tool
     * @param args arguments to pass to the constructor.
     * @returns an instance of the registered Tool class, or undefined if toolId is not registered.
     */
    create(toolId, ...args) {
        const toolClass = this.find(toolId);
        return toolClass ? new toolClass(...args) : undefined;
    }
    /**
     * Look up a tool by toolId and, if found, create an instance with the supplied arguments and run it.
     * @param toolId toolId of the immediate tool
     * @param args arguments to pass to the constructor, and to run.
     * @return true if the tool was found and successfully run.
     */
    async run(toolId, ...args) {
        const tool = this.create(toolId, ...args);
        return tool !== undefined && tool.run(...args);
    }
    /**
     * Split key-in into and array of string arguments. Handles embedded quoted strings.
     * @param keyin keyin string to process
     * #return an Array of string argument
     */
    tokenize(keyin) {
        const isWhitespace = (char) => "" === char.trim();
        const tokens = [];
        let index = 0;
        let firstQuotedIndex;
        while (index < keyin.length) {
            // Looking for beginning of next token.
            const ch = keyin[index];
            if (isWhitespace(ch)) {
                ++index;
                continue;
            }
            if ('"' !== ch) {
                // Unquoted token.
                let endIndex = keyin.length;
                for (let i = index + 1; i < keyin.length; i++) {
                    if (isWhitespace(keyin[i])) {
                        endIndex = i;
                        break;
                    }
                }
                tokens.push(keyin.substring(index, endIndex));
                index = endIndex;
                continue;
            }
            // Quoted argument.
            if (undefined === firstQuotedIndex)
                firstQuotedIndex = tokens.length;
            let endQuoteIndex;
            let searchIndex = index + 1;
            let anyEmbeddedQuotes = false;
            while (searchIndex < keyin.length) {
                searchIndex = keyin.indexOf('"', searchIndex);
                if (-1 === searchIndex)
                    break;
                // A literal " is embedded as ""
                if (searchIndex + 1 > keyin.length || keyin[searchIndex + 1] !== '"') {
                    endQuoteIndex = searchIndex;
                    break;
                }
                anyEmbeddedQuotes = true;
                searchIndex = searchIndex + 2;
            }
            if (undefined === endQuoteIndex) {
                return { tokens, mismatchedQuotes: true };
            }
            else {
                let token = keyin.substring(index + 1, endQuoteIndex);
                if (anyEmbeddedQuotes) {
                    const regex = /""/g;
                    token = token.replace(regex, '"');
                }
                tokens.push(token);
                index = endQuoteIndex + 1;
            }
        }
        return { tokens, firstQuotedIndex };
    }
    /** Given a string consisting of a toolId followed by any number of arguments, locate the corresponding Tool and parse the arguments.
     * Tokens are delimited by whitespace.
     * The Tool is determined by finding the longest string of unquoted tokens starting at the beginning of the key-in string that matches a registered Tool's
     * `keyin` or `englishKeyin`.
     * Tokens following the Tool's keyin are parsed as arguments.
     * Arguments may be quoted using "double quotes". The opening quote must be preceded by whitespace. Examples, assuming the tool Id is `my keyin`:
     *  - `my keyin "abc" "def"` => two arguments: `abc` and `def`
     *  - `my keyin abc"def"` => one argument: `abc"def"`
     * A literal double-quote character can be embedded in a quoted argument as follows:
     *  - `my keyin "abc""def"` => one argument: `abc"def`.
     * @param keyin A string consisting of a toolId followed by any number of arguments. The arguments are separated by whitespace.
     * @returns The tool, if found, along with an array of parsed arguments.
     * @public
     */
    parseKeyin(keyin) {
        const tools = this.getToolList();
        let tool;
        const args = [];
        const findTool = (lowerKeyin) => tools.find((x) => x.keyin.toLowerCase() === lowerKeyin || x.englishKeyin.toLowerCase() === lowerKeyin);
        // try the trivial, common case first
        tool = findTool(keyin.toLowerCase());
        if (undefined !== tool)
            return { ok: true, tool, args };
        // Tokenize to separate keyin from arguments
        // ###TODO there's actually nothing that prevents a Tool from including leading/trailing spaces in its keyin, or sequences of more than one space...we will fail to find such tools if they exist...
        const split = this.tokenize(keyin);
        const tokens = split.tokens;
        if (split.mismatchedQuotes)
            return { ok: false, error: KeyinParseError.MismatchedQuotes };
        else if (tokens.length <= 1)
            return { ok: false, error: KeyinParseError.ToolNotFound };
        // Find the longest starting substring that matches a tool's keyin.
        const maxIndex = undefined !== split.firstQuotedIndex ? split.firstQuotedIndex - 1 : tokens.length - 2;
        for (let i = maxIndex; i >= 0; i--) {
            let substr = tokens[0];
            for (let j = 1; j <= i; j++) {
                substr += " ";
                substr += tokens[j];
            }
            tool = findTool(substr.toLowerCase());
            if (undefined !== tool) {
                // Any subsequent tokens are arguments.
                for (let k = i + 1; k < tokens.length; k++)
                    args.push(tokens[k]);
                break;
            }
        }
        return tool ? { ok: true, tool, args } : { ok: false, error: KeyinParseError.ToolNotFound };
    }
    /** Get a list of Tools currently registered, excluding hidden tools */
    getToolList() {
        if (this._keyinList === undefined) {
            this._keyinList = [];
            this.tools.forEach((thisTool) => {
                if (!thisTool.hidden)
                    this._keyinList.push(thisTool);
            });
        }
        return this._keyinList;
    }
    /** Given a string consisting of a toolId followed by any number of arguments, parse the keyin string and invoke the corresponding tool's `parseAndRun` method.
     * @param keyin A string consisting of a toolId followed by any number of arguments.
     * @returns A status indicating whether the keyin was successfully parsed and executed.
     * @see [[parseKeyin]] to parse the keyin string and for a detailed description of the syntax.
     * @throws any Error thrown by the tool's `parseAndRun` method.
     * @public
     */
    async parseAndRun(keyin) {
        const parsed = this.parseKeyin(keyin);
        if (!parsed.ok) {
            switch (parsed.error) {
                case KeyinParseError.MismatchedQuotes: return ParseAndRunResult.MismatchedQuotes;
                case KeyinParseError.ToolNotFound: return ParseAndRunResult.ToolNotFound;
            }
        }
        assert(parsed.ok); // exhaustive switch above...
        const maxArgs = parsed.tool.maxArgs;
        if (parsed.args.length < parsed.tool.minArgs || (undefined !== maxArgs && parsed.args.length > maxArgs))
            return ParseAndRunResult.BadArgumentCount;
        const tool = new parsed.tool();
        return await tool.parseAndRun(...parsed.args) ? ParseAndRunResult.Success : ParseAndRunResult.FailedToRun;
    }
    /**
     * Find a tool by its localized keyin using a FuzzySearch
     * @param keyin the localized keyin string of the Tool.
     * @note Make sure the i18n resources are all loaded (e.g. `await IModelApp.i81n.waitForAllRead()`) before calling this method.
     * @public
     */
    findPartialMatches(keyin) {
        return new FuzzySearch().search(this.getToolList(), ["keyin"], keyin.toLowerCase());
    }
    /**
     * Find a tool by its localized keyin.
     * @param keyin the localized keyin string of the Tool.
     * @returns the Tool class, if an exact match is found, otherwise returns undefined.
     * @note Make sure the i18n resources are all loaded (e.g. `await IModelApp.i81n.waitForAllRead()`) before calling this method.
     * @public
     */
    findExactMatch(keyin) {
        keyin = keyin.toLowerCase();
        return this.getToolList().find((thisTool) => thisTool.keyin.toLowerCase() === keyin);
    }
}
/** @internal */
export class CoreTools {
    static translate(prompt) { return IModelApp.localization.getLocalizedString(this.tools + prompt); }
    static outputPromptByKey(key) { return IModelApp.notifications.outputPromptByKey(this.tools + key); }
}
CoreTools.namespace = "CoreTools";
CoreTools.tools = "CoreTools:tools.";
//# sourceMappingURL=Tool.js.map