/* -----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
import { JSONExt, PromiseDelegate, Token } from '@lumino/coreutils';
import { AttachedProperty } from '@lumino/properties';
/**
 * The layout restorer token.
 */
export const ILayoutRestorer = new Token('@jupyterlab/application:ILayoutRestorer');
/**
 * The data connector key for restorer data.
 */
const KEY = 'layout-restorer:data';
/**
 * The default implementation of a layout restorer.
 *
 * #### Notes
 * The lifecycle for state restoration is subtle. The sequence of events is:
 *
 * 1. The layout restorer plugin is instantiated and makes a `fetch` call to
 *    the data connector that stores the layout restoration data. The `fetch`
 *    call returns a promise that resolves in step 6, below.
 *
 * 2. Other plugins that care about state restoration require the layout
 *    restorer as a dependency.
 *
 * 3. As each load-time plugin initializes (which happens before the front-end
 *    application has `started`), it instructs the layout restorer whether
 *    the restorer ought to `restore` its widgets by passing in its widget
 *    tracker.
 *    Alternatively, a plugin that does not require its own widget tracker
 *    (because perhaps it only creates a single widget, like a command palette),
 *    can simply `add` its widget along with a persistent unique name to the
 *    layout restorer so that its layout state can be restored when the lab
 *    application restores.
 *
 * 4. After all the load-time plugins have finished initializing, the front-end
 *    application `started` promise will resolve. This is the `first`
 *    promise that the layout restorer waits for. By this point, all of the
 *    plugins that care about restoration will have instructed the layout
 *    restorer to `restore` their widget trackers.
 *
 * 5. The layout restorer will then instruct each plugin's widget tracker
 *    to restore its state and reinstantiate whichever widgets it wants. The
 *    tracker returns a promise to the layout restorer that resolves when it
 *    has completed restoring the tracked widgets it cares about.
 *
 * 6. As each widget tracker finishes restoring the widget instances it cares
 *    about, it resolves the promise that was returned to the layout restorer
 *    (in step 5). After all of the promises that the restorer is awaiting have
 *    settled, the restorer then resolves the outstanding `fetch` promise
 *    (from step 1) and hands off a layout state object to the application
 *    shell's `restoreLayout` method for restoration.
 *
 * 7. Once the application shell has finished restoring the layout, the
 *    JupyterLab application's `restored` promise is resolved.
 *
 * Of particular note are steps 5 and 6: since data restoration of plugins
 * is accomplished by executing commands, the command that is used to restore
 * the data of each plugin must return a promise that only resolves when the
 * widget has been created and added to the plugin's widget tracker.
 */
export class LayoutRestorer {
    /**
     * Create a layout restorer.
     */
    constructor(options) {
        this._deferred = new Array();
        this._deferredMainArea = null;
        this._firstDone = false;
        this._promisesDone = false;
        this._promises = [];
        this._restored = new PromiseDelegate();
        this._trackers = new Set();
        this._widgets = new Map();
        this._mode = 'multiple-document';
        this._connector = options.connector;
        this._first = options.first;
        this._registry = options.registry;
        if (options.mode) {
            this._mode = options.mode;
        }
        void this._first
            .then(() => {
            this._firstDone = true;
        })
            .then(() => Promise.all(this._promises))
            .then(() => {
            this._promisesDone = true;
            // Release the tracker set.
            this._trackers.clear();
        })
            .then(() => {
            this._restored.resolve(void 0);
        });
    }
    /**
     * Whether full layout restoration is deferred and is currently incomplete.
     *
     * #### Notes
     * This flag is useful for tracking when the application has started in
     * 'single-document' mode and the main area has not yet been restored.
     */
    get isDeferred() {
        return this._deferred.length > 0;
    }
    /**
     * A promise resolved when the layout restorer is ready to receive signals.
     */
    get restored() {
        return this._restored.promise;
    }
    /**
     * Add a widget to be tracked by the layout restorer.
     */
    add(widget, name) {
        Private.nameProperty.set(widget, name);
        this._widgets.set(name, widget);
        widget.disposed.connect(this._onWidgetDisposed, this);
    }
    /**
     * Fetch the layout state for the application.
     *
     * #### Notes
     * Fetching the layout relies on all widget restoration to be complete, so
     * calls to `fetch` are guaranteed to return after restoration is complete.
     */
    async fetch() {
        const blank = {
            fresh: true,
            mainArea: null,
            downArea: null,
            leftArea: null,
            rightArea: null,
            relativeSizes: null
        };
        const layout = this._connector.fetch(KEY);
        try {
            const [data] = await Promise.all([layout, this.restored]);
            if (!data) {
                return blank;
            }
            const { main, down, left, right, relativeSizes } = data;
            // If any data exists, then this is not a fresh session.
            const fresh = false;
            // Rehydrate main area.
            let mainArea = null;
            if (this._mode === 'multiple-document') {
                mainArea = this._rehydrateMainArea(main);
            }
            else {
                this._deferredMainArea = main;
            }
            // Rehydrate down area.
            const downArea = this._rehydrateDownArea(down);
            // Rehydrate left area.
            const leftArea = this._rehydrateSideArea(left);
            // Rehydrate right area.
            const rightArea = this._rehydrateSideArea(right);
            return {
                fresh,
                mainArea,
                downArea,
                leftArea,
                rightArea,
                relativeSizes: relativeSizes || null
            };
        }
        catch (error) {
            return blank;
        }
    }
    /**
     * Restore the widgets of a particular widget tracker.
     *
     * @param tracker - The widget tracker whose widgets will be restored.
     *
     * @param options - The restoration options.
     */
    async restore(tracker, options) {
        if (this._firstDone) {
            throw new Error('restore() must be called before `first` has resolved.');
        }
        const { namespace } = tracker;
        if (this._trackers.has(namespace)) {
            throw new Error(`The tracker "${namespace}" is already restored.`);
        }
        const { args, command, name, when } = options;
        // Add the tracker to the private trackers collection.
        this._trackers.add(namespace);
        // Whenever a new widget is added to the tracker, record its name.
        tracker.widgetAdded.connect((_, widget) => {
            const widgetName = name(widget);
            if (widgetName) {
                this.add(widget, `${namespace}:${widgetName}`);
            }
        }, this);
        // Whenever a widget is updated, get its new name.
        tracker.widgetUpdated.connect((_, widget) => {
            const widgetName = name(widget);
            if (widgetName) {
                const name = `${namespace}:${widgetName}`;
                Private.nameProperty.set(widget, name);
                this._widgets.set(name, widget);
            }
        });
        const first = this._first;
        if (this._mode == 'multiple-document') {
            const promise = tracker
                .restore({
                args: args || (() => JSONExt.emptyObject),
                command,
                connector: this._connector,
                name,
                registry: this._registry,
                when: when ? [first].concat(when) : first
            })
                .catch(error => {
                console.error(error);
            });
            this._promises.push(promise);
            return promise;
        }
        tracker.defer({
            args: args || (() => JSONExt.emptyObject),
            command,
            connector: this._connector,
            name,
            registry: this._registry,
            when: when ? [first].concat(when) : first
        });
        this._deferred.push(tracker);
    }
    /**
     * Restore the application layout if its restoration has been deferred.
     *
     * @returns - the rehydrated main area.
     */
    async restoreDeferred() {
        if (!this.isDeferred) {
            return null;
        }
        // Empty the deferred list and wait for all trackers to restore.
        const wait = Promise.resolve();
        const promises = this._deferred.map(t => wait.then(() => t.restore()));
        this._deferred.length = 0;
        await Promise.all(promises);
        // Rehydrate the main area layout.
        return this._rehydrateMainArea(this._deferredMainArea);
    }
    /**
     * Save the layout state for the application.
     */
    save(data) {
        // If there are promises that are unresolved, bail.
        if (!this._promisesDone) {
            const warning = 'save() was called prematurely.';
            console.warn(warning);
            return Promise.reject(warning);
        }
        const dehydrated = {};
        // Save the cached main area layout if restoration is deferred.
        dehydrated.main = this.isDeferred
            ? this._deferredMainArea
            : this._dehydrateMainArea(data.mainArea);
        dehydrated.down = this._dehydrateDownArea(data.downArea);
        dehydrated.left = this._dehydrateSideArea(data.leftArea);
        dehydrated.right = this._dehydrateSideArea(data.rightArea);
        dehydrated.relativeSizes = data.relativeSizes;
        return this._connector.save(KEY, dehydrated);
    }
    /**
     * Dehydrate a main area description into a serializable object.
     */
    _dehydrateMainArea(area) {
        if (!area) {
            return null;
        }
        return Private.serializeMain(area);
    }
    /**
     * Reydrate a serialized main area description object.
     *
     * #### Notes
     * This function consumes data that can become corrupted, so it uses type
     * coercion to guarantee the dehydrated object is safely processed.
     */
    _rehydrateMainArea(area) {
        if (!area) {
            return null;
        }
        return Private.deserializeMain(area, this._widgets);
    }
    /**
     * Dehydrate a down area description into a serializable object.
     */
    _dehydrateDownArea(area) {
        if (!area) {
            return null;
        }
        const dehydrated = {
            size: area.size
        };
        if (area.currentWidget) {
            const current = Private.nameProperty.get(area.currentWidget);
            if (current) {
                dehydrated.current = current;
            }
        }
        if (area.widgets) {
            dehydrated.widgets = area.widgets
                .map(widget => Private.nameProperty.get(widget))
                .filter(name => !!name);
        }
        return dehydrated;
    }
    /**
     * Reydrate a serialized side area description object.
     *
     * #### Notes
     * This function consumes data that can become corrupted, so it uses type
     * coercion to guarantee the dehydrated object is safely processed.
     */
    _rehydrateDownArea(area) {
        var _a;
        if (!area) {
            return { currentWidget: null, size: 0.0, widgets: null };
        }
        const internal = this._widgets;
        const currentWidget = area.current && internal.has(`${area.current}`)
            ? internal.get(`${area.current}`)
            : null;
        const widgets = !Array.isArray(area.widgets)
            ? null
            : area.widgets
                .map(name => internal.has(`${name}`) ? internal.get(`${name}`) : null)
                .filter(widget => !!widget);
        return {
            currentWidget: currentWidget,
            size: (_a = area.size) !== null && _a !== void 0 ? _a : 0.0,
            widgets: widgets
        };
    }
    /**
     * Dehydrate a side area description into a serializable object.
     */
    _dehydrateSideArea(area) {
        if (!area) {
            return null;
        }
        const dehydrated = { collapsed: area.collapsed };
        if (area.currentWidget) {
            const current = Private.nameProperty.get(area.currentWidget);
            if (current) {
                dehydrated.current = current;
            }
        }
        if (area.widgets) {
            dehydrated.widgets = area.widgets
                .map(widget => Private.nameProperty.get(widget))
                .filter(name => !!name);
        }
        return dehydrated;
    }
    /**
     * Reydrate a serialized side area description object.
     *
     * #### Notes
     * This function consumes data that can become corrupted, so it uses type
     * coercion to guarantee the dehydrated object is safely processed.
     */
    _rehydrateSideArea(area) {
        var _a;
        if (!area) {
            return { collapsed: true, currentWidget: null, widgets: null };
        }
        const internal = this._widgets;
        const collapsed = (_a = area.collapsed) !== null && _a !== void 0 ? _a : false;
        const currentWidget = area.current && internal.has(`${area.current}`)
            ? internal.get(`${area.current}`)
            : null;
        const widgets = !Array.isArray(area.widgets)
            ? null
            : area.widgets
                .map(name => internal.has(`${name}`) ? internal.get(`${name}`) : null)
                .filter(widget => !!widget);
        return {
            collapsed,
            currentWidget: currentWidget,
            widgets: widgets
        };
    }
    /**
     * Handle a widget disposal.
     */
    _onWidgetDisposed(widget) {
        const name = Private.nameProperty.get(widget);
        this._widgets.delete(name);
    }
}
/*
 * A namespace for private data.
 */
var Private;
(function (Private) {
    /**
     * An attached property for a widget's ID in the serialized restore data.
     */
    Private.nameProperty = new AttachedProperty({
        name: 'name',
        create: owner => ''
    });
    /**
     * Serialize individual areas within the main area.
     */
    function serializeArea(area) {
        if (!area || !area.type) {
            return null;
        }
        if (area.type === 'tab-area') {
            return {
                type: 'tab-area',
                currentIndex: area.currentIndex,
                widgets: area.widgets
                    .map(widget => Private.nameProperty.get(widget))
                    .filter(name => !!name)
            };
        }
        return {
            type: 'split-area',
            orientation: area.orientation,
            sizes: area.sizes,
            children: area.children
                .map(serializeArea)
                .filter(area => !!area)
        };
    }
    /**
     * Return a dehydrated, serializable version of the main dock panel.
     */
    function serializeMain(area) {
        const dehydrated = {
            dock: (area && area.dock && serializeArea(area.dock.main)) || null
        };
        if (area) {
            if (area.currentWidget) {
                const current = Private.nameProperty.get(area.currentWidget);
                if (current) {
                    dehydrated.current = current;
                }
            }
        }
        return dehydrated;
    }
    Private.serializeMain = serializeMain;
    /**
     * Deserialize individual areas within the main area.
     *
     * #### Notes
     * Because this data comes from a potentially unreliable foreign source, it is
     * typed as a `JSONObject`; but the actual expected type is:
     * `ITabArea | ISplitArea`.
     *
     * For fault tolerance, types are manually checked in deserialization.
     */
    function deserializeArea(area, names) {
        if (!area) {
            return null;
        }
        // Because this data is saved to a foreign data source, its type safety is
        // not guaranteed when it is retrieved, so exhaustive checks are necessary.
        const type = area.type || 'unknown';
        if (type === 'unknown' || (type !== 'tab-area' && type !== 'split-area')) {
            console.warn(`Attempted to deserialize unknown type: ${type}`);
            return null;
        }
        if (type === 'tab-area') {
            const { currentIndex, widgets } = area;
            const hydrated = {
                type: 'tab-area',
                currentIndex: currentIndex || 0,
                widgets: (widgets &&
                    widgets
                        .map(widget => names.get(widget))
                        .filter(widget => !!widget)) ||
                    []
            };
            // Make sure the current index is within bounds.
            if (hydrated.currentIndex > hydrated.widgets.length - 1) {
                hydrated.currentIndex = 0;
            }
            return hydrated;
        }
        const { orientation, sizes, children } = area;
        const hydrated = {
            type: 'split-area',
            orientation: orientation,
            sizes: sizes || [],
            children: (children &&
                children
                    .map(child => deserializeArea(child, names))
                    .filter(widget => !!widget)) ||
                []
        };
        return hydrated;
    }
    /**
     * Return the hydrated version of the main dock panel, ready to restore.
     *
     * #### Notes
     * Because this data comes from a potentially unreliable foreign source, it is
     * typed as a `JSONObject`; but the actual expected type is: `IMainArea`.
     *
     * For fault tolerance, types are manually checked in deserialization.
     */
    function deserializeMain(area, names) {
        if (!area) {
            return null;
        }
        const name = area.current || null;
        const dock = area.dock || null;
        return {
            currentWidget: (name && names.has(name) && names.get(name)) || null,
            dock: dock ? { main: deserializeArea(dock, names) } : null
        };
    }
    Private.deserializeMain = deserializeMain;
})(Private || (Private = {}));
//# sourceMappingURL=layoutrestorer.js.map