import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IconButton, Size } from 'browndash-components';
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { RichTextField } from '../../../fields/RichTextField';
import { listSpec } from '../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { emptyFunction, lightOrDark, return18, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType } from '../../util/DragManager';
import { LinkManager } from '../../util/LinkManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SelectionManager } from '../../util/SelectionManager';
import { SettingsManager } from '../../util/SettingsManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoable, undoBatch, UndoManager } from '../../util/UndoManager';
import { EditableView } from '../EditableView';
import { TREE_BULLET_WIDTH } from '../global/globalCssVariables.scss';
import { DocumentView, DocumentViewInternal, DocumentViewProps, OpenWhere, StyleProviderFunc } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
import { KeyValueBox } from '../nodes/KeyValueBox';
import { StyleProp } from '../StyleProvider';
import { CollectionTreeView, TreeViewType } from './CollectionTreeView';
import { CollectionView } from './CollectionView';
import { TreeSort } from './TreeSort';
import './TreeView.scss';
import React = require('react');

export interface TreeViewProps {
    treeView: CollectionTreeView;
    parentTreeView: TreeView | CollectionTreeView | undefined;
    observeHeight: (ref: any) => void;
    unobserveHeight: (ref: any) => void;
    prevSibling?: Doc;
    document: Doc;
    dataDoc?: Doc;
    treeViewParent: Doc;
    renderDepth: number;
    dragAction: dropActionType;
    addDocTab: (doc: Doc, where: OpenWhere) => boolean;
    panelWidth: () => number;
    panelHeight: () => number;
    addDocument: (doc: Doc | Doc[], annotationKey?: string, relativeTo?: Doc, before?: boolean) => boolean;
    removeDoc: ((doc: Doc | Doc[]) => boolean) | undefined;
    moveDocument: DragManager.MoveFunction;
    isContentActive: (outsideReaction?: boolean) => boolean;
    whenChildContentsActiveChanged: (isActive: boolean) => void;
    indentDocument?: (editTitle: boolean) => void;
    outdentDocument?: (editTitle: boolean) => void;
    ScreenToLocalTransform: () => Transform;
    contextMenuItems?: { script: ScriptField; filter: ScriptField; icon: string; label: string }[];
    dontRegisterView?: boolean;
    styleProvider?: StyleProviderFunc | undefined;
    treeViewHideHeaderFields: () => boolean;
    renderedIds: string[]; // list of document ids rendered used to avoid unending expansion of items in a cycle
    onCheckedClick?: () => ScriptField;
    onChildClick?: () => ScriptField;
    skipFields?: string[];
    firstLevel: boolean;
    // TODO: [AL] add these
    AddToMap?: (treeViewDoc: Doc, index: number[]) => void;
    RemFromMap?: (treeViewDoc: Doc, index: number[]) => void;
    hierarchyIndex?: number[];
}

const treeBulletWidth = function () {
    return Number(TREE_BULLET_WIDTH.replace('px', ''));
};

/**
 * Renders a treeView of a collection of documents
 *
 * special fields:
 * treeView_Open : flag denoting whether the documents sub-tree (contents) is visible or hidden
 * treeView_ExpandedView : name of field whose contents are being displayed as the document's subtree
 */
@observer
export class TreeView extends React.Component<TreeViewProps> {
    static _editTitleOnLoad: Opt<{ id: string; parent: TreeView | CollectionTreeView | undefined }>;
    static _openTitleScript: Opt<ScriptField | undefined>;
    static _openLevelScript: Opt<ScriptField | undefined>;
    private _header: React.RefObject<HTMLDivElement> = React.createRef();
    private _tref = React.createRef<HTMLDivElement>();
    @observable _docRef: Opt<DocumentView>;
    private _disposers: { [name: string]: IReactionDisposer } = {};
    private _editTitleScript: (() => ScriptField) | undefined;
    private _openScript: (() => ScriptField) | undefined;
    private _treedropDisposer?: DragManager.DragDropDisposer;

    get treeViewOpenIsTransient() {
        return this.props.treeView.doc.treeView_OpenIsTransient || Doc.IsDataProto(this.doc);
    }
    set treeViewOpen(c: boolean) {
        if (this.treeViewOpenIsTransient) this._transientOpenState = c;
        else {
            this.doc.treeView_Open = c;
            this._transientOpenState = false;
        }
    }
    @observable _transientOpenState = false; // override of the treeView_Open field allowing the display state to be independent of the document's state
    @observable _editTitle: boolean = false;
    @observable _dref: DocumentView | undefined | null;
    get displayName() {
        return 'TreeView(' + this.props.document.title + ')';
    } // this makes mobx trace() statements more descriptive
    get defaultExpandedView() {
        return this.doc._type_collection === CollectionViewType.Docking
            ? this.fieldKey
            : this.props.treeView.dashboardMode
            ? this.fieldKey
            : this.props.treeView.fileSysMode
            ? this.doc.isFolder
                ? this.fieldKey
                : 'data' // file system folders display their contents (data).  used to be they displayed their embeddings but now its a tree structure and not a flat list
            : this.props.treeView.outlineMode || this.childDocs
            ? this.fieldKey
            : Doc.noviceMode
            ? 'layout'
            : StrCast(this.props.treeView.doc.treeView_ExpandedView, 'fields');
    }

    @computed get doc() {
        return this.props.document;
    }
    @computed get treeViewOpen() {
        return (!this.treeViewOpenIsTransient && Doc.GetT(this.doc, 'treeView_Open', 'boolean', true)) || this._transientOpenState;
    }
    @computed get treeViewExpandedView() {
        return this.validExpandViewTypes.includes(StrCast(this.doc.treeView_ExpandedView)) ? StrCast(this.doc.treeView_ExpandedView) : this.defaultExpandedView;
    }
    @computed get MAX_EMBED_HEIGHT() {
        return NumCast(this.props.treeViewParent.maxEmbedHeight, 200);
    }
    @computed get dataDoc() {
        return this.props.document.treeView_ChildrenOnRoot ? this.doc : this.doc[DocData];
    }
    @computed get layoutDoc() {
        return Doc.Layout(this.doc);
    }
    @computed get fieldKey() {
        return StrCast(this.doc._treeView_FieldKey, Doc.LayoutFieldKey(this.doc));
    }
    @computed get childDocs() {
        return this.childDocList(this.fieldKey);
    }
    @computed get childLinks() {
        return this.childDocList('links');
    }
    @computed get childEmbeddings() {
        return this.childDocList('proto_embeddings');
    }
    @computed get childAnnos() {
        return this.childDocList(this.fieldKey + '_annotations');
    }
    @computed get selected() {
        return this._docRef?.SELECTED;
    }

    childDocList(field: string) {
        const layout = Cast(Doc.LayoutField(this.doc), Doc, null);
        return DocListCast(this.props.dataDoc?.[field], DocListCast(layout?.[field], DocListCast(this.doc[field])));
    }
    moving: boolean = false;
    @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
        if (this.doc !== target && addDoc !== returnFalse) {
            const canAdd1 = (this.props.parentTreeView as any).dropping || !(ComputedField.WithoutComputed(() => FieldValue(this.props.parentTreeView?.doc.data)) instanceof ComputedField);

            // bcz: this should all be running in a Temp undo batch instead of hackily testing for returnFalse
            if (canAdd1 && this.props.removeDoc?.(doc) === true) {
                this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.moving = true);
                const res = addDoc(doc);
                this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.moving = false);
                return res;
            }
        }
        return false;
    };
    @undoBatch @action remove = (doc: Doc | Doc[], key: string) => {
        this.props.treeView.props.select(false);
        const ind = DocListCast(this.dataDoc[key]).indexOf(doc instanceof Doc ? doc : doc.lastElement());

        const res = (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true);
        res && ind > 0 && DocumentManager.Instance.getDocumentView(DocListCast(this.dataDoc[key])[ind - 1], this.props.treeView.props.DocumentView?.())?.select(false);
        return res;
    };

    @action setEditTitle = (docView?: DocumentView) => {
        this._disposers.selection?.();
        if (!docView) {
            this._editTitle = false;
        } else if (docView.isSelected()) {
            this._editTitle = true;
            this._disposers.selection = reaction(
                () => docView.isSelected(),
                isSel => !isSel && this.setEditTitle(undefined)
            );
        } else {
            docView.select(false);
        }
    };
    @action
    openLevel = (docView: DocumentView) => {
        if (this.props.document.isFolder || Doc.IsSystem(this.props.document)) {
            this.treeViewOpen = !this.treeViewOpen;
        } else {
            // choose an appropriate embedding or make one. --- choose the first embedding that (1) user owns,  (2) has no context field ... otherwise make a new embedding
            const bestEmbedding = docView.rootDoc.author === Doc.CurrentUserEmail && !Doc.IsDataProto(docView.props.Document) ? docView.rootDoc : Doc.BestEmbedding(docView.rootDoc);
            this.props.addDocTab(bestEmbedding, OpenWhere.lightbox);
        }
    };

    @undoBatch
    @action
    recurToggle = (childList: Doc[]) => {
        if (childList.length > 0) {
            childList.forEach(child => {
                child.runProcess = !!!child.runProcess;
                TreeView.ToggleChildrenRun.get(child)?.();
            });
        }
    };

    @undoBatch
    @action
    getRunningChildren = (childList: Doc[]) => {
        if (childList.length === 0) {
            return [];
        }

        const runningChildren: FieldResult[] = [];
        childList.forEach(child => {
            if (child.runProcess && TreeView.GetRunningChildren.get(child)) {
                if (child.runProcess) {
                    runningChildren.push(child);
                }
                runningChildren.push(...(TreeView.GetRunningChildren.get(child)?.() ?? []));
            }
        });
        return runningChildren;
    };

    static GetRunningChildren = new Map<Doc, any>();
    static ToggleChildrenRun = new Map<Doc, () => void>();
    constructor(props: any) {
        super(props);
        if (!TreeView._openLevelScript) {
            TreeView._openTitleScript = ScriptField.MakeScript('scriptContext.setEditTitle(documentView)', { scriptContext: 'any', documentView: 'any' });
            TreeView._openLevelScript = ScriptField.MakeScript(`scriptContext.openLevel(documentView)`, { scriptContext: 'any', documentView: 'any' });
        }
        this._openScript = Doc.IsSystem(this.props.document) ? undefined : () => TreeView._openLevelScript!;
        this._editTitleScript = Doc.IsSystem(this.props.document) ? () => TreeView._openLevelScript! : () => TreeView._openTitleScript!;

        // set for child processing highligting
        this.dataDoc.hasChildren = this.childDocs.length > 0;
        // this.dataDoc.children = this.childDocs;
        TreeView.ToggleChildrenRun.set(this.doc, () => {
            this.recurToggle(this.childDocs);
        });

        TreeView.GetRunningChildren.set(this.doc, () => {
            return this.getRunningChildren(this.childDocs);
        });
    }

    _treeEle: any;
    protected createTreeDropTarget = (ele: HTMLDivElement) => {
        this._treedropDisposer?.();
        ele && ((this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this), undefined, this.preTreeDrop.bind(this))), this.doc);
        if (this._treeEle) this.props.unobserveHeight(this._treeEle);
        this.props.observeHeight((this._treeEle = ele));
    };

    componentWillUnmount() {
        this._renderTimer && clearTimeout(this._renderTimer);
        Object.values(this._disposers).forEach(disposer => disposer?.());
        this._treeEle && this.props.unobserveHeight(this._treeEle);
        document.removeEventListener('pointermove', this.onDragMove, true);
        document.removeEventListener('pointermove', this.onDragUp, true);
        // TODO: [AL] add these
        this.props.hierarchyIndex !== undefined && this.props.RemFromMap?.(this.doc, this.props.hierarchyIndex);
    }

    componentDidUpdate() {
        this._disposers.opening = reaction(
            () => this.treeViewOpen,
            open => !open && (this._renderCount = 20)
        );
        this.props.hierarchyIndex !== undefined && this.props.AddToMap?.(this.doc, this.props.hierarchyIndex);
    }

    componentDidMount() {
        this.props.hierarchyIndex !== undefined && this.props.AddToMap?.(this.doc, this.props.hierarchyIndex);
    }

    onDragUp = (e: PointerEvent) => {
        document.removeEventListener('pointerup', this.onDragUp, true);
        document.removeEventListener('pointermove', this.onDragMove, true);
    };
    onPointerEnter = (e: React.PointerEvent): void => {
        this.props.isContentActive(true) && Doc.BrushDoc(this.dataDoc);
        if (e.buttons === 1 && SnappingManager.GetIsDragging() && this.props.isContentActive()) {
            this._header.current!.className = 'treeView-header';
            document.removeEventListener('pointermove', this.onDragMove, true);
            document.removeEventListener('pointerup', this.onDragUp, true);
            document.addEventListener('pointermove', this.onDragMove, true);
            document.addEventListener('pointerup', this.onDragUp, true);
        }
    };
    onPointerLeave = (e: React.PointerEvent): void => {
        Doc.UnBrushDoc(this.dataDoc);
        if (this._header.current?.className !== 'treeView-header-editing') {
            this._header.current!.className = 'treeView-header';
        }
        document.removeEventListener('pointerup', this.onDragUp, true);
        document.removeEventListener('pointermove', this.onDragMove, true);
    };
    onDragMove = (e: PointerEvent): void => {
        Doc.UnBrushDoc(this.dataDoc);
        const pt = [e.clientX, e.clientY];
        const rect = this._header.current!.getBoundingClientRect();
        const before = pt[1] < rect.top + rect.height / 2;
        const inside = pt[0] > rect.left + rect.width * 0.33 || (!before && this.treeViewOpen && this.childDocs?.length);
        this._header.current!.className = 'treeView-header';
        if (!this.props.treeView.outlineMode || DragManager.DocDragData?.treeViewDoc === this.props.treeView.rootDoc) {
            if (inside) this._header.current!.className += ' treeView-header-inside';
            else if (before) this._header.current!.className += ' treeView-header-above';
            else if (!before) this._header.current!.className += ' treeView-header-below';
        }
        e.stopPropagation();
    };

    public static makeTextBullet() {
        const bullet = Docs.Create.TextDocument('', {
            layout: CollectionView.LayoutString('data'),
            title: '-title-',
            treeView_ExpandedViewLock: true,
            treeView_ExpandedView: 'data',
            _type_collection: CollectionViewType.Tree,
            layout_hideLinkButton: true,
            _layout_showSidebar: true,
            _layout_fitWidth: true,
            treeView_Type: TreeViewType.outline,
            x: 0,
            y: 0,
            _xMargin: 0,
            _yMargin: 0,
            _layout_autoHeight: true,
            _createDocOnCR: true,
            _width: 1000,
            _height: 10,
        });
        Doc.GetProto(bullet).title = ComputedField.MakeFunction('self.text?.Text');
        Doc.GetProto(bullet).data = new List<Doc>([]);
        DocumentManager.Instance.AddViewRenderedCb(bullet, dv => dv.ComponentView?.setFocus?.());

        return bullet;
    }

    makeTextCollection = () => {
        const bullet = TreeView.makeTextBullet();
        TreeView._editTitleOnLoad = { id: bullet[Id], parent: this };
        return this.props.addDocument(bullet);
    };

    makeFolder = () => {
        const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true });
        TreeView._editTitleOnLoad = { id: folder[Id], parent: this.props.parentTreeView };
        return this.props.addDocument(folder);
    };

    preTreeDrop = (e: Event, de: DragManager.DropEvent) => {
        const dragData = de.complete.docDragData;
        dragData && (dragData.dropAction = this.props.treeView.props.Document === dragData.treeViewDoc ? 'same' : dragData.dropAction);
    };

    @undoBatch
    treeDrop = (e: Event, de: DragManager.DropEvent) => {
        const pt = [de.x, de.y];
        if (!this._header.current) return false;
        const rect = this._header.current.getBoundingClientRect();
        const before = pt[1] < rect.top + rect.height / 2;
        const inside = this.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > rect.left + rect.width * 0.33 || (!before && this.treeViewOpen && this.childDocs?.length ? true : false);
        if (de.complete.linkDragData) {
            const sourceDoc = de.complete.linkDragData.linkSourceGetAnchor();
            const destDoc = this.doc;
            DocUtils.MakeLink(sourceDoc, destDoc, { link_relationship: 'tree link' });
            e.stopPropagation();
            return true;
        }
        const docDragData = de.complete.docDragData;
        if (docDragData && pt[0] < rect.left + rect.width) {
            if (docDragData.draggedDocuments[0] === this.doc) return true;
            const added = this.dropDocuments(
                docDragData.droppedDocuments, //
                before,
                inside,
                docDragData.dropAction,
                docDragData.removeDocument,
                docDragData.moveDocument,
                docDragData.treeViewDoc === this.props.treeView.props.Document
            );
            e.stopPropagation();
            !added && e.preventDefault();
            return added;
        }
        return false;
    };

    dropping: boolean = false;
    dropDocuments(droppedDocuments: Doc[], before: boolean, inside: number | boolean, dropAction: dropActionType, removeDocument: DragManager.RemoveFunction | undefined, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean) {
        const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, undefined, before);
        const localAdd = (doc: Doc | Doc[]) => {
            const innerAdd = (doc: Doc) => {
                const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[this.fieldKey])) instanceof ComputedField;
                const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc);
                dataIsComputed && Doc.SetContainer(doc, DocCast(this.doc.embedContainer));
                return added;
            };
            return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean);
        };
        const addDoc = inside ? localAdd : parentAddDoc;
        const move = (!dropAction || dropAction === 'proto' || dropAction === 'move' || dropAction === 'same' || dropAction === 'inSame') && moveDocument;
        const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.treeViewParent)?.treeView_FreezeChildren).includes('add')) || forceAdd;
        if (canAdd && (dropAction !== 'inSame' || droppedDocuments.every(d => d.embedContainer === this.props.parentTreeView?.doc))) {
            this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.dropping = true);
            const res = droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === 'proto' ? addDoc(d) : false) : addDoc(d)) || added, false);
            this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.dropping = false);
            return res;
        }
        return false;
    }

    refTransform = (ref: HTMLDivElement | undefined | null) => {
        if (!ref) return this.props.ScreenToLocalTransform();
        const { scale, translateX, translateY } = Utils.GetScreenTransform(ref);
        const outerXf = Utils.GetScreenTransform(this.props.treeView.MainEle());
        const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
        return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]);
    };
    docTransform = () => this.refTransform(this._dref?.ContentRef?.current);
    getTransform = () => this.refTransform(this._tref.current);
    embeddedPanelWidth = () => this.props.panelWidth() / (this.props.treeView.props.NativeDimScaling?.() || 1);
    embeddedPanelHeight = () => {
        const layoutDoc = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc;
        return Math.min(
            NumCast(layoutDoc._height),
            this.MAX_EMBED_HEIGHT,
            (() => {
                const aspect = Doc.NativeAspect(layoutDoc);
                if (aspect) return this.embeddedPanelWidth() / (aspect || 1);
                return layoutDoc._layout_fitWidth
                    ? !Doc.NativeHeight(layoutDoc)
                        ? NumCast(layoutDoc._height)
                        : Math.min((this.embeddedPanelWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc))) / (Doc.NativeWidth(layoutDoc) || NumCast(this.props.treeViewParent._height)))
                    : (this.embeddedPanelWidth() * NumCast(layoutDoc._height)) / NumCast(layoutDoc._width);
            })()
        );
    };
    @computed get expandedField() {
        const ids: { [key: string]: string } = {};
        const rows: JSX.Element[] = [];
        const doc = this.doc;
        doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key));

        for (const key of Object.keys(ids).slice().sort()) {
            if (this.props.skipFields?.includes(key) || key === 'title' || key === 'treeView_Open') continue;
            const contents = doc[key];
            let contentElement: (JSX.Element | null)[] | JSX.Element = [];

            let leftOffset = observable({ width: 0 });
            const expandedWidth = () => this.props.panelWidth() - leftOffset.width;
            if (contents instanceof Doc || (Cast(contents, listSpec(Doc)) && Cast(contents, listSpec(Doc))!.length && Cast(contents, listSpec(Doc))![0] instanceof Doc)) {
                const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key);
                const moveDoc = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.move(doc, target, addDoc);
                const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => {
                    const innerAdd = (doc: Doc) => {
                        const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField;
                        const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true);
                        dataIsComputed && Doc.SetContainer(doc, DocCast(this.doc.embedContainer));
                        return added;
                    };
                    return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean);
                };
                contentElement = TreeView.GetChildElements(
                    contents instanceof Doc ? [contents] : DocListCast(contents),
                    this.props.treeView,
                    this,
                    doc,
                    undefined,
                    this.props.treeViewParent,
                    this.props.prevSibling,
                    addDoc,
                    remDoc,
                    moveDoc,
                    this.props.dragAction,
                    this.props.addDocTab,
                    this.titleStyleProvider,
                    this.props.ScreenToLocalTransform,
                    this.props.isContentActive,
                    expandedWidth,
                    this.props.renderDepth,
                    this.props.treeViewHideHeaderFields,
                    [...this.props.renderedIds, doc[Id]],
                    this.props.onCheckedClick,
                    this.props.onChildClick,
                    this.props.skipFields,
                    false,
                    this.props.whenChildContentsActiveChanged,
                    this.props.dontRegisterView,
                    emptyFunction,
                    emptyFunction,
                    this.childContextMenuItems(),
                    // TODO: [AL] Add these
                    this.props.AddToMap,
                    this.props.RemFromMap,
                    this.props.hierarchyIndex
                );
            } else {
                contentElement = (
                    <EditableView
                        key="editableView"
                        contents={contents !== undefined ? Field.toString(contents as Field) : 'null'}
                        height={13}
                        fontSize={12}
                        GetValue={() => Field.toKeyValueString(doc, key)}
                        SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)}
                    />
                );
            }
            rows.push(
                <div style={{ display: 'flex', overflow: 'auto' }} key={key}>
                    <span
                        ref={action((r: any) => {
                            if (r) leftOffset.width = r.getBoundingClientRect().width;
                        })}
                        style={{ fontWeight: 'bold' }}>
                        {key + ':'}
                        &nbsp;
                    </span>
                    {contentElement}
                </div>
            );
        }
        rows.push(
            <div style={{ display: 'flex', overflow: 'auto' }} key={'newKeyValue'}>
                <EditableView
                    key="editableView"
                    contents={'+key:value'}
                    height={13}
                    fontSize={12}
                    GetValue={returnEmptyString}
                    SetValue={value => value.indexOf(':') !== -1 && KeyValueBox.SetField(doc, value.substring(0, value.indexOf(':')), value.substring(value.indexOf(':') + 1, value.length), true)}
                />
            </div>
        );
        return rows;
    }

    _renderTimer: any;
    @observable _renderCount = 1;
    @computed get renderContent() {
        TraceMobx();
        const expandKey = this.treeViewExpandedView;
        const sortings = (this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewSortings) as { [key: string]: { color: string; icon: JSX.Element | string } }) ?? {};
        if (['links', 'annotations', 'embeddings', this.fieldKey].includes(expandKey)) {
            const sorting = StrCast(this.doc.treeView_SortCriterion, TreeSort.WhenAdded);
            const sortKeys = Object.keys(sortings);
            const curSortIndex = Math.max(
                0,
                sortKeys.findIndex(val => val === sorting)
            );
            const key = (expandKey === 'annotations' ? `${this.fieldKey}-` : '') + expandKey;
            const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key);
            const moveDoc = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.move(doc, target, addDoc);
            const localAdd = (doc: Doc, addBefore?: Doc, before?: boolean) => {
                // if there's a sort ordering specified that can be modified on drop (eg, zorder can be modified, alphabetical can't),
                // then the modification would be done here
                const ordering = StrCast(this.doc.treeView_SortCriterion);
                if (ordering === TreeSort.Zindex) {
                    const docs = TreeView.sortDocs(this.childDocs || ([] as Doc[]), ordering);
                    doc.zIndex = addBefore ? NumCast(addBefore.zIndex) + (before ? -0.5 : 0.5) : 1000;
                    docs.push(doc);
                    docs.sort((a, b) => (NumCast(a.zIndex) > NumCast(b.zIndex) ? 1 : -1)).forEach((d, i) => (d.zIndex = i));
                }
                const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField;
                const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true);
                !dataIsComputed && added && Doc.SetContainer(doc, this.doc);

                return added;
            };
            const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true);
            const docs = expandKey === 'embeddings' ? this.childEmbeddings : expandKey === 'links' ? this.childLinks : expandKey === 'annotations' ? this.childAnnos : this.childDocs;
            let downX = 0,
                downY = 0;
            if (docs?.length && this._renderCount < docs?.length) {
                this._renderTimer && clearTimeout(this._renderTimer);
                this._renderTimer = setTimeout(
                    action(() => {
                        this._renderCount = Math.min(docs!.length, this._renderCount + 20);
                    })
                );
            }
            return (
                <div>
                    {!docs?.length || this.props.AddToMap /* hack to identify pres box trees */ ? null : (
                        <div className="treeView-sorting">
                            <IconButton
                                color={sortings[sorting]?.color}
                                size={Size.XSMALL}
                                tooltip={`Sorted by : ${this.doc.treeView_SortCriterion}. click to cycle`}
                                icon={sortings[sorting]?.icon}
                                onPointerDown={e => {
                                    downX = e.clientX;
                                    downY = e.clientY;
                                    e.stopPropagation();
                                }}
                                onClick={undoable(e => {
                                    if (this.props.isContentActive() && Math.abs(e.clientX - downX) < 3 && Math.abs(e.clientY - downY) < 3) {
                                        !this.props.treeView.outlineMode && (this.doc.treeView_SortCriterion = sortKeys[(curSortIndex + 1) % sortKeys.length]);
                                        e.stopPropagation();
                                    }
                                }, 'sort order')}
                            />
                        </div>
                    )}
                    <ul
                        style={{ cursor: 'inherit' }}
                        key={expandKey + 'more'}
                        title={`Sorted by : ${this.doc.treeView_SortCriterion}. click to cycle`}
                        className="" //this.doc.treeView_HideTitle ? 'no-indent' : ''}
                        onPointerDown={e => {
                            downX = e.clientX;
                            downY = e.clientY;
                            e.stopPropagation();
                        }}
                        onClick={undoable(e => {
                            if (this.props.isContentActive() && Math.abs(e.clientX - downX) < 3 && Math.abs(e.clientY - downY) < 3) {
                                !this.props.treeView.outlineMode && (this.doc.treeView_SortCriterion = sortKeys[(curSortIndex + 1) % sortKeys.length]);
                                e.stopPropagation();
                            }
                        }, 'sort order')}>
                        {!docs
                            ? null
                            : TreeView.GetChildElements(
                                  docs,
                                  this.props.treeView,
                                  this,
                                  this.layoutDoc,
                                  this.dataDoc,
                                  this.props.treeViewParent,
                                  this.props.prevSibling,
                                  addDoc,
                                  remDoc,
                                  moveDoc,
                                  StrCast(this.doc.childDragAction, this.props.dragAction) as dropActionType,
                                  this.props.addDocTab,
                                  this.titleStyleProvider,
                                  this.props.ScreenToLocalTransform,
                                  this.props.isContentActive,
                                  this.props.panelWidth,
                                  this.props.renderDepth,
                                  this.props.treeViewHideHeaderFields,
                                  [...this.props.renderedIds, this.doc[Id]],
                                  this.props.onCheckedClick,
                                  this.props.onChildClick,
                                  this.props.skipFields,
                                  false,
                                  this.props.whenChildContentsActiveChanged,
                                  this.props.dontRegisterView,
                                  emptyFunction,
                                  emptyFunction,
                                  this.childContextMenuItems(),
                                  // TODO: [AL] add these
                                  this.props.AddToMap,
                                  this.props.RemFromMap,
                                  this.props.hierarchyIndex,
                                  this._renderCount
                              )}
                    </ul>
                </div>
            );
        } else if (this.treeViewExpandedView === 'fields') {
            return (
                <ul key={this.doc[Id] + this.doc.title} style={{ cursor: 'inherit' }}>
                    <div>{this.expandedField}</div>
                </ul>
            );
        }
        return (
            <ul
                style={{}}
                onPointerDown={e => {
                    e.preventDefault();
                    e.stopPropagation();
                }}>
                {this.renderEmbeddedDocument(false, this.props.treeView.props.childDocumentsActive ?? returnFalse)}
            </ul>
        ); // "layout"
    }

    get onCheckedClick() {
        return this.doc.type === DocumentType.COL ? undefined : this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick);
    }

    @action
    bulletClick = (e: React.MouseEvent) => {
        if (this.onCheckedClick) {
            this.onCheckedClick?.script.run(
                {
                    this: this.doc.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.doc,
                    heading: this.props.treeViewParent.title,
                    checked: this.doc.treeView_Checked === 'check' ? 'x' : this.doc.treeView_Checked === 'x' ? 'remove' : 'check',
                    containingTreeView: this.props.treeView.props.Document,
                },
                console.log
            );
        } else {
            this.treeViewOpen = !this.treeViewOpen;
        }
        e.stopPropagation();
    };

    @computed get renderBullet() {
        TraceMobx();
        const iconType = this.props.treeView.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewIcon + (this.treeViewOpen ? ':open' : !this.childDocs.length ? ':empty' : '')) || 'question';
        const color = SettingsManager.userColor;
        const checked = this.onCheckedClick ? this.doc.treeView_Checked ?? 'unchecked' : undefined;
        return (
            <div
                className={`bullet${this.props.treeView.outlineMode ? '-outline' : ''}`}
                key="bullet"
                title={this.childDocs?.length ? `click to see ${this.childDocs?.length} items` : `view ${this.props.document.type} content`}
                onClick={this.bulletClick}
                style={
                    this.props.treeView.outlineMode
                        ? {
                              opacity: this.titleStyleProvider?.(this.doc, this.props.treeView.props, StyleProp.Opacity),
                          }
                        : {
                              pointerEvents: this.props.isContentActive() ? 'all' : undefined,
                              opacity: checked === 'unchecked' || typeof iconType !== 'string' ? undefined : 0.4,
                              color: checked === 'unchecked' ? SettingsManager.userColor : 'inherit',
                          }
                }>
                {this.props.treeView.outlineMode ? (
                    !(this.doc.text as RichTextField)?.Text ? null : (
                        <IconButton color={color} icon={<FontAwesomeIcon icon={[this.childDocs?.length && !this.treeViewOpen ? 'fas' : 'far', 'circle']} />} size={Size.XSMALL} />
                    )
                ) : (
                    <div className="treeView-bulletIcons">
                        <div className={`treeView-${this.onCheckedClick ? 'checkIcon' : 'expandIcon'}`}>
                            <FontAwesomeIcon
                                size="sm"
                                style={{ display: this.childDocs?.length >= 1 ? 'block' : 'none' }}
                                icon={checked === 'check' ? 'check' : checked === 'x' ? 'times' : checked === 'unchecked' ? 'square' : !this.treeViewOpen ? 'caret-right' : 'caret-down'}
                            />
                        </div>
                        {this.onCheckedClick ? null : typeof iconType === 'string' ? <FontAwesomeIcon icon={iconType as IconProp} /> : iconType}
                    </div>
                )}
            </div>
        );
    }

    @computed get validExpandViewTypes() {
        const annos = () => (DocListCast(this.doc[this.fieldKey + '_annotations']).length && !this.props.treeView.dashboardMode ? 'annotations' : '');
        const links = () => (LinkManager.Links(this.doc).length && !this.props.treeView.dashboardMode ? 'links' : '');
        const data = () => (this.childDocs || this.props.treeView.dashboardMode ? this.fieldKey : '');
        const embeddings = () => (this.props.treeView.dashboardMode ? '' : 'embeddings');
        const fields = () => (Doc.noviceMode ? '' : 'fields');
        const layout = Doc.noviceMode || this.doc._type_collection === CollectionViewType.Docking ? [] : ['layout'];
        return [data(), ...layout, ...(this.props.treeView.fileSysMode ? [embeddings(), links(), annos()] : []), fields()].filter(m => m);
    }
    @action
    expandNextviewType = () => {
        if (this.treeViewOpen && !this.doc.isFolder && !this.props.treeView.outlineMode && !this.doc.treeView_ExpandedViewLock) {
            const next = (modes: any[]) => modes[(modes.indexOf(StrCast(this.treeViewExpandedView)) + 1) % modes.length];
            this.doc.treeView_ExpandedView = next(this.validExpandViewTypes);
        }
        this.treeViewOpen = true;
    };

    @observable headerEleWidth = 0;
    @computed get titleButtons() {
        const customHeaderButtons = this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.Decorations);
        const color = SettingsManager.userColor;
        return this.props.treeViewHideHeaderFields() || this.doc.treeView_HideHeaderFields ? null : (
            <>
                {customHeaderButtons} {/* e.g.,. hide button is set by dashboardStyleProvider */}
                <IconButton
                    color={color}
                    icon={<FontAwesomeIcon icon="bars" />}
                    size={Size.XSMALL}
                    onClick={e => {
                        this.showContextMenu(e);
                        e.stopPropagation();
                    }}
                />
                {Doc.noviceMode ? null : this.doc.treeView_ExpandedViewLock || Doc.IsSystem(this.doc) ? null : (
                    <span className="collectionTreeView-keyHeader" title="type of expanded data" key={this.treeViewExpandedView} onPointerDown={this.expandNextviewType}>
                        {this.treeViewExpandedView}
                    </span>
                )}
            </>
        );
    }

    showContextMenu = (e: React.MouseEvent) => {
        DocumentViewInternal.SelectAfterContextMenu = false;
        simulateMouseClick(this._docRef?.ContentDiv, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30);
        DocumentViewInternal.SelectAfterContextMenu = true;
    };
    contextMenuItems = () => {
        const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: 'any' })!, icon: 'folder-plus', label: 'New Folder' };
        const folderOp = this.childDocs?.length ? [makeFolder] : [];
        const openEmbedding = { script: ScriptField.MakeFunction(`openDoc(getEmbedding(self), "${OpenWhere.addRight}")`)!, icon: 'copy', label: 'Open New Embedding' };
        const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: 'eye', label: 'Focus or Open' };
        const reopenDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: 'eye', label: 'Reopen' };
        return [
            ...(this.props.contextMenuItems ?? []).filter(mi => (!mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result)),
            ...(this.doc.isFolder
                ? folderOp
                : Doc.IsSystem(this.doc)
                ? []
                : this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc)
                ? [openEmbedding, makeFolder]
                : this.doc._type_collection === CollectionViewType.Docking
                ? []
                : this.props.treeView.rootDoc === Doc.MyRecentlyClosed
                ? [reopenDoc]
                : [openEmbedding, focusDoc]),
        ];
    };
    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 }));
    };

    onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!);

    onChildDoubleClick = () => ScriptCast(this.props.treeView.Document.treeView_ChildDoubleClick, !this.props.treeView.outlineMode ? this._openScript?.() : null);

    refocus = () => this.props.treeView.props.focus(this.props.treeView.props.Document, {});
    ignoreEvent = (e: any) => {
        if (this.props.isContentActive(true)) {
            e.stopPropagation();
            e.preventDefault();
        }
    };
    titleStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
        if (!doc || doc !== this.doc) return this.props?.treeView?.props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView

        const treeView = this.props.treeView;
        // prettier-ignore
        switch (property.split(':')[0]) {
            case StyleProp.Opacity:         return this.props.treeView.outlineMode ? undefined : 1;
            case StyleProp.BackgroundColor: return this.selected ? '#7089bb' : StrCast(doc._backgroundColor, StrCast(doc.backgroundColor));
            case StyleProp.Highlighting:    if (this.props.treeView.outlineMode) return undefined;
            case StyleProp.BoxShadow:       return undefined;
            case StyleProp.DocContents:
                const highlightIndex = this.props.treeView.outlineMode ? Doc.DocBrushStatus.unbrushed : Doc.GetBrushHighlightStatus(doc);
                const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
                return treeView.outlineMode ? null : (
                    <div
                        className="treeView-label"
                        style={{
                            // just render a title for a tree view label (identified by treeViewDoc being set in 'props')
                            maxWidth: props?.PanelWidth() || undefined,
                            background: props?.styleProvider?.(doc, props, StyleProp.BackgroundColor),
                            outline: SnappingManager.GetIsDragging() ? undefined: `solid ${highlightColor} ${highlightIndex}px`,
                            paddingLeft: NumCast(treeView.rootDoc.childXPadding, NumCast(treeView.props.childXPadding, Doc.IsComicStyle(doc)?20:0)),
                            paddingRight: NumCast(treeView.rootDoc.childXPadding, NumCast(treeView.props.childXPadding, Doc.IsComicStyle(doc)?20:0)),
                            paddingTop: treeView.props.childYPadding,
                            paddingBottom: treeView.props.childYPadding,
                        }}>
                        {StrCast(doc?.title)}
                    </div>
                );
        }
        return treeView.props.styleProvider?.(doc, props, property);
    };
    embeddedStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
        if (property.startsWith(StyleProp.Decorations)) return null;
        return this.props?.treeView?.props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView
    };
    onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => {
        if (this.doc.treeView_HideHeader || (this.doc.treeView_HideHeaderIfTemplate && this.props.treeView.props.childLayoutTemplate?.()) || this.props.treeView.outlineMode) {
            switch (e.key) {
                case 'Tab':
                    e.stopPropagation?.();
                    e.preventDefault?.();
                    setTimeout(() => RichTextMenu.Instance.TextView?.EditorView?.focus(), 150);
                    UndoManager.RunInBatch(() => (e.shiftKey ? this.props.outdentDocument?.(true) : this.props.indentDocument?.(true)), 'tab');
                    return true;
                case 'Backspace':
                    if (!(this.doc.text as RichTextField)?.Text && this.props.removeDoc?.(this.doc)) {
                        e.stopPropagation?.();
                        e.preventDefault?.();
                        return true;
                    }
                    break;
                case 'Enter':
                    e.stopPropagation?.();
                    e.preventDefault?.();
                    return UndoManager.RunInBatch(this.makeTextCollection, 'bullet');
            }
        }
        return false;
    };
    titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth())) / (this.props.treeView.props.NativeDimScaling?.() || 1) - this.headerEleWidth - treeBulletWidth();

    /**
     * Renders the EditableView title element for placement into the tree.
     */
    @computed
    get renderTitle() {
        TraceMobx();
        const view = this._editTitle ? (
            <EditableView
                key="_editTitle"
                oneLine={true}
                display={'inline-block'}
                editing={this._editTitle}
                background={'#7089bb'}
                contents={StrCast(this.doc.title)}
                height={12}
                sizeToContent={true}
                fontSize={12}
                isEditingCallback={action(e => (this._editTitle = e))}
                GetValue={() => StrCast(this.doc.title)}
                OnTab={undoBatch((shift?: boolean) => {
                    if (!shift) this.props.indentDocument?.(true);
                    else this.props.outdentDocument?.(true);
                })}
                OnEmpty={undoBatch(() => this.props.treeView.outlineMode && this.props.removeDoc?.(this.doc))}
                OnFillDown={val => this.props.treeView.fileSysMode && this.makeFolder()}
                SetValue={undoBatch((value: string, shiftKey: boolean, enterKey: boolean) => {
                    Doc.SetInPlace(this.doc, 'title', value, false);
                    this.props.treeView.outlineMode && enterKey && this.makeTextCollection();
                })}
            />
        ) : (
            <DocumentView
                key="title"
                ref={action((r: any) => {
                    this._docRef = r ? r : undefined;
                    if (this._docRef && TreeView._editTitleOnLoad?.id === this.props.document[Id] && TreeView._editTitleOnLoad.parent === this.props.parentTreeView) {
                        this._docRef.select(false);
                        this.setEditTitle(this._docRef);
                        TreeView._editTitleOnLoad = undefined;
                    }
                })}
                Document={this.doc}
                DataDoc={undefined} // or this.dataDoc?
                layout_fitWidth={returnTrue}
                scriptContext={this}
                hideDecorationTitle={this.props.treeView.outlineMode}
                hideResizeHandles={this.props.treeView.outlineMode}
                styleProvider={this.titleStyleProvider}
                onClickScriptDisable="never" // tree docViews have a script to show fields, etc.
                docViewPath={this.props.treeView.props.docViewPath}
                treeViewDoc={this.props.treeView.props.Document}
                addDocument={undefined}
                addDocTab={this.props.addDocTab}
                rootSelected={returnFalse}
                pinToPres={emptyFunction}
                onClick={this.onChildClick}
                onDoubleClick={this.onChildDoubleClick}
                dragAction={this.props.dragAction}
                moveDocument={this.move}
                removeDocument={this.props.removeDoc}
                ScreenToLocalTransform={this.getTransform}
                NativeHeight={return18}
                NativeWidth={returnZero}
                shouldNotScale={returnTrue}
                PanelWidth={this.titleWidth}
                PanelHeight={return18}
                contextMenuItems={this.contextMenuItems}
                renderDepth={1}
                isContentActive={emptyFunction} //this.props.isContentActive}
                isDocumentActive={this.props.isContentActive}
                focus={this.refocus}
                whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
                bringToFront={emptyFunction}
                disableBrushing={this.props.treeView.props.disableBrushing}
                hideLinkButton={BoolCast(this.props.treeView.props.Document.childHideLinkButton)}
                dontRegisterView={BoolCast(this.props.treeView.props.Document.childDontRegisterViews, this.props.dontRegisterView)}
                xPadding={NumCast(this.props.treeView.props.Document.childXPadding, this.props.treeView.props.childXPadding)}
                yPadding={NumCast(this.props.treeView.props.Document.childYPadding, this.props.treeView.props.childYPadding)}
                childFilters={returnEmptyFilter}
                childFiltersByRanges={returnEmptyFilter}
                searchFilterDocs={returnEmptyDoclist}
            />
        );
        return (
            <>
                <div
                    className={`docContainer${Doc.IsSystem(this.props.document) || this.props.document.isFolder ? '-system' : ''}`}
                    ref={this._tref}
                    title="click to edit title. Double Click or Drag to Open"
                    style={{
                        backgroundColor: Doc.IsSystem(this.props.document) || this.props.document.isFolder ? SettingsManager.userVariantColor : undefined,
                        color: Doc.IsSystem(this.props.document) || this.props.document.isFolder ? lightOrDark(SettingsManager.userVariantColor) : undefined,
                        fontWeight: Doc.IsSearchMatch(this.doc) !== undefined ? 'bold' : undefined,
                        textDecoration: Doc.GetT(this.doc, 'title', 'string', true) ? 'underline' : undefined,
                        outline: this.doc === Doc.ActiveDashboard ? 'dashed 1px #06123232' : undefined,
                        pointerEvents: !this.props.isContentActive() ? 'none' : undefined,
                    }}>
                    {view}
                </div>
                <div className="treeView-rightButtons" ref={action((r: any) => r && (this.headerEleWidth = r.getBoundingClientRect().width))}>
                    {this.titleButtons}
                </div>
            </>
        );
    }

    renderBulletHeader = (contents: JSX.Element, editing: boolean) => {
        return (
            <>
                <div
                    className={`treeView-header` + (editing ? '-editing' : '')}
                    key="titleheader"
                    ref={this._header}
                    onClick={this.ignoreEvent}
                    onPointerDown={this.ignoreEvent}
                    onPointerEnter={this.onPointerEnter}
                    onPointerLeave={this.onPointerLeave}>
                    <div
                        className="treeView-background"
                        style={{
                            background: SettingsManager.userColor,
                        }}
                    />
                    {contents}
                </div>
                {this.renderBorder}
            </>
        );
    };

    fitWidthFilter = (doc: Doc) => (doc.type === DocumentType.IMG ? false : undefined);
    renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => {
        return (
            <div style={{ height: this.embeddedPanelHeight(), width: this.embeddedPanelWidth() }}>
                <DocumentView
                    key={this.doc[Id]}
                    ref={action((r: DocumentView | null) => (this._dref = r))}
                    Document={this.doc}
                    DataDoc={undefined}
                    layout_fitWidth={this.fitWidthFilter}
                    PanelWidth={this.embeddedPanelWidth}
                    PanelHeight={this.embeddedPanelHeight}
                    LayoutTemplateString={asText ? FormattedTextBox.LayoutString('text') : undefined}
                    LayoutTemplate={this.props.treeView.props.childLayoutTemplate}
                    isContentActive={isActive}
                    isDocumentActive={isActive}
                    styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider}
                    hideTitle={asText}
                    //fitContentsToBox={returnTrue}
                    hideDecorationTitle={this.props.treeView.outlineMode}
                    hideResizeHandles={this.props.treeView.outlineMode}
                    onClick={this.onChildClick}
                    focus={this.refocus}
                    onKey={this.onKeyDown}
                    hideLinkButton={BoolCast(this.props.treeView.props.Document.childHideLinkButton)}
                    dontRegisterView={BoolCast(this.props.treeView.props.Document.childDontRegisterViews, this.props.dontRegisterView)}
                    ScreenToLocalTransform={this.docTransform}
                    renderDepth={this.props.renderDepth + 1}
                    treeViewDoc={this.props.treeView?.props.Document}
                    rootSelected={returnFalse}
                    docViewPath={this.props.treeView.props.docViewPath}
                    childFilters={returnEmptyFilter}
                    childFiltersByRanges={returnEmptyFilter}
                    searchFilterDocs={returnEmptyDoclist}
                    addDocument={this.props.addDocument}
                    moveDocument={this.move}
                    removeDocument={this.props.removeDoc}
                    whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
                    xPadding={NumCast(this.props.treeView.props.Document.childXPadding, this.props.treeView.props.childXPadding)}
                    yPadding={NumCast(this.props.treeView.props.Document.childYPadding, this.props.treeView.props.childYPadding)}
                    addDocTab={this.props.addDocTab}
                    pinToPres={this.props.treeView.props.pinToPres}
                    disableBrushing={this.props.treeView.props.disableBrushing}
                    bringToFront={returnFalse}
                    scriptContext={this}
                />
            </div>
        );
    };

    // renders the text version of a document as the header.  This is used in the file system mode and in other vanilla tree views.
    @computed get renderTitleAsHeader() {
        return this.props.treeView.Document.treeView_HideUnrendered && this.doc.layout_unrendered && !this.doc.treeView_FieldKey ? (
            <div></div>
        ) : (
            <>
                {this.renderBullet}
                {this.renderTitle}
            </>
        );
    }

    // renders the document in the header field instead of a text proxy.
    renderDocumentAsHeader = (asText: boolean) => {
        return (
            <>
                {this.renderBullet}
                {this.renderEmbeddedDocument(asText, this.props.isContentActive)}
            </>
        );
    };

    @computed get renderBorder() {
        const sorting = StrCast(this.doc.treeView_SortCriterion, TreeSort.WhenAdded);
        const sortings = (this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewSortings) ?? {}) as { [key: string]: { color: string; label: string } };
        return (
            <div className={`treeView-border${this.props.treeView.outlineMode ? TreeViewType.outline : ''}`} style={{ borderColor: sortings[sorting]?.color }}>
                {!this.treeViewOpen ? null : this.renderContent}
            </div>
        );
    }

    onTreeDrop = (de: React.DragEvent) => {
        const pt = [de.clientX, de.clientY];
        const rect = this._header.current!.getBoundingClientRect();
        const before = pt[1] < rect.top + rect.height / 2;
        const inside = this.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > rect.left + rect.width * 0.33 || (!before && this.treeViewOpen && this.childDocs?.length ? true : false);

        const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, 'copy', undefined, undefined, false));
    };

    render() {
        TraceMobx();
        const hideTitle = this.doc.treeView_HideHeader || (this.doc.treeView_HideHeaderIfTemplate && this.props.treeView.props.childLayoutTemplate?.()) || this.props.treeView.outlineMode;
        return this.props.renderedIds?.indexOf(this.doc[Id]) !== -1 ? (
            '<' + this.doc.title + '>' // just print the title of documents we've previously rendered in this hierarchical path to avoid cycles
        ) : (
            <div
                className={`treeView-container${this.props.isContentActive() ? '-active' : ''}`}
                ref={this.createTreeDropTarget}
                onDrop={this.onTreeDrop}
                //onPointerDown={e => this.props.isContentActive(true) && SelectionManager.DeselectAll()} // bcz: this breaks entering a text filter in a filterBox since it deselects the filter's target document
                // onKeyDown={this.onKeyDown}
            >
                <li className="collection-child">
                    {hideTitle && this.doc.type !== DocumentType.RTF && !this.doc.treeView_RenderAsBulletHeader // should test for prop 'treeView_RenderDocWithBulletAsHeader"
                        ? this.renderEmbeddedDocument(false, returnFalse)
                        : this.renderBulletHeader(hideTitle ? this.renderDocumentAsHeader(!this.doc.treeView_RenderAsBulletHeader) : this.renderTitleAsHeader, this._editTitle)}
                </li>
            </div>
        );
    }

    public static sortDocs(childDocs: Doc[], criterion: string | undefined) {
        const docs = childDocs.slice();
        if (criterion !== TreeSort.WhenAdded) {
            const sortAlphaNum = (a: string, b: string): 0 | 1 | -1 => {
                const reN = /[0-9]*$/;
                const aA = a.replace(reN, '') ? a.replace(reN, '') : +a; // get rid of trailing numbers
                const bA = b.replace(reN, '') ? b.replace(reN, '') : +b;
                if (aA === bA) {
                    // if header string matches, then compare numbers numerically
                    const aN = parseInt(a.match(reN)![0], 10);
                    const bN = parseInt(b.match(reN)![0], 10);
                    return aN === bN ? 0 : aN > bN ? 1 : -1;
                } else {
                    return aA > bA ? 1 : -1;
                }
            };
            docs.sort(function (d1, d2): 0 | 1 | -1 {
                const a = criterion === TreeSort.AlphaUp ? d2 : d1;
                const b = criterion === TreeSort.AlphaUp ? d1 : d2;
                const first = a[criterion === TreeSort.Zindex ? 'zIndex' : 'title'];
                const second = b[criterion === TreeSort.Zindex ? 'zIndex' : 'title'];
                if (typeof first === 'number' && typeof second === 'number') return first - second > 0 ? 1 : -1;
                if (typeof first === 'string' && typeof second === 'string') return sortAlphaNum(first, second);
                return criterion ? 1 : -1;
            });
        }
        return docs;
    }

    public static GetChildElements(
        childDocs: Doc[],
        treeView: CollectionTreeView,
        parentTreeView: CollectionTreeView | TreeView | undefined,
        treeView_Parent: Doc,
        dataDoc: Doc | undefined,
        parentCollectionDoc: Doc | undefined,
        containerPrevSibling: Doc | undefined,
        add: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean,
        remove: undefined | ((doc: Doc | Doc[]) => boolean),
        move: DragManager.MoveFunction,
        dragAction: dropActionType,
        addDocTab: (doc: Doc, where: OpenWhere) => boolean,
        styleProvider: undefined | StyleProviderFunc,
        screenToLocalXf: () => Transform,
        isContentActive: (outsideReaction?: boolean) => boolean,
        panelWidth: () => number,
        renderDepth: number,
        treeView_HideHeaderFields: () => boolean,
        renderedIds: string[],
        onCheckedClick: undefined | (() => ScriptField),
        onChildClick: undefined | (() => ScriptField),
        skipFields: string[] | undefined,
        firstLevel: boolean,
        whenChildContentsActiveChanged: (isActive: boolean) => void,
        dontRegisterView: boolean | undefined,
        observerHeight: (ref: any) => void,
        unobserveHeight: (ref: any) => void,
        contextMenuItems: { script: ScriptField; filter: ScriptField; label: string; icon: string }[],
        // TODO: [AL] add these
        AddToMap?: (treeViewDoc: Doc, index: number[]) => void,
        RemFromMap?: (treeViewDoc: Doc, index: number[]) => void,
        hierarchyIndex?: number[],
        renderCount?: number
    ) {
        const viewSpecScript = Cast(treeView_Parent.viewSpecScript, ScriptField);
        if (viewSpecScript) {
            childDocs = childDocs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result);
        }

        const docs = TreeView.sortDocs(childDocs, StrCast(treeView_Parent.treeView_SortCriterion, TreeSort.WhenAdded));
        const rowWidth = () => panelWidth() - treeBulletWidth() * (treeView.props.NativeDimScaling?.() || 1);
        const treeView_Refs = new Map<Doc, TreeView | undefined>();
        return docs
            .filter(child => child instanceof Doc)
            .map((child, i) => {
                if (renderCount && i > renderCount) return null;
                const pair = Doc.GetLayoutDataDocPair(treeView_Parent, dataDoc, child);
                if (!pair.layout || pair.data instanceof Promise) {
                    return null;
                }

                const dentDoc = (editTitle: boolean, newParent: Doc, addAfter: Doc | undefined, parent: TreeView | CollectionTreeView | undefined) => {
                    if (parent instanceof TreeView && parent.props.treeView.fileSysMode && !newParent.isFolder) return;
                    const fieldKey = Doc.LayoutFieldKey(newParent);
                    if (remove && fieldKey && Cast(newParent[fieldKey], listSpec(Doc)) !== undefined) {
                        remove(child);
                        FormattedTextBox.SelectOnLoad = child[Id];
                        TreeView._editTitleOnLoad = editTitle ? { id: child[Id], parent } : undefined;
                        Doc.AddDocToList(newParent, fieldKey, child, addAfter, false);
                        newParent.treeView_Open = true;
                        Doc.SetContainer(child, treeView.Document);
                    }
                };
                const indent = i === 0 ? undefined : (editTitle: boolean) => dentDoc(editTitle, docs[i - 1], undefined, treeView_Refs.get(docs[i - 1]));
                const outdent = !parentCollectionDoc ? undefined : (editTitle: boolean) => dentDoc(editTitle, parentCollectionDoc, containerPrevSibling, parentTreeView instanceof TreeView ? parentTreeView.props.parentTreeView : undefined);
                const addDocument = (doc: Doc | Doc[], annotationKey?: string, relativeTo?: Doc, before?: boolean) => add(doc, relativeTo ?? docs[i], before !== undefined ? before : false);
                const childLayout = Doc.Layout(pair.layout);
                const rowHeight = () => {
                    const aspect = Doc.NativeAspect(childLayout);
                    return aspect ? Math.min(NumCast(childLayout._width), rowWidth()) / aspect : NumCast(childLayout._height);
                };
                return (
                    <TreeView
                        key={child[Id]}
                        ref={r => treeView_Refs.set(child, r ? r : undefined)}
                        document={pair.layout}
                        dataDoc={pair.data}
                        treeViewParent={treeView_Parent}
                        prevSibling={docs[i]}
                        // TODO: [AL] add these
                        hierarchyIndex={hierarchyIndex ? [...hierarchyIndex, i + 1] : undefined}
                        AddToMap={AddToMap}
                        RemFromMap={RemFromMap}
                        treeView={treeView}
                        indentDocument={indent}
                        outdentDocument={outdent}
                        onCheckedClick={onCheckedClick}
                        onChildClick={onChildClick}
                        renderDepth={renderDepth}
                        removeDoc={StrCast(treeView_Parent.treeView_FreezeChildren).includes('remove') ? undefined : remove}
                        addDocument={addDocument}
                        styleProvider={styleProvider}
                        panelWidth={rowWidth}
                        panelHeight={rowHeight}
                        dontRegisterView={dontRegisterView}
                        moveDocument={move}
                        dragAction={dragAction}
                        addDocTab={addDocTab}
                        ScreenToLocalTransform={screenToLocalXf}
                        isContentActive={isContentActive}
                        treeViewHideHeaderFields={treeView_HideHeaderFields}
                        renderedIds={renderedIds}
                        skipFields={skipFields}
                        firstLevel={firstLevel}
                        whenChildContentsActiveChanged={whenChildContentsActiveChanged}
                        parentTreeView={parentTreeView}
                        observeHeight={observerHeight}
                        unobserveHeight={unobserveHeight}
                        contextMenuItems={contextMenuItems}
                    />
                );
            });
    }
}

ScriptingGlobals.add(function TreeView_addNewFolder() {
    TreeView._editTitleOnLoad = { id: Utils.GenerateGuid(), parent: undefined };
    const opts = { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true };
    return Doc.AddDocToList(Doc.MyFilesystem, 'data', Docs.Create.TreeDocument([], opts, TreeView._editTitleOnLoad.id));
});
