import { html } from 'lit';
import { nanoid } from 'nanoid';
import { SplitItem } from './SplitItem.js';
import { LayoutType, PanelState, SplitCloseDirection, SplitDirection, SplitPanelTarget, SplitRegion, } from './type.js';
import missingIndexes from '../../lib/MissingIndexes.js';
import '../../define/layout/split-view.js';
/**
 * The class represents a panel model for the split layout.
 * A panel can only hold other panels or layout items. When a panel
 * defines both, items are ignored.
 *
 * A panel can render ony one item at a time. TO view more items
 * the application needs to add panels to this panel and distribute items among them.
 */
export class SplitPanel {
    manager;
    /**
     * The key used as an internal reference to the definitions map.
     */
    key = nanoid();
    layout = SplitDirection.horizontal;
    items = [];
    /**
     * The item being rendered in the panel.
     * Note, this is the `key` of the item and not the `id` of the item.
     */
    selected;
    /**
     * The current state of the panel.
     */
    state = PanelState.idle;
    get hasPanels() {
        return this.items.some((i) => i.type === LayoutType.panel);
    }
    get hasItems() {
        return this.items.some((i) => i.type === LayoutType.item);
    }
    static fromLayout(manager, layout = SplitDirection.horizontal) {
        const schema = {
            layout,
        };
        return new SplitPanel(manager, schema);
    }
    /**
     * @param manager A reference to the manager.
     * @param parentPanel A reference to a parent panel.
     */
    constructor(manager, schema) {
        this.manager = manager;
        if (schema) {
            this.new(schema);
        }
    }
    new(schema) {
        const { key = nanoid(), layout = SplitDirection.horizontal, items, selected } = schema;
        this.key = key;
        this.layout = layout;
        if (Array.isArray(items)) {
            this.items = items.map((i) => ({ ...i }));
        }
        else {
            this.items = [];
        }
        if (selected) {
            this.selected = selected;
        }
        else {
            this.selected = undefined;
        }
        this.state = PanelState.idle;
    }
    toJSON() {
        const result = {
            layout: this.layout,
            key: this.key,
        };
        if (Array.isArray(this.items) && this.items.length) {
            result.items = this.items.map((i) => ({ ...i }));
        }
        if (this.selected) {
            result.selected = this.selected;
        }
        return result;
    }
    getParent() {
        return this.manager.getParents(this.key)[0];
    }
    /**
     * @returns True when the panel accepts drop events.
     */
    canDrop() {
        return !!this.items.length;
    }
    /**
     * @returns Returns a **copy** of the items array sorted by index.
     */
    sortedItems() {
        const { items, manager } = this;
        const splitItems = [];
        items.forEach((i) => {
            if (i.type === LayoutType.item) {
                const value = manager.findItem(i.key);
                if (value) {
                    splitItems.push(value);
                }
            }
        });
        return [...splitItems].sort((a, b) => (a.index || 0) - (b.index || 0));
    }
    /**
     * Finds an index where to put an item.
     * This finds "holes" in items where an index is not set for a given position.
     * In this case the resulting index is the missing index.
     * Otherwise it returns next after the highest index.
     *
     * @returns The index of the next item
     */
    nextIndex() {
        const { items, manager } = this;
        if (!items.length) {
            return 0;
        }
        const set = new Set();
        for (const item of items) {
            const value = manager.findItem(item.key);
            if (!value) {
                continue;
            }
            const { index = 0 } = value;
            set.add(index);
        }
        const sorted = [...set].sort();
        const missing = missingIndexes(sorted);
        if (missing.length) {
            return missing[0];
        }
        const max = sorted.pop();
        return max + 1;
    }
    /**
     * Checks whether this panel contains an item
     * @param key The key of the item to test.
     * @returns true if the panel contains an item
     */
    hasItem(key) {
        return this.items.some((i) => i.key === key);
    }
    /**
     * Adds an item to the layout.
     *
     * @param item The item to add
     * @param opts Layout adding item options
     * @returns Whether a new item was added to the layout. false when the item is already in the layout panel.
     */
    addItem(item, opts = {}) {
        const { region = SplitRegion.center } = opts;
        const hasItem = region === SplitRegion.center && this.hasItem(item.key);
        if (hasItem) {
            if (item.key !== this.selected) {
                this.selected = item.key;
                this.manager.notifyChange();
                this.updateView();
            }
            return this.manager.findItem(item.key);
        }
        const hasOptionIndex = typeof opts.index === 'number';
        const hasItemIndex = typeof item.index === 'number';
        const hasIndex = hasOptionIndex || hasItemIndex;
        let index;
        if (hasOptionIndex) {
            index = opts.index;
        }
        else if (hasItemIndex) {
            index = item.index;
        }
        else {
            index = this.nextIndex();
        }
        if (region === SplitRegion.center || (!this.hasItems && !this.hasPanels)) {
            if (hasIndex) {
                this.increaseIndex(index); //  + 1
            }
            const instance = item.toJSON ? item : new SplitItem(this.manager, item);
            instance.index = index;
            this.items.push({
                type: LayoutType.item,
                key: item.key,
            });
            this.manager.definitions.set(item.key, {
                type: LayoutType.item,
                value: instance,
            });
            // this.manager.dispatchNameItem(instance); ???? it's a fresh definition, should have a current name.
            this.selected = item.key;
            this.manager.notifyChange();
            this.updateView();
            return instance;
        }
        const panel = this.splitByRegion(region);
        // we don't call request update here because
        // the app has to redo the view rendering first as
        // the panel purpose has changed.
        return panel.addItem(item, { index });
    }
    splitByRegion(region) {
        let panel;
        if (region === 'east') {
            ;
            [, panel] = this.split({
                layout: SplitDirection.horizontal,
                itemsTarget: SplitPanelTarget.first,
                noSideEffects: true,
            });
        }
        else if (region === 'west') {
            ;
            [panel] = this.split({
                layout: SplitDirection.horizontal,
                itemsTarget: SplitPanelTarget.other,
                noSideEffects: true,
            });
        }
        else if (region === 'south') {
            ;
            [, panel] = this.split({
                layout: SplitDirection.vertical,
                itemsTarget: SplitPanelTarget.first,
                noSideEffects: true,
            });
        }
        else {
            ;
            [panel] = this.split({
                layout: SplitDirection.vertical,
                itemsTarget: SplitPanelTarget.other,
                noSideEffects: true,
            });
        }
        return panel;
    }
    /**
     * Splits this panel into 2 panels.
     * This to be used when the panel has no other panels. Only items are allowed.
     * It produces 2 new panels and moves the items to the first one leaving the other one available.
     */
    split(opts = {}) {
        const { layout = SplitDirection.horizontal, itemsTarget = SplitPanelTarget.first } = opts;
        if (this.hasPanels) {
            throw new Error(`Invalid state. Panels can be split only when containing items only.`);
        }
        this.layout = layout;
        const { items, selected } = this;
        this.items = [];
        this.selected = undefined;
        const p1 = this.addPanel();
        const p2 = this.addPanel();
        // const p1 = new SplitPanel(this.manager);
        // const p2 = new SplitPanel(this.manager);
        // this.manager.definitions.set(p1.key, {
        //   type: LayoutType.panel,
        //   value: p1,
        // });
        // this.manager.definitions.set(p2.key, {
        //   type: LayoutType.panel,
        //   value: p2,
        // });
        // this.items.push({
        //   type: LayoutType.panel,
        //   key: p1.key,
        // });
        // this.items.push({
        //   type: LayoutType.panel,
        //   key: p2.key,
        // });
        const panel = itemsTarget === SplitPanelTarget.first ? p1 : p2;
        panel.items = items;
        panel.selected = selected;
        if (!opts.noSideEffects) {
            this.manager.notifyChange();
        }
        return [p1, p2];
    }
    unsplit() {
        const panelItem = this.items.find((i) => i.type === LayoutType.panel);
        if (!panelItem) {
            throw new Error('Invalid state. This panel has no panels to unshift.');
        }
        const panel = this.manager.findPanel(panelItem.key);
        if (!panel) {
            throw new Error('Invalid state. This panel is not managed by the SplitManager.');
        }
        const { items, selected } = panel;
        this.items = items;
        this.selected = selected;
        this.manager.notifyChange();
    }
    /**
     * Decreases items index by 1 to all items with index at least equal to `fromIndex`.
     * @param fromIndex The minimal index to affect.
     */
    decreaseIndex(fromIndex) {
        for (const item of this.items) {
            if (item.type !== LayoutType.item) {
                continue;
            }
            const definition = this.manager.findItem(item.key);
            if (!definition) {
                continue;
            }
            if (!definition.index) {
                definition.index = 0;
            }
            const { index } = definition;
            if (index >= fromIndex && index > 0) {
                definition.index -= 1;
            }
        }
    }
    /**
     * Increases items index by 1 to all items with index at least equal to `fromIndex`.
     * @param fromIndex The minimal index to affect.
     */
    increaseIndex(fromIndex) {
        for (const item of this.items) {
            if (item.type !== LayoutType.item) {
                continue;
            }
            const definition = this.manager.findItem(item.key);
            if (!definition) {
                continue;
            }
            if (!definition.index) {
                definition.index = 0;
            }
            const { index } = definition;
            if (index >= fromIndex) {
                definition.index += 1;
            }
        }
    }
    removeChildItem(item, opts = {}) {
        const index = this.items.findIndex((i) => i.key === item.key);
        if (index < 0) {
            return undefined;
        }
        const itemInfo = this.items[index];
        this.items.splice(index, 1);
        this.decreaseIndex(item.index || 0);
        const parents = this.manager.getParents(item.key);
        if (!parents.length) {
            // remove the definition since no other panel have this item.
            this.manager.definitions.delete(itemInfo.key);
        }
        if (this.items.length === 0) {
            // remove panel (self)
            const parent = this.getParent();
            if (parent) {
                parent.removePanel(this.key);
                // Note, ignore `opts.noSideEffects` for tab close. It won't get dispatched elsewhere.
                this.manager.notifyTabClose(item.key, this.key);
                // no point doing the stuff below as the whole panel is removed.
                return item;
            }
        }
        if (this.selected === item.key) {
            let nextKey;
            if (this.items[index]) {
                nextKey = this.items[index].key;
            }
            else if (this.items[index - 1]) {
                nextKey = this.items[index - 1].key;
            }
            else if (this.items.length) {
                const [other] = this.items;
                nextKey = other.key;
            }
            this.selected = nextKey;
        }
        if (!opts.noSideEffects) {
            this.manager.notifyTabClose(item.key, this.key);
            this.manager.notifyChange();
            this.manager.updateView(this.key);
        }
        return item;
    }
    /**
     * Removes an item from the layout
     * @param key The `key` of the item.
     * @returns The removed item, if any.
     */
    removeItem(key, opts) {
        const removed = this.manager.findItem(key);
        if (!removed) {
            return undefined;
        }
        return this.removeChildItem(removed, opts);
        // const index = this.items.findIndex(i => i.key === key);
        // if (index < 0) {
        //   return undefined;
        // }
        // const itemInfo = this.items[index];
        // this.items.splice(index, 1);
        // this.manager.definitions.delete(itemInfo.key);
        // this.decreaseIndex(removed.index || 0);
        // if (this.items.length === 0) {
        //   // remove panel (self)
        //   const parent = this.getParent();
        //   if (parent) {
        //     parent.removePanel(this.key);
        //     // Note, ignore `opts.noSideEffects` for tab close. It won't get dispatched elsewhere.
        //     this.manager.notifyTabClose(key);
        //     // no point doing the stuff below as the whole panel is removed.
        //     return removed;
        //   }
        // }
        // if (this.selected === key) {
        //   let nextKey: string | undefined;
        //   if (this.items[index]) {
        //     nextKey = this.items[index].key;
        //   } else if (this.items[index - 1]) {
        //     nextKey = this.items[index - 1].key;
        //   } else if (this.items.length) {
        //     const [item] = this.items;
        //     nextKey = item.key;
        //   }
        //   this.selected = nextKey;
        // }
        // if (!opts.noSideEffects) {
        //   this.manager.notifyTabClose(key);
        //   this.manager.notifyChange();
        //   this.manager.updateView(this.key);
        // }
        // return removed;
    }
    /**
     * @param key The key of the item to perform a relative operation from.
     * @param dir The direction to which close other items. Default to both directions leaving only the `key` item
     */
    removeRelative(key, dir = SplitCloseDirection.both) {
        const items = this.sortedItems();
        const index = items.findIndex((i) => i.key === key);
        if (index < 0) {
            return;
        }
        const item = items[index];
        let removed = [];
        if (dir === SplitCloseDirection.both) {
            removed = items.filter((i) => i !== item).map((i) => i.key);
        }
        else if (dir === SplitCloseDirection.left) {
            removed = items.slice(0, index).map((i) => i.key);
        }
        else {
            removed = this.items.slice(index + 1).map((i) => i.key);
        }
        this.items = this.items.filter((i) => !removed.includes(i.key));
        this.selected = item.key;
        removed.forEach((i) => {
            const parents = this.manager.getParents(i);
            if (!parents.length) {
                this.manager.definitions.delete(i);
            }
            this.manager.notifyTabClose(i, this.key);
        });
        this.manager.notifyChange();
        this.manager.updateView(this.key);
    }
    removePanel(id, opts = {}) {
        const index = this.items.findIndex((p) => p.key === id);
        if (index < 0) {
            return;
        }
        const item = this.items[index];
        const panel = this.manager.findPanel(item.key);
        panel.clearItems();
        this.items.splice(index, 1);
        this.manager.definitions.delete(item.key);
        if (this.items.length === 1) {
            this.unsplit();
        }
        if (!opts.noSideEffects) {
            this.manager.notifyChange();
            this.manager.updateView(this.key);
        }
    }
    clearItems(opts) {
        const { items } = this;
        items.forEach((childItem) => {
            if (childItem.type === LayoutType.item) {
                this.removeItem(childItem.key, opts);
            }
            else {
                this.removePanel(childItem.key, opts);
            }
        });
    }
    /**
     * Moves an item to a new index.
     *
     * @param key The item key
     * @param toIndex The new index. When not set it moves the item to the end.
     */
    moveItem(key, opts = {}) {
        const info = this.items.find((i) => i.key === key);
        if (!info) {
            return;
        }
        const item = this.manager.findItem(info.key);
        if (!item) {
            return;
        }
        const { index, region } = opts;
        if (region) {
            this.moveToRegion(item, region);
        }
        else if (typeof index === 'number') {
            this.moveToIndex(item, index);
        }
        else {
            this.moveToEnd(item);
        }
    }
    moveToRegion(item, region) {
        if (this.items.length < 2) {
            // nothing to split.
            return;
        }
        if (region === SplitRegion.center) {
            // the item is already there.
            return;
        }
        const itemsIndex = this.items.findIndex((i) => i.key === item.key);
        if (itemsIndex < 0) {
            return;
        }
        this.items.splice(itemsIndex, 1);
        const index = typeof item.index === 'number' ? item.index : this.nextIndex();
        const panel = this.splitByRegion(region);
        panel.addItem(item, { index });
    }
    moveToIndex(item, index) {
        if (item.index === index) {
            return;
        }
        const hasTargetAtTarget = !!this.items[index];
        this.decreaseIndex(item.index);
        if (hasTargetAtTarget) {
            this.increaseIndex(index);
        }
        item.index = index;
        this.manager.notifyChange();
        this.manager.updateView(this.key);
    }
    moveToEnd(item) {
        if (item.index !== undefined) {
            this.decreaseIndex(item.index);
        }
        item.index = this.nextIndex();
        this.manager.notifyChange();
        this.manager.updateView(this.key);
    }
    updateView() {
        const layout = this.manager.findView(this.key);
        if (!layout) {
            return;
        }
        layout.requestUpdate();
    }
    addPanel(layout = SplitDirection.horizontal) {
        return this.manager.addPanel({ layout, parent: this.key });
    }
    panelTemplate(panel, itemCallback) {
        const { layout, items, selected, manager } = panel;
        const renderPanels = items.filter((i) => i.type === LayoutType.panel);
        const renderItems = items.filter((i) => i.type === LayoutType.item);
        if (!renderPanels.length && !renderItems.length) {
            return undefined;
        }
        const { dragTypes, constrain } = manager.options;
        let content;
        if (renderPanels.length) {
            content = renderPanels.map((item) => {
                const targetPanel = manager.findPanel(item.key);
                return targetPanel && this.panelTemplate(targetPanel, itemCallback);
            });
        }
        else {
            content = renderItems.map((item) => {
                const targetItem = manager.findItem(item.key);
                return targetItem && this.itemTemplate(targetItem, targetItem.key === selected, itemCallback);
            });
        }
        const valid = content.filter((i) => !!i);
        if (!valid.length) {
            return undefined;
        }
        return html `
      <split-view
        layout="${layout}"
        .dragTypes="${dragTypes}"
        .panel="${panel}"
        .key="${panel.key}"
        ?constrain="${constrain}"
      >
        ${content}
      </split-view>
    `;
    }
    itemTemplate(item, visible, itemCallback) {
        return itemCallback(item, visible);
    }
    render(itemCallback) {
        return this.panelTemplate(this, itemCallback);
    }
}
//# sourceMappingURL=SplitPanel.js.map