import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as CSS from 'csstype';
import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, Opt } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { DivHeight, emptyFunction, returnEmptyDoclist, returnFalse, returnNone, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { CollectionViewType } from '../../documents/DocumentTypes';
import { DragManager, dropActionType } from '../../util/DragManager';
import { SettingsManager } from '../../util/SettingsManager';
import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { EditableView } from '../EditableView';
import { LightboxView } from '../LightboxView';
import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
import { DocumentView } from '../nodes/DocumentView';
import { FocusViewOptions, FieldViewProps } from '../nodes/FieldView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { StyleProp } from '../StyleProvider';
import { CollectionMasonryViewFieldRow } from './CollectionMasonryViewFieldRow';
import './CollectionStackingView.scss';
import { CollectionStackingViewFieldColumn } from './CollectionStackingViewFieldColumn';
import { CollectionSubView } from './CollectionSubView';
const _global = (window /* browser */ || global) /* node */ as any;

export type collectionStackingViewProps = {
    sortFunc?: (a: Doc, b: Doc) => number;
    chromeHidden?: boolean;
    // view type is stacking
    type_collection?: CollectionViewType;
    NativeWidth?: () => number;
    NativeHeight?: () => number;
};

@observer
export class CollectionStackingView extends CollectionSubView<Partial<collectionStackingViewProps>>() {
    _disposers: { [key: string]: IReactionDisposer } = {};
    _masonryGridRef: HTMLDivElement | null = null;
    // used in a column dragger, likely due for the masonry grid view. We want to use this
    _draggerRef = React.createRef<HTMLDivElement>();
    // keeping track of documents. Updated on internal and external drops. What's the difference?
    _docXfs: { height: () => number; width: () => number; stackedDocTransform: () => Transform }[] = [];
    // Doesn't look like this field is being used anywhere. Obsolete?
    _columnStart: number = 0;

    @observable _refList: any[] = [];
    // map of node headers to their heights. Used in Masonry
    @observable _heightMap = new Map<string, number>();
    // Assuming that this is the current css cursor style
    @observable _cursor: CSS.Property.Cursor = 'ew-resize';
    // gets reset whenever we scroll. Not sure what it is
    @observable _scroll = 0; // used to force the document decoration to update when scrolling
    // does this mean whether the browser is hidden? Or is chrome something else entirely?
    @computed get chromeHidden() {
        return this._props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden);
    }
    // it looks like this gets the column headers that Mehek was showing just now
    @computed get colHeaderData() {
        return Cast(this.dataDoc['_' + this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null);
    }
    // Still not sure what a pivot is, but it appears that we can actually filter docs somehow?
    @computed get pivotField() {
        return StrCast(this.layoutDoc._pivotField);
    }
    // filteredChildren is what you want to work with. It's the list of things that you're currently displaying
    @computed get filteredChildren() {
        const children = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout);
        if (this._props.sortFunc) children.sort(this._props.sortFunc);
        return children;
    }
    // how much margin we give the header
    @computed get headerMargin() {
        return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin);
    }
    @computed get xMargin() {
        return NumCast(this.layoutDoc._xMargin, Math.max(3, 0.05 * this._props.PanelWidth()));
    }
    @computed get yMargin() {
        return this._props.yPadding || NumCast(this.layoutDoc._yMargin, Math.min(5, 0.05 * this._props.PanelWidth()));
    }

    @computed get gridGap() {
        return NumCast(this.layoutDoc._gridGap, 5);
    }
    // are we stacking or masonry?
    @computed get isStackingView() {
        return (this._props.type_collection ?? this.layoutDoc._type_collection) === CollectionViewType.Stacking;
    }
    // this is the number of StackingViewFieldColumns that we have
    @computed get numGroupColumns() {
        return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1;
    }
    // reveals a button to add a group in masonry view
    @computed get showAddAGroup() {
        return this.pivotField && !this.chromeHidden;
    }
    // columnWidth handles the margin on the left and right side of the documents
    @computed get columnWidth() {
        return Math.min(this._props.PanelWidth() - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : this.layoutDoc._columnWidth === -1 ? this._props.PanelWidth() - 2 * this.xMargin : NumCast(this.layoutDoc._columnWidth, 250));
    }

    @computed get NodeWidth() {
        return this._props.PanelWidth() - this.gridGap;
    }

    constructor(props: any) {
        super(props);
        makeObservable(this);
        if (this.colHeaderData === undefined) {
            // TODO: what is a layout doc? Is it literally how this document is supposed to be layed out?
            // here we're making an empty list of column headers (again, what Mehek showed us)
            this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>();
        }
    }

    // TODO: plj - these are the children
    children = (docs: Doc[]) => {
        //TODO: can somebody explain me to what exactly TraceMobX is?
        TraceMobx();
        // appears that we are going to reset the _docXfs. TODO: what is Xfs?
        this._docXfs.length = 0;
        this._renderCount < docs.length && setTimeout(action(() => (this._renderCount = Math.min(docs.length, this._renderCount + 5))));
        return docs.map((d, i) => {
            const height = () => this.getDocHeight(d);
            const width = () => this.getDocWidth(d);
            const trans = () => this.getDocTransition(d);
            // assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns
            const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);
            // just getting the style
            const style = this.isStackingView ? { margin: this.Document._stacking_alignCenter ? 'auto' : undefined, transition: trans(), width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` };
            // So we're choosing whether we're going to render a column or a masonry doc
            return (
                <div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}>
                    {this.getDisplayDoc(d, width, trans, i)}
                </div>
            );
        });
    };
    @action
    setDocHeight = (key: string, sectionHeight: number) => {
        this._heightMap.set(key, sectionHeight);
    };

    // is sections that all collections inherit? I think this is how we show the masonry/columns
    //TODO: this seems important
    get Sections() {
        // appears that pivot field IS actually for sorting
        if (!this.pivotField || this.colHeaderData instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();

        if (this.colHeaderData === undefined) {
            setTimeout(() => (this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>()), 0);
            return new Map<SchemaHeaderField, Doc[]>();
        }
        const colHeaderData = Array.from(this.colHeaderData);
        const fields = new Map<SchemaHeaderField, Doc[]>(colHeaderData.map(sh => [sh, []] as [SchemaHeaderField, []]));
        let changed = false;
        this.filteredChildren.map(d => {
            const sectionValue = (d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`) as object;
            // the next five lines ensures that floating point rounding errors don't create more than one section -syip
            const parsed = parseInt(sectionValue.toString());
            const castedSectionValue = !isNaN(parsed) ? parsed : sectionValue;

            // look for if header exists already
            const existingHeader = colHeaderData.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`));
            if (existingHeader) {
                fields.get(existingHeader)!.push(d);
            } else {
                const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`);
                fields.set(newSchemaHeader, [d]);
                colHeaderData.push(newSchemaHeader);
                changed = true;
            }
        });
        // remove all empty columns if hideHeadings is set
        // we will want to have something like this, so that we can hide columns and add them back in
        if (this.layoutDoc._columnsHideIfEmpty) {
            Array.from(fields.keys())
                .filter(key => !fields.get(key)!.length)
                .map(header => {
                    fields.delete(header);
                    colHeaderData.splice(colHeaderData.indexOf(header), 1);
                    changed = true;
                });
        }
        changed &&
            setTimeout(
                action(() => this.colHeaderData?.splice(0, this.colHeaderData.length, ...colHeaderData)),
                0
            );
        return fields;
    }

    componentDidMount() {
        super.componentDidMount?.();
        this._props.setContentViewBox?.(this);

        // reset section headers when a new filter is inputted
        this._disposers.pivotField = reaction(
            () => this.pivotField,
            () => (this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List())
        );
        this._disposers.autoHeight = reaction(
            () => this.layoutDoc._layout_autoHeight,
            layout_autoHeight => layout_autoHeight && this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0)))
        );
        this._disposers.refList = reaction(
            () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !LightboxView.Contains(this.DocumentView?.()) }),
            ({ refList, autoHeight }) => {
                this.observer.disconnect();
                if (autoHeight) refList.forEach(r => this.observer.observe(r));
            },
            { fireImmediately: true }
        );
    }

    componentWillUnmount() {
        super.componentWillUnmount();
        this.observer.disconnect();
        Object.keys(this._disposers).forEach(key => this._disposers[key]());
    }

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

    moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
        return this._props.removeDocument?.(doc) && addDocument?.(doc) ? true : false;
    };

    onChildClickHandler = () => 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: any) => node.id === doc[Id]);
        if (found) {
            const top = found.getBoundingClientRect().top;
            const localTop = this.ScreenToLocalBoxXf().transformPoint(0, top);
            if (Math.floor(localTop[1]) !== 0) {
                let 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) => {
        if (property === StyleProp.Opacity && doc) {
            if (this._props.childOpacity) {
                return this._props.childOpacity();
            }
            if (this.Document._currentFrame !== undefined) {
                return CollectionFreeFormDocumentView.getValues(doc, NumCast(this.Document._currentFrame))?.opacity;
            }
        }
        return this._props.styleProvider?.(doc, props, property);
    };
    @undoBatch
    onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => {
        if (['Enter'].includes(e.key) && e.ctrlKey) {
            e.stopPropagation?.();
            const below = !e.altKey && e.key !== 'Tab';
            const layout_fieldKey = StrCast(fieldProps.fieldKey);
            const newDoc = Doc.MakeCopy(fieldProps.Document, true);
            const dataField = fieldProps.Document[Doc.LayoutFieldKey(newDoc)];
            newDoc[DocData][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined;
            if (layout_fieldKey !== 'layout' && fieldProps.Document[layout_fieldKey] instanceof Doc) {
                newDoc[layout_fieldKey] = fieldProps.Document[layout_fieldKey];
            }
            newDoc[DocData].text = undefined;
            FormattedTextBox.SetSelectOnLoad(newDoc);
            return this.addDocument?.(newDoc);
        }
    };
    isContentActive = () => (this._props.isContentActive() ? true : this._props.isSelected() === false || this._props.isContentActive() === false ? false : undefined);

    @observable _renderCount = 5;
    isChildContentActive = () =>
        this._props.isContentActive?.() === false
            ? false
            : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive))
              ? true
              : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
                ? false
                : undefined;
    isChildButtonContentActive = () => (this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false ? false : undefined);
    @observable docRefs = new ObservableMap<Doc, DocumentView>();
    childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null));
    // this is what renders the document that you see on the screen
    // called in Children: this actually adds a document to our children list
    getDisplayDoc(doc: Doc, width: () => number, trans: () => string, count: number) {
        const dataDoc = doc.isTemplateDoc || doc.isTemplateForField ? this._props.TemplateDataDocument : undefined;
        const height = () => this.getDocHeight(doc);
        const panelHeight = () => (this.isStackingView ? height() : Math.min(height(), this._props.PanelHeight()));
        const panelWidth = () => (this.isStackingView ? width() : this.columnWidth);
        const stackedDocTransform = () => this.getDocTransform(doc);
        this._docXfs.push({ stackedDocTransform, width, height });
        return count > this._renderCount ? null : (
            <DocumentView
                ref={action((r: DocumentView) => r?.ContentDiv && this.docRefs.set(doc, r))}
                Document={doc}
                TemplateDataDocument={dataDoc ?? (Doc.AreProtosEqual(doc[DocData], doc) ? undefined : doc[DocData])}
                renderDepth={this._props.renderDepth + 1}
                PanelWidth={panelWidth}
                PanelHeight={panelHeight}
                pointerEvents={this.DocumentView?.()._props.onClickScript?.() ? returnNone : undefined} // if the stack has an onClick, then we don't want the contents to be interactive (see CollectionPileView)
                styleProvider={this.styleProvider}
                containerViewPath={this.childContainerViewPath}
                layout_fitWidth={this.childFitWidth}
                isContentActive={doc.onClick ? this.isChildButtonContentActive : this.isChildContentActive}
                onKey={this.onKeyDown}
                DataTransition={trans}
                onBrowseClickScript={this._props.onBrowseClickScript}
                isDocumentActive={this.isContentActive}
                LayoutTemplate={this._props.childLayoutTemplate}
                LayoutTemplateString={this._props.childLayoutString}
                NativeWidth={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (this.childFitWidth(doc) && !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) || (this.childFitWidth(doc) && !Doc.NativeHeight(doc)) ? height : undefined}
                dontCenter={this._props.childIgnoreNativeSize ? 'xy' : (StrCast(this.layoutDoc.layout_dontCenter) as any)}
                dontRegisterView={BoolCast(this.layoutDoc.childDontRegisterViews, this._props.dontRegisterView)} // used to be true if DataDoc existed, but template textboxes won't layout_autoHeight resize if dontRegisterView is set, but they need to.
                rootSelected={this.rootSelected}
                layout_showTitle={this._props.childlayout_showTitle}
                dragAction={(this.layoutDoc.childDragAction ?? this._props.childDragAction) as dropActionType}
                onClickScript={this.onChildClickHandler}
                onDoubleClickScript={this.onChildDoubleClickHandler}
                ScreenToLocalTransform={stackedDocTransform}
                focus={this.focusDocument}
                childFilters={this.childDocFilters}
                hideDecorationTitle={this._props.childHideDecorationTitle}
                hideResizeHandles={this._props.childHideResizeHandles}
                childFiltersByRanges={this.childDocRangeFilters}
                searchFilterDocs={this.searchFilterDocs}
                xPadding={NumCast(this.layoutDoc._childXPadding, this._props.childXPadding)}
                yPadding={NumCast(this.layoutDoc._childYPadding, this._props.childYPadding)}
                addDocument={this._props.addDocument}
                moveDocument={this._props.moveDocument}
                removeDocument={this._props.removeDocument}
                contentPointerEvents={StrCast(this.layoutDoc.childContentPointerEvents) as any}
                whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
                addDocTab={this._props.addDocTab}
                pinToPres={this._props.pinToPres}
            />
        );
    }

    getDocTransform(doc: Doc) {
        const dref = this.docRefs.get(doc);
        this._scroll; // must be referenced for document decorations to update when the text box container is scrolled
        const { translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv);
        // 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);
    }
    getDocWidth(d?: Doc) {
        if (!d) return 0;
        const childLayoutDoc = Doc.Layout(d, this._props.childLayoutTemplate?.());
        const maxWidth = this.columnWidth / this.numGroupColumns;
        if (!this.layoutDoc._columnsFill && !this.childFitWidth(childLayoutDoc)) {
            return Math.min(NumCast(d._width), maxWidth);
        }
        return maxWidth;
    }
    getDocTransition(d?: Doc) {
        if (!d) return '';
        return StrCast(d.dataTransition);
    }
    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) || (!this.childFitWidth(childLayoutDoc) ? NumCast(d._width) : 0);
        const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!this.childFitWidth(childLayoutDoc) ? NumCast(d._height) : 0);
        if (nw && nh) {
            const colWid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
            const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d), colWid);
            return Math.min(maxHeight, (docWid * nh) / nw);
        }
        const childHeight = NumCast(childLayoutDoc._height);
        const panelHeight = this.childFitWidth(childLayoutDoc) ? Number.MAX_SAFE_INTEGER : this._props.PanelHeight() - 2 * this.yMargin;
        return Math.min(childHeight, maxHeight, panelHeight);
    }

    // This following three functions must be from the view Mehek showed
    columnDividerDown = (e: React.PointerEvent) => {
        runInAction(() => (this._cursor = 'grabbing'));
        const batch = UndoManager.StartBatch('stacking width');
        setupMoveUpEvents(
            this,
            e,
            this.onDividerMove,
            action(() => {
                this._cursor = 'ew-resize';
                batch.end();
            }),
            emptyFunction
        );
    };
    @action
    onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => {
        this.layoutDoc._columnWidth = Math.max(10, this.columnWidth + delta[0]);
        return false;
    };

    @computed get columnDragger() {
        return (
            <div
                className="collectionStackingView-columnDragger"
                onPointerDown={this.columnDividerDown}
                ref={this._draggerRef}
                style={{ cursor: this._cursor, color: SettingsManager.userColor, left: `${this.columnWidth + this.xMargin}px`, top: `${Math.max(0, this.yMargin - 9)}px` }}>
                <FontAwesomeIcon icon={'arrows-alt-h'} />
            </div>
        );
    }

    @undoBatch
    onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
        // Fairly confident that this is where the swapping of nodes in the various arrays happens
        const where = [de.x, de.y];
        // start at -1 until we're sure we want to add it to the column
        let dropInd = -1;
        let dropAfter = 0;
        if (de.complete.docDragData) {
            // going to re-add the docs to the _docXFs based on position of where we just dropped
            this._docXfs.map((cd, i) => {
                const pos = cd
                    .stackedDocTransform()
                    .inverse()
                    .transformPoint(-2 * this.gridGap, -2 * this.gridGap);
                const pos1 = cd.stackedDocTransform().inverse().transformPoint(cd.width(), cd.height());
                if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && (i === this._docXfs.length - 1 || where[1] < pos1[1])) {
                    dropInd = i;
                    const axis = this.isStackingView ? 1 : 0;
                    dropAfter = where[axis] > (pos[axis] + pos1[axis]) / 2 ? 1 : 0;
                }
            });
            const oldDocs = this.childDocs.length;
            if (super.onInternalDrop(e, de)) {
                // check to see if we actually need anything to the new column of nodes (if droppedDocs != empty)
                const droppedDocs = this.childDocs.slice().filter((d: Doc, ind: number) => ind >= oldDocs); // 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; // if nothing was added to the end of the list, then presumably the dropped documents were already in the list, but possibly got reordered so we use them.

                const docs = this.childDocList;
                // still figuring out where to add the document
                if (docs && newDocs.length) {
                    newDocs.forEach(newdoc => docs.indexOf(newdoc) !== -1 && docs.splice(docs.indexOf(newdoc), 1));
                    const insertInd = dropInd === -1 ? docs.length : dropInd + dropAfter;
                    const offset = newDocs.reduce((off, ndoc) => (this.filteredChildren.find((fdoc, i) => ndoc === fdoc && i < insertInd) ? off + 1 : off), 0);
                    newDocs.filter(ndoc => docs.indexOf(ndoc) !== -1).forEach(ndoc => docs.splice(docs.indexOf(ndoc), 1));
                    docs.splice(insertInd - offset, 0, ...newDocs);
                }
                return true;
            }
        } else if (de.complete.linkDragData?.dragDocument.embedContainer === this.Document && de.complete.linkDragData?.linkDragView?.CollectionFreeFormDocumentView) {
            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);
        }
        e.preventDefault();
        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.Document;
        };
        return true;
    }

    /// an item from outside of Dash is being dropped onto this stacking view (e.g, a document from the file system)
    @undoBatch
    onExternalDrop = async (e: React.DragEvent): Promise<void> => {
        const where = [e.clientX, e.clientY];
        let targInd = -1;
        this._docXfs.map((cd, i) => {
            const pos = cd
                .stackedDocTransform()
                .inverse()
                .transformPoint(-2 * this.gridGap, -2 * this.gridGap);
            const pos1 = cd.stackedDocTransform().inverse().transformPoint(cd.width(), cd.height());
            if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) {
                targInd = i;
            }
        });
        super.onExternalDrop(e, {}, (docs: Doc[]) => {
            if (targInd === -1) {
                this.addDocument(docs);
            } else {
                const childDocs = this.childDocList;
                if (childDocs) {
                    childDocs.splice(targInd, 0, ...docs);
                }
            }
        });
    };
    headings = () => Array.from(this.Sections);
    // what a section looks like if we're in stacking view
    sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
        const key = this.pivotField;
        let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined = undefined;
        if (this.pivotField) {
            const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]);
            if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
                type = types[0];
            }
        }
        return (
            <CollectionStackingViewFieldColumn
                refList={this._refList}
                addDocument={this.addDocument}
                chromeHidden={this.chromeHidden}
                colHeaderData={this.colHeaderData}
                Document={this.Document}
                TemplateDataDocument={this._props.TemplateDataDocument}
                renderChildren={this.children}
                columnWidth={this.columnWidth}
                numGroupColumns={this.numGroupColumns}
                gridGap={this.gridGap}
                pivotField={this.pivotField}
                key={heading?.heading ?? ''}
                headings={this.headings}
                heading={heading?.heading ?? ''}
                headingObject={heading}
                docList={docList}
                yMargin={this.yMargin}
                type={type}
                createDropTarget={this.createDashEventsTarget}
                screenToLocalTransform={this.ScreenToLocalBoxXf}
            />
        );
    };

    // what a section looks like if we're in masonry. Shouldn't actually need to use this.
    sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[], first: boolean) => {
        const key = this.pivotField;
        let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined = undefined;
        const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]);
        if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
            type = types[0];
        }
        const rows = () => (!this.isStackingView ? 1 : Math.max(1, Math.min(docList.length, Math.floor((this._props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))));
        return (
            <CollectionMasonryViewFieldRow
                showHandle={first}
                Document={this.Document}
                chromeHidden={this.chromeHidden}
                pivotField={this.pivotField}
                refList={this._refList}
                key={heading ? heading.heading : ''}
                rows={rows}
                headings={this.headings}
                heading={heading ? heading.heading : ''}
                headingObject={heading}
                docList={docList}
                parent={this}
                type={type}
                createDropTarget={this.createDashEventsTarget}
                screenToLocalTransform={this.ScreenToLocalBoxXf}
                setDocHeight={this.setDocHeight}
            />
        );
    };

    /// add a new group category (column) to the active set of note categories.  (e.g., if the pivot field is 'transportation', groups might be 'car', 'plane', 'bike', etc)
    @action
    addGroup = (value: string) => {
        if (value && this.colHeaderData) {
            const schemaHdrField = new SchemaHeaderField(value);
            this.colHeaderData.push(schemaHdrField);
            return true;
        }
        return false;
    };

    sortFunc = (a: [SchemaHeaderField, Doc[]], b: [SchemaHeaderField, Doc[]]): 1 | -1 => {
        const descending = StrCast(this.layoutDoc._columnsSort) === 'descending';
        const firstEntry = descending ? b : a;
        const secondEntry = descending ? a : b;
        return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1;
    };

    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 cm = ContextMenu.Instance;
            const options = cm.findByDescription('Options...');
            const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
            optionItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' });
            optionItems.push({ description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), icon: 'plus' });
            optionItems.push({ description: 'Clear All', event: () => (this.dataDoc[this.fieldKey ?? 'data'] = new List([])), icon: 'times' });
            !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
        }
    };

    //
    @computed get renderedSections() {
        TraceMobx();
        let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]];
        if (this.pivotField) {
            const entries = Array.from(this.Sections.entries());
            sections = this.layoutDoc._columnsSort ? entries.sort(this.sortFunc) : entries;
        }
        return sections.map((section, i) => (this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1], i === 0)));
    }

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

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

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

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

    observer = new _global.ResizeObserver(() => this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0))));

    onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
    _oldWheel: any;
    render() {
        TraceMobx();
        const editableViewProps = {
            GetValue: () => '',
            SetValue: this.addGroup,
            contents: '+ ADD A GROUP',
        };
        const buttonMenu = this.layoutDoc.layout_headerButton;
        const noviceExplainer = this.layoutDoc.layout_explainer;
        return (
            <>
                {buttonMenu || noviceExplainer ? (
                    <div className="documentButtonMenu">
                        {buttonMenu ? this.buttonMenu : null}
                        {Doc.noviceMode && noviceExplainer ? <div className="documentExplanation">{StrCast(noviceExplainer)}</div> : null}
                    </div>
                ) : null}
                <div className="collectionStackingMasonry-cont">
                    <div
                        className={this.isStackingView ? 'collectionStackingView' : 'collectionMasonryView'}
                        ref={ele => {
                            this._masonryGridRef = ele;
                            this.createDashEventsTarget(ele); //so the whole grid is the drop target?
                            this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
                            this._oldWheel = ele;
                            // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
                            ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
                        }}
                        style={{
                            overflowY: this.isContentActive() ? 'auto' : 'hidden',
                            background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor),
                            pointerEvents: (this._props.pointerEvents?.() as any) ?? this.backgroundEvents,
                        }}
                        onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))}
                        onDrop={this.onExternalDrop.bind(this)}
                        onContextMenu={this.onContextMenu}
                        onWheel={e => this.isContentActive() && e.stopPropagation()}>
                        {this.renderedSections}
                        {!this.showAddAGroup ? null : (
                            <div key={`${this.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" style={{ width: !this.isStackingView ? '100%' : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}>
                                <EditableView {...editableViewProps} />
                            </div>
                        )}
                    </div>
                </div>
            </>
        );
    }
}
