import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { DivHeight, emptyFunction, returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnOne, returnTrue, returnZero, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType } from '../../util/DragManager';
import { SelectionManager } from '../../util/SelectionManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { EditableView } from '../EditableView';
import { DocumentView } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { StyleProp } from '../StyleProvider';
import { CollectionFreeFormView } from './collectionFreeForm';
import { CollectionSubView } from './CollectionSubView';
import './CollectionTreeView.scss';
import { TreeView } from './TreeView';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
const _global = (window /* browser */ || global) /* node */ as any;

export type collectionTreeViewProps = {
    treeViewExpandedView?: 'fields' | 'layout' | 'links' | 'data';
    treeViewOpen?: boolean;
    treeViewHideTitle?: boolean;
    treeViewHideHeaderFields?: boolean;
    treeViewSkipFields?: string[]; // prevents specific fields from being displayed (see LinkBox)
    onCheckedClick?: () => ScriptField;
    onChildClick?: () => ScriptField;
    // TODO: [AL] add these fields
    AddToMap?: (treeViewDoc: Doc, index: number[]) => void;
    RemFromMap?: (treeViewDoc: Doc, index: number[]) => void;
    hierarchyIndex?: number[];
};

export enum TreeViewType {
    outline = 'outline',
    fileSystem = 'fileSystem',
    default = 'default',
}

@observer
export class CollectionTreeView extends CollectionSubView<Partial<collectionTreeViewProps>>() {
    public static AddTreeFunc = 'addTreeFolder(this.embedContainer)';
    private _treedropDisposer?: DragManager.DragDropDisposer;
    private _mainEle?: HTMLDivElement;
    private _titleRef?: HTMLDivElement | HTMLInputElement | null;
    private _disposers: { [name: string]: IReactionDisposer } = {};
    private _isDisposing = false; // notes that instance is in process of being disposed
    private refList: Set<any> = new Set(); // list of tree view items to monitor for height changes
    private observer: any; // observer for monitoring tree view items.

    constructor(props: any) {
        super(props);
        makeObservable(this);
    }

    get dataDoc() {
        return this._props.TemplateDataDocument || this.Document;
    }
    @computed get treeViewtruncateTitleWidth() {
        return NumCast(this.Document.treeView_TruncateTitleWidth, this.panelWidth());
    }
    @computed get treeChildren() {
        TraceMobx();
        return this._props.childDocuments || this.childDocs;
    }
    @computed get outlineMode() {
        return this.Document.treeView_Type === TreeViewType.outline;
    }
    @computed get fileSysMode() {
        return this.Document.treeView_Type === TreeViewType.fileSystem;
    }
    @computed get dashboardMode() {
        return this.Document === Doc.MyDashboards;
    }

    @observable _titleHeight = 0; // height of the title bar

    MainEle = () => this._mainEle;

    // these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent
    @observable _isAnyChildContentActive = false;
    whenChildContentsActiveChanged = action((isActive: boolean) => this._props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)));
    isContentActive = (outsideReaction?: boolean) => (this._isAnyChildContentActive ? true : this._props.isContentActive() ? true : false);

    componentWillUnmount() {
        this._isDisposing = true;
        super.componentWillUnmount();
        this._treedropDisposer?.();
        Object.values(this._disposers).forEach(disposer => disposer?.());
    }

    componentDidMount() {
        //this._props.setContentView?.(this);
        this._disposers.autoheight = reaction(
            () => this.layoutDoc.layout_autoHeight,
            auto => auto && this.computeHeight(),
            { fireImmediately: true }
        );
    }

    computeHeight = () => {
        if (!this._isDisposing) {
            const titleHeight = !this._titleRef ? this.marginTop() : DivHeight(this._titleRef);
            const bodyHeight = Array.from(this.refList).reduce((p, r) => p + DivHeight(r), this.marginBot()) + 6;
            this.layoutDoc._layout_autoHeightMargins = bodyHeight;
            !this._props.dontRegisterView && this._props.setHeight?.(bodyHeight + titleHeight);
        }
    };
    unobserveHeight = (ref: any) => {
        this.refList.delete(ref);
        this.layoutDoc.layout_autoHeight && this.computeHeight();
    };
    observeHeight = (ref: any) => {
        if (ref) {
            this.refList.add(ref);
            this.observer = new _global.ResizeObserver(
                action((entries: any) => {
                    if (this.layoutDoc.layout_autoHeight && ref && this.refList.size && !SnappingManager.IsDragging) {
                        this.computeHeight();
                    }
                })
            );
            this.layoutDoc.layout_autoHeight && this.computeHeight();
            this.observer.observe(ref);
        }
    };
    protected createTreeDropTarget = (ele: HTMLDivElement) => {
        this._treedropDisposer?.();
        if ((this._mainEle = ele)) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.Document, this.onInternalPreDrop.bind(this));
    };

    protected onInternalDrop(e: Event, de: DragManager.DropEvent) {
        const res = super.onInternalDrop(e, de);
        if (res && de.complete.docDragData) {
            if (this.Document !== Doc.MyRecentlyClosed)
                de.complete.docDragData.droppedDocuments.forEach(doc => {
                    if (this.Document !== Doc.MyRecentlyClosed) Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, doc);
                });
        }
        return res;
    }

    protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) => {
        const dragData = de.complete.docDragData;
        if (dragData) {
            const sourceDragAction = dragData.dropAction;
            const sameTree = () => Doc.AreProtosEqual(dragData.treeViewDoc, this.Document);
            const isAlreadyInTree = () => sameTree || dragData.draggedDocuments.some(d => d.embedContainer === this.Document && this.childDocs.includes(d));
            dragData.dropAction =
                targetDropAction && !isAlreadyInTree() // if dropped document is not in the tree
                    ? targetDropAction // then use the target's drop action if it's specified
                    : !sameTree() || sourceDragAction === dropActionType.inPlace // if doc from another tree, or a non inPlace source drag action is specified
                      ? sourceDragAction // use the source dragAction
                      : dropActionType.same; // otherwise use same tree semantics to move within tree
            e.stopPropagation();
        }
    };

    dragConfig = (dragData: DragManager.DocumentDragData) => (dragData.treeViewDoc = this.Document);

    screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, -this._headerHeight);

    @action
    remove = (doc: Doc | Doc[]): boolean => {
        const docs = doc instanceof Doc ? [doc] : doc;
        const targetDataDoc = this.Document[DocData];
        const value = DocListCast(targetDataDoc[this._props.fieldKey]);
        const result = value.filter(v => !docs.includes(v));
        if ((doc instanceof Doc ? [doc] : doc).some(doc => SelectionManager.Views.some(dv => Doc.AreProtosEqual(dv.Document, doc)))) SelectionManager.DeselectAll();
        if (result.length !== value.length) {
            if (doc instanceof Doc) {
                const ind = DocListCast(targetDataDoc[this._props.fieldKey]).indexOf(doc);
                const prev = ind && DocListCast(targetDataDoc[this._props.fieldKey])[ind - 1];
                this._props.removeDocument?.(doc);
                if (ind > 0 && prev) {
                    FormattedTextBox.SetSelectOnLoad(prev);
                    DocumentManager.Instance.getDocumentView(prev, this.DocumentView?.())?.select(false);
                }
                return true;
            }
            return this._props.removeDocument?.(doc) ?? false;
        }
        return false;
    };

    @action
    addDoc = (docs: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => {
        const doclist = docs instanceof Doc ? [docs] : docs;
        const addDocRelativeTo = (doc: Doc | Doc[]) => doclist.reduce((flg, doc) => flg && Doc.AddDocToList(this.Document[DocData], this._props.fieldKey, doc, relativeTo, before), true);
        if (this.Document.resolvedDataDoc instanceof Promise) return false;
        const res = relativeTo === undefined ? this._props.addDocument?.(docs) || false : addDocRelativeTo(docs);
        res &&
            doclist.forEach(doc => {
                Doc.SetContainer(doc, this.Document);
                if (this.Document !== Doc.MyRecentlyClosed) Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, doc);
            });
        return res;
    };
    onContextMenu = (e: React.MouseEvent): void => {
        // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
        const layoutItems: ContextMenuProps[] = [];
        const menuDoc = ScriptCast(Cast(this.layoutDoc.layout_headerButton, Doc, null)?.onClick).script.originalScript === CollectionTreeView.AddTreeFunc;
        menuDoc && layoutItems.push({ description: 'Create new folder', event: () => CollectionTreeView.addTreeFolder(this.Document), icon: 'paint-brush' });
        if (!Doc.noviceMode) {
            layoutItems.push({
                description: 'Make tree state ' + (this.Document.treeView_OpenIsTransient ? 'persistent' : 'transient'),
                event: () => (this.Document.treeView_OpenIsTransient = !this.Document.treeView_OpenIsTransient),
                icon: 'paint-brush',
            });
            layoutItems.push({ description: (this.Document.treeView_HideHeaderFields ? 'Show' : 'Hide') + ' Header Fields', event: () => (this.Document.treeView_HideHeaderFields = !this.Document.treeView_HideHeaderFields), icon: 'paint-brush' });
            layoutItems.push({ description: (this.Document.treeView_HideTitle ? 'Show' : 'Hide') + ' Title', event: () => (this.Document.treeView_HideTitle = !this.Document.treeView_HideTitle), icon: 'paint-brush' });
        }
        ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' });
        if (!Doc.noviceMode) {
            const existingOnClick = ContextMenu.Instance.findByDescription('OnClick...');
            const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : [];
            onClicks.push({ description: 'Edit onChecked Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.Document, undefined, 'onCheckedClick'), 'edit onCheckedClick'), icon: 'edit' });
            !existingOnClick && ContextMenu.Instance.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
        }
    };
    onTreeDrop = (e: React.DragEvent, addDocs?: (docs: Doc[]) => void) => this.onExternalDrop(e, {}, addDocs);

    @undoBatch
    makeTextCollection = (childDocs: Doc[]) => this.addDoc(TreeView.makeTextBullet(), childDocs.length ? childDocs[0] : undefined, true);

    get editableTitle() {
        return (
            <EditableView
                contents={this.dataDoc.title}
                display={'block'}
                maxHeight={72}
                height={'auto'}
                GetValue={() => StrCast(this.dataDoc.title)}
                SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => {
                    if (enter && this.Document.treeView_Type === TreeViewType.outline) this.makeTextCollection(this.treeChildren);
                    this.dataDoc.title = value;
                    return true;
                })}
            />
        );
    }

    onKey = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => {
        if (this.outlineMode && e.key === 'Enter') {
            e.stopPropagation();
            this.makeTextCollection(this.treeChildren);
            return true;
        }
    };
    get documentTitle() {
        return (
            <FormattedTextBox
                {...this._props}
                fieldKey="text"
                renderDepth={this._props.renderDepth + 1}
                isContentActive={this.isContentActive}
                isDocumentActive={this.isContentActive}
                forceAutoHeight={true} // needed to make the title resize even if the rest of the tree view is not layout_autoHeight
                PanelWidth={this.documentTitleWidth}
                PanelHeight={this.documentTitleHeight}
                NativeDimScaling={returnOne}
                onKey={this.onKey}
                childFilters={returnEmptyFilter}
                childFiltersByRanges={returnEmptyFilter}
                searchFilterDocs={returnEmptyDoclist}
                addDocument={returnFalse}
                moveDocument={returnFalse}
                removeDocument={returnFalse}
                whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
            />
        );
    }
    childContextMenuItems = () => {
        const customScripts = Cast(this.Document.childContextMenuScripts, listSpec(ScriptField), []);
        const customFilters = Cast(this.Document.childContextMenuFilters, listSpec(ScriptField), []);
        const icons = StrListCast(this.Document.childContextMenuIcons);
        return StrListCast(this.Document.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label }));
    };
    headerFields = () => this._props.treeViewHideHeaderFields || BoolCast(this.Document.treeView_HideHeaderFields);
    @observable _renderCount = 1;
    @computed get treeViewElements() {
        TraceMobx();
        const dragAction = StrCast(this.Document.childDragAction) as any as dropActionType;
        const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before);
        const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this._props.moveDocument?.(d, target, addDoc) || false;
        if (this._renderCount < this.treeChildren.length) setTimeout(action(() => (this._renderCount = Math.min(this.treeChildren.length, this._renderCount + 20))));
        return TreeView.GetChildElements(
            this.treeChildren,
            this,
            this,
            this.Document,
            this._props.TemplateDataDocument,
            undefined,
            undefined,
            addDoc,
            this.remove,
            moveDoc,
            dragAction,
            this._props.addDocTab,
            this._props.styleProvider,
            this.screenToLocalTransform,
            this.isContentActive,
            this.panelWidth,
            this._props.renderDepth,
            this.headerFields,
            [],
            this._props.onCheckedClick,
            this.onChildClick,
            this._props.treeViewSkipFields,
            true,
            this.whenChildContentsActiveChanged,
            this._props.dontRegisterView || Cast(this.Document.childDontRegisterViews, 'boolean', null),
            this.observeHeight,
            this.unobserveHeight,
            this.childContextMenuItems(),
            //TODO: [AL] add these
            this._props.AddToMap,
            this._props.RemFromMap,
            this._props.hierarchyIndex,
            this._renderCount
        );
    }
    @computed get titleBar() {
        return this.dataDoc === null ? null : (
            <div
                className="collectionTreeView-titleBar"
                ref={action((r: any) => (this._titleRef = r) && (this._titleHeight = r.getBoundingClientRect().height * this.ScreenToLocalBoxXf().Scale))}
                key={this.Document[Id]}
                style={!this.outlineMode ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}}>
                {this.outlineMode ? this.documentTitle : this.editableTitle}
            </div>
        );
    }

    @computed get noviceExplainer() {
        return !Doc.noviceMode || !this.layoutDoc.layout_explainer ? null : <div className="documentExplanation"> {StrCast(this.layoutDoc.layout_explainer)} </div>;
    }

    return35 = () => 35;
    @computed get buttonMenu() {
        const menuDoc = Cast(this.layoutDoc.layout_headerButton, Doc, null);
        // To create a multibutton menu add a CollectionLinearView
        return !menuDoc ? null : (
            <div className="buttonMenu-docBtn" style={{ width: NumCast(menuDoc._width, 30), height: NumCast(menuDoc._height, 30) }}>
                <DocumentView
                    Document={menuDoc}
                    TemplateDataDocument={menuDoc}
                    isContentActive={this._props.isContentActive}
                    isDocumentActive={returnTrue}
                    addDocument={this._props.addDocument}
                    moveDocument={this._props.moveDocument}
                    removeDocument={this._props.removeDocument}
                    addDocTab={this._props.addDocTab}
                    pinToPres={this._props.pinToPres}
                    rootSelected={this.rootSelected}
                    ScreenToLocalTransform={Transform.Identity}
                    PanelWidth={this.return35}
                    PanelHeight={this.return35}
                    renderDepth={this._props.renderDepth + 1}
                    focus={emptyFunction}
                    styleProvider={this._props.styleProvider}
                    containerViewPath={returnEmptyDoclist}
                    whenChildContentsActiveChanged={emptyFunction}
                    childFilters={this._props.childFilters}
                    childFiltersByRanges={this._props.childFiltersByRanges}
                    searchFilterDocs={this._props.searchFilterDocs}
                />
            </div>
        );
    }

    @computed get nativeWidth() {
        return Doc.NativeWidth(this.Document, undefined, true);
    }
    @computed get nativeHeight() {
        return Doc.NativeHeight(this.Document, undefined, true);
    }

    /// scale factor for tree view so that it will fit within it's panel bounds
    @computed get nativeDimScaling() {
        const nw = this.nativeWidth;
        const nh = this.nativeHeight;
        const hscale = nh ? this._props.PanelHeight() / nh : 1;
        const wscale = nw ? this._props.PanelWidth() / nw : 1;
        return wscale < hscale ? wscale : hscale;
    }
    marginX = () => NumCast(this.Document._xMargin);
    marginTop = () => NumCast(this.Document._yMargin);
    marginBot = () => NumCast(this.Document._yMargin);
    documentTitleWidth = () => Math.min(NumCast(this.layoutDoc?._width), this.panelWidth());
    documentTitleHeight = () => NumCast(this.layoutDoc?._height) - NumCast(this.layoutDoc.layout_autoHeightMargins);
    truncateTitleWidth = () => this.treeViewtruncateTitleWidth;
    onChildClick = () => this._props.onChildClick?.() || ScriptCast(this.Document.onChildClick);
    panelWidth = () => Math.max(0, this._props.PanelWidth() - 2 * this.marginX() * (this._props.NativeDimScaling?.() || 1));

    addAnnotationDocument = (doc: Doc | Doc[]) => this.addDocument(doc, `${this._props.fieldKey}_annotations`) || false;
    remAnnotationDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, `${this._props.fieldKey}_annotations`) || false;
    moveAnnotationDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) =>
        this.moveDocument(doc, targetCollection, addDocument, `${this._props.fieldKey}_annotations`) || false;

    @observable _headerHeight = 0;
    @computed get content() {
        const background = () => this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor);
        const color = () => this._props.styleProvider?.(this.Document, this._props, StyleProp.Color);
        const pointerEvents = () => (this._props.isContentActive() === false ? 'none' : undefined);
        const titleBar = this._props.treeViewHideTitle || this.Document.treeView_HideTitle ? null : this.titleBar;
        return (
            <div style={{ display: 'flex', flexDirection: 'column', height: '100%', pointerEvents: 'all' }}>
                {!this.buttonMenu && !this.noviceExplainer ? null : (
                    <div className="documentButtonMenu" ref={action((r: HTMLDivElement | null) => r && (this._headerHeight = DivHeight(r)))}>
                        {this.buttonMenu}
                        {this.noviceExplainer}
                    </div>
                )}
                <div
                    className="collectionTreeView-contents"
                    key="tree"
                    ref={r => !this.Document.treeView_HasOverlay && r && this.createTreeDropTarget(r)}
                    style={{
                        ...(!titleBar ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
                        color: color(),
                        overflow: 'auto',
                        width: '100%',
                        height: '100%',
                    }}>
                    {titleBar}
                    <div
                        className="collectionTreeView-container"
                        style={{
                            marginLeft: `${this.marginX()}px`,
                            minHeight: `calc(100% - ${this._titleHeight}px)`,
                        }}
                        onContextMenu={this.onContextMenu}>
                        <div
                            className="collectionTreeView-dropTarget"
                            style={{
                                background: background(),
                                pointerEvents: pointerEvents(),
                                height: `max-content`,
                                minHeight: '100%',
                            }}
                            onWheel={e => e.stopPropagation()}
                            onClick={e => (!this.layoutDoc.forceActive ? this._props.select(false) : SelectionManager.DeselectAll())}
                            onDrop={this.onTreeDrop}>
                            <ul className={`no-indent${this.outlineMode ? '-outline' : ''}`}>{this.treeViewElements}</ul>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
    render() {
        TraceMobx();

        const scale = this._props.NativeDimScaling?.() || 1;
        return (
            <div style={{ transform: `scale(${scale})`, transformOrigin: 'top left', width: `${100 / scale}%`, height: `${100 / scale}%` }}>
                {!(this.Document instanceof Doc) || !this.treeChildren ? null : this.Document.treeView_HasOverlay ? (
                    <CollectionFreeFormView
                        {...this._props}
                        setContentViewBox={emptyFunction}
                        NativeWidth={returnZero}
                        NativeHeight={returnZero}
                        pointerEvents={this._props.isContentActive() && SnappingManager.IsDragging ? returnAll : returnNone}
                        isAnnotationOverlay={true}
                        isAnnotationOverlayScrollable={true}
                        childDocumentsActive={this._props.isContentActive}
                        fieldKey={this._props.fieldKey + '_annotations'}
                        dropAction={dropActionType.move}
                        select={emptyFunction}
                        addDocument={this.addAnnotationDocument}
                        removeDocument={this.remAnnotationDocument}
                        moveDocument={this.moveAnnotationDocument}
                        renderDepth={this._props.renderDepth + 1}>
                        {this.content}
                    </CollectionFreeFormView>
                ) : (
                    this.content
                )}
            </div>
        );
    }
    static addTreeFolder(container: Doc) {
        TreeView._editTitleOnLoad = { id: Utils.GenerateGuid(), parent: undefined };
        const opts = { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true };
        return Doc.AddDocToList(container, 'data', Docs.Create.TreeDocument([], opts, TreeView._editTitleOnLoad.id));
    }
}

ScriptingGlobals.add(function addTreeFolder(doc: Doc) {
    CollectionTreeView.addTreeFolder(doc);
});
