/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Views
 */
import { BeEvent, BentleyStatus, BeUiEvent } from "@itwin/core-bentley";
import { IModelApp } from "./IModelApp";
import { DisclosedTileTreeSet } from "./tile/internal";
import { EventHandled } from "./tools/Tool";
import { System } from "./render/webgl/System";
/** The ViewManager holds the list of opened views, plus the *selected view*. It also provides notifications of view open/close and suspend/resume.
 * Applications must call [[addViewport]] when new Viewports that should be associated with user events are created.
 *
 * A single ViewManager is created when [[IModelApp.startup]] is called. It can be accessed via the static member [[IModelApp.viewManager]].
 *
 * The ViewManager controls the render loop, which causes the contents of each registered [[Viewport]] to update on the screen.
 * @public
 * @extensions
 */
export class ViewManager {
    constructor() {
        this.inDynamicsMode = false;
        this.cursor = "default";
        this._viewports = [];
        this.decorators = [];
        this._invalidateScenes = false;
        this._skipSceneCreation = false;
        this._doIdleWork = false;
        /** @internal */
        this.toolTipProviders = [];
        /** Called after the selected view changes.
         * @param old Previously selected viewport.
         * @param current Currently selected viewport.
         */
        this.onSelectedViewportChanged = new BeUiEvent();
        /** Called after a view is opened. This can happen when the iModel is first opened or when a user opens a new view. */
        this.onViewOpen = new BeUiEvent();
        /** Called after a view is closed. This can happen when the iModel is closed or when a user closes an open view. */
        this.onViewClose = new BeUiEvent();
        /** Called after a view is suspended. This happens when the application is minimized or, on a tablet, when the application
         * is moved to the background.
         */
        this.onViewSuspend = new BeUiEvent();
        /** Called after a suspended view is resumed. This can happen when a minimized application is restored
         * or, on a tablet, when the application is moved to the foreground.
         */
        this.onViewResume = new BeUiEvent();
        /** Called at the beginning of each tick of the render loop, before any viewports have been updated.
         * The render loop is typically invoked by a requestAnimationFrame() callback. It will not be invoked if the ViewManager is tracking no viewports.
         * @note Due to the frequency of this event, avoid performing expensive work inside event listeners.
         * @see [[ViewManager.onFinishRender]]
         */
        this.onBeginRender = new BeEvent();
        /** Called at the end of each tick of the render loop, after all viewports have been updated.
         * The render loop is typically invoked by a requestAnimationFrame() callback. It will not be invoked if the ViewManager is tracking no viewports.
         * @note Due to the frequency of this event, avoid performing expensive work inside event listeners.
         * @see [[ViewManager.onBeginRender]]
         */
        this.onFinishRender = new BeEvent();
    }
    _beginIdleWork() {
        const idleWork = () => {
            if (undefined === this._idleWorkTimer)
                return;
            if (this._viewports.length > 0) {
                this._idleWorkTimer = undefined;
                return;
            }
            if (IModelApp.renderSystem.doIdleWork())
                this._idleWorkTimer = setTimeout(idleWork, 1);
            else
                this._idleWorkTimer = undefined;
        };
        if (undefined === this._idleWorkTimer)
            this._idleWorkTimer = setTimeout(idleWork, 1);
    }
    /** @internal */
    onInitialized() {
        this.addDecorator(IModelApp.accuSnap);
        this.addDecorator(IModelApp.tentativePoint);
        this.addDecorator(IModelApp.accuDraw);
        this.addDecorator(IModelApp.toolAdmin);
        this.cursor = "default";
        const options = IModelApp.renderSystem.options;
        this._doIdleWork = true === options.doIdleWork;
        if (this._doIdleWork)
            this._beginIdleWork();
    }
    /** @internal */
    onShutDown() {
        if (undefined !== this._idleWorkTimer) {
            clearTimeout(this._idleWorkTimer);
            this._idleWorkTimer = undefined;
        }
        this._viewports.map((viewport) => {
            if (!viewport.isDisposed)
                this.dropViewport(viewport, true);
        });
        this._viewports.length = 0;
        this.decorators.length = 0;
        this.toolTipProviders.length = 0;
        this._selectedView = undefined;
    }
    /** Returns true if the specified viewport is currently being managed by this ViewManager.
     * @see [[addViewport]] to enable management of a viewport and [[dropViewport]] to disable it.
     */
    hasViewport(viewport) {
        return this._viewports.includes(viewport);
    }
    /** @internal */
    endDynamicsMode() {
        if (!this.inDynamicsMode)
            return;
        this.inDynamicsMode = false;
        const cursorVp = IModelApp.toolAdmin.cursorView;
        if (cursorVp)
            cursorVp.changeDynamics(undefined);
        for (const vp of this._viewports) {
            if (vp !== cursorVp)
                vp.changeDynamics(undefined);
        }
    }
    /** @internal */
    beginDynamicsMode() { this.inDynamicsMode = true; }
    /** @internal */
    get doesHostHaveFocus() { return document.hasFocus(); }
    /** Set the selected [[Viewport]] to undefined. */
    clearSelectedView() {
        const previousVp = this.selectedView;
        this._selectedView = undefined;
        this.notifySelectedViewportChanged(previousVp, undefined);
    }
    /** Sets the selected [[Viewport]]. */
    async setSelectedView(vp) {
        if (undefined === vp)
            vp = this.getFirstOpenView();
        if (vp === this.selectedView) // already the selected view
            return BentleyStatus.SUCCESS;
        if (undefined === vp) {
            this.clearSelectedView();
            return BentleyStatus.ERROR;
        }
        const previousVp = this.selectedView;
        this._selectedView = vp;
        this.notifySelectedViewportChanged(previousVp, vp);
        if (undefined === previousVp)
            await IModelApp.toolAdmin.startDefaultTool();
        return BentleyStatus.SUCCESS;
    }
    /** @internal */
    notifySelectedViewportChanged(previous, current) {
        IModelApp.toolAdmin.onSelectedViewportChanged(previous, current); // eslint-disable-line @typescript-eslint/no-floating-promises
        this.onSelectedViewportChanged.emit({ previous, current });
    }
    /** The "selected view" is the default for certain operations.  */
    get selectedView() { return this._selectedView; }
    /** Get the first opened view. */
    getFirstOpenView() { return this._viewports.length > 0 ? this._viewports[0] : undefined; }
    /** Check if only a single viewport is being used.  If so, render directly on-screen using its WebGL canvas.  Otherwise, render each view offscreen. */
    updateRenderToScreen() {
        const renderToScreen = 1 === this._viewports.length;
        for (const vp of this)
            vp.rendersToScreen = renderToScreen;
    }
    /** Add a new Viewport to the list of opened views and create an EventController for it.
     * @param newVp the Viewport to add
     * @returns SUCCESS if vp was successfully added, ERROR if it was already present.
     * @note raises onViewOpen event with newVp.
     */
    addViewport(newVp) {
        if (this.hasViewport(newVp)) // make sure its not already added
            return BentleyStatus.ERROR;
        newVp.onViewManagerAdd();
        this._viewports.push(newVp);
        this.updateRenderToScreen();
        this.setSelectedView(newVp); // eslint-disable-line @typescript-eslint/no-floating-promises
        // Start up the render loop if necessary.
        if (1 === this._viewports.length)
            IModelApp.startEventLoop();
        this.onViewOpen.emit(newVp);
        return BentleyStatus.SUCCESS;
    }
    /** Remove a Viewport from the list of opened views, and optionally dispose of it.
     * Typically a Viewport is dropped when it is no longer of any use to the application, in which case it should also be
     * disposed of as it may hold significant GPU resources.
     * However in some cases a Viewport may be temporarily dropped to suspend rendering; and subsequently re-added to
     * resume rendering - for example, when the Viewport is temporarily hidden by other UI elements.
     * In the latter case it is up to the caller to ensure the Viewport is properly disposed of when it is no longer needed.
     * Attempting to invoke any function on a Viewport after it has been disposed is an error.
     * @param vp the Viewport to remove.
     * @param disposeOfViewport Whether or not to dispose of the Viewport. Defaults to true.
     * @return SUCCESS if vp was successfully removed, ERROR if it was not present.
     * @note raises onViewClose event with vp.
     */
    dropViewport(vp, disposeOfViewport = true) {
        const index = this._viewports.indexOf(vp);
        if (index === -1)
            return BentleyStatus.ERROR;
        this.onViewClose.emit(vp);
        // make sure tools don't think the cursor is still in this viewport
        IModelApp.toolAdmin.forgetViewport(vp);
        vp.onViewManagerDrop();
        this._viewports.splice(index, 1);
        if (this.selectedView === vp) // if removed viewport was selectedView, set it to undefined.
            this.setSelectedView(undefined); // eslint-disable-line @typescript-eslint/no-floating-promises
        vp.rendersToScreen = false;
        this.updateRenderToScreen();
        if (disposeOfViewport)
            vp.dispose();
        if (this._doIdleWork && this._viewports.length === 0)
            this._beginIdleWork();
        return BentleyStatus.SUCCESS;
    }
    /** Iterate over the viewports registered with the view manager. */
    [Symbol.iterator]() {
        return this._viewports[Symbol.iterator]();
    }
    /** Instruct each registered [[Viewport]] that the cached [[Decorations]] for the specified `decorator` should be discarded and recreated on the next frame.
     * @see [[Viewport.invalidateCachedDecorations]] to invalidate the cached decorations for a single viewport.
     */
    invalidateCachedDecorationsAllViews(decorator) {
        if (decorator.useCachedDecorations)
            for (const vp of this)
                vp.invalidateCachedDecorations(decorator);
    }
    /** Force each registered [[Viewport]] to regenerate its [[Decorations]] on the next frame. */
    invalidateDecorationsAllViews() {
        for (const vp of this)
            vp.invalidateDecorations();
    }
    /** Force each registered [[Viewport]] to regenerate its [[FeatureSymbology.Overrides]] on the next frame.
     * This is rarely needed - viewports keep track of their own states to detect when the overrides need to be recreated.
     */
    invalidateSymbologyOverridesAllViews() {
        for (const vp of this)
            vp.setFeatureOverrideProviderChanged();
    }
    /** @internal */
    onSelectionSetChanged(_iModel) {
        for (const vp of this)
            vp.markSelectionSetDirty();
        IModelApp.requestNextAnimation();
    }
    /** @internal */
    invalidateViewportScenes() {
        for (const vp of this)
            vp.invalidateScene();
    }
    /** @internal */
    validateViewportScenes() {
        for (const vp of this)
            vp.setValidScene();
    }
    /** Requests that [[Viewport.createScene]] be invoked for every viewport on the next frame.
     * This is rarely useful - viewports keep track of their own states to detect when the scene needs to be recreated.
     */
    invalidateScenes() {
        this._invalidateScenes = true;
        IModelApp.requestNextAnimation();
    }
    /** @internal */
    get sceneInvalidated() { return this._invalidateScenes; }
    /** Invoked by ToolAdmin event loop.
     * @internal
     */
    renderLoop() {
        if (0 === this._viewports.length)
            return;
        if (this._skipSceneCreation)
            this.validateViewportScenes();
        else if (this._invalidateScenes)
            this.invalidateViewportScenes();
        this._invalidateScenes = false;
        this.onBeginRender.raiseEvent();
        for (const vp of this._viewports)
            vp.renderFrame();
        this.onFinishRender.raiseEvent();
    }
    /** Purge TileTrees that haven't been drawn since the specified time point and are not currently in use by any ScreenViewport.
     * Intended strictly for debugging purposes - TileAdmin takes care of properly purging.
     * @internal
     */
    purgeTileTrees(olderThan) {
        // A single viewport can display tiles from more than one IModelConnection.
        // NOTE: A viewport may be displaying no trees - but we need to record its IModel so we can purge those which are NOT being displayed
        //  NOTE: That won't catch external tile trees previously used by that viewport.
        const trees = new DisclosedTileTreeSet();
        const treesByIModel = new Map();
        for (const vp of this._viewports) {
            vp.discloseTileTrees(trees);
            if (undefined === treesByIModel.get(vp.iModel))
                treesByIModel.set(vp.iModel, new Set());
        }
        for (const tree of trees) {
            let set = treesByIModel.get(tree.iModel);
            if (undefined === set) {
                set = new Set();
                treesByIModel.set(tree.iModel, set);
            }
            set.add(tree);
        }
        for (const entry of treesByIModel) {
            const iModel = entry[0];
            iModel.tiles.purge(olderThan, entry[1]);
        }
    }
    /** Compute the tooltip for a persistent element.
     * This method calls the backend method [Element.getToolTipMessage]($backend), and replaces all instances of `${localizeTag}` with localized string from IModelApp.i18n.
     */
    async getElementToolTip(hit) {
        const msg = await hit.iModel.getToolTipMessage(hit.sourceId); // wait for the locate message(s) from the backend
        return IModelApp.formatElementToolTip(msg);
    }
    /** Register a new [[ToolTipProvider]] to customize the locate tooltip.
     * @param provider The new tooltip provider to add.
     * @throws Error if `provider` is already registered.
     * @returns a function that may be called to remove this provider (in lieu of calling [[dropToolTipProvider]].)
     */
    addToolTipProvider(provider) {
        if (this.toolTipProviders.includes(provider))
            throw new Error("tooltip provider already registered");
        this.toolTipProviders.push(provider);
        return () => this.dropToolTipProvider(provider);
    }
    /** Drop (remove) a [[ToolTipProvider]] so it is no longer active.
     * @param provider The tooltip provider to drop.
     * @note Does nothing if provider is not currently active.
     */
    dropToolTipProvider(provider) {
        const index = this.toolTipProviders.indexOf(provider);
        if (index >= 0)
            this.toolTipProviders.splice(index, 1);
    }
    /** Add a new [[Decorator]] to display decorations into the active views.
     * @param decorator The new decorator to add.
     * @throws Error if decorator is already active.
     * @returns a function that may be called to remove this decorator (in lieu of calling [[dropDecorator]].)
     * @see [[dropDecorator]]
     */
    addDecorator(decorator) {
        if (this.decorators.includes(decorator))
            throw new Error("decorator already registered");
        this.decorators.push(decorator);
        this.invalidateDecorationsAllViews();
        return () => this.dropDecorator(decorator);
    }
    /** Drop (remove) a [[Decorator]] so it is no longer active.
     * @param decorator The Decorator to drop.
     * @returns true if the decorator was found and removed; false if the decorator was not found.
     */
    dropDecorator(decorator) {
        const index = this.decorators.indexOf(decorator);
        if (index < 0)
            return false;
        this.invalidateCachedDecorationsAllViews(decorator);
        this.decorators.splice(index, 1);
        this.invalidateDecorationsAllViews();
        return true;
    }
    /** Get the tooltip for a pickable decoration.
     * @internal
     */
    async getDecorationToolTip(hit) {
        for (const decorator of this.decorators) {
            if (undefined !== decorator.testDecorationHit && undefined !== decorator.getDecorationToolTip && decorator.testDecorationHit(hit.sourceId))
                return decorator.getDecorationToolTip(hit);
        }
        return hit.viewport ? hit.viewport.getToolTip(hit) : "";
    }
    /** Allow a pickable decoration to handle a button event that identified it for the SelectTool.
     * @internal
     */
    async onDecorationButtonEvent(hit, ev) {
        for (const decorator of IModelApp.viewManager.decorators) {
            if (undefined !== decorator.testDecorationHit && undefined !== decorator.onDecorationButtonEvent && decorator.testDecorationHit(hit.sourceId))
                return decorator.onDecorationButtonEvent(hit, ev);
        }
        return EventHandled.No;
    }
    /** Allow a pickable decoration to be snapped to by AccuSnap or TentativePoint.
     * @internal
     */
    getDecorationGeometry(hit) {
        for (const decorator of IModelApp.viewManager.decorators) {
            if (undefined !== decorator.testDecorationHit && undefined !== decorator.getDecorationGeometry && decorator.testDecorationHit(hit.sourceId))
                return decorator.getDecorationGeometry(hit);
        }
        return undefined;
    }
    /** Allow a pickable decoration created using a persistent element id to augment or replace the the persistent element's tooltip.
     * @internal
     */
    async overrideElementToolTip(hit) {
        for (const decorator of this.decorators) {
            if (undefined !== decorator.overrideElementHit && undefined !== decorator.getDecorationToolTip && decorator.overrideElementHit(hit))
                return decorator.getDecorationToolTip(hit);
        }
        return this.getElementToolTip(hit);
    }
    /** Allow a pickable decoration created using a persistent element id to handle a button event that identified it for the SelectTool.
     * @internal
     */
    async overrideElementButtonEvent(hit, ev) {
        for (const decorator of IModelApp.viewManager.decorators) {
            if (undefined !== decorator.overrideElementHit && undefined !== decorator.onDecorationButtonEvent && decorator.overrideElementHit(hit))
                return decorator.onDecorationButtonEvent(hit, ev);
        }
        return EventHandled.No;
    }
    /** Allow a pickable decoration created using a persistent element id to control whether snapping uses the persistent element's geometry.
     * @internal
     */
    overrideElementGeometry(hit) {
        for (const decorator of IModelApp.viewManager.decorators) {
            if (undefined !== decorator.overrideElementHit && undefined !== decorator.getDecorationGeometry && decorator.overrideElementHit(hit))
                return decorator.getDecorationGeometry(hit);
        }
        return undefined;
    }
    get crossHairCursor() { return `url(${IModelApp.publicPath}cursors/crosshair.cur), crosshair`; }
    get dynamicsCursor() { return `url(${IModelApp.publicPath}cursors/dynamics.cur), move`; }
    get grabCursor() { return `url(${IModelApp.publicPath}cursors/openHand.cur), auto`; }
    get grabbingCursor() { return `url(${IModelApp.publicPath}cursors/closedHand.cur), auto`; }
    get walkCursor() { return `url(${IModelApp.publicPath}cursors/walk.cur), auto`; }
    get rotateCursor() { return `url(${IModelApp.publicPath}cursors/rotate.cur), auto`; }
    get lookCursor() { return `url(${IModelApp.publicPath}cursors/look.cur), auto`; }
    get zoomCursor() { return `url(${IModelApp.publicPath}cursors/zoom.cur), auto`; }
    /** Change the cursor shown in all Viewports.
     * @param cursor The new cursor to display. If undefined, the default cursor is used.
     */
    setViewCursor(cursor = "default") {
        if (cursor === this.cursor)
            return;
        this.cursor = cursor;
        for (const vp of this._viewports)
            vp.setCursor(cursor);
    }
    /** Intended strictly as a temporary solution for interactive editing applications, until official support for such apps is implemented.
     * Call this after editing one or more models, passing in the Ids of those models, to cause new tiles to be generated reflecting the changes.
     * Pass undefined if you are unsure which models changed (this is less efficient as it discards all tiles for all viewed models in all viewports).
     * @internal
     */
    refreshForModifiedModels(modelIds) {
        for (const vp of this._viewports)
            vp.refreshForModifiedModels(modelIds);
    }
    /** Sets the number of [MSAA]($docs/learning/display/MSAA.md) samples for all currently- and subsequently-opened [[ScreenViewport]]s.
     * @param numSamples The number of samples as a power of two. Values of 1 or less indicates anti-aliasing should be disabled. Non-power-of-two values are rounded
     * down to the nearest power of two. The maximum number of samples supported depends upon the client's graphics hardware capabilities. Higher values produce
     * a higher-quality image but also may also reduce framerate.
     * @see [[Viewport.antialiasSamples]] to adjust the number of samples for a specific viewport.
     */
    setAntialiasingAllViews(numSamples) {
        for (const vp of this)
            vp.antialiasSamples = numSamples;
        System.instance.antialiasSamples = numSamples;
    }
}
//# sourceMappingURL=ViewManager.js.map