/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module IModelApp
 */
var _a;
// @ts-expect-error package.json will resolve from the lib/{cjs,esm} dir without copying it into the build output we deliver
// eslint-disable-next-line @itwin/import-within-package
import packageJson from "../../package.json";
/** @public */
// eslint-disable-next-line @typescript-eslint/no-var-requires
export const ITWINJS_CORE_VERSION = packageJson.version;
const COPYRIGHT_NOTICE = 'Copyright © 2017-2024 <a href="https://www.bentley.com" target="_blank" rel="noopener noreferrer">Bentley Systems, Inc.</a>';
import { UiAdmin } from "@itwin/appui-abstract";
import { BeDuration, BeEvent, BentleyStatus, DbResult, dispose, Guid, Logger, ProcessDetector } from "@itwin/core-bentley";
import { IModelStatus, RpcConfiguration, } from "@itwin/core-common";
import { ITwinLocalization } from "@itwin/core-i18n";
import { TelemetryManager } from "@itwin/core-telemetry";
import { queryRenderCompatibility } from "@itwin/webgl-compatibility";
import { AccuDraw } from "./AccuDraw";
import { AccuSnap } from "./AccuSnap";
import * as auxCoordState from "./AuxCoordSys";
import * as categorySelectorState from "./CategorySelectorState";
import { ExtensionAdmin } from "./extension/ExtensionAdmin";
import * as displayStyleState from "./DisplayStyleState";
import * as drawingViewState from "./DrawingViewState";
import { ElementLocateManager } from "./ElementLocateManager";
import { EntityState } from "./EntityState";
import { FrontendLoggerCategory } from "./common/FrontendLoggerCategory";
import * as modelselector from "./ModelSelectorState";
import * as modelState from "./ModelState";
import { NotificationManager } from "./NotificationManager";
import { QuantityFormatter } from "./quantity-formatting/QuantityFormatter";
import { RenderSystem } from "./render/RenderSystem";
import { System } from "./render/webgl/System";
import * as sheetState from "./SheetViewState";
import * as spatialViewState from "./SpatialViewState";
import { TentativePoint } from "./TentativePoint";
import { RealityDataSourceProviderRegistry } from "./RealityDataSource";
import { MapLayerFormatRegistry, TerrainProviderRegistry, TileAdmin } from "./tile/internal";
import * as accudrawTool from "./tools/AccuDrawTool";
import * as clipViewTool from "./tools/ClipViewTool";
import * as idleTool from "./tools/IdleTool";
import * as measureTool from "./tools/MeasureTool";
import * as selectTool from "./tools/SelectTool";
import { ToolRegistry } from "./tools/Tool";
import { ToolAdmin } from "./tools/ToolAdmin";
import * as viewTool from "./tools/ViewTool";
import { ViewManager } from "./ViewManager";
import * as viewState from "./ViewState";
import "./IModeljs-css";
/**
 * Global singleton that connects the user interface with the iTwin.js services. There can be only one IModelApp active in a session. All
 * members of IModelApp are static, and it serves as a singleton object for gaining access to session information.
 *
 * Before any interactive operations may be performed by the `@itwin/core-frontend package`, [[IModelApp.startup]] must be called and awaited.
 * Applications may customize the frontend behavior of iTwin.js by supplying options to [[IModelApp.startup]].
 *
 * @public
 */
export class IModelApp {
    // No instances of IModelApp may be created. All members are static and must be on the singleton object IModelApp.
    constructor() { }
    /** The [[MapLayerFormatRegistry]] for this session. */
    static get mapLayerFormatRegistry() { return this._mapLayerFormatRegistry; }
    /** The [[TerrainProviderRegistry]] for this session. */
    static get terrainProviderRegistry() { return this._terrainProviderRegistry; }
    /** The [[RealityDataSourceProviderRegistry]] for this session.
     * @alpha
     */
    static get realityDataSourceProviders() { return this._realityDataSourceProviders; }
    /** The [[RenderSystem]] for this session. */
    static get renderSystem() { return this._renderSystem; }
    /** The [[ViewManager]] for this session. */
    static get viewManager() { return this._viewManager; }
    /** The [[NotificationManager]] for this session. */
    static get notifications() { return this._notifications; }
    /** The [[TileAdmin]] for this session. */
    static get tileAdmin() { return this._tileAdmin; }
    /** The [[QuantityFormatter]] for this session. */
    static get quantityFormatter() { return this._quantityFormatter; }
    /** The [[ToolAdmin]] for this session. */
    static get toolAdmin() { return this._toolAdmin; }
    /** The [[AccuDraw]] for this session. */
    static get accuDraw() { return this._accuDraw; }
    /** The [[AccuSnap]] for this session. */
    static get accuSnap() { return this._accuSnap; }
    static get locateManager() { return this._locateManager; }
    /** The [[TentativePoint]] for this session]]. */
    static get tentativePoint() { return this._tentativePoint; }
    /** The [[Localization]] for this session. */
    static get localization() { return this._localization; }
    /** The [[UserPreferencesAccess]] for this session.
     * @beta
     */
    static get userPreferences() { return this._userPreferences; }
    /** The Id of this application. Applications must set this to the Global Product Registry ID (GPRID) for usage logging. */
    static get applicationId() { return this._applicationId; }
    /** The version of this application. Must be set for usage logging. */
    static get applicationVersion() { return this._applicationVersion; }
    /** True after [[startup]] has been called, until [[shutdown]] is called. */
    static get initialized() { return this._initialized; }
    /** Provides access to IModelHub services. */
    static get hubAccess() { return this._hubAccess; }
    /** Provides access to the RealityData service implementation for this IModelApp
     * @beta
     */
    static get realityDataAccess() { return this._realityDataAccess; }
    /** Whether the [renderSystem[]] has been successfully initialized.
     * This will always be `false` before calling [[startup]] and after calling [[shutdown]].
     * In rare circumstances (e.g., while executing in a headless test environment) it may remain `false` due to a failure to
     * obtain a [WebGL rendering context](https://www.google.com/search?channel=fs&client=ubuntu-sn&q=mdn+webglrenderingcontext).
     * As long as you have called [[startup]], you can generally assume it to be `true`.
     */
    static get hasRenderSystem() {
        return this._renderSystem !== undefined && this._renderSystem.isValid;
    }
    /** The [[UiAdmin]] for this session. */
    static get uiAdmin() { return this._uiAdmin; }
    /** The requested security options for the frontend. */
    static get securityOptions() { return this._securityOptions; }
    /** If present, overrides where public assets are fetched. The default is to fetch assets relative to the current URL.
     * The path should always end with a trailing `/`.
     */
    static get publicPath() { return this._publicPath; }
    /** Register all of the subclasses of EntityState from a module.
     * @internal
     */
    static registerModuleEntities(moduleObj) {
        for (const thisMember in moduleObj) { // eslint-disable-line guard-for-in
            const thisEntityState = moduleObj[thisMember];
            if (thisEntityState.prototype instanceof EntityState) {
                this.registerEntityState(thisEntityState.classFullName, thisEntityState);
            }
        }
    }
    /** Register an EntityState class by its classFullName
     * @internal
     */
    static registerEntityState(classFullName, classType) {
        const lowerName = classFullName.toLowerCase();
        if (this._entityClasses.has(lowerName)) {
            const errMsg = `Class ${classFullName} is already registered. Make sure static schemaName and className members are correct on class ${classType.name}`;
            Logger.logError(FrontendLoggerCategory.IModelConnection, errMsg);
            throw new Error(errMsg);
        }
        this._entityClasses.set(lowerName, classType);
    }
    /** @internal */
    static lookupEntityClass(classFullName) { return this._entityClasses.get(classFullName.toLowerCase()); }
    /**
     * Obtain WebGL rendering compatibility information for the client system.  This information describes whether the client meets the
     * minimum rendering capabilities.  It also describes whether the system lacks any optional capabilities that could improve quality
     * and/or performance.
     * @note As of 4.x, iTwin.js requires WebGL 2. If the client does not support WebGL 2, the `status` field of the returned compatibility info will be [WebGLRenderCompatibilityStatus.CannotCreateContext]($webgl-compatibility).
     */
    static queryRenderCompatibility() {
        return queryRenderCompatibility(true, (canvas, useWebGL2, inputContextAttributes) => System.createContext(canvas, useWebGL2, inputContextAttributes));
    }
    /**
     * This method must be called before any other `@itwin/core-frontend` methods are used.
     * Somewhere in your startup code, call [[IModelApp.startup]]. E.g.:
     * ``` ts
     * await IModelApp.startup( {applicationId: myAppId} );
     * ```
     * @param opts The options for configuring IModelApp
     */
    static async startup(opts) {
        if (this._initialized)
            return; // we're already initialized, do nothing.
        this._initialized = true;
        opts = opts ?? {};
        this._securityOptions = opts.security ?? {};
        if (process.env.NODE_ENV === "development") {
            // Make IModelApp globally accessible for debugging purposes. We'll remove it on shutdown.
            window.iModelAppForDebugger = this;
        }
        this.sessionId = opts.sessionId ?? Guid.createValue();
        this._applicationId = opts.applicationId ?? "2686"; // Default to product id of iTwin.js
        this._applicationVersion = opts.applicationVersion ?? "1.0.0";
        this.authorizationClient = opts.authorizationClient;
        this._hubAccess = opts.hubAccess;
        this._noRender = opts.noRender ?? false;
        this._setupRpcRequestContext();
        this._localization = opts.localization ?? new ITwinLocalization();
        const toolsNs = "CoreTools";
        await this.localization.initialize(["iModelJs", toolsNs]);
        [
            selectTool,
            idleTool,
            viewTool,
            clipViewTool,
            measureTool,
            accudrawTool,
        ].forEach((tool) => this.tools.registerModule(tool, toolsNs));
        this.registerEntityState(EntityState.classFullName, EntityState);
        [
            modelState,
            sheetState,
            viewState,
            drawingViewState,
            spatialViewState,
            displayStyleState,
            modelselector,
            categorySelectorState,
            auxCoordState,
        ].forEach((module) => this.registerModuleEntities(module));
        this._renderSystem = (opts.renderSys instanceof RenderSystem) ? opts.renderSys : this.createRenderSys(opts.renderSys);
        if (opts.userPreferences)
            this._userPreferences = opts.userPreferences;
        this._viewManager = opts.viewManager ?? new ViewManager();
        this._tileAdmin = await TileAdmin.create(opts.tileAdmin);
        this._notifications = opts.notifications ?? new NotificationManager();
        this._toolAdmin = opts.toolAdmin ?? new ToolAdmin();
        this._accuDraw = opts.accuDraw ?? new AccuDraw();
        this._accuSnap = opts.accuSnap ?? new AccuSnap();
        this._locateManager = opts.locateManager ?? new ElementLocateManager();
        this._tentativePoint = opts.tentativePoint ?? new TentativePoint();
        this._quantityFormatter = opts.quantityFormatter ?? new QuantityFormatter();
        this._uiAdmin = opts.uiAdmin ?? new UiAdmin();
        this._mapLayerFormatRegistry = new MapLayerFormatRegistry(opts.mapLayerOptions);
        this._terrainProviderRegistry = new TerrainProviderRegistry();
        this._realityDataSourceProviders = new RealityDataSourceProviderRegistry();
        this._realityDataAccess = opts.realityDataAccess;
        this._publicPath = opts.publicPath ?? "";
        [
            this.renderSystem,
            this.viewManager,
            this.toolAdmin,
            this.accuDraw,
            this.accuSnap,
            this.locateManager,
            this.tentativePoint,
            this.uiAdmin,
        ].forEach((sys) => sys.onInitialized());
        await this.quantityFormatter.onInitialized();
        this.onAfterStartup.raiseEvent();
    }
    /** Must be called before the application exits to release any held resources. */
    static async shutdown() {
        if (!this._initialized)
            return;
        // notify listeners that this IModelApp is about to be shut down.
        this.onBeforeShutdown.raiseEvent();
        this.onBeforeShutdown.clear();
        if (process.env.NODE_ENV === "development") {
            window.iModelAppForDebugger = undefined;
        }
        this._wantEventLoop = false;
        window.removeEventListener("resize", () => _a.requestNextAnimation());
        this.clearIntervalAnimation();
        [this.toolAdmin, this.viewManager, this.tileAdmin].forEach((sys) => sys.onShutDown());
        this.tools.shutdown();
        this._renderSystem = dispose(this._renderSystem);
        this._entityClasses.clear();
        this.authorizationClient = undefined;
        this._initialized = false;
        this.onAfterStartup.clear();
    }
    /** Controls how frequently the application polls for changes that may require a new animation frame to be requested.
     * Such changes include resizing a Viewport or changing the device pixel ratio by zooming in or out in the browser.
     * The default interval is 1 second. It may be desirable to override the default for specific apps and/or devices.
     *  - Increasing the interval can conserve battery life on battery-powered devices at the expense of slower response to resize events.
     *  - An application that only displays a single Viewport whose dimensions only change when the dimensions of the application window change, and which does not support changing application zoom level, could disable the interval altogether.
     * @param interval The interval at which to poll for changes. If undefined (or negative), the application will never poll. If zero, the application will poll as frequently as possible.
     * @beta
     */
    static get animationInterval() { return _a._animationInterval; }
    static set animationInterval(interval) {
        if (undefined !== interval && interval.isTowardsPast)
            interval = undefined;
        if (interval !== _a._animationInterval) {
            _a._animationInterval = interval;
            if (_a._wantEventLoop)
                _a.requestIntervalAnimation();
        }
    }
    /** Request that the event loop execute on the next [animation frame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
     * There is generally no reason for applications to invoke this method directly.
     */
    static requestNextAnimation() {
        // Only want to call requestAnimationFrame if it is defined. Need to check whether current iModelApp is a NoRenderApp.
        if (_a._noRender)
            return;
        if (!_a._animationRequested) {
            _a._animationRequested = true;
            requestAnimationFrame(() => _a.eventLoop());
        }
    }
    /** @internal */
    static clearIntervalAnimation() {
        if (undefined !== _a._animationIntervalId) {
            window.clearInterval(_a._animationIntervalId);
            _a._animationIntervalId = undefined;
        }
    }
    /** @internal */
    static requestIntervalAnimation() {
        _a.clearIntervalAnimation();
        if (undefined !== _a.animationInterval)
            _a._animationIntervalId = window.setInterval(() => {
                _a.requestNextAnimation();
            }, _a.animationInterval.milliseconds);
    }
    /** @internal */
    static startEventLoop() {
        if (!_a._wantEventLoop) {
            _a._wantEventLoop = true;
            window.addEventListener("resize", () => _a.requestNextAnimation());
            _a.requestIntervalAnimation();
            _a.requestNextAnimation();
        }
    }
    /** Strictly for tests. @internal */
    static stopEventLoop() {
        this._wantEventLoop = false;
    }
    /** The main event processing loop for Tools and rendering. */
    static eventLoop() {
        _a._animationRequested = false;
        if (!_a._wantEventLoop) // flag turned on at startup
            return;
        try {
            _a.toolAdmin.processEvent(); // eslint-disable-line @typescript-eslint/no-floating-promises
            _a.viewManager.renderLoop();
            _a.tileAdmin.process();
        }
        catch (exception) {
            ToolAdmin.exceptionHandler(exception); // eslint-disable-line @typescript-eslint/no-floating-promises
            _a._wantEventLoop = false;
            _a._animationRequested = true; // unrecoverable after exception, don't request any further frames.
            window.removeEventListener("resize", () => _a.requestNextAnimation());
        }
    }
    /** Get the user's access token for this IModelApp, or a blank string if none is available.
     * @note Access tokens expire periodically and are automatically refreshed, if possible. Therefore tokens should not be saved, and the value
     * returned by this method may change over time throughout the course of a session.
     */
    static async getAccessToken() {
        try {
            return (await this.authorizationClient?.getAccessToken()) ?? "";
        }
        catch (e) {
            return "";
        }
    }
    /** @internal */
    static createRenderSys(opts) { return System.create(opts); }
    static _setupRpcRequestContext() {
        RpcConfiguration.requestContext.getId = (_request) => {
            return Guid.createValue();
        };
        RpcConfiguration.requestContext.serialize = async (_request) => {
            const id = _request.id;
            const serialized = {
                id,
                applicationId: this.applicationId,
                applicationVersion: this.applicationVersion,
                sessionId: this.sessionId,
                authorization: ProcessDetector.isMobileAppFrontend ? "" : await this.getAccessToken(),
            };
            const csrf = _a.securityOptions.csrfProtection;
            if (csrf && csrf.enabled) {
                const cookieName = csrf.cookieName || "XSRF-TOKEN";
                const cookieValue = document.cookie.split("; ").find((r) => r.startsWith(`${cookieName}=`));
                if (cookieValue) {
                    const headerName = csrf.headerName || "X-XSRF-TOKEN";
                    const headerValue = cookieValue.split("=")[1];
                    serialized.csrfToken = { headerName, headerValue };
                }
            }
            return serialized;
        };
    }
    /** Shortcut for creating an HTMLElement with optional parent, className, id, innerHTML, innerText. */
    static makeHTMLElement(type, opt) {
        const el = document.createElement(type);
        if (undefined !== opt) {
            if (undefined !== opt.className)
                el.className = opt.className;
            if (undefined !== opt.id)
                el.id = opt.id;
            if (undefined !== opt.innerHTML)
                el.innerHTML = opt.innerHTML;
            if (undefined !== opt.innerText)
                el.innerText = opt.innerText;
            if (undefined !== opt.parent)
                opt.parent.appendChild(el);
        }
        return el;
    }
    /** Shortcut for making a modal dialog on top of the root of the application. The returned HTMLDivElement will be placed topmost, all other application
     * windows will be covered with a semi-transparent background that intercepts all key/mouse/touch events until the modal is dismissed.
     * @param options The options that describe how the modal should work.
     */
    static makeModalDiv(options) {
        const root = options.rootDiv ? options.rootDiv : document.body;
        // create the overlay div to "black out" the application to indicate everything is inactive until the modal has been dismissed.
        const overlay = _a.makeHTMLElement("div", { parent: root, className: "imodeljs-modal-overlay" });
        overlay.tabIndex = -1; // so we can catch keystroke events
        // function to remove modal dialog
        const stop = (ev) => {
            root.removeChild(overlay);
            ev.stopPropagation();
        };
        if (options.autoClose) {
            overlay.onclick = overlay.oncontextmenu = stop;
            overlay.onkeydown = overlay.onkeyup = (ev) => {
                switch (ev.key) {
                    case "Enter":
                    case "Escape":
                        stop(ev);
                        return;
                }
                ev.stopPropagation();
            };
            overlay.focus();
        }
        const modal = _a.makeHTMLElement("div", { parent: overlay, className: "imodeljs-modal" });
        if (undefined !== options.width) {
            modal.style.width = `${options.width}px`;
            // allow the dialog to be smaller than the width
            modal.style.maxWidth = `min(100% - (2 * var(--width-border)), ${options.width}px)`;
        }
        if (options.closeBox) {
            const close = _a.makeHTMLElement("p", { parent: modal, className: "imodeljs-modal-close" });
            close.innerText = "\u00d7"; // unicode "times" symbol
            close.onclick = stop;
        }
        return { modal, stop };
    }
    /** Make a new Logo Card. Call this method from your implementation of [[IModelApp.applicationLogoCard]]
     * @param opts Options for Logo Card
     * @beta
     */
    static makeLogoCard(opts) {
        const card = _a.makeHTMLElement("tr");
        const iconCell = _a.makeHTMLElement("td", { parent: card, className: "logo-card-logo" });
        if (undefined !== opts.iconSrc) {
            if (typeof opts.iconSrc === "string") {
                const logo = _a.makeHTMLElement("img");
                logo.src = opts.iconSrc;
                logo.width = opts.iconWidth ? opts.iconWidth : 64;
                opts.iconSrc = logo;
            }
            iconCell.appendChild(opts.iconSrc);
        }
        const noticeCell = _a.makeHTMLElement("td", { parent: card, className: "logo-card-message" });
        if (undefined !== opts.heading) {
            if (typeof opts.heading === "string")
                _a.makeHTMLElement("h2", { parent: noticeCell, innerHTML: opts.heading, className: "logo-card-header" });
            else
                noticeCell.appendChild(opts.heading);
        }
        if (undefined !== opts.notice) {
            if (typeof opts.notice === "string")
                _a.makeHTMLElement("p", { parent: noticeCell, innerHTML: opts.notice, className: "logo-cards" });
            else
                noticeCell.appendChild(opts.notice);
        }
        return card;
    }
    /** Make the logo card for the library itself. This card gets placed at the top of the stack.
     *  @internal
     */
    static makeIModelJsLogoCard() {
        return this.makeLogoCard({
            iconSrc: `${this.publicPath}images/about-imodeljs.svg`,
            heading: `<span style="font-weight:normal">${this.localization.getLocalizedString("iModelJs:Notices.PoweredBy")}</span>&nbsp;iTwin.js`,
            notice: `${ITWINJS_CORE_VERSION}<br>${COPYRIGHT_NOTICE}`,
        });
    }
    /** Format the tooltip strings returned by [[IModelConnection.getToolTipMessage]].
     * @alpha
     */
    static formatElementToolTip(msg) {
        let out = "";
        msg.forEach((line) => out += `${_a.localization?.getLocalizedKeys(line)}<br>`);
        const div = document.createElement("div");
        div.innerHTML = out;
        return div;
    }
    /** Localize an error status
     * @param status one of the status values from [BentleyStatus]($core-bentley), [IModelStatus]($core-bentley) or [DbResult]($core-bentley)
     * @returns a localized error message
     * @beta
     */
    static translateStatus(status) {
        let key;
        if (typeof status !== "number") {
            key = { scope: "Errors", val: "IllegalValue" };
        }
        else {
            key = { scope: "BentleyStatus", val: BentleyStatus[status] };
            if (!key.val)
                key = { scope: "IModelStatus", val: IModelStatus[status] };
            if (!key.val)
                key = { scope: "DbResult", val: DbResult[status] };
            if (!key.val)
                key = { scope: "Errors", val: "Status", status: status.toString() };
        }
        return this.localization.getLocalizedString(`iModelJs:${key.scope}.${key.val}`, key);
    }
    /**
     * Creates an instance of the ExtensionAdmin
     * and registers an event to execute after startup is complete
     * @returns an instance of ExtensionAdmin
     */
    static _createExtensionAdmin() {
        const extensionAdmin = new ExtensionAdmin();
        _a.onAfterStartup.addListener(extensionAdmin.onStartup);
        return extensionAdmin;
    }
}
_a = IModelApp;
IModelApp._initialized = false;
IModelApp._wantEventLoop = false;
IModelApp._animationRequested = false;
IModelApp._animationInterval = BeDuration.fromSeconds(1);
/** Event raised just before the frontend IModelApp is to be [[shutdown]]. */
IModelApp.onBeforeShutdown = new BeEvent();
/** Event raised after IModelApp [[startup]] completes. */
IModelApp.onAfterStartup = new BeEvent();
/** The [[ToolRegistry]] for this session. */
IModelApp.tools = new ToolRegistry();
/** The [[TelemetryManager]] for this session
 * @internal
 */
IModelApp.telemetry = new TelemetryManager();
/** @alpha */
IModelApp.extensionAdmin = _a._createExtensionAdmin();
/** Map of classFullName to EntityState class */
IModelApp._entityClasses = new Map();
//# sourceMappingURL=IModelApp.js.map