import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ClientUtils, DivHeight, lightOrDark, returnZero, smoothScroll } from '../../../ClientUtils';
import { Doc, Opt, StrListCast } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { Copy, Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { emptyFunction } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { DocUtils } from '../../documents/DocUtils';
import { DragManager } from '../../util/DragManager';
import { dropActionType } from '../../util/DropActionTypes';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoable, undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { FieldsDropdown } from '../FieldsDropdown';
import { Colors } from '../global/globalEnums';
import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
import { DocumentView } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { FocusViewOptions } from '../nodes/FocusViewOptions';
import { StyleProp } from '../StyleProp';
import './CollectionNoteTakingView.scss';
import { CollectionNoteTakingViewColumn } from './CollectionNoteTakingViewColumn';
import { CollectionNoteTakingViewDivider } from './CollectionNoteTakingViewDivider';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
import { Property } from 'csstype';

/**
 * CollectionNoteTakingView is a column-based view for displaying documents. In this view, the user can (1)
 * add and remove columns (2) change column sizes and (3) move documents within and between columns. This
 * view is reminiscent of Kanban-style web apps like Trello, or the 'Board' view in Notion. Each column is
 * headed by a SchemaHeaderField followed by the column's documents. SchemaHeaderFields are NOT present in
 * the rest of Dash, so it may be worthwhile to transition the headers to simple documents.
 */
@observer
export class CollectionNoteTakingView extends CollectionSubView() {
    _disposers: { [key: string]: IReactionDisposer } = {};
    _masonryGridRef: HTMLDivElement | null = null;
    _draggerRef = React.createRef<HTMLDivElement>();
    @computed get notetakingCategoryField() {
        return StrCast(this.dataDoc.notetaking_column, StrCast(this.layoutDoc.pivotField, 'notetaking_column'));
    }
    toHeader = (d: Doc) => (d[this.notetakingCategoryField] instanceof List ? StrListCast(d[this.notetakingCategoryField]).join('.') : (d[this.notetakingCategoryField] ?? 'unset'));
    public DividerWidth = 16;
    @observable docsDraggedRowCol: number[] = [];
    @observable _scroll = 0;
    @observable _refList: HTMLElement[] = [];

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

    @computed get chromeHidden() {
        return BoolCast(this.layoutDoc.chromeHidden) || SnappingManager.ExploreMode;
    }
    // columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns
    @computed get colHeaderData() {
        const colHeaderData = Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null);
        const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !colHeaderData?.find(sh => sh.heading === 'unset'));
        if (needsUnsetCategory || colHeaderData === undefined || colHeaderData.length === 0) {
            setTimeout(() => {
                const columnHeaders = Array.from(Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null) ?? []);
                if (needsUnsetCategory || columnHeaders.length === 0) {
                    columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1));
                    this.resizeColumns(columnHeaders);
                }
            });
        }
        return colHeaderData ?? ([] as SchemaHeaderField[]);
    }
    @computed get headerMargin() {
        return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) as number;
    }
    @computed get xMargin() {
        return NumCast(this.layoutDoc._xMargin, 5);
    }
    @computed get yMargin() {
        return NumCast(this.layoutDoc._yMargin, 5);
    }
    @computed get gridGap() {
        return NumCast(this.layoutDoc._gridGap, 10);
    }
    // numGroupColumns returns the number of columns
    @computed get numGroupColumns() {
        return this.colHeaderData.length;
    }
    // PanelWidth returns the size of the total available space the view occupies
    @computed get PanelWidth() {
        return this._props.PanelWidth();
    }
    // maxColWidth returns the maximum column width, which is slightly less than the total available space.
    @computed get maxColWidth() {
        return this._props.PanelWidth();
    }
    // availableWidth is the total amount of non-divider width. Since widths are stored relatively,
    // we use availableWidth to convert from a percentage to a pixel count.
    @computed get availableWidth() {
        const numDividers = this.numGroupColumns - 1;
        return this.maxColWidth - numDividers * this.DividerWidth - 2 * NumCast(this.layoutDoc.xMargin);
    }

    // children is passed as a prop to the NoteTakingField, which uses this function
    // to render the docs you see within an individual column.
    children = (docs: Doc[]) => {
        TraceMobx();
        return docs.map(d => {
            const height = () => this.getDocHeight(d);
            const width = () => this.getDocWidth(d);
            const style = { width: width(), marginTop: this.gridGap, height: height() };
            return (
                <div className="collectionNoteTakingView-columnDoc" key={d[Id]} style={style}>
                    {this.getDisplayDoc(d, width)}
                </div>
            );
        });
    };

    // Sections is one of the more important functions in this file, rendering the the documents
    // for the UI. It properly renders documents being dragged between columns.
    // [CAVEATS] (1) keep track of the offsetting
    // (2) documentView gets unmounted as you remove it from the list
    @computed get Sections() {
        TraceMobx();
        const columnHeaders = this.colHeaderData;
        // filter out the currently dragged docs from the child docs, since we will insert them later
        const docs = this.childDocs.filter(d => !DragManager.docsBeingDragged.includes(d));
        const sections = new Map<SchemaHeaderField, Doc[]>(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []]));
        const rowCol = this.docsDraggedRowCol;
        // this will sort the docs into the correct columns (minus the ones you're currently dragging)
        docs.forEach(d => {
            const sectionValue = this.toHeader(d);
            // look for if header exists already
            const existingHeader = columnHeaders.find(sh => sh.heading === sectionValue.toString());
            if (existingHeader) {
                sections.get(existingHeader)!.push(d);
            }
        });
        // now we add back in the docs that we're dragging
        if (rowCol.length && columnHeaders.length > rowCol[1]) {
            const offset = 0;
            sections.get(columnHeaders[rowCol[1]])?.splice(rowCol[0] - offset, 0, ...DragManager.docsBeingDragged);
        }
        return sections;
    }

    removeDocDragHighlight = () => {
        setTimeout(
            action(() => {
                this.docsDraggedRowCol.length = 0;
            }),
            100
        );
    };

    @computed get allFieldValues() {
        return new Set(this.childDocs.map(doc => (doc[this.notetakingCategoryField] instanceof List ? StrListCast(doc[this.notetakingCategoryField]).join('.') : StrCast(doc[this.notetakingCategoryField]))));
    }

    componentDidMount() {
        super.componentDidMount?.();
        document.addEventListener('pointerup', this.removeDocDragHighlight, true);

        this._disposers.autoColumns = reaction(
            () => (this.layoutDoc._notetaking_columns_autoCreate ? Array.from(this.allFieldValues) : undefined),
            columns => undoable(() => columns?.filter(col => !this.colHeaderData.some(h => h.heading === col)).forEach(col => this.addColumn(col)), 'adding columns')(),
            { fireImmediately: true }
        );

        this._disposers.refList = reaction(
            () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }),
            ({ refList, autoHeight }) => {
                if (autoHeight) {
                    refList.forEach(r => this.observer.observe(r));
                    this._props.setHeight?.(this.headerMargin + Math.max(...this._refList.map(DivHeight)));
                } else this.observer.disconnect();
            },
            { fireImmediately: true }
        );
    }

    componentWillUnmount() {
        this.observer.disconnect();
        document.removeEventListener('pointerup', this.removeDocDragHighlight, true);
        super.componentWillUnmount();
        Object.keys(this._disposers).forEach(key => this._disposers[key]());
    }

    moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean) => !!(this._props.removeDocument?.(doc) && addDocument?.(doc));

    createRef = (ele: HTMLDivElement | null) => {
        this._masonryGridRef = ele;
        this.createDashEventsTarget(ele!);
    };

    @computed get onChildClickHandler() {
        return () => this._props.childClickScript || ScriptCast(this.Document.onChildClick);
    }

    @computed get onChildDoubleClickHandler() {
        return () => this._props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
    }

    scrollToBottom = () => {
        smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight, 'ease');
    };

    // let's dive in and get the actual document we want to drag/move around
    focusDocument = (doc: Doc, options: FocusViewOptions) => {
        Doc.BrushDoc(doc);
        const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find(node => node.id === doc[Id]);
        if (found) {
            const { top } = found.getBoundingClientRect();
            const localTop = this.ScreenToLocalBoxXf().transformPoint(0, top);
            if (Math.floor(localTop[1]) !== 0 && Math.ceil(this._props.PanelHeight()) < (this._mainCont?.scrollHeight || 0)) {
                const focusSpeed = options.zoomTime ?? 500;
                smoothScroll(focusSpeed, this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc);
                return focusSpeed;
            }
        }
        return undefined;
    };

    styleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => {
        switch (property) {
            case StyleProp.BoxShadow:
                if (doc && DragManager.docsBeingDragged.includes(doc)) {
                    return `#9c9396 ${StrCast(doc?.layout_boxShadow, '10px 10px 0.9vw')}`;
                }
                break;
            case StyleProp.Opacity:
                if (doc && this._props.childOpacity) {
                    return this._props.childOpacity();
                }
                break;
            default:
        }
        return this._props.styleProvider?.(doc, props, property);
    };

    isContentActive = () => this._props.isContentActive();

    blockPointerEventsWhenDragging = () => (this.docsDraggedRowCol.length ? 'none' : undefined);
    // getDisplayDoc returns the rules for displaying a document in this view (ie. DocumentView)
    getDisplayDoc(doc: Doc, width: () => number) {
        const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField ? undefined : this._props.TemplateDataDocument;
        const height = () => this.getDocHeight(doc);
        let dref: Opt<DocumentView>;
        const noteTakingDocTransform = () => this.getDocTransform(doc, dref);
        return (
            <DocumentView
                ref={r => {
                    dref = r || undefined;
                }}
                Document={doc}
                TemplateDataDocument={dataDoc ?? (!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined)}
                pointerEvents={this.blockPointerEventsWhenDragging}
                renderDepth={this._props.renderDepth + 1}
                PanelWidth={width}
                PanelHeight={height}
                styleProvider={this.styleProvider}
                containerViewPath={this.childContainerViewPath}
                fitWidth={this._props.childLayoutFitWidth}
                isContentActive={emptyFunction}
                onKey={this.onKeyDown}
                // TODO: change this from a prop to a parameter passed into a function
                dontHideOnDrag
                isDocumentActive={this.isContentActive}
                LayoutTemplate={this._props.childLayoutTemplate}
                LayoutTemplateString={this._props.childLayoutString}
                NativeWidth={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (doc._layout_fitWidth && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox
                NativeHeight={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (doc._layout_fitWidth && !Doc.NativeHeight(doc)) ? height : undefined}
                dontCenter={this._props.childIgnoreNativeSize ? 'xy' : undefined}
                dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this._props.dontRegisterView)}
                rootSelected={this.rootSelected}
                showTitle={this._props.childlayout_showTitle}
                dragAction={StrCast(this.layoutDoc.childDragAction) as dropActionType}
                onClickScript={this.onChildClickHandler}
                onDoubleClickScript={this.onChildDoubleClickHandler}
                ScreenToLocalTransform={noteTakingDocTransform}
                focus={this.focusDocument}
                childFilters={this.childDocFilters}
                hideDecorationTitle={this._props.childHideDecorationTitle}
                hideResizeHandles={this._props.childHideResizeHandles}
                childFiltersByRanges={this.childDocRangeFilters}
                searchFilterDocs={this.searchFilterDocs}
                addDocument={this._props.addDocument}
                moveDocument={this._props.moveDocument}
                removeDocument={this._props.removeDocument}
                contentPointerEvents={StrCast(this.layoutDoc.childContentPointerEvents) as Property.PointerEvents}
                whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
                addDocTab={this._props.addDocTab}
                pinToPres={this._props.pinToPres}
            />
        );
    }

    // getDocTransform is used to get the coordinates of a document when we go from a view like freeform to columns
    getDocTransform(doc: Doc, dref?: DocumentView) {
        this._scroll; // required for document decorations to update when the text box container is scrolled
        const { translateX, translateY } = ClientUtils.GetScreenTransform(dref?.ContentDiv || undefined);
        // the document view may center its contents and if so, will prepend that onto the screenToLocalTansform.  so we have to subtract that off
        return new Transform(-translateX + (dref?.centeringX || 0), -translateY + (dref?.centeringY || 0), 1).scale(this.ScreenToLocalBoxXf().Scale);
    }

    // how to get the width of a document. Currently returns the width of the column (minus margins)
    // if a note doc. Otherwise, returns the normal width (for graphs, images, etc...)
    getDocWidth = (d: Doc) => {
        const heading = this.toHeader(d);
        const existingHeader = this.colHeaderData.find(sh => sh.heading === heading);
        const existingWidth = this.layoutDoc._notetaking_columns_autoSize ? 1 / (this.colHeaderData.length ?? 1) : existingHeader?.width ? existingHeader.width : 0;
        const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth : this.maxColWidth;
        const width = d.layout_fitWidth ? maxWidth : NumCast(d._width);
        return Math.min(maxWidth - CollectionNoteTakingViewColumn.ColumnMargin, width < maxWidth ? width : maxWidth);
    };

    // how to get the height of a document. Nothing special here.
    getDocHeight(d?: Doc) {
        if (!d || d.hidden) return 0;
        const childLayoutDoc = Doc.Layout(d, this._props.childLayoutTemplate?.());
        const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField ? undefined : this._props.TemplateDataDocument;
        const maxHeight = (lim => (lim === 0 ? this._props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1));
        const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this._props.childLayoutFitWidth?.(d)) ? NumCast(d._width) : 0);
        const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this._props.childLayoutFitWidth?.(d)) ? NumCast(d._height) : 0);
        if (nw && nh) {
            const docWid = this.getDocWidth(d);
            return Math.min(maxHeight, (docWid * nh) / nw);
        }
        const childHeight = NumCast(childLayoutDoc._height);
        const panelHeight = childLayoutDoc._layout_fitWidth || this._props.childLayoutFitWidth?.(d) ? Number.MAX_SAFE_INTEGER : this._props.PanelHeight() - 2 * this.yMargin;
        return Math.min(childHeight, maxHeight, panelHeight);
    }

    // resizeColumns is called whenever a user adds or removes a column. When removing,
    // this function renormalizes the column widths to fill the newly available space
    // in the panel. When adding, this function renormalizes the existing columns to take up
    // (n - 1)/n space, since the new column will be allocated 1/n of the total space.
    // Column widths are relative (portion of available space) and stored  in the 'width'
    // field of SchemaHeaderFields.
    //
    // Removing example: column widths are [0.5, 0.30, 0.20] --> user deletes the final column --> column widths are [0.625, 0.375].
    // Adding example: column widths are [0.6, 0.4] --> user adds column at end --> column widths are [0.4, 0.267, 0.33]
    @action
    resizeColumns = (headers: SchemaHeaderField[]) => {
        const curWidths = headers.reduce((sum, hdr) => sum + Math.abs(hdr.width), 0);
        const scaleFactor = 1 / curWidths;
        this.dataDoc[this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>(
            headers.map(h => {
                h.setWidth(Math.abs(h.width) * scaleFactor);
                return h;
            })
        );
        return true;
    };

    // onPointerMove is used to preview where a document will drop in a column once a drag is complete.
    @action
    onPointerMove = (force: boolean, ex: number, ey: number) => {
        const dragDoc = DragManager.DraggedDocs?.lastElement();
        if ((dragDoc && this.childDocList?.includes(dragDoc)) || force || SnappingManager.CanEmbed) {
            // get the current docs for the column based on the mouse's x coordinate
            const xCoord = this.ScreenToLocalBoxXf().transformPoint(ex, ey)[0] - 2 * this.gridGap;
            const colDocs = this.getDocsFromXCoord(xCoord);
            // get the index for where you need to insert the doc you are currently dragging
            const clientY = this.ScreenToLocalBoxXf().transformPoint(ex, ey)[1];
            let dropInd = -1;
            let pos0 = (this._refList.lastElement() as HTMLDivElement).children[0].getBoundingClientRect().height + this.yMargin * 2;
            colDocs.forEach((doc, i) => {
                let pos1 = this.getDocHeight(doc) + 2 * this.gridGap;
                pos1 += pos0;
                // updating drop position based on y coordinates
                const yCoordInBetween = clientY > pos0 && clientY < pos1;
                if (yCoordInBetween || (clientY < pos0 && i === 0)) {
                    dropInd = i;
                } else if (i === colDocs.length - 1 && dropInd === -1) {
                    dropInd = !colDocs.includes(DragManager.docsBeingDragged.lastElement()) ? i + 1 : i;
                }
                pos0 = pos1;
            });
            // we alter the pivot fields of the docs in case they are moved to a new column.
            const colIndex = this.getColumnFromXCoord(xCoord);
            const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading);
            DragManager.docsBeingDragged
                .map(doc => doc[DocData])
                .forEach(d => {
                    d[this.notetakingCategoryField] = colHeader;
                });
            // used to notify sections to re-render
            this.docsDraggedRowCol.length = 0;
            const columnFromCoord = this.getColumnFromXCoord(xCoord);
            columnFromCoord !== undefined && this.docsDraggedRowCol.push(dropInd, columnFromCoord);
        }
    };

    // getColumnFromXCoord returns the column index for a given x-coordinate (currently always the client's mouse coordinate).
    // This function is used to know which document a column SHOULD be in while it is being dragged.
    getColumnFromXCoord = (xCoord: number): number | undefined => {
        let colIndex: number | undefined;
        const numColumns = this.colHeaderData.length;
        const coords = [];
        let colStartXCoord = 0;
        for (let i = 0; i < numColumns; i++) {
            coords.push(colStartXCoord);
            colStartXCoord += this.colHeaderData[i].width * this.availableWidth + this.DividerWidth;
        }
        coords.push(this.PanelWidth);
        for (let i = 0; i < numColumns; i++) {
            if (xCoord > coords[i] && xCoord < coords[i + 1]) {
                colIndex = i;
                break;
            }
        }
        return colIndex;
    };

    // getDocsFromXCoord returns the docs of a column based on the x-coordinate provided.
    getDocsFromXCoord = (xCoord: number): Doc[] => {
        const docsMatchingHeader: Doc[] = [];
        const colIndex = this.getColumnFromXCoord(xCoord);
        const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading);
        this.childDocs?.forEach(d => {
            if (d instanceof Promise) return;
            const sectionValue = this.toHeader(d);
            if (sectionValue.toString() === colHeader) {
                docsMatchingHeader.push(d);
            }
        });
        return docsMatchingHeader;
    };

    @undoBatch
    onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => {
        if ((e.ctrlKey || fieldProps.Document._createDocOnCR) && ['Enter'].includes(e.key)) {
            e.stopPropagation?.();
            const newDoc = Doc.MakeCopy(fieldProps.Document, true);
            newDoc.$text = undefined;
            DocumentView.SetSelectOnLoad(newDoc);
            return this.addDocument?.(newDoc);
        }
        return undefined;
    };

    // onInternalDrop is used when dragging and dropping a document within the view, such as dragging
    // a document to a new column or changing its order within the column.
    @undoBatch
    onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
        if (de.complete.docDragData) {
            if (super.onInternalDrop(e, de)) {
                // filter out the currently dragged docs from the child docs, since we will insert them later
                const rowCol = this.docsDraggedRowCol;
                const droppedDocs = this.childDocs.filter((d: Doc, ind: number) => ind >= this.childDocs.length); // if the drop operation adds something to the end of the list, then use that as the new document (may be different than what was dropped e.g., in the case of a button which is dropped but which creates say, a note).
                const newDocs = droppedDocs.length ? droppedDocs : de.complete.docDragData.droppedDocuments;
                const docs = this.childDocList;
                if (docs && newDocs.length) {
                    // remove the dragged documents from the childDocList
                    newDocs.filter(d => docs.indexOf(d) !== -1).forEach(d => docs.splice(docs.indexOf(d), 1));
                    // if the doc starts a columnm (or the drop index is undefined), we can just push it to the front. Otherwise we need to add it to the column properly
                    if (rowCol[0] <= 0) {
                        docs.splice(0, 0, ...newDocs);
                    } else {
                        const colDocs = this.getDocsFromXCoord(this.ScreenToLocalBoxXf().transformPoint(de.x, de.y)[0]);
                        const previousDoc = colDocs[rowCol[0] - 1];
                        const previousDocIndex = docs.indexOf(previousDoc);
                        docs.splice(previousDocIndex + 1, 0, ...newDocs);
                    }
                }
                return true;
            }
        } else if (de.complete.linkDragData?.dragDocument.embedContainer === this.Document && CollectionFreeFormDocumentView.from(de.complete.linkDragData?.linkDragView)) {
            const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _layout_fitWidth: true, title: 'dropped annotation' });
            if (!this._props.addDocument?.(source)) e.preventDefault();
            de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { link_relationship: 'doc annotation' }); // TODODO this is where in text links get passed
            e.stopPropagation();
            return true;
        } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) {
            return this.internalAnchorAnnoDrop(e, de.complete.annoDragData);
        }
        return false;
    };

    @undoBatch
    internalAnchorAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData) {
        const dropCreator = annoDragData.dropDocCreator;
        annoDragData.dropDocCreator = (annotationOn: Doc | undefined) => dropCreator(annotationOn) || this.Document;
        return true;
    }

    // onExternalDrop is used when dragging a document out from a CollectionNoteTakingView
    // to another tab/view/collection
    onExternalDrop = async (e: React.DragEvent): Promise<void> => {
        const targInd = this.docsDraggedRowCol?.[0] || 0;
        const colInd = this.docsDraggedRowCol?.[1] || 0;
        super.onExternalDrop(
            e,
            {},
            undoable(
                action(docus => {
                    this.onPointerMove(true, e.clientX, e.clientY);
                    docus?.map((doc: Doc) => this.addDocument(doc));
                    const newDoc = this.childDocs.lastElement();
                    const colHeader = colInd === undefined ? 'unset' : StrCast(this.colHeaderData[colInd].heading);
                    newDoc[this.notetakingCategoryField] = colHeader;
                    const docs = this.childDocList;
                    if (docs && targInd !== -1) {
                        docs.splice(docs.length - 1, 1);
                        docs.splice(targInd, 0, newDoc);
                    }
                    this.removeDocDragHighlight();
                }),
                'drop into note view'
            )
        );
    };

    headings = () => Array.from(this.Sections);

    editableViewProps = () => ({
        GetValue: () => '',
        SetValue: this.addColumn,
        contents: '+ Column',
    });

    refList = () => this._refList;
    backgroundColor = () => this.DocumentView?.()?.backgroundColor();

    // sectionNoteTaking returns a CollectionNoteTakingViewColumn (which is an individual column)
    sectionNoteTaking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => (
        <CollectionNoteTakingViewColumn
            key={heading?.heading ?? 'unset'}
            PanelWidth={this._props.PanelWidth}
            refList={this._refList}
            backgroundColor={this.backgroundColor}
            select={this._props.select}
            isContentActive={this.isContentActive}
            addDocument={this.addDocument}
            chromeHidden={this.chromeHidden}
            colHeaderData={this.colHeaderData}
            Document={this.Document}
            TemplateDataDocument={this._props.TemplateDataDocument}
            resizeColumns={this.resizeColumns}
            renderChildren={this.children}
            numGroupColumns={this.numGroupColumns}
            gridGap={this.gridGap}
            pivotField={this.notetakingCategoryField}
            fieldKey={this.fieldKey}
            dividerWidth={this.DividerWidth}
            maxColWidth={this.maxColWidth}
            availableWidth={this.availableWidth}
            headings={this.headings}
            heading={heading?.heading ?? 'unset'}
            headingObject={heading}
            docList={docList}
            yMargin={this.yMargin}
            createDropTarget={this.createDashEventsTarget}
            screenToLocalTransform={this.ScreenToLocalBoxXf}
            editableViewProps={this.editableViewProps}
        />
    );

    @undoBatch
    remColumn = (value: SchemaHeaderField) => {
        const colHdrData = Array.from(Cast(this._props.Document[this._props.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null));
        if (value) {
            const index = colHdrData.indexOf(value);
            index !== -1 && colHdrData.splice(index, 1);
            this.resizeColumns(colHdrData);
        }
    };

    // addGroup is called when adding a new columnHeader, adding a SchemaHeaderField to our list of
    // columnHeaders and resizing the existing columns to make room for our new one.
    @undoBatch
    addColumn = (value: string) => {
        this.colHeaderData.forEach(header => {
            if (header.heading === value) {
                alert('You cannot use an existing column name. Please try a new column name');
                return value;
            }
            return undefined;
        });
        const columnHeaders = Array.from(Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null));
        const newColWidth = 1 / (this.numGroupColumns + 1);
        columnHeaders.push(new SchemaHeaderField(value, undefined, undefined, newColWidth));
        value && this.resizeColumns(columnHeaders);
        return true;
    };

    removeEmptyColumns = undoable(() => {
        this.colHeaderData.filter(h => !this.allFieldValues.has(h.heading)).forEach(this.remColumn);
    }, 'remove empty Columns');

    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 (!e.isPropagationStopped()) {
            const subItems: ContextMenuProps[] = [];
            subItems.push({
                description: `${this.layoutDoc._notetaking_columns_autoCreate ? 'Manually' : 'Automatically'} Create columns`,
                event: () => {
                    this.layoutDoc._notetaking_columns_autoCreate = !this.layoutDoc._notetaking_columns_autoCreate;
                },
                icon: 'computer',
            });
            subItems.push({ description: 'Remove Empty Columns', event: this.removeEmptyColumns, icon: 'computer' });
            subItems.push({
                description: `${this.layoutDoc._notetaking_columns_autoSize ? 'Variable Size' : 'Autosize'} Columns`,
                event: () => {
                    this.layoutDoc._notetaking_columns_autoSize = !this.layoutDoc._notetaking_columns_autoSize;
                },
                icon: 'plus',
            });
            subItems.push({
                description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`,
                event: () => {
                    this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight;
                },
                icon: 'plus',
            });
            subItems.push({
                description: 'Clear All',
                event: () => {
                    this.dataDoc.data = new List([]);
                },
                icon: 'times',
            });
            ContextMenu.Instance.addItem({ description: 'Options...', subitems: subItems, icon: 'eye' });
        }
    };

    // setColumnStartXCoords is used to update column widths when using the drag handlers between columns
    @action
    setColumnStartXCoords = (movementXScreen: number, colIndex: number) => {
        const movementX = this.ScreenToLocalBoxXf().transformDirection(movementXScreen, 0)[0];
        const leftHeader = this.colHeaderData[colIndex];
        const rightHeader = this.colHeaderData[colIndex + 1];
        leftHeader.setWidth(leftHeader.width + movementX / this.availableWidth);
        rightHeader.setWidth(rightHeader.width - movementX / this.availableWidth);
        const headers = Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null);
        headers.splice(headers.indexOf(leftHeader), 1, leftHeader[Copy]());
    };

    // renderedSections returns a list of all of the JSX elements used (columns and dividers). If the view
    // has more than one column, those columns will be separated by a CollectionNoteTakingViewDivider that
    // allows the user to adjust the column widths.
    @computed get renderedSections() {
        TraceMobx();
        const sections = Array.from(this.Sections.entries());
        return sections.reduce((list, sec, i) => {
            list.push(this.sectionNoteTaking(sec[0], sec[1]));
            i !== sections.length - 1 && list.push(<CollectionNoteTakingViewDivider key={`divider${i}`} isContentActive={this.isContentActive} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />);
            return list;
        }, [] as JSX.Element[]);
    }

    @computed get nativeWidth() {
        return Doc.NativeWidth(this.layoutDoc);
    }
    @computed get nativeHeight() {
        return Doc.NativeHeight(this.layoutDoc);
    }

    @computed get scaling() {
        return !this.nativeWidth ? 1 : this._props.PanelHeight() / this.nativeHeight;
    }

    @computed get backgroundEvents() {
        return this.isContentActive() === false ? 'none' : undefined;
    }

    observer = new ResizeObserver(() => this._props.setHeight?.(this.headerMargin + Math.max(...this._refList.map(DivHeight))));

    render() {
        TraceMobx();
        return (
            <div
                className={`collectionNoteTakingView ${lightOrDark(this.backgroundColor()) === Colors.WHITE ? 'collectionNoteTakingViewLight' : ''}`}
                ref={this.createRef}
                style={{
                    overflowY: this.isContentActive() ? 'auto' : 'hidden',
                    background: this.backgroundColor(),
                    pointerEvents: this.backgroundEvents,
                }}
                onScroll={action(e => {
                    this._scroll = e.currentTarget.scrollTop;
                })}
                onPointerLeave={action(() => {
                    this.docsDraggedRowCol.length = 0;
                })}
                onPointerMove={e => e.buttons && this.onPointerMove(false, e.clientX, e.clientY)}
                onDragOver={e => this.onPointerMove(true, e.clientX, e.clientY)}
                onDrop={this.onExternalDrop.bind(this)}
                onContextMenu={this.onContextMenu}
                onWheel={e => this._props.isContentActive() && e.stopPropagation()}>
                {this.renderedSections}
                <div className="collectionNotetaking-pivotField" style={{ right: 0, top: 0, position: 'absolute' }}>
                    <FieldsDropdown
                        Document={this.Document}
                        selectFunc={undoable(fieldKey => {
                            this.layoutDoc._pivotField = fieldKey;
                            this.removeEmptyColumns();
                        }, 'change pivot field')}
                        placeholder={StrCast(this.layoutDoc._pivotField)}
                    />
                </div>
            </div>
        );
    }
}
