import { action, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import ResizeObserver from 'resize-observer-polyfill';
import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, DivWidth, incrementTitleCopy, returnTrue, UpdateIcon } from '../../../ClientUtils';
import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc';
import { AclAdmin, AclEdit, DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
import { FieldType } from '../../../fields/ObjectField';
import { ImageCast, NumCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { GetEffectiveAcl, inheritParentAcls, SetPropSetterCb } from '../../../fields/util';
import { emptyFunction } from '../../../Utils';
import { DocServer } from '../../DocServer';
import { Docs } from '../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
import * as GoldenLayout from '../../goldenLayout';
import { DragManager } from '../../util/DragManager';
import { InteractionUtils } from '../../util/InteractionUtils';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SnappingManager } from '../../util/SnappingManager';
import { undoable, undoBatch, UndoManager } from '../../util/UndoManager';
import { DashboardView } from '../DashboardView';
import { DocumentView } from '../nodes/DocumentView';
import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
import { OverlayView } from '../OverlayView';
import { ScriptingRepl } from '../ScriptingRepl';
import { UndoStack } from '../UndoStack';
import './CollectionDockingView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
import { TabDocView, TabHTMLElement } from './TabDocView';

@observer
export class CollectionDockingView extends CollectionSubView() {
    static tabClass?: typeof TabDocView;
    /**
     * Initialize by assigning the add split method to DocumentView and by
     * configuring golden layout to render its documents using the specified React component
     * @param ele - typically would be set to TabDocView
     */
    public static Init(ele: typeof TabDocView) {
        this.tabClass = ele;
        DocumentView.addSplit = CollectionDockingView.AddSplit;
    }
    // eslint-disable-next-line no-use-before-define
    @observable public static Instance: CollectionDockingView | undefined = undefined;

    private _reactionDisposer?: IReactionDisposer;
    private _lightboxReactionDisposer?: IReactionDisposer;
    private _containerRef = React.createRef<HTMLDivElement>();
    private _flush: UndoManager.Batch | undefined;
    private _unmounting = false;
    private _ignoreStateChange = '';
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    private _goldenLayout: any = null;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    public tabMap: Set<any> = new Set();
    public get HasFullScreen() {
        return this._goldenLayout._maximisedItem !== null;
    }
    static _highlightStyleSheet = addStyleSheet();

    constructor(props: SubCollectionViewProps) {
        super(props);
        makeObservable(this);
        if (this._props.renderDepth < 0) CollectionDockingView.Instance = this;
        // Why is this here?
        (window as unknown as { React: unknown }).React = React;
        (window as unknown as { ReactDOM: unknown }).ReactDOM = ReactDOM;
        DragManager.StartWindowDrag = this.StartOtherDrag;
        this.Document.myTrails; // this is equivalent to having a prefetchProxy for myTrails which is needed for the My Trails button in the UI which assumes that Doc.ActiveDashboard.myTrails is legit...
    }

    /**
     * Switches from dragging a document around a freeform canvas to dragging it as a tab to be docked.
     *
     * @param e fake mouse down event position data containing pageX and pageY coordinates
     * @param dragDocs the documents to be dragged
     * @param batch optionally an undo batch that has been started to use instead of starting a new batch
     */
    public StartOtherDrag = (e: { pageX: number; pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => {
        this._flush = this._flush ?? UndoManager.StartBatch('golden layout drag');
        const config = dragDocs.length === 1 ? DashboardView.makeDocumentConfig(dragDocs[0]) : { type: 'row', content: dragDocs.map(doc => DashboardView.makeDocumentConfig(doc)) };
        const dragSource = CollectionDockingView.Instance?._goldenLayout.createDragSource(document.createElement('div'), config);
        this.tabDragStart(dragSource, finishDrag);
        dragSource._dragListener.onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 });
        return true;
    };

    tabItemDropped = () => DragManager.CompleteWindowDrag?.(false);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    tabDragStart = (proxy: any, finishDrag?: (aborted: boolean) => void) => {
        this._flush = this._flush ?? UndoManager.StartBatch('tab move');
        //const dashDoc = proxy?._contentItem?.tab?.DashDoc as Doc;
        //dashDoc && (DragManager.DocDragData = new DragManager.DocumentDragData([proxy._contentItem.tab.DashDoc]));
        DragManager.CompleteWindowDrag = (aborted: boolean) => {
            if (aborted) {
                proxy._dragListener.AbortDrag();
                if (this._flush) {
                    this._flush.cancel(); // cancel the undo change being logged
                    this.setupGoldenLayout(); // restore golden layout to where it was before the drag (this is a no-op when using StartOtherDrag because the proxy dragged item was never in the golden layout)
                }
                DragManager.CompleteWindowDrag = undefined;
            }
            finishDrag?.(aborted);
            setTimeout(this.endUndoBatch, 100);
        };
    };
    @undoBatch
    public CloseFullScreen = () => {
        this._goldenLayout._maximisedItem?.toggleMaximise();
        this.stateChanged();
    };

    @undoBatch
    public static CloseSplit(document: Opt<Doc>, panelName?: string): boolean {
        if (CollectionDockingView.Instance) {
            const tab = Array.from(CollectionDockingView.Instance.tabMap.keys()).find(tabView => (panelName ? tabView.contentItem.config.props.panelName === panelName : tabView.DashDoc === document));
            if (tab) {
                const j = tab.header.parent.contentItems.indexOf(tab.contentItem);
                if (j !== -1) {
                    tab.header.parent.contentItems[j].remove();
                    CollectionDockingView.Instance.endUndoBatch();
                    return CollectionDockingView.Instance.layoutChanged();
                }
            }
        }

        return false;
    }

    @undoBatch
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    public static ReplaceTab(document: Doc, mods: OpenWhereMod, stack: any, panelName: string, addToSplit?: boolean, keyValue?: boolean): boolean {
        const instance = CollectionDockingView.Instance;
        if (!instance) return false;
        const newConfig = DashboardView.makeDocumentConfig(document, panelName, undefined, keyValue);
        if (!panelName && stack) {
            const activeContentItemIndex = stack.contentItems.findIndex((item: { config: unknown }) => item.config === stack._activeContentItem.config);
            const newContentItem = stack.layoutManager.createContentItem(newConfig, instance._goldenLayout);
            stack.addChild(newContentItem.contentItems[0], undefined);
            stack.contentItems[activeContentItemIndex].remove();
            return instance.layoutChanged();
        }
        const tab = Array.from(instance.tabMap.keys()).find(tabView => tabView.contentItem.config.props.panelName === panelName);
        if (tab) {
            const j = tab.header.parent.contentItems.indexOf(tab.contentItem);
            if (newConfig.props.documentId !== tab.header.parent.contentItems[j].config.props.documentId) {
                tab.header.parent.addChild(newConfig, undefined);
                !addToSplit && j !== -1 && tab.header.parent.contentItems[j].remove();
                return instance.layoutChanged();
            }
            return false;
        }
        return CollectionDockingView.AddSplit(document, mods, stack, panelName);
    }

    @undoBatch
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    public static ToggleSplit(doc: Doc, location: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) {
        return Array.from(CollectionDockingView.Instance?.tabMap.keys() ?? []).findIndex(tab => tab.DashDoc === doc) !== -1 ? CollectionDockingView.CloseSplit(doc) : CollectionDockingView.AddSplit(doc, location, stack, panelName, keyValue);
    }

    //
    //  Creates a split on any side of the docking view based on the passed input pullSide and then adds the Document to the requested side
    //
    @action
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) {
        if (document?._type_collection === CollectionViewType.Docking && !keyValue) return DashboardView.openDashboard(document);
        if (!CollectionDockingView.Instance) return false;
        const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tabView => tabView.DashDoc === document && !tabView.contentItem.config.props.keyValue && !keyValue);
        if (tab) {
            tab.header.parent.setActiveContentItem(tab.contentItem);
            return true;
        }
        const instance = CollectionDockingView.Instance;
        const glayRoot = instance._goldenLayout.root;
        if (!instance) return false;
        const docContentConfig = DashboardView.makeDocumentConfig(document, panelName, undefined, keyValue);

        CollectionDockingView.Instance._flush = CollectionDockingView.Instance._flush ?? UndoManager.StartBatch('Add Split');
        setTimeout(CollectionDockingView.Instance.endUndoBatch, 100);
        if (!pullSide && stack) {
            stack.addChild(docContentConfig, undefined);
            setTimeout(() => stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]));
        } else {
            const newContentItem = () => {
                const newItem = glayRoot.layoutManager.createContentItem({ type: 'stack', content: [docContentConfig] }, instance._goldenLayout);
                newItem.callDownwards('_$init');
                return newItem;
            };
            if (glayRoot.contentItems.length === 0) {
                // if no rows / columns
                glayRoot.addChild(newContentItem());
            } else if (glayRoot.contentItems[0].isStack) {
                glayRoot.contentItems[0].addChild(docContentConfig);
            } else if (glayRoot.contentItems.length === 1 && glayRoot.contentItems[0].contentItems.length === 1 && glayRoot.contentItems[0].contentItems[0].contentItems.length === 0) {
                glayRoot.contentItems[0].contentItems[0].addChild(docContentConfig);
            } else if (instance._goldenLayout.root.contentItems[0].isRow) {
                // if row
                switch (pullSide) {
                    default:
                    case OpenWhereMod.none:
                    case OpenWhereMod.right:
                        glayRoot.contentItems[0].addChild(newContentItem());
                        break;
                    case OpenWhereMod.left:
                        glayRoot.contentItems[0].addChild(newContentItem(), 0);
                        break;
                    case OpenWhereMod.top:
                    case OpenWhereMod.bottom: {
                        // if not going in a row layout, must add already existing content into column
                        const rowlayout = glayRoot.contentItems[0];
                        const newColumn = rowlayout.layoutManager.createContentItem({ type: 'column' }, instance._goldenLayout);

                        const newItem = newContentItem();
                        instance._goldenLayout.saveScrollTops(rowlayout.element);
                        rowlayout.parent.replaceChild(rowlayout, newColumn);
                        if (pullSide === 'top') {
                            newColumn.addChild(rowlayout, undefined, true);
                            newColumn.addChild(newItem, 0, true);
                        } else if (pullSide === 'bottom') {
                            newColumn.addChild(newItem, undefined, true);
                            newColumn.addChild(rowlayout, 0, true);
                        }
                        instance._goldenLayout.restoreScrollTops(rowlayout.element);

                        rowlayout.config.height = 50;
                        newItem.config.height = 50;
                    }
                }
            } else {
                // if (instance._goldenLayout.root.contentItems[0].isColumn) { // if column
                switch (pullSide) {
                    case 'top':
                        glayRoot.contentItems[0].addChild(newContentItem(), 0);
                        break;
                    case 'bottom':
                        glayRoot.contentItems[0].addChild(newContentItem());
                        break;
                    case 'left':
                    case 'right':
                    default: {
                        // if not going in a row layout, must add already existing content into column
                        const collayout = glayRoot.contentItems[0];
                        const newRow = collayout.layoutManager.createContentItem({ type: 'row' }, instance._goldenLayout);

                        const newItem = newContentItem();
                        instance._goldenLayout.saveScrollTops(collayout.element);
                        collayout.parent.replaceChild(collayout, newRow);
                        if (pullSide === 'left') {
                            newRow.addChild(collayout, undefined, true);
                            newRow.addChild(newItem, 0, true);
                        } else {
                            newRow.addChild(newItem, undefined, true);
                            newRow.addChild(collayout, 0, true);
                        }
                        instance._goldenLayout.restoreScrollTops(collayout.element);

                        collayout.config.width = 50;
                        newItem.config.width = 50;
                    }
                }
            }
            instance._ignoreStateChange = JSON.stringify(instance._goldenLayout.toConfig());
        }

        return instance.layoutChanged();
    }

    @undoBatch
    @action
    layoutChanged() {
        this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
        this._goldenLayout.emit('stateChanged');
        this.stateChanged();
        return true;
    }
    setupGoldenLayout = async () => {
        if (this._unmounting) return;
        // const config = StrCast(this.Document.dockingConfig, JSON.stringify(DashboardView.resetDashboard(this.Document)));
        const config = StrCast(this.Document.dockingConfig);
        if (config) {
            const matches = config.match(/"documentId":"[a-z0-9-]+"/g);
            const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', '')) ?? [];

            await Promise.all(docids.map(id => DocServer.GetRefField(id)));

            if (this._goldenLayout) {
                if (config === JSON.stringify(this._goldenLayout.toConfig())) {
                    return;
                }
                try {
                    this._goldenLayout.unbind('tabCreated', this.tabCreated);
                    this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
                    this._goldenLayout.unbind('stackCreated', this.stackCreated);
                } catch {
                    /* empty */
                }
                this.tabMap.clear();
                this._goldenLayout.destroy();
            }
            const glay = (this._goldenLayout = new GoldenLayout(JSON.parse(config)));
            glay.on('tabCreated', this.tabCreated);
            glay.on('tabDestroyed', this.tabDestroyed);
            glay.on('stackCreated', this.stackCreated);
            glay.registerComponent('DocumentFrameRenderer', CollectionDockingView.tabClass);
            glay.container = this._containerRef.current;
            glay.init();
            glay.root.layoutManager.on('itemDropped', this.tabItemDropped);
            glay.root.layoutManager.on('dragStart', this.tabDragStart);
            glay.root.layoutManager.on('activeContentItemChanged', this.stateChanged);
        } else {
            console.log('ERROR: no config for dashboard!!');
        }
    };

    /**
     * This publishes Docs having titles starting with '@' to Doc.myPublishedDocs
     * Once published, any text that uses the 'title' in its body will automatically
     * be linked to this published document.
     * @param target
     * @param title
     */
    titleChanged = (target: Doc, value: FieldType) => {
        const title = Field.toString(value);
        if (title.startsWith('@') && !title.substring(1).match(/[()[\]@]/) && title.length > 1) {
            const embedding = DocListCast(target.proto_embeddings).lastElement();
            embedding && Doc.AddToMyPublished(embedding);
        } else if (!title.startsWith('@')) {
            DocListCast(target.proto_embeddings).forEach(doc => Doc.RemFromMyPublished(doc));
        }
    };

    componentDidMount: () => void = async () => {
        this._props.setContentViewBox?.(this);
        this._unmounting = false;
        SetPropSetterCb('title', this.titleChanged); // this overrides any previously assigned callback for the property
        if (this._containerRef.current) {
            this._lightboxReactionDisposer = reaction(
                () => DocumentView.LightboxDoc(),
                doc => setTimeout(() => !doc && this.onResize())
            );
            new ResizeObserver(this.onResize).observe(this._containerRef.current);
            this._reactionDisposer = reaction(
                () => StrCast(this.Document.dockingConfig),
                config => {
                    if (!this._goldenLayout || this._ignoreStateChange !== config) {
                        //  bcz: TODO! really need to diff config with ignoreStateChange and modify the current goldenLayout instead of building a new one.
                        this.setupGoldenLayout();
                    }
                    this._ignoreStateChange = '';
                }
            );
            reaction(
                () => this._props.PanelWidth(),
                width => !this._goldenLayout && width > 20 && setTimeout(() => this.setupGoldenLayout()), // need to wait for the collectiondockingview-container to have it's width/height since golden layout reads that to configure its windows
                { fireImmediately: true }
            );
            reaction(
                () => [SnappingManager.userBackgroundColor, SnappingManager.userBackgroundColor],
                () => {
                    clearStyleSheetRules(CollectionDockingView._highlightStyleSheet);
                    addStyleSheetRule(CollectionDockingView._highlightStyleSheet, 'lm_controls', { background: `${SnappingManager.userBackgroundColor} !important` });
                    addStyleSheetRule(CollectionDockingView._highlightStyleSheet, 'lm_controls', { color: `${SnappingManager.userColor} !important` });
                    addStyleSheetRule(SnappingManager.SettingsStyle, 'lm_header', { background: `${SnappingManager.userBackgroundColor} !important` });
                },
                { fireImmediately: true }
            );
        }
    };

    componentWillUnmount: () => void = () => {
        this._unmounting = true;
        try {
            this._goldenLayout.unbind('stackCreated', this.stackCreated);
            this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
        } catch {
            /* empty */
        }
        this._goldenLayout?.destroy();
        window.removeEventListener('resize', this.onResize);
        window.removeEventListener('mouseup', this.onPointerUp);

        this._reactionDisposer?.();
        this._lightboxReactionDisposer?.();
    };

    // ViewBoxInterface overrides
    override isUnstyledView = returnTrue;

    @action
    onResize = () => {
        const cur = this._containerRef.current;
        // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed
        !DocumentView.LightboxDoc() && cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height);
    };

    endUndoBatch = () => {
        const json = JSON.stringify(this._goldenLayout.toConfig());
        const matches = json.match(/"documentId":"[a-z0-9-]+"/g);
        const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', ''));
        const docs = !docids
            ? []
            : docids
                  .map(id => DocServer.GetCachedRefField(id))
                  .filter(f => f)
                  .map(f => f as Doc);
        const changesMade = this.Document.dockingConfig !== json;
        if (changesMade) {
            if (![AclAdmin, AclEdit].includes(GetEffectiveAcl(this.dataDoc))) {
                this.layoutDoc.dockingConfig = json;
                this.layoutDoc.data = new List<Doc>(docs);
            } else {
                Doc.SetInPlace(this.Document, 'dockingConfig', json, true);
                Doc.SetInPlace(this.Document, 'data', new List<Doc>(docs), true);
            }
        }
        this._flush?.end();
        this._flush = undefined;
    };

    @action
    onPointerUp = (): void => {
        window.removeEventListener('pointerup', this.onPointerUp);
        DragManager.CompleteWindowDrag = undefined;
        setTimeout(this.endUndoBatch, 100);
    };

    @action
    onPointerDown = (e: React.PointerEvent): void => {
        let hitFlyout = false;
        for (let par = e.target as HTMLElement | null; !hitFlyout && par; par = par.parentElement) {
            hitFlyout = par.className === 'dockingViewButtonSelector';
        }
        if (!hitFlyout) {
            const htmlTarget = e.target as HTMLElement;
            window.addEventListener('mouseup', this.onPointerUp);
            if (!htmlTarget.closest('*.lm_content') && (htmlTarget.closest('*.lm_tab') || htmlTarget.closest('*.lm_stack'))) {
                const className = typeof htmlTarget.className === 'string' ? htmlTarget.className : '';
                if (className.includes('lm_maximise')) {
                    // this._flush = UndoManager.StartBatch('tab maximize');
                } else {
                    const tabTarget = (e.target as HTMLElement)?.parentElement?.className.includes('lm_tab') ? (e.target as HTMLElement).parentElement : (e.target as HTMLElement);
                    const map = Array.from(this.tabMap).find(tab => tab.element[0] === tabTarget);
                    if (map?.DashDoc && DocumentView.getDocumentView(map.DashDoc, this.DocumentView?.())) {
                        DocumentView.SelectView(DocumentView.getDocumentView(map.DashDoc, this.DocumentView?.()), false);
                    }
                }
            }
        }
        if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && Doc.ActiveTool !== InkTool.Ink) {
            e.stopPropagation();
        }
    };

    public CaptureThumbnail() {
        const content = this.DocumentView?.()?.ContentDiv;
        if (content) {
            const _width = DivWidth(content);
            const _height = DivHeight(content);
            return UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', iconFile => {
                const proto = this.dataDoc; // Cast(img.proto, Doc, null)!;
                proto.thumb_nativeWidth = _width;
                proto.thumb_nativeHeight = _height;
                proto.thumb = new ImageField(iconFile);
            });
        }
        return undefined;
    }
    public static async TakeSnapshot(doc: Doc | undefined, clone = false) {
        if (!doc) return undefined;
        let json = StrCast(doc.dockingConfig);
        if (clone) {
            const cloned = await Doc.MakeClone(doc);
            Array.from(cloned.map.entries()).forEach(entry => {
                json = json.replace(entry[0], entry[1][Id]);
            });
            cloned.clone[DocData].dockingConfig = json;
            return DashboardView.openDashboard(cloned.clone);
        }
        const matches = json.match(/"documentId":"[a-z0-9-]+"/g);
        const origtabids = matches?.map(m => m.replace('"documentId":"', '').replace('"', '')) || [];
        const origtabs = origtabids
            .map(id => DocServer.GetCachedRefField(id))
            .filter(f => f)
            .map(f => f as Doc);
        const newtabs = origtabs.map(origtab => {
            const origtabdocs = DocListCast(origtab.data);
            const newtab = origtabdocs.length ? Doc.MakeCopy(origtab, true, undefined, true) : Doc.MakeEmbedding(origtab);
            const newtabdocs = origtabdocs.map(origtabdoc => Doc.MakeEmbedding(origtabdoc));
            if (newtabdocs.length) {
                newtab[DocData].data = new List<Doc>(newtabdocs);
                newtabdocs.forEach(ntab => Doc.SetContainer(ntab, newtab));
            }
            json = json.replace(origtab[Id], newtab[Id]);
            return newtab;
        });
        const dashboardDoc = Docs.Create.DockDocument(newtabs, json, { title: incrementTitleCopy(StrCast(doc.title)) });

        dashboardDoc.pane_count = 1;
        dashboardDoc.myOverlayDocs = new List<Doc>();
        dashboardDoc.myPublishedDocs = new List<Doc>();

        DashboardView.SetupDashboardTrails();
        DashboardView.SetupDashboardCalendars(); // Zaul TODO: needed?
        return DashboardView.openDashboard(dashboardDoc);
    }

    @action
    stateChanged = () => {
        this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
        const json = JSON.stringify(this._goldenLayout.toConfig());
        const changesMade = this.Document.dockingConfig !== json;
        return changesMade;
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    tabDestroyed = (tab: any) => {
        this._flush = this._flush ?? UndoManager.StartBatch('tab movement');
        const dashDoc = tab.DashDoc;
        if (dashDoc && ![DocumentType.PRES].includes(dashDoc.type) && !tab.contentItem.config.props.keyValue) {
            Doc.AddDocToList(Doc.MyHeaderBar, 'data', dashDoc, undefined, undefined, true);
            // if you close a tab that is not embedded somewhere else (an embedded Doc can be opened simultaneously in a tab), then add the tab to recently closed
            if (dashDoc.embedContainer === this.Document) dashDoc.embedContainer = undefined;
            if (!dashDoc.embedContainer) {
                Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', dashDoc, undefined, true, true);
                Doc.RemoveEmbedding(dashDoc, dashDoc);
            }
        }
        if (CollectionDockingView.Instance) {
            const dview = CollectionDockingView.Instance.Document;
            const { fieldKey } = CollectionDockingView.Instance.props;
            Doc.RemoveDocFromList(dview, fieldKey, dashDoc);
            this.tabMap.delete(tab);
            tab._disposers && Object.values(tab._disposers).forEach(disposer => (disposer as () => void)());
            this.stateChanged();
        }
    };
    tabCreated = (tab: { contentItem: { element: HTMLElement[] } }) => {
        this.tabMap.add(tab);
        // InitTab is added to the tab's HTMLElement in TabDocView
        const tabdocviewContent = tab.contentItem.element[0]?.firstChild?.firstChild as TabHTMLElement;
        tabdocviewContent?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous tabs (ie, when dragging a tab around a new tab is created for the old content)
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    stackCreated = (stackIn: any) => {
        const stack = stackIn.header ? stackIn : stackIn.origin;
        stack.header?.element.on('mousedown', (e: MouseEvent) => {
            const dashboard = Doc.ActiveDashboard;
            if (dashboard && e.target === stack.header?.element[0] && e.button === 2) {
                dashboard.pane_count = NumCast(dashboard.pane_count) + 1;
                const docToAdd = Docs.Create.FreeformDocument([], {
                    _width: this._props.PanelWidth(),
                    _height: this._props.PanelHeight(),
                    _freeform_backgroundGrid: true,
                    _layout_fitWidth: true,
                    title: `Untitled Tab ${NumCast(dashboard.pane_count)}`,
                });
                Doc.AddDocToList(Doc.MyHeaderBar, 'data', docToAdd, undefined, undefined, true);
                inheritParentAcls(this.Document, docToAdd, false);
                CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack);
            }
        });

        const addNewDoc = undoable(() => {
            const dashboard = Doc.ActiveDashboard;
            if (dashboard) {
                dashboard.pane_count = NumCast(dashboard.pane_count) + 1;
                const docToAdd = Docs.Create.FreeformDocument([], {
                    _width: this._props.PanelWidth(),
                    _height: this._props.PanelHeight(),
                    _layout_fitWidth: true,
                    _freeform_backgroundGrid: true,
                    title: `Untitled Tab ${NumCast(dashboard.pane_count)}`,
                });
                Doc.AddDocToList(Doc.MyHeaderBar, 'data', docToAdd, undefined, undefined, true);
                inheritParentAcls(this.dataDoc, docToAdd, false);
                CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack);
            }
        }, 'add new tab');

        stack.header?.controlsContainer
            .find('.lm_close') // get the close icon
            .off('click') // unbind the current click handler
            .click(
                action(() => {
                    // if (confirm('really close this?')) {
                    if ((!stack.parent.isRoot && !stack.parent.parent.isRoot) || stack.parent.contentItems.length > 1) {
                        const batch = UndoManager.StartBatch('close stack');
                        stack.remove();
                        setTimeout(() => {
                            this.stateChanged();
                            batch.end();
                        });
                    } else {
                        alert('cant delete the last stack');
                    }
                })
            );

        stack.element.click((e: { originalEvent: MouseEvent }) => {
            if (stack.contentItems.length === 0 && Array.from(document.elementsFromPoint(e.originalEvent.x, e.originalEvent.y)).some(ele => ele?.className === 'empty-tabs-message')) {
                addNewDoc();
            }
        });
        stack.header?.controlsContainer
            .find('.lm_maximise') // get the close icon
            .click(() => setTimeout(this.stateChanged));
        stack.header?.controlsContainer
            .find('.lm_popout') // get the popout icon
            .off('click') // unbind the current click handler
            .click(addNewDoc);
    };

    render() {
        const href = ImageCast(this.Document.thumb)?.url?.href;
        return this._props.renderDepth > -1 ? (
            <div>
                {href ? (
                    <img
                        alt="thumbnail of nested dashboard"
                        style={{ background: 'white', top: 0, position: 'absolute' }}
                        src={href} // + '?d=' + (new Date()).getTime()}
                        width={this._props.PanelWidth()}
                        height={this._props.PanelHeight()}
                    />
                ) : (
                    <p>nested dashboard has no thumbnail</p>
                )}
            </div>
        ) : (
            <div className="collectiondockingview-container" onPointerDown={this.onPointerDown} ref={this._containerRef} />
        );
    }
}

ScriptingGlobals.add(
    // eslint-disable-next-line prefer-arrow-callback
    function openInLightbox(doc: Doc) {
        CollectionDockingView.Instance?._props.addDocTab(doc, OpenWhere.lightboxAlways);
    },
    'opens up document in a lightbox',
    '(doc: any)'
);
ScriptingGlobals.add(
    // eslint-disable-next-line prefer-arrow-callback
    function openDoc(doc: Doc | string, where: OpenWhere) {
        switch (where) {
            case OpenWhere.addRight:
                return doc instanceof Doc && CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
            case OpenWhere.overlay:
            default:
                switch (doc) {
                    case '<ScriptingRepl />': return OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' });
                    case "<UndoStack />": return OverlayView.Instance.addWindow(<UndoStack />, { x: 300, y: 100, width: 200, height: 200, title: 'Undo stack' });
                    default: return doc instanceof Doc && Doc.AddToMyOverlay(doc);
                } // prettier-ignore
        }
    },
    'opens up document in location specified',
    '(doc: any)'
);
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(async function snapshotDashboard() {
    const batch = UndoManager.StartBatch('snapshot');
    await CollectionDockingView.TakeSnapshot(Doc.ActiveDashboard);
    batch.end();
}, 'creates a snapshot copy of a dashboard');
