import { SplitItem } from './SplitItem.js';
import { SplitPanel } from './SplitPanel.js';
import { LayoutType, SplitDirection, } from './type.js';
/**
 * Split layout manager creates a view where the application can put "items" into
 * and render them inside "panels". The application can create as many panels as needed.
 * Panels can be layout vertically or horizontally.
 *
 * ## Basic usage
 *
 * ```javascript
 * const layout = new SplitLayout({
 *  stateCallback: async (state: ISplitLayout): Promise<void> => {
 *    localStorage.setItem('layout', JSON.stringify(state));
 *  }
 * });
 * const restored = await readStoredState(); // a function that reads the state from the store
 * await layout.initialize(restored);
 *
 * const content = layout.render((itemToRender: SplitItem): TemplateResult => { return html`...` });
 * ```
 *
 * Each time the layout configuration change the optional `stateCallback` function
 * is called.
 * It's not mandatory to set this callback function but if you want the state to
 * persist you have to serialize the manager manually when needed.
 * Since the manage implement the `toJSON()` method it is safe to pass the instance of the manager
 * to a idb store or serialize with `JSON.stringify()`.
 *
 * @fires change - When the layout configuration change. This is the best time to perform DOM update.
 * @fires nameitem - When the layout requests to name an item. This usually happens then a code calls
 *                   the `requestNameUpdate()` method after the item's name changed.
 * @fires closetab - When a tab wa closed via a user interaction.
 */
export class SplitLayout extends EventTarget {
    options;
    /**
     * The list of all definitions of panels and items.
     */
    definitions = new Map();
    /**
     * Holds an ordered list of panels in the layout.
     * Though, this interface is consistent with other panels, it can only keep panels, not items.
     */
    items = [];
    activeInternal;
    /**
     * The key of the panel that is marked as "active", that is,
     * the panel the user last interacted with.
     */
    get active() {
        return this.activeInternal;
    }
    /**
     * Returns an object that is marked as active or undefined if none is marked as active.
     */
    get activeObject() {
        const { activeInternal } = this;
        if (!activeInternal) {
            return undefined;
        }
        return this.definitions.get(activeInternal);
    }
    /**
     * An active panel that the user last interacted with.
     */
    get activePanel() {
        const { activeObject } = this;
        if (!activeObject) {
            return undefined;
        }
        const { type, value } = activeObject;
        if (type === LayoutType.panel) {
            return value;
        }
        const item = value;
        return this.getParents(item.key)[0];
    }
    /**
     * An active panel item that the user last interacted with.
     */
    get activeItem() {
        const { activeObject } = this;
        if (!activeObject) {
            return undefined;
        }
        const { type, value } = activeObject;
        if (type === LayoutType.item) {
            return value;
        }
        return undefined;
    }
    /**
     * Whether the manager is dirty, that is, a change ocurred
     * while storing the layout.
     */
    isDirty = false;
    /**
     * A flag that determines that the state is being stored in the store.
     */
    storing = false;
    /**
     * The value of the debouncer timeout.
     */
    debouncer;
    constructor(options = {}) {
        super();
        this.options = options;
        this.handleFocusIn = this.handleFocusIn.bind(this);
    }
    /**
     * Initializes the split layout manager.
     * When passed the `state` parameter it restores the previously serialized state.
     *
     * @param state A state to restore. When not set an empty layout is rendered.
     */
    initialize(state) {
        if (state) {
            this.new(state);
        }
        document.body.addEventListener('focusin', this.handleFocusIn);
    }
    reset() {
        this.definitions = new Map();
        this.items = [];
        this.activeInternal = undefined;
    }
    /**
     * Clears the state and disposes of all resources.
     */
    dispose() {
        this.reset();
        document.body.removeEventListener('focusin', this.handleFocusIn);
    }
    /**
     * Overrides the current state.
     * Note, this operation does not call the `stateCallback()` callback.
     * @param state the state to set.
     */
    new(state) {
        this.reset();
        const { definitions, items, active } = state;
        if (definitions) {
            // in the first step restore definitions
            definitions.forEach((value) => {
                if (value.type === LayoutType.item) {
                    const instance = new SplitItem(this, value.value);
                    this.definitions.set(instance.key, {
                        type: LayoutType.item,
                        value: instance,
                    });
                }
                else if (value.type === LayoutType.panel) {
                    const instance = new SplitPanel(this, value.value);
                    this.definitions.set(instance.key, {
                        type: LayoutType.panel,
                        value: instance,
                    });
                }
            });
        }
        // now that definitions are set we can restore panels.
        if (Array.isArray(items)) {
            items.forEach((key) => {
                this.items.push({
                    type: LayoutType.panel,
                    key,
                });
            });
        }
        this.activeInternal = active;
    }
    toJSON() {
        const result = {
            definitions: [],
            items: this.items.map((i) => i.key),
        };
        if (this.activeInternal) {
            result.active = this.activeInternal;
        }
        this.definitions.forEach((value) => {
            const serialized = value.value.toJSON();
            result.definitions.push({
                type: value.type,
                value: serialized,
            });
        });
        return result;
    }
    /**
     * Schedules a layout store operation in a debouncer.
     * Once active all other request to store the layout are ignored until
     * the timeout is triggered.
     */
    scheduleStore() {
        if (typeof this.debouncer === 'number') {
            return;
        }
        this.debouncer = requestAnimationFrame(() => {
            delete this.debouncer;
            this.storeLayout();
        });
    }
    /**
     * This function is called automatically when the store configuration
     * change. There's no need to call it manually unless you want to trigger
     * the storing state flow. However, at that moment the state is already stored
     * (unless there was an error).
     */
    async storeLayout() {
        const { stateCallback } = this.options;
        if (!stateCallback) {
            return;
        }
        if (this.storing) {
            this.isDirty = true;
            return;
        }
        this.storing = true;
        try {
            await stateCallback(this.toJSON());
        }
        finally {
            this.storing = false;
        }
        if (this.isDirty) {
            this.isDirty = false;
            await this.storeLayout();
        }
    }
    handleFocusIn(e) {
        const layout = this.findViewFromEvent(e);
        if (layout) {
            const key = layout.key;
            const def = this.definitions.get(key);
            if (def) {
                this.setActive(key);
            }
        }
    }
    /**
     * Sets an active panel or item.
     * @param panelOrItemKey the key of the panel or item that is active.
     */
    setActive(panelOrItemKey) {
        this.activeInternal = panelOrItemKey;
    }
    findViewFromEvent(e) {
        const path = e.composedPath();
        while (path.length) {
            const node = path.shift();
            if (node.nodeType !== Node.ELEMENT_NODE) {
                continue;
            }
            if (node.localName === 'split-view') {
                return node;
            }
        }
        return undefined;
    }
    /**
     * @param panelKey The panel key to search for the view for.
     * @returns The `split-view` element in the document
     */
    findView(panelKey) {
        const layout = document.querySelector(`split-view[key="${panelKey}"]`);
        return layout || undefined;
    }
    /**
     * Finds parents an item or another panel is anchored to.
     * An item can have multiple parents. A panel can only have a single parent.
     *
     * @param itemOrPanelKey The key of the panel or an item.
     * @returns THe list of panels the item is added. It can only return up to 1 result when looking for a panel's parent.
     */
    getParents(itemOrPanelKey) {
        const result = [];
        for (const { type, value } of this.definitions.values()) {
            if (type !== LayoutType.panel) {
                continue;
            }
            const panel = value;
            const has = panel.items.some((i) => i.key === itemOrPanelKey);
            if (has) {
                result.push(panel);
            }
        }
        return result;
    }
    /**
     * Finds a panel by id.
     *
     * @param panelKey The id of the panel.
     * @returns The panel if found
     */
    findPanel(panelKey) {
        const result = this.definitions.get(panelKey);
        if (!result) {
            return undefined;
        }
        if (result.type !== LayoutType.panel) {
            return undefined;
        }
        return result.value;
    }
    /**
     * Finds a panel item by id.
     *
     * @param itemKey The id of the panel item.
     * @returns The item if found
     */
    findItem(itemKey) {
        const result = this.definitions.get(itemKey);
        if (!result) {
            return undefined;
        }
        if (result.type !== LayoutType.item) {
            return undefined;
        }
        return result.value;
    }
    /**
     * Informs the screen that something has changed
     */
    notifyChange() {
        this.dispatchEvent(new Event('change'));
        this.scheduleStore();
    }
    /**
     * Dispatches the `nameitem` event.
     * The detail object has the item to be added to the items.
     * The event handler can manipulate properties of the item, except for the index which will be set by the manager.
     *
     * Note, use the `requestNameUpdate()` when requesting a name update. The other method includes side
     * effects like updating the view.
     *
     * @param item The item to notify.
     */
    dispatchNameItem(item) {
        this.dispatchEvent(new CustomEvent('nameitem', {
            detail: item,
        }));
    }
    /**
     * Do not call this method from the outside of the layout manager logic.
     * It is a way to communicate a tab was closed.
     */
    notifyTabClose(itemKey, panel) {
        this.dispatchEvent(new CustomEvent('closetab', {
            bubbles: true,
            cancelable: true,
            composed: true,
            detail: {
                tab: itemKey,
                panel,
            },
        }));
    }
    /**
     * Requests an update on the view element.
     *
     * @param panelKey The id of the panel. When not set it uses the active panel
     */
    updateView(panelKey) {
        const panel = panelKey ? this.findPanel(panelKey) : this.activePanel;
        if (!panel) {
            return;
        }
        panel.updateView();
    }
    /**
     * Requests to dispatch the `nameitem` event so the application can update the name of the tab.
     *
     * @param itemKey The key of the item.
     */
    requestNameUpdate(itemKey) {
        const item = this.findItem(itemKey);
        if (!item) {
            return;
        }
        const before = item.label;
        this.dispatchNameItem(item);
        if (before !== item.label) {
            const panels = this.getParents(item.key);
            panels.forEach((panel) => this.updateView(panel.key));
        }
    }
    /**
     * Dispatches the `nameitem` event so the application
     * can update the name of an item that has a specific `parent`.
     *
     * @param parentValue The key of the `parent` property to look for.
     */
    requestNameUpdateByParent(parentValue) {
        const updatePanels = new Set();
        for (const item of this.parentItemsIterator(parentValue)) {
            const before = item.label;
            this.dispatchNameItem(item);
            if (before !== item.label) {
                const panels = this.getParents(item.key);
                panels.forEach((panel) => updatePanels.add(panel));
            }
        }
        for (const panel of updatePanels) {
            this.updateView(panel.key);
        }
        this.notifyChange();
    }
    /**
     * A shortcut to rename an item.
     * @param itemKey The key of the item to rename
     * @param label The label to set. It won't trigger side effects if the label is the same.
     */
    setItemLabel(itemKey, label) {
        const item = this.findItem(itemKey);
        if (!item) {
            return;
        }
        item.setLabel(label);
    }
    addPanel(opts = {}) {
        const { layout = SplitDirection.horizontal, parent } = opts;
        let parentInstance;
        if (parent) {
            parentInstance = this.findPanel(parent);
            if (!parentInstance) {
                throw new Error(`Parent panel not found.`);
            }
            if (parentInstance.hasItems) {
                throw new Error(`Unable to create panel in a panel that has items.`);
            }
        }
        else {
            // eslint-disable-next-line @typescript-eslint/no-this-alias
            parentInstance = this;
        }
        const panel = SplitPanel.fromLayout(this, layout);
        this.definitions.set(panel.key, {
            type: LayoutType.panel,
            value: panel,
        });
        parentInstance.items.push({
            key: panel.key,
            type: LayoutType.panel,
        });
        return panel;
    }
    /**
     * Adds an item to the active panel.
     *
     * To add an item to a specific panel find a reference to
     * the panel instance and call `addItem()` there.
     *
     * @param item The item to add.
     */
    addItem(item) {
        let panel;
        const { activePanel } = this;
        // try the active panel
        if (activePanel) {
            panel = activePanel;
        }
        else {
            // try any panel that has no panels in it.
            for (const p of this.panelIterator()) {
                if (!p.hasPanels) {
                    panel = p;
                    break;
                }
            }
        }
        // finally add a new panel.
        if (!panel) {
            panel = this.addPanel();
        }
        return panel.addItem(item);
    }
    /**
     * Removes an item from the layout. This means removing an item from all panels.
     * If you want to remove an item from a specific panel then find the panel first and then call `removeItem()`.
     *
     * @param key The key of the item to remove.
     */
    removeItem(key) {
        const item = this.findItem(key);
        if (!item) {
            return;
        }
        const parents = this.getParents(key);
        parents.forEach((parent) => parent.removeChildItem(item));
    }
    /**
     * Moves a tab between panels or inside a panel
     *
     * @param fromPanelKey The id of the source panel of the item
     * @param toPanelKey The id of the target panel of the item
     * @param itemKey The key of the item
     * @param toIndex The index to which add the item. Default as the last.
     */
    moveItem(fromPanelKey, toPanelKey, itemKey, opts) {
        const singlePanel = fromPanelKey === toPanelKey;
        const from = this.findPanel(fromPanelKey);
        if (!from) {
            throw new Error(`Source layout panel not found.`);
        }
        if (singlePanel) {
            from.moveItem(itemKey, opts);
        }
        else {
            const to = this.findPanel(toPanelKey);
            if (!to) {
                throw new Error(`Target layout panel not found.`);
            }
            const removed = from.removeItem(itemKey);
            if (!removed) {
                throw new Error(`Item not found.`);
            }
            to.addItem(removed, opts);
        }
        this.notifyChange();
    }
    /**
     * Iterates over panels from the root.
     * This iterates the panels structure in order defined in the `items` array on
     * each panel.
     *
     * @param parentPanel The parent SplitLayout to start the iteration from.
     */
    *panelIterator(parentPanel) {
        const root = parentPanel || this;
        const { items } = root;
        for (const info of items) {
            if (info.type === LayoutType.panel) {
                const panel = this.findPanel(info.key);
                if (panel) {
                    yield panel;
                    for (const result of this.panelIterator(panel)) {
                        yield result;
                    }
                }
            }
        }
    }
    /**
     * Iterates over items that have specific `parent` property set.
     *
     * This is useful to close multiple object that belong to a parent.
     *
     * @param key The key of the `parent` property of a `SplitItem` to include in the iteration
     * @param parentPanel The `SplitPanel` to start the iteration from.
     */
    *parentItemsIterator(key, parentPanel) {
        const root = parentPanel || this;
        const { items } = root;
        for (const info of items) {
            const def = this.definitions.get(info.key);
            if (!def) {
                continue;
            }
            if (def.type === LayoutType.item && def.value.parent === key) {
                yield def.value;
            }
            if (def.type === LayoutType.panel) {
                for (const result of this.parentItemsIterator(key, def.value)) {
                    yield result;
                }
            }
        }
    }
    /**
     * Finds all items in all panels that have specified parent.
     * @param parentValue The key of the parent to search for.
     */
    removeByParent(parentValue) {
        for (const item of this.parentItemsIterator(parentValue)) {
            this.removeItem(item.key);
        }
    }
    /**
     * Checks whether the item with the given key is already managed by the manager.
     * @param itemKey The key of the item to check
     * @returns true if the item is in the definitions.
     */
    hasItem(itemKey) {
        const def = this.definitions.get(itemKey);
        if (!def) {
            return false;
        }
        return def.type === LayoutType.item;
    }
    /**
     * Selects an item in a panel.
     *
     * When the parent is give then it tries to select the item on that panel.
     * Otherwise it checks whether the item is in the current panel and if so it selects it.
     * Otherwise, it selects the item on a first panel the item is in.
     *
     * @param itemKey The key of the item to set selected in the view.
     * @param panelKey The optional panel key. When not set it finds the first panel the item is on.
     */
    selectItem(itemKey, panelKey) {
        const { activePanel } = this;
        let panel;
        if (panelKey) {
            // specified panel
            panel = this.findPanel(panelKey);
        }
        else if (activePanel?.hasItem(itemKey)) {
            // try the current active panel
            panel = activePanel;
        }
        else {
            // any panel that has the item
            ;
            [panel] = this.getParents(itemKey);
        }
        if (!panel) {
            return;
        }
        panel.selected = itemKey;
        this.updateView(panel.key);
        this.notifyChange();
    }
    /**
     * Checks whether the manager has at least one panel that has items.
     *
     * This is useful to render the empty state when no items are added.
     */
    isEmpty() {
        for (const panel of this.panelIterator()) {
            if (panel.hasItems) {
                return false;
            }
        }
        return true;
    }
    render(itemCallback) {
        const result = [];
        const { items } = this;
        for (const info of items) {
            const panel = this.findPanel(info.key);
            const content = panel?.render(itemCallback);
            if (content) {
                result.push(content);
            }
        }
        return result;
    }
}
//# sourceMappingURL=SplitLayout.js.map