import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
import { DataSym, Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
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 { emptyFunction, OmitKeys, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { CurrentUserUtils } from '../../util/CurrentUserUtils';
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 { 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 React = require("react");
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;
};

@observer
export class CollectionTreeView extends CollectionSubView<Partial<collectionTreeViewProps>>() {
    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.
    private static expandViewLabelSize = 20;

    @computed get doc() { return this.props.Document; }
    @computed get dataDoc() { return this.props.DataDoc || this.doc; }
    @computed get treeViewtruncateTitleWidth() { return NumCast(this.doc.treeViewTruncateTitleWidth, this.panelWidth()); }
    @computed get treeChildren() { TraceMobx(); return this.props.childDocuments || this.childDocs; }
    @computed get outlineMode() { return this.doc.treeViewType === "outline"; }
    @computed get fileSysMode() { return this.doc.treeViewType === "fileSystem"; }
    @computed get dashboardMode() { return this.doc === Doc.UserDoc().myDashboards; }

    @observable _explainerHeight = 0; // height of the description of the tree view

    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) => (CurrentUserUtils.SelectedTool !== InkTool.None ||
        (this.props.isContentActive?.() || this.props.Document.forceActive ||
            this.props.isSelected(outsideReaction) || this._isAnyChildContentActive ||
            this.props.rootSelected(outsideReaction)) ? true : false)

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

    componentDidMount() {
        this._disposers.autoheight = reaction(() => this.rootDoc.autoHeight,
            auto => auto && this.computeHeight(),
            { fireImmediately: true });
    }

    computeHeight = () => {
        if (!this._isDisposing) {
            const titleHeight = !this._titleRef ? this.marginTop() : Number(getComputedStyle(this._titleRef).height.replace("px", ""));
            const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), this.marginBot());
            this.layoutDoc._autoHeightMargins = bodyHeight;
            this.props.setHeight?.(bodyHeight + titleHeight);
        }
    }
    unobserveHeight = (ref: any) => {
        this.refList.delete(ref);
        this.rootDoc.autoHeight && this.computeHeight();
    }
    observeHeight = (ref: any) => {
        if (ref) {
            this.refList.add(ref);
            this.observer = new _global.ResizeObserver(action((entries: any) => {
                if (this.rootDoc.autoHeight && ref && this.refList.size && !SnappingManager.GetIsDragging()) {
                    this.computeHeight();
                }
            }));
            this.rootDoc.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.doc, this.onInternalPreDrop.bind(this));
    }

    protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
        const dragData = de.complete.docDragData;
        if (dragData) {
            const isInTree = () => Doc.AreProtosEqual(dragData.treeViewDoc, this.props.Document) || dragData.draggedDocuments.some(d => d.context === this.doc && this.childDocs.includes(d));
            dragData.dropAction = targetAction && !isInTree() ? targetAction : this.doc === dragData?.treeViewDoc ? "same" : dragData.dropAction;
        }
    }

    @action
    remove = (doc: Doc | Doc[]): boolean => {
        const docs = doc instanceof Doc ? [doc] : doc;
        const targetDataDoc = this.doc[DataSym];
        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.rootDoc, doc)))) SelectionManager.DeselectAll();
        if (result.length !== value.length) {
            const ind = targetDataDoc[this.props.fieldKey].indexOf(doc);
            const prev = ind && targetDataDoc[this.props.fieldKey][ind - 1];
            this.props.removeDocument?.(doc);
            if (ind > 0) {
                FormattedTextBox.SelectOnLoad = prev[Id];
                DocumentManager.Instance.getDocumentView(prev, this.props.CollectionView)?.select(false);
            }
            return true;
        }
        return false;
    }

    @action
    addDoc = (docs: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => {
        const doAddDoc = (doc: Doc | Doc[]) =>
            (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => {
                const res = flg && Doc.AddDocToList(this.doc[DataSym], this.props.fieldKey, doc, relativeTo, before);
                res && (doc.context = this.props.Document);
                return res;
            }, true);
        if (this.doc.resolvedDataDoc instanceof Promise) return false;
        return relativeTo === undefined ? this.props.addDocument?.(docs) || false : doAddDoc(docs);
    }
    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
        if (!Doc.UserDoc().noviceMode) {
            const layoutItems: ContextMenuProps[] = [];
            layoutItems.push({ description: "Make tree state " + (this.doc.treeViewOpenIsTransient ? "persistent" : "transient"), event: () => this.doc.treeViewOpenIsTransient = !this.doc.treeViewOpenIsTransient, icon: "paint-brush" });
            layoutItems.push({ description: (this.doc.treeViewHideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.doc.treeViewHideHeaderFields = !this.doc.treeViewHideHeaderFields, icon: "paint-brush" });
            layoutItems.push({ description: (this.doc.treeViewHideTitle ? "Show" : "Hide") + " Title", event: () => this.doc.treeViewHideTitle = !this.doc.treeViewHideTitle, icon: "paint-brush" });
            ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "eye" });
            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.doc, 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.props.Document.treeViewType === "outline") this.makeTextCollection(this.treeChildren);
                this.dataDoc.title = value;
                return true;
            })} />;
    }


    get documentTitle() {
        return <FormattedTextBox
            {...this.props}
            fieldKey={"text"}
            renderDepth={this.props.renderDepth + 1}
            isContentActive={this.isContentActive}
            isDocumentActive={this.isContentActive}
            rootSelected={returnTrue}
            forceAutoHeight={true} // needed to make the title resize even if the rest of the tree view is not autoHeight
            PanelWidth={this.documentTitleWidth}
            PanelHeight={this.documentTitleHeight}
            scaling={returnOne}
            docFilters={returnEmptyFilter}
            docRangeFilters={returnEmptyFilter}
            searchFilterDocs={returnEmptyDoclist}
            ContainingCollectionDoc={this.doc}
            ContainingCollectionView={this.props.CollectionView}
            addDocument={returnFalse}
            moveDocument={returnFalse}
            removeDocument={returnFalse}
            whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
            bringToFront={returnFalse}
        />;
    }
    childContextMenuItems = () => {
        const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
        const customFilters = Cast(this.doc.childContextMenuFilters, listSpec(ScriptField), []);
        const icons = StrListCast(this.doc.childContextMenuIcons);
        return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label }));
    }
    @computed get treeViewElements() {
        TraceMobx();
        const dropAction = StrCast(this.doc.childDropAction) 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;
        return TreeView.GetChildElements(
            this.treeChildren,
            this,
            this,
            this.doc,
            this.props.DataDoc,
            this.props.ContainingCollectionDoc,
            undefined,
            addDoc,
            this.remove,
            moveDoc,
            dropAction,
            this.props.addDocTab,
            this.props.styleProvider,
            this.props.ScreenToLocalTransform,
            this.isContentActive,
            this.panelWidth,
            this.props.renderDepth,
            () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields),
            [],
            this.props.onCheckedClick,
            this.onChildClick,
            this.props.treeViewSkipFields,
            true,
            this.whenChildContentsActiveChanged,
            this.props.dontRegisterView || Cast(this.props.Document.childDontRegisterViews, "boolean", null),
            this.observeHeight,
            this.unobserveHeight,
            this.childContextMenuItems()
        );
    }
    @computed get titleBar() {
        return this.dataDoc === null ? (null) :
            <div className="collectionTreeView-titleBar" key={this.doc[Id]}
                style={!this.outlineMode ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}}
                ref={r => this._titleRef = r}
                onKeyDown={e => {
                    if (this.outlineMode) {
                        e.stopPropagation();
                        e.key === "Enter" && this.makeTextCollection(this.treeChildren);
                    }
                }}>
                {this.outlineMode ? this.documentTitle : this.editableTitle}
            </div>;
    }

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

    return35 = () => 35;
    @computed get buttonMenu() {
        const menuDoc = Cast(this.rootDoc.buttonMenuDoc, 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}
                    DataDoc={menuDoc}
                    isContentActive={this.props.isContentActive}
                    isDocumentActive={returnTrue}
                    addDocument={this.props.addDocument}
                    moveDocument={this.props.moveDocument}
                    addDocTab={this.props.addDocTab}
                    pinToPres={emptyFunction}
                    rootSelected={this.props.isSelected}
                    removeDocument={this.props.removeDocument}
                    ScreenToLocalTransform={Transform.Identity}
                    PanelWidth={this.return35}
                    PanelHeight={this.return35}
                    renderDepth={this.props.renderDepth + 1}
                    focus={emptyFunction}
                    styleProvider={this.props.styleProvider}
                    docViewPath={returnEmptyDoclist}
                    whenChildContentsActiveChanged={emptyFunction}
                    bringToFront={emptyFunction}
                    docFilters={this.props.docFilters}
                    docRangeFilters={this.props.docRangeFilters}
                    searchFilterDocs={this.props.searchFilterDocs}
                    ContainingCollectionView={undefined}
                    ContainingCollectionDoc={undefined}
                />
            </div>);
    }

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

    @computed get contentScaling() {
        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.doc._xMargin);
    marginTop = () => NumCast(this.doc._yMargin);
    marginBot = () => NumCast(this.doc._yMargin);
    documentTitleWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.panelWidth());
    documentTitleHeight = () => (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.autoHeightMargins);
    truncateTitleWidth = () => this.treeViewtruncateTitleWidth;
    onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick);
    panelWidth = () => Math.max(0, this.props.PanelWidth() - this.marginX() - CollectionTreeView.expandViewLabelSize) * (this.props.scaling?.() || 1);

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

    contentFunc = () => {
        const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor);
        const pointerEvents = () => !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined;
        const titleBar = this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? (null) : this.titleBar;
        return [
            <div className="collectionTreeView-contents" key="tree" style={{
                ...(!titleBar ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
                overflow: "auto",
                height: "100%"//this.layoutDoc._autoHeight ? "max-content" : "100%"
            }} >
                {titleBar}
                <div className="collectionTreeView-container"
                    style={{
                        transform: this.outlineMode ? `scale(${this.contentScaling})` : "",
                        paddingLeft: `${this.marginX()}px`,
                        width: this.outlineMode ? `calc(${100 / this.contentScaling}%)` : ""
                    }}
                    onContextMenu={this.onContextMenu}>
                    {!this.buttonMenu && !this.noviceExplainer ? (null) :
                        <div className="documentButtonMenu" ref={action((r: HTMLDivElement) => r && (this._explainerHeight = r.getBoundingClientRect().height))}>
                            {this.buttonMenu}
                            {this.noviceExplainer}
                        </div>
                    }
                    <div className="collectionTreeView-dropTarget"
                        style={{
                            background: background(),
                            height: `calc(100% - ${this._explainerHeight}px)`,
                            pointerEvents: pointerEvents()
                        }}
                        onWheel={e => e.stopPropagation()}
                        onDrop={this.onTreeDrop}
                        ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)}>
                        <ul className={`no-indent${this.outlineMode ? "-outline" : ""}`} >
                            {this.treeViewElements}
                        </ul>
                    </div >
                </div>
            </div>
        ];
    }
    render() {
        TraceMobx();

        return !(this.doc instanceof Doc) || !this.treeChildren ? (null) :
            this.doc.treeViewHasOverlay ?
                <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
                    isAnnotationOverlay={true}
                    isAnnotationOverlayScrollable={true}
                    childDocumentsActive={this.props.isDocumentActive}
                    fieldKey={this.props.fieldKey + "-annotations"}
                    dropAction={"move"}
                    select={emptyFunction}
                    addDocument={this.addAnnotationDocument}
                    removeDocument={this.remAnnotationDocument}
                    moveDocument={this.moveAnnotationDocument}
                    bringToFront={emptyFunction}
                    renderDepth={this.props.renderDepth + 1} >
                    {this.contentFunc}
                </CollectionFreeFormView> :
                this.contentFunc();
    }
}