/*---------------------------------------------------------------------------------------------
 * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
 * See LICENSE.md in the project root for license terms and full copyright notice.
 *--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Notification
 */
import classnames from "classnames";
import isEqual from "lodash/isEqual.js";
import * as React from "react";
import { ActivityMessageDetails, IModelApp, MessageBoxIconType, MessageBoxType, OutputMessageAlert, OutputMessagePriority, OutputMessageType, } from "@itwin/core-frontend";
import { MessageSeverity, UiEvent } from "@itwin/appui-abstract";
import { MessageContainer, MessageRenderer } from "@itwin/core-react";
import { StandardMessageBox } from "../dialog/StandardMessageBox.js";
import { ElementTooltip } from "../feedback/ElementTooltip.js";
import { UiFramework } from "../UiFramework.js";
import { PointerMessage } from "./Pointer.js";
import { StatusMessageManager } from "./StatusMessageManager.js";
import { SvgInfo, SvgStatusError, SvgStatusSuccess, SvgStatusWarning, } from "@itwin/itwinui-icons-react";
import { Text } from "@itwin/itwinui-react";
import { BeUiEvent } from "@itwin/core-bentley";
import { ConfigurableUiActionId } from "../redux/ConfigurableUiState.js";
import { useTranslation } from "../hooks/useTranslation.js";
class MessageBoxCallbacks {
    onFulfilled;
    onRejected;
    constructor(onFulfilled, onRejected) {
        this.onFulfilled = onFulfilled;
        this.onRejected = onRejected;
    }
    handleMessageBoxResult = (result) => {
        this.onFulfilled(result);
    };
}
/** Message Added Event class.
 * @public
 * @deprecated in 4.13.0. This class should not be used by applications to instantiate objects.
 */
// eslint-disable-next-line @typescript-eslint/no-deprecated
export class MessageAddedEvent extends UiEvent {
}
/** Messages Updated Event class.
 * @public
 * @deprecated in 4.13.0. This class should not be used by applications to instantiate objects.
 */
// eslint-disable-next-line @typescript-eslint/no-deprecated
export class MessagesUpdatedEvent extends UiEvent {
}
/** Activity Message Added Event class.
 * @public
 * @deprecated in 4.13.0. This class should not be used by applications to instantiate objects.
 */
// eslint-disable-next-line @typescript-eslint/no-deprecated
export class ActivityMessageUpdatedEvent extends UiEvent {
}
/** Activity Message Cancelled Event class.
 * @public
 * @deprecated in 4.13.0. This class should not be used by applications to instantiate objects.
 */
// eslint-disable-next-line @typescript-eslint/no-deprecated
export class ActivityMessageCancelledEvent extends UiEvent {
}
/** Input Field Message Added Event class
 * @public
 * @deprecated in 4.13.0. This class should not be used by applications to instantiate objects.
 */
// eslint-disable-next-line @typescript-eslint/no-deprecated
export class InputFieldMessageAddedEvent extends UiEvent {
}
/** Input Field Message Removed Event class.
 * @public
 * @deprecated in 4.13.0. This class should not be used by applications to instantiate objects.
 */
// eslint-disable-next-line @typescript-eslint/no-deprecated
export class InputFieldMessageRemovedEvent extends UiEvent {
}
/** Open Message Center Event class.
 * @public
 * @deprecated in 4.13.0. This class should not be used by applications to instantiate objects.
 */
// eslint-disable-next-line @typescript-eslint/no-deprecated
export class OpenMessageCenterEvent extends UiEvent {
}
/** Tool Assistance Changed event class
 * @public
 * @deprecated in 4.13.0. This class should not be used by applications to instantiate objects.
 */
// eslint-disable-next-line @typescript-eslint/no-deprecated
export class ToolAssistanceChangedEvent extends UiEvent {
}
/**
 * Keeps track of the current activity message, and updates whenever
 * setupActivityMessageDetails() or setupActivityMessageValues()
 * is called.
 * Used to display tracked progress in ActivityMessage.
 */
class OngoingActivityMessage {
    message = "";
    percentage = 0;
    details = new ActivityMessageDetails(true, true, true);
    isRestored = false;
}
/** The MessageManager class manages messages and prompts. It is used by the [[AppNotificationManager]] class.
 * @public
 */
export class MessageManager {
    static _maxCachedMessages = 500;
    static _maxDisplayedStickyMessages = 3;
    static _messages = [];
    static _ongoingActivityMessage = new OngoingActivityMessage();
    static _lastMessage;
    static _activeMessageManager = new StatusMessageManager();
    static _animateOutToElement = [];
    static _toastCloseCallbacks = [];
    /** The MessageAddedEvent is fired when a message is added via outputMessage(). */
    // eslint-disable-next-line @typescript-eslint/no-deprecated
    static onMessageAddedEvent = new MessageAddedEvent();
    /** The MessagesUpdatedEvent is fired when a message is added or the messages are cleared. */
    // eslint-disable-next-line @typescript-eslint/no-deprecated
    static onMessagesUpdatedEvent = new MessagesUpdatedEvent();
    /** The ActivityMessageUpdatedEvent is fired when an Activity message updates via outputActivityMessage(). */
    static onActivityMessageUpdatedEvent = new ActivityMessageUpdatedEvent(); // eslint-disable-line @typescript-eslint/no-deprecated
    /** The ActivityMessageCancelledEvent is fired when an Activity message is cancelled via
     * endActivityMessage(ActivityMessageEndReason.Cancelled) or
     * by the user clicking the 'Cancel' link.
     */
    static onActivityMessageCancelledEvent = new ActivityMessageCancelledEvent(); // eslint-disable-line @typescript-eslint/no-deprecated
    static onInputFieldMessageAddedEvent = new InputFieldMessageAddedEvent(); // eslint-disable-line @typescript-eslint/no-deprecated
    static onInputFieldMessageRemovedEvent = new InputFieldMessageRemovedEvent(); // eslint-disable-line @typescript-eslint/no-deprecated
    static onOpenMessageCenterEvent = new OpenMessageCenterEvent(); // eslint-disable-line @typescript-eslint/no-deprecated
    /** @internal */
    static onDisplayMessage = new BeUiEvent();
    /** The ToolAssistanceChangedEvent is fired when a tool calls IModelApp.notifications.setToolAssistance().
     * @public
     */
    static onToolAssistanceChangedEvent = new ToolAssistanceChangedEvent(); // eslint-disable-line @typescript-eslint/no-deprecated
    /** List of messages as NotifyMessageDetailsType. */
    static get messages() {
        return this._messages;
    }
    /** Manager of active messages. */
    static get activeMessageManager() {
        return this._activeMessageManager;
    }
    /** Clear the message list. */
    static clearMessages() {
        this._messages.splice(0);
        this._activeMessageManager.initialize();
        this._toastCloseCallbacks.forEach(({ close }) => close());
        this._toastCloseCallbacks.splice(0);
        this.onMessagesUpdatedEvent.emit({});
        this._lastMessage = undefined;
    }
    /** Update the message list. */
    static updateMessages() {
        this.onMessagesUpdatedEvent.emit({});
    }
    /** Set the maximum number of cached message. */
    static setMaxCachedMessages(max) {
        this._maxCachedMessages = max;
        this.checkMaxCachedMessages();
    }
    /** The maximum number of displayed Sticky messages. */
    static get maxDisplayedStickyMessages() {
        return this._maxDisplayedStickyMessages;
    }
    static set maxDisplayedStickyMessages(max) {
        this._maxDisplayedStickyMessages = max;
    }
    /** Output a message and/or alert to the user.
     * @param  message  Details about the message to output.
     */
    static outputMessage(message) {
        if (message.msgType === OutputMessageType.Pointer) {
            PointerMessage.showMessage(message);
        }
        else if (message.msgType === OutputMessageType.InputField) {
            if (message.inputField)
                MessageManager.displayInputFieldMessage(message.inputField, message.briefMessage, message.detailedMessage, message.priority);
            else
                message.msgType = OutputMessageType.Sticky; // Note: Changing the message.msgType here for InputField without inputField
        }
        else if (message.msgType === OutputMessageType.Alert) {
            if (message.openAlert === OutputMessageAlert.Balloon)
                message.msgType = OutputMessageType.Sticky;
            // Note: Changing the message.msgType here for Balloon
            else
                MessageManager.showAlertMessageBox(message);
        }
        MessageManager.addMessage(message);
    }
    /** Set the element where messages should be animated out to on exit.
     * @param  element `HTMLElement` to animate out to.
     */
    static registerAnimateOutToElement(element) {
        this._animateOutToElement = !element
            ? []
            : [element, ...this._animateOutToElement];
    }
    /** Handles disconnected element modal frontstages, revert to last still displayed. */
    static get animateOutToElement() {
        if (this._animateOutToElement.length === 0) {
            return undefined;
        }
        if (this._animateOutToElement[0]?.isConnected) {
            return this._animateOutToElement[0];
        }
        this._animateOutToElement.splice(0, 1);
        return this.animateOutToElement;
    }
    /** Display a message.
     * Works only with `Sticky` and `Toast` message types.
     * @param  message  Details about the message to display.
     * @param options Optionally override individual toast parameters.
     * @param settings Optionally override all toasts settings (i.e. placement or order).
     * @returns Object with reference to the message (i.e. to close it programmatically) if it was displayed.
     */
    static displayMessage(message, options, settings) {
        if (message.msgType !== OutputMessageType.Sticky &&
            message.msgType !== OutputMessageType.Toast) {
            return undefined;
        }
        const args = {
            message,
            options,
            settings,
            animateOutToElement: this.animateOutToElement,
        };
        this.onDisplayMessage.emit(args);
        return {
            close: () => {
                args.close?.();
            },
        };
    }
    /** Output a message and/or alert to the user.
     * @param message Details about the message to output.
     */
    static addMessage(message) {
        const activeMessage = this.activeMessageManager.messages.find((m) => isEqual(message, m.messageDetails));
        if (activeMessage)
            return;
        this.activeMessageManager.add(message);
        this.refreshToastMessages();
        this.onMessageAddedEvent.emit({ message });
        if (!isEqual(message, this._lastMessage)) {
            // Add message to message center if it is not the same as the last message.
            this.addToMessageCenter(message);
            this._lastMessage = message;
        }
    }
    /**
     * Closes sticky messages that are beyond the limit,
     * opens new toast and sticky messages from the active message manager.
     */
    static refreshToastMessages() {
        this._toastCloseCallbacks = this._toastCloseCallbacks.filter((t) => {
            if (MessageManager.activeMessageManager.messages.some((msg) => t.id === msg.id)) {
                return true;
            }
            t.close();
            return false;
        });
        const messagesToAdd = MessageManager.activeMessageManager.messages.filter((msg) => !this._toastCloseCallbacks.find((m) => m.id === msg.id));
        messagesToAdd.forEach((msg) => {
            const displayedMessage = MessageManager.displayMessage(msg.messageDetails, {
                onRemove: () => {
                    MessageManager.activeMessageManager.remove(msg.id);
                },
            });
            if (!!displayedMessage)
                this._toastCloseCallbacks.push({
                    close: displayedMessage.close,
                    id: msg.id,
                });
        });
    }
    /** Add a message to the Message Center.
     * @param  message  Details about the message to output.
     */
    static addToMessageCenter(message) {
        this._messages.push(message);
        this.onMessagesUpdatedEvent.emit({});
        this.checkMaxCachedMessages();
    }
    /** Checks number of messages against the maximum. */
    static checkMaxCachedMessages() {
        if (this._messages.length > this._maxCachedMessages) {
            const numToErase = this._maxCachedMessages / 4;
            this._messages.splice(0, numToErase);
            this.onMessagesUpdatedEvent.emit({});
        }
    }
    /**
     * Sets details for setting up an Activity message.
     * @param details    Details for setup of ActivityMessage
     * @returns true if details is valid and can be used to display ActivityMessage
     */
    static setupActivityMessageDetails(details) {
        this._ongoingActivityMessage.details = details;
        this._ongoingActivityMessage.isRestored = details.showDialogInitially;
        return true;
    }
    /**
     * Output an activity message to the user.
     * @param message         The message text.
     * @param percentComplete The percentage of completion.
     * @return true if the message was displayed, false if the message could not be displayed.
     */
    static outputActivityMessage(message, percentComplete) {
        return MessageManager.setupActivityMessageValues(message, percentComplete);
    }
    /**
     * Sets values on _OngoingActivityMessage to be referenced when displaying
     * an ActivityMessage.
     * @param message     Message of the process that ActivityMessage is tracking
     * @param percentage  Progress made by activity in percentage
     * @param restored    True if original ActivityMessage has been closed and
     *                    is now being restored from the status bar.
     * @returns true if details is valid and can be used to display ActivityMessage
     */
    static setupActivityMessageValues(message, percentage, restored) {
        this._ongoingActivityMessage.message = message;
        this._ongoingActivityMessage.percentage = percentage;
        this.onActivityMessageUpdatedEvent.emit({
            message,
            percentage,
            details: this._ongoingActivityMessage.details,
            restored: restored !== undefined
                ? restored
                : this._ongoingActivityMessage.isRestored,
        });
        this._ongoingActivityMessage.isRestored = false;
        return true;
    }
    /**
     * Dismisses current ActivityMessage and ends activity if canceled.
     * @param isCompleted   True if the activity was completed, false if it was canceled
     * @returns True if both ActivityMessage and activity process are ended.
     */
    static endActivityMessage(isCompleted) {
        this.endActivityProcessing(isCompleted);
        this.onActivityMessageCancelledEvent.emit({});
        return true;
    }
    /**
     * Ends processing for activity according to message definition.
     * @param isCompleted   True if the activity was completed, false if it was canceled
     */
    static endActivityProcessing(isCompleted) {
        if (isCompleted)
            this._ongoingActivityMessage.details.onActivityCompleted();
        else
            this._ongoingActivityMessage.details.onActivityCancelled();
    }
    /**
     * Displays an input field message near target element.
     * @param target  The currently focused or recently focused element to place the
     *                input field message near.
     * @param messageText  Text to display in the message.
     * @param detailedMessage   Optional detailed message text to display.
     * @param priority   Optional message priority which controls icon to display.
     */
    static displayInputFieldMessage(target, messageText, detailedMessage = "", priority = OutputMessagePriority.Error) {
        this.onInputFieldMessageAddedEvent.emit({
            target,
            messageText,
            detailedMessage,
            priority,
        });
    }
    /**
     * Hides the currently displayed input field message.
     */
    static hideInputFieldMessage() {
        this.onInputFieldMessageRemovedEvent.emit({});
    }
    /**
     * Opens message center.
     */
    static openMessageCenter() {
        this.onOpenMessageCenterEvent.emit({});
    }
    /** Output a prompt to the user. A 'prompt' indicates an action the user should take to proceed. */
    static outputPrompt(prompt) {
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        if (UiFramework.frameworkState) {
            // eslint-disable-next-line @typescript-eslint/no-deprecated
            UiFramework.dispatchActionToStore(
            // eslint-disable-next-line @typescript-eslint/no-deprecated
            ConfigurableUiActionId.SetToolPrompt, prompt, true);
            return;
        }
    }
    /** Extracts the message severity from the message details and returns the corresponding React icon.
     * @param details NotifyMessageDetailsType
     * @returns IconSpec
     * @deprecated in 4.16.0. Use {@link https://itwinui.bentley.com/ iTwinUI icons} instead.
     */
    static getIconSpecFromDetails(details
    // eslint-disable-next-line @typescript-eslint/no-deprecated
    ) {
        const severity = MessageManager.getSeverity(details);
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        let iconSpec = React.createElement(SvgStatusSuccess, null);
        switch (severity) {
            case MessageSeverity.Error:
            case MessageSeverity.Fatal:
                iconSpec = React.createElement(SvgStatusError, null);
                break;
            case MessageSeverity.Warning:
                iconSpec = React.createElement(SvgStatusWarning, null);
                break;
            case MessageSeverity.Information:
                iconSpec = React.createElement(SvgInfo, null);
                break;
        }
        return iconSpec;
    }
    /** Gets an icon CSS class name based on a given NotifyMessageDetailsType.
     * @public
     * @deprecated in 4.16.0. Used internally.
     */
    static getIconClassName(details) {
        const severity = MessageManager.getSeverity(details);
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        const className = MessageContainer.getIconClassName(severity);
        const iconClassName = classnames("icon", "notifymessage-icon", className);
        return iconClassName;
    }
    /** Gets a MessageSeverity based on a given NotifyMessageDetailsType. */
    static getSeverity(details) {
        let severity = MessageSeverity.None;
        switch (details.priority) {
            case OutputMessagePriority.None:
                severity = MessageSeverity.Success;
                break;
            case OutputMessagePriority.Success:
                severity = MessageSeverity.Success;
                break;
            case OutputMessagePriority.Info:
                severity = MessageSeverity.Information;
                break;
            case OutputMessagePriority.Warning:
                severity = MessageSeverity.Warning;
                break;
            case OutputMessagePriority.Error:
                severity = MessageSeverity.Error;
                break;
            case OutputMessagePriority.Fatal:
                severity = MessageSeverity.Fatal;
                break;
        }
        return severity;
    }
    /** Gets a MessageBoxIconType based on a given NotifyMessageDetailsType. */
    static getIconType(details) {
        let iconType = MessageBoxIconType.NoSymbol;
        switch (details.priority) {
            case OutputMessagePriority.None:
                iconType = MessageBoxIconType.NoSymbol;
                break;
            case OutputMessagePriority.Success:
                iconType = MessageBoxIconType.Success;
                break;
            case OutputMessagePriority.Info:
                iconType = MessageBoxIconType.Information;
                break;
            case OutputMessagePriority.Warning:
                iconType = MessageBoxIconType.Warning;
                break;
            case OutputMessagePriority.Error:
                iconType = MessageBoxIconType.Critical;
                break;
            case OutputMessagePriority.Fatal:
                iconType = MessageBoxIconType.Critical;
                break;
        }
        return iconType;
    }
    /** Output a MessageBox and wait for response from the user.
     * @param mbType       The MessageBox type.
     * @param message      The message to display.
     * @param icon         The MessageBox icon type.
     * @return the response from the user.
     */
    static async openMessageBox(mbType, message, icon) {
        return new Promise((onFulfilled, onRejected) => {
            const messageBoxCallbacks = new MessageBoxCallbacks(onFulfilled, onRejected);
            // eslint-disable-next-line @typescript-eslint/no-deprecated
            const messageElement = React.createElement(MessageRenderer, { message: message, useSpan: true });
            UiFramework.dialogs.modal.open(
            // eslint-disable-next-line @typescript-eslint/no-deprecated
            React.createElement(StandardMessageBox, { opened: true, messageBoxType: mbType, iconType: icon, title: UiFramework.translate("general.alert"), onResult: messageBoxCallbacks.handleMessageBoxResult }, messageElement));
        });
    }
    /** @internal */
    static showAlertMessageBox(messageDetails) {
        UiFramework.dialogs.modal.open(React.createElement(AlertDialog, { messageDetails: messageDetails }));
    }
    /** Setup tool assistance instructions for a tool. The instructions include the main instruction, which includes the current prompt.
     * @param instructions The tool assistance instructions.
     * @public
     */
    static setToolAssistance(instructions) {
        MessageManager.onToolAssistanceChangedEvent.emit({ instructions });
    }
    /** Show a tooltip window. Saves tooltip location for AccuSnap to test if cursor has moved far enough away to close tooltip.
     * @param htmlElement The HTMLElement that anchors the tooltip.
     * @param message     What to display inside the tooltip.
     * @param location    An optional location, relative to the origin of htmlElement, for the tooltip. If undefined, center of `htmlElement`.
     * @param options     Options that supply additional information about how the tooltip should function.
     */
    static openToolTip(htmlElement, message, location, options) {
        IModelApp.notifications.toolTipLocation.setFrom(location);
        ElementTooltip.showTooltip(htmlElement, message, location, options);
    }
    /** @internal */
    static closeAllMessages() {
        ElementTooltip.hideTooltip();
        PointerMessage.hideMessage();
        MessageManager.clearMessages();
        MessageManager.hideInputFieldMessage();
        MessageManager.endActivityMessage(false);
    }
}
function AlertDialog({ messageDetails }) {
    const { briefMessage, detailedMessage, priority } = messageDetails;
    const iconType = MessageManager.getIconType(messageDetails);
    const content = (React.createElement(React.Fragment, null,
        React.createElement(Text, { variant: "leading" },
            React.createElement(MessageRenderer, { message: briefMessage, useSpan: true })),
        detailedMessage && (React.createElement("p", null,
            React.createElement(Text, { variant: "body" },
                React.createElement(MessageRenderer, { message: detailedMessage, useSpan: true }))))));
    const { translate } = useTranslation();
    const title = React.useMemo(() => {
        switch (priority) {
            case OutputMessagePriority.Error:
            case OutputMessagePriority.Fatal:
                return translate("general.error");
            case OutputMessagePriority.Warning:
                return translate("general.warning");
            case OutputMessagePriority.Info:
                return translate("general.information");
            case OutputMessagePriority.Success:
                return translate("general.success");
        }
        return translate("general.alert");
    }, [priority, translate]);
    return (
    // eslint-disable-next-line @typescript-eslint/no-deprecated
    React.createElement(StandardMessageBox, { opened: true, messageBoxType: MessageBoxType.Ok, iconType: iconType, title: title }, content));
}
//# sourceMappingURL=MessageManager.js.map