import React = require('react');
import { CursorProperty } from 'csstype';
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { DataSym, Doc, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
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, returnEmptyDoclist, returnFalse, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { DragManager, dropActionType } from '../../util/DragManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { LightboxView } from '../LightboxView';
import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { StyleProp } from '../StyleProvider';
import './CollectionNoteTakingView.scss';
import { CollectionNoteTakingViewColumn } from './CollectionNoteTakingViewColumn';
import { CollectionNoteTakingViewDivider } from './CollectionNoteTakingViewDivider';
import { CollectionSubView } from './CollectionSubView';
const _global = (window /* browser */ || global) /* node */ as any;

/**
 * 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>();
    notetakingCategoryField = 'NotetakingCategory';
    public DividerWidth = 16;
    @observable docsDraggedRowCol: number[] = [];
    @observable _cursor: CursorProperty = 'grab';
    @observable _scroll = 0;
    @computed get chromeHidden() {
        return BoolCast(this.layoutDoc.chromeHidden);
    }
    // columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns
    @computed get columnHeaders() {
        const columnHeaders = Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null);
        const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset'));
        if (needsUnsetCategory || columnHeaders === undefined || columnHeaders.length === 0) {
            setTimeout(() => {
                const columnHeaders = Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null);
                const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset'));
                if (needsUnsetCategory || columnHeaders === undefined || columnHeaders.length === 0) {
                    if (columnHeaders) columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1));
                    else this.dataDoc.columnHeaders = new List<SchemaHeaderField>();
                }
            });
        }
        return columnHeaders ?? ([] as SchemaHeaderField[]);
    }
    @computed get headerMargin() {
        return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin);
    }
    @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.columnHeaders.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;
    }

    // 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, i) => {
            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.columnHeaders;
        // 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.map(d => {
            const sectionValue = (d[this.notetakingCategoryField] as object) ?? `unset`;
            // 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
        );
    };

    componentDidMount() {
        super.componentDidMount?.();
        document.addEventListener('pointerup', this.removeDocDragHighlight, true);
        this._disposers.autoHeight = reaction(
            () => this.layoutDoc._autoHeight,
            autoHeight => autoHeight && this.props.setHeight?.(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), this.headerMargin + Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', ''))))))
        );
    }

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

    @action
    moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean): boolean => {
        return this.props.removeDocument?.(doc) && addDocument?.(doc) ? true : false;
    };

    createRef = (ele: HTMLDivElement | null) => {
        this._masonryGridRef = ele;
        this.createDashEventsTarget(ele!); //so the whole grid is the drop target?
    };

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

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

    addDocTab = (doc: Doc, where: string) => {
        if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) {
            this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]);
            return true;
        }
        return this.props.addDocTab(doc, where);
    };

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

    // let's dive in and get the actual document we want to drag/move around
    focusDocument = (doc: Doc, options?: DocFocusOptions) => {
        Doc.BrushDoc(doc);
        let focusSpeed = 0;
        const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]);
        if (found) {
            const top = found.getBoundingClientRect().top;
            const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top);
            if (Math.floor(localTop[1]) !== 0) {
                smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
            }
        }
        const endFocus = async (moved: boolean) => (options?.afterFocus ? options?.afterFocus(moved) : ViewAdjustment.doNothing);
        this.props.focus(this.rootDoc, {
            willZoom: options?.willZoom,
            scale: options?.scale,
            afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)),
        });
    };

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

    isContentActive = () => this.props.isSelected() || this.props.isContentActive();

    // getDisplayDoc returns the rules for displaying a document in this view (ie. DocumentView)
    getDisplayDoc(doc: Doc, width: () => number) {
        const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc;
        const height = () => this.getDocHeight(doc);
        let dref: Opt<DocumentView>;
        const noteTakingDocTransform = () => this.getDocTransform(doc, dref);
        return (
            <DocumentView
                ref={r => (dref = r || undefined)}
                Document={doc}
                DataDoc={dataDoc || (!Doc.AreProtosEqual(doc[DataSym], doc) && doc[DataSym])}
                renderDepth={this.props.renderDepth + 1}
                PanelWidth={width}
                PanelHeight={height}
                styleProvider={this.styleProvider}
                docViewPath={this.props.docViewPath}
                fitWidth={this.props.childFitWidth}
                isContentActive={emptyFunction}
                onKey={this.onKeyDown}
                //TODO: change this from a prop to a parameter passed into a function
                dontHideOnDrag={true}
                isDocumentActive={this.isContentActive}
                LayoutTemplate={this.props.childLayoutTemplate}
                LayoutTemplateString={this.props.childLayoutString}
                NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox
                NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._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.childShowTitle}
                dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType}
                onClick={this.onChildClickHandler}
                onDoubleClick={this.onChildDoubleClickHandler}
                ScreenToLocalTransform={noteTakingDocTransform}
                focus={this.focusDocument}
                docFilters={this.childDocFilters}
                hideDecorationTitle={this.props.childHideDecorationTitle?.()}
                hideResizeHandles={this.props.childHideResizeHandles?.()}
                hideTitle={this.props.childHideTitle?.()}
                docRangeFilters={this.childDocRangeFilters}
                searchFilterDocs={this.searchFilterDocs}
                ContainingCollectionDoc={this.props.CollectionView?.props.Document}
                ContainingCollectionView={this.props.CollectionView}
                addDocument={this.props.addDocument}
                moveDocument={this.props.moveDocument}
                removeDocument={this.props.removeDocument}
                contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)}
                whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
                addDocTab={this.addDocTab}
                bringToFront={returnFalse}
                scriptContext={this.props.scriptContext}
                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) {
        const y = this._scroll; // required for document decorations to update when the text box container is scrolled
        const { scale, translateX, translateY } = Utils.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.props.ScreenToLocalTransform().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 = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as Field);
        const existingHeader = this.columnHeaders.find(sh => sh.heading === heading);
        const existingWidth = existingHeader?.width ? existingHeader.width : 0;
        const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth : this.maxColWidth;
        const width = d.fitWidth ? maxWidth : d[WidthSym]();
        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 && !d.PARAMS ? undefined : this.props.DataDoc;
        const maxHeight = (lim => (lim === 0 ? this.props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1));
        const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[WidthSym]() : 0);
        const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[HeightSym]() : 0);
        if (nw && nh) {
            const docWid = this.getDocWidth(d);
            return Math.min(maxHeight, (docWid * nh) / nw);
        }
        const childHeight = NumCast(childLayoutDoc._height);
        const panelHeight = childLayoutDoc._fitWidth || this.props.childFitWidth?.(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 = (isAdd: boolean, colWidth: number, colIndex: number) => {
        const n = this.columnHeaders.length;
        if (n == 1) {
            this.columnHeaders[0].setWidth(1);
            return true;
        }
        const scaleFactor = isAdd ? 1 - colWidth : 1 / (1 - colWidth);
        this.columnHeaders.forEach((h, i) => {
            if (!(isAdd && i == colIndex)) {
                h.width < 0 ? h.setWidth(1 / n) : h.setWidth(h.width * scaleFactor);
            }
        });
        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) => {
        if (this.childDocList && (this.childDocList.includes(DragManager.DocDragData?.draggedDocuments.lastElement()!) || force || this.isContentActive())) {
            // get the current docs for the column based on the mouse's x coordinate
            const xCoord = this.props.ScreenToLocalTransform().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.props.ScreenToLocalTransform().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.columnHeaders[colIndex].heading);
            DragManager.docsBeingDragged.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 = undefined;
        const numColumns = this.columnHeaders.length;
        const coords = [];
        let colStartXCoord = 0;
        for (let i = 0; i < numColumns; i++) {
            coords.push(colStartXCoord);
            colStartXCoord += this.columnHeaders[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.columnHeaders[colIndex].heading);
        this.childDocs?.map(d => {
            if (d instanceof Promise) return;
            const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset';
            if (sectionValue.toString() == colHeader) {
                docsMatchingHeader.push(d);
            }
        });
        return docsMatchingHeader;
    };

    @undoBatch
    @action
    onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => {
        const docView = fieldProps.DocumentView?.();
        if (docView && (e.ctrlKey || docView.rootDoc._singleLine) && ['Enter'].includes(e.key)) {
            e.stopPropagation?.();
            const newDoc = Doc.MakeCopy(docView.rootDoc, true);
            Doc.GetProto(newDoc).text = undefined;
            FormattedTextBox.SelectOnLoad = newDoc[Id];
            return this.addDocument?.(newDoc);
        }
    };

    // 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
    @action
    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.slice().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.props.ScreenToLocalTransform().transformPoint(de.x, de.y)[0]);
                        const previousDoc = colDocs[rowCol[0] - 1];
                        const previousDocIndex = docs.indexOf(previousDoc);
                        docs.splice(previousDocIndex + 1, 0, ...newDocs);
                    }
                }
            }
        } else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) {
            const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' });
            this.props.addDocument?.(source);
            de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceGetAnchor() }, 'doc annotation', ''); // TODODO this is where in text links get passed
            e.stopPropagation();
        } 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) => {
            const dropDoc = dropCreator(annotationOn);
            return dropDoc || this.rootDoc;
        };
        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,
            {},
            undoBatch(
                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.columnHeaders[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();
                })
            )
        );
    };

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

    refList: any[] = [];

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

    // sectionNoteTaking returns a CollectionNoteTakingViewColumn (which is an individual column)
    sectionNoteTaking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
        const type = 'number';
        return (
            <CollectionNoteTakingViewColumn
                unobserveHeight={ref => this.refList.splice(this.refList.indexOf(ref), 1)}
                observeHeight={ref => {
                    if (ref) {
                        this.refList.push(ref);
                        this.observer = new _global.ResizeObserver(
                            action((entries: any) => {
                                if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) {
                                    const height = this.headerMargin + Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', '')))));
                                    if (!LightboxView.IsLightboxDocView(this.props.docViewPath())) {
                                        this.props.setHeight?.(height);
                                    }
                                }
                            })
                        );
                        this.observer.observe(ref);
                    }
                }}
                addDocument={this.addDocument}
                chromeHidden={this.chromeHidden}
                columnHeaders={this.columnHeaders}
                Document={this.props.Document}
                DataDoc={this.props.DataDoc}
                resizeColumns={this.resizeColumns}
                renderChildren={this.children}
                numGroupColumns={this.numGroupColumns}
                gridGap={this.gridGap}
                pivotField={this.notetakingCategoryField}
                dividerWidth={this.DividerWidth}
                maxColWidth={this.maxColWidth}
                availableWidth={this.availableWidth}
                PanelWidth={this.PanelWidth}
                key={heading?.heading ?? 'unset'}
                headings={this.headings}
                heading={heading?.heading ?? 'unset'}
                headingObject={heading}
                docList={docList}
                yMargin={this.yMargin}
                type={type}
                createDropTarget={this.createDashEventsTarget}
                screenToLocalTransform={this.props.ScreenToLocalTransform}
                editableViewProps={this.editableViewProps}
            />
        );
    };

    // 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
    @action
    addGroup = (value: string) => {
        if (this.columnHeaders) {
            for (const header of this.columnHeaders) {
                if (header.heading == value) {
                    alert('You cannot use an existing column name. Please try a new column name');
                    return value;
                }
            }
        }
        const columnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null);
        const newColWidth = 1 / (this.numGroupColumns + 1);
        value && columnHeaders?.push(new SchemaHeaderField(value, undefined, undefined, newColWidth)) && this.resizeColumns(true, newColWidth, this.columnHeaders.length - 1);
        this.dataDoc.columnHeaders = new List<SchemaHeaderField>(columnHeaders.map(header => header[Copy]()));
        return true;
    };

    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._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' });
            subItems.push({ description: `${this.layoutDoc._autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._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.props.ScreenToLocalTransform().transformDirection(movementXScreen, 0)[0];
        const leftHeader = this.columnHeaders[colIndex];
        const rightHeader = this.columnHeaders[colIndex + 1];
        leftHeader.setWidth(leftHeader.width + movementX / this.availableWidth);
        rightHeader.setWidth(rightHeader.width - movementX / this.availableWidth);
    };

    // 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 entries = Array.from(this.Sections.entries());
        const sections = entries;
        const eles: JSX.Element[] = [];
        for (let i = 0; i < sections.length; i++) {
            const col = this.sectionNoteTaking(sections[i][0], sections[i][1]);
            eles.push(col);
            if (i < sections.length - 1) {
                eles.push(<CollectionNoteTakingViewDivider key={`divider${i}`} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />);
            }
        }
        return eles;
    }

    @computed get buttonMenu() {
        const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
        if (menuDoc) {
            const width = NumCast(menuDoc._width, 30);
            const height = NumCast(menuDoc._height, 30);
            return (
                <div className="buttonMenu-docBtn" style={{ width, height }}>
                    <DocumentView
                        Document={menuDoc}
                        DataDoc={menuDoc}
                        isContentActive={this.props.isContentActive}
                        isDocumentActive={returnTrue}
                        addDocument={this.props.addDocument}
                        moveDocument={this.props.moveDocument}
                        addDocTab={this.props.addDocTab}
                        pinToPres={emptyFunction}
                        rootSelected={this.props.isSelected}
                        removeDocument={this.props.removeDocument}
                        ScreenToLocalTransform={Transform.Identity}
                        PanelWidth={() => 35}
                        PanelHeight={() => 35}
                        renderDepth={this.props.renderDepth}
                        focus={emptyFunction}
                        styleProvider={this.props.styleProvider}
                        docViewPath={returnEmptyDoclist}
                        whenChildContentsActiveChanged={emptyFunction}
                        bringToFront={emptyFunction}
                        docFilters={this.props.docFilters}
                        docRangeFilters={this.props.docRangeFilters}
                        searchFilterDocs={this.props.searchFilterDocs}
                        ContainingCollectionView={undefined}
                        ContainingCollectionDoc={undefined}
                    />
                </div>
            );
        }
    }

    @computed get nativeWidth() {
        return Doc.NativeWidth(this.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 SnappingManager.GetIsDragging();
    }

    observer: any;

    render() {
        TraceMobx();
        const buttonMenu = this.rootDoc.buttonMenu;
        const noviceExplainer = StrCast(this.rootDoc.explainer);
        return (
            <>
                {buttonMenu || noviceExplainer ? (
                    <div className="documentButtonMenu" key="buttons">
                        {buttonMenu ? this.buttonMenu : null}
                        {Doc.UserDoc().noviceMode && noviceExplainer ? <div className="documentExplanation">{noviceExplainer}</div> : null}
                    </div>
                ) : null}
                <div
                    className="collectionNoteTakingView"
                    ref={this.createRef}
                    key="notes"
                    style={{
                        overflowY: this.props.isContentActive() ? 'auto' : 'hidden',
                        background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor),
                        pointerEvents: this.backgroundEvents ? 'all' : undefined,
                    }}
                    onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))}
                    onPointerLeave={action(e => (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(true) && e.stopPropagation()}>
                    {this.renderedSections}
                </div>
            </>
        );
    }
}
