/* eslint-disable no-restricted-syntax */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IconButton, Popup, PopupTrigger, Size, Type } from 'browndash-components';
import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, observe, override, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ClientUtils, returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../ClientUtils';
import { emptyFunction } from '../../../../Utils';
import { Doc, DocListCast, Field, FieldType, IdToDoc, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
import { DocData } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
import { ColumnType } from '../../../../fields/SchemaHeaderField';
import { BoolCast, NumCast, StrCast } from '../../../../fields/Types';
import { DocUtils } from '../../../documents/DocUtils';
import { Docs, DocumentOptions, FInfo } from '../../../documents/Documents';
import { DragManager } from '../../../util/DragManager';
import { dropActionType } from '../../../util/DropActionTypes';
import { undoBatch, undoable } from '../../../util/UndoManager';
import { ContextMenu } from '../../ContextMenu';
import { EditableView } from '../../EditableView';
import { ObservableReactComponent } from '../../ObservableReactComponent';
import { StyleProp } from '../../StyleProp';
import { DefaultStyleProvider } from '../../StyleProvider';
import { Colors } from '../../global/globalEnums';
import { DocumentView } from '../../nodes/DocumentView';
import { FieldViewProps } from '../../nodes/FieldView';
import { FocusViewOptions } from '../../nodes/FocusViewOptions';
import { CollectionSubView } from '../CollectionSubView';
import './CollectionSchemaView.scss';
import { SchemaColumnHeader } from './SchemaColumnHeader';
import { SchemaRowBox } from './SchemaRowBox';
import { ContextMenuProps } from '../../ContextMenuItem';
import { DocumentManager } from '../../../util/DocumentManager';
import { SchemaCellField } from './SchemaCellField';
import { SnappingManager } from '../../../util/SnappingManager';

/**
 * The schema view offers a spreadsheet-like interface for users to interact with documents. Within the schema,
 * each doc is represented by its own row. Each column represents a field, for example the author or title fields. 
 * Users can apply varoius filters and sorts to columns to change what is displayed. The schemaview supports equations for
 * cell linking.
 * 
 * This class supports the main functionality for choosing which docs to render in the view, applying visual
 * updates to rows and columns (such as user dragging or sort-related highlighting), applying edits to multiple cells 
 * at once, and applying filters and sorts to columns. It contains SchemaRowBoxes (which themselves contain SchemaTableCells,
 * and SchemaCellFields) and SchemaColumnHeaders.
 */

const { SCHEMA_NEW_NODE_HEIGHT } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore

export const FInfotoColType: { [key: string]: ColumnType } = {
    string: ColumnType.String,
    number: ColumnType.Number,
    boolean: ColumnType.Boolean,
    date: ColumnType.Date,
    image: ColumnType.Image,
    rtf: ColumnType.RTF,
    enumeration: ColumnType.Enumeration,
};

const defaultColumnKeys: string[] = ['title', 'type', 'author', 'author_date', 'text'];

@observer
export class CollectionSchemaView extends CollectionSubView() {
    private _keysDisposer: any;
    private _disposers: { [name: string]: IReactionDisposer } = {};
    private _previewRef: HTMLDivElement | null = null;
    private _makeNewColumn: boolean = false;
    private _documentOptions: DocumentOptions = new DocumentOptions();
    private _tableContentRef: HTMLDivElement | null = null;
    private _menuTarget = React.createRef<HTMLDivElement>();
    private _headerRefs: SchemaColumnHeader[] = [];
    private _eqHighlightColors: Array<[{r: number, g: number, b: number}, {r: number, g: number, b: number}]> = [];

    constructor(props: any) {
        super(props);
        makeObservable(this);
        const lightenedColor = (r: number, g: number, b:number) => { const lightened = ClientUtils.lightenRGB(r, g, b, 165); return {r: lightened[0], g: lightened[1], b: lightened[2]}} // prettier-ignore
        const colors = (r: number, g: number, b: number): [any, any] => {return [{r: r, g: g, b: b}, lightenedColor(r, g, b)]} // prettier-ignore
        this._eqHighlightColors.push(colors(70, 150, 50));
        this._eqHighlightColors.push(colors(180, 70, 20));
        this._eqHighlightColors.push(colors(70, 50, 150));
        this._eqHighlightColors.push(colors(0, 140, 140));
        this._eqHighlightColors.push(colors(140, 30, 110));
        this._eqHighlightColors.push(colors(20, 50, 200));
        this._eqHighlightColors.push(colors(210, 30, 40));
        this._eqHighlightColors.push(colors(120, 130, 30));
        this._eqHighlightColors.push(colors(50, 150, 70));
        this._eqHighlightColors.push(colors(10, 90, 180));
    }

    static _rowHeight: number = 50;
    static _rowSingleLineHeight: number = 32;
    public static _minColWidth: number = 25;
    public static _rowMenuWidth: number = 60;
    public static _previewDividerWidth: number = 4;
    public static _newNodeInputHeight: number = Number(SCHEMA_NEW_NODE_HEIGHT);
    public fieldInfos = new ObservableMap<string, FInfo>();

    @observable _menuKeys: string[] = [];
    @observable _rowEles: ObservableMap = new ObservableMap<Doc, HTMLDivElement>();
    @observable _colEles: HTMLDivElement[] = [];
    @observable _displayColumnWidths: number[] | undefined = undefined;
    @observable _columnMenuIndex: number | undefined = undefined;
    @observable _newFieldWarning: string = '';
    @observable _makeNewField: boolean = false;
    @observable _newFieldDefault: any = 0;
    @observable _newFieldType: ColumnType = ColumnType.Number;
    @observable _menuValue: string = '';
    @observable _filterColumnIndex: number | undefined = undefined;
    @observable _filterSearchValue: string = ''; //the current text inside the filter search bar, used to determine which values to display
    @observable _selectedCol: number = 0;
    @observable _selectedCells: Array<Doc> = [];
    @observable _mouseCoordinates = { x: 0, y: 0, prevX: 0, prevY: 0 };
    @observable _lowestSelectedIndex: number = -1; //lowest index among selected rows; used to properly sync dragged docs with cursor position
    @observable _relCursorIndex: number = -1; //cursor index relative to the current selected cells
    @observable _draggedColIndex: number = 0;
    @observable _colBeingDragged: boolean = false; //whether a column is being dragged by the user
    @observable _colKeysFiltered: boolean = false;
    @observable _cellTags: ObservableMap = new ObservableMap<Doc, Array<string>>();
    @observable _highlightedCellsInfo: Array<[doc: Doc, field: string]> = [];
    @observable _cellHighlightColors: ObservableMap = new ObservableMap<string, string[]>();
    @observable _containedDocs: Doc[] = []; //all direct children of the schema
    @observable _referenceSelectMode: {enabled: boolean, currEditing: SchemaCellField | undefined} = {enabled: false, currEditing: undefined}
    
    // target HTMLelement portal for showing a popup menu to edit cell values.
    public get MenuTarget() {
        return this._menuTarget.current;
    }

    @computed get _selectedDocs() {
        // get all selected documents then filter out any whose parent is not this schema document
        const selected = DocumentView.SelectedDocs().filter(doc => this.docs.includes(doc));
        //&& this._selectedCells.includes(doc)
        if (!selected.length) {
            // if no schema doc is directly selected, test if a child of a schema doc is selected (such as in the preview window)
            const childOfSchemaDoc = DocumentView.SelectedDocs().find(sel => DocumentView.getContextPath(sel, true).includes(this.Document));
            if (childOfSchemaDoc) {
                const contextPath = DocumentView.getContextPath(childOfSchemaDoc, true);
                return [contextPath[contextPath.indexOf(childOfSchemaDoc) - 1]]; // the schema doc that is "selected" by virtue of one of its children being selected
            }
        }
        return selected;
    }

    @computed get highlightedCells() {
        return this._highlightedCellsInfo.map(info => this.getCellElement(info[0], info[1]));
    }

    @computed get documentKeys() {
        return Array.from(this.fieldInfos.keys());
    }

    @computed get previewWidth() {
        return NumCast(this.layoutDoc.schema_previewWidth);
    }

    @computed get tableWidth() {
        return this._props.PanelWidth() - this.previewWidth - (this.previewWidth === 0 ? 0 : CollectionSchemaView._previewDividerWidth);
    }

    @computed get columnKeys() {
        return StrListCast(this.layoutDoc.schema_columnKeys, defaultColumnKeys);
    }

    @computed get storedColumnWidths() {
        const widths = NumListCast(
            this.layoutDoc.schema_columnWidths,
            this.columnKeys.map(() => (this.tableWidth - CollectionSchemaView._rowMenuWidth) / this.columnKeys.length)
        );

        const totalWidth = widths.reduce((sum, width) => sum + width, 0);
        if (totalWidth !== this.tableWidth - CollectionSchemaView._rowMenuWidth) {
            return widths.map(w => (w / totalWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth));
        }
        return widths;
    }

    @computed get rowHeights() {
        return this.docs.map(() => this.rowHeightFunc());
    }

    @computed get displayColumnWidths() {
        return this._displayColumnWidths ?? this.storedColumnWidths;
    }

    @computed get sortField() {
        return StrCast(this.layoutDoc.sortField);
    }

    @computed get sortDesc() {
        return BoolCast(this.layoutDoc.sortDesc);
    }

    componentDidMount() {
        this._props.setContentViewBox?.(this);
        document.addEventListener('keydown', this.onKeyDown);

        Object.entries(this._documentOptions).forEach((pair: [string, FInfo]) => this.fieldInfos.set(pair[0], pair[1]));
        this._keysDisposer = observe(
            this.dataDoc[this.fieldKey ?? 'data'] as List<Doc>,
            (change: any) => {
                switch (change.type) {
                    case 'splice':
                        // prettier-ignore
                        (change as any).added.forEach((doc: Doc) => // for each document added 
                            Doc.GetAllPrototypes(doc.value as Doc).forEach(proto => // for all of its prototypes (and itself)
                                Object.keys(proto).forEach(action(key => // check if any of its keys are new, and add them
                                    !this.fieldInfos.get(key) && this.fieldInfos.set(key, new FInfo("-no description-", key === 'author'))))));
                        break;
                    case 'update': // let oldValue = change.oldValue; // fill this in if the entire child list will ever be reassigned with a new list
                        break;
                    default:
                }
            },
            true
        );
        this._disposers.docdata = reaction(
            () => DocListCast(this.dataDoc[this.fieldKey]), 
            (docs) => this._containedDocs = docs,
            {fireImmediately: true}
        )
        this._disposers.sortHighlight = reaction(
            () => [this.sortField, this._containedDocs, this._selectedDocs, this._highlightedCellsInfo], 
            () => {this.sortField && setTimeout(() => this.highlightSortedColumn())},
            {fireImmediately: true}
        )
    }

    componentWillUnmount() {
        this._keysDisposer?.();
        Object.values(this._disposers).forEach(disposer => disposer?.());
        document.removeEventListener('keydown', this.onKeyDown);
    }

    // ViewBoxInterface overrides
    override isUnstyledView = returnTrue; // used by style provider : turns off  opacity, animation effects, scaling

    removeDoc = (doc: Doc) => {
        this.removeDocument(doc);
        this._containedDocs = this._containedDocs.filter(d => d !== doc)
    }

    rowIndex = (doc: Doc) => this.docsWithDrag.docs.indexOf(doc);

    @action
    onKeyDown = (e: KeyboardEvent) => {
        if (this._selectedDocs.length > 0) {
            switch (e.key) {
                case 'ArrowDown':
                    {
                        const lastDoc = this._selectedDocs.lastElement();
                        const lastIndex = this.rowIndex(lastDoc);
                        const curDoc = this.docs[lastIndex];
                        if (lastIndex >= 0 && lastIndex < this.childDocs.length - 1) {
                            const newDoc = this.docs[lastIndex + 1];
                            if (this._selectedDocs.includes(newDoc)) {
                                DocumentView.DeselectView(DocumentView.getFirstDocumentView(curDoc));
                                this.deselectCell(curDoc);
                            } else {
                                this.selectCell(newDoc, this._selectedCol, e.shiftKey, e.ctrlKey);
                                this.scrollToDoc(newDoc, {});
                            }
                        }
                        e.stopPropagation();
                        e.preventDefault();
                    }
                    break;
                case 'ArrowUp':
                    {
                        const firstDoc = this._selectedDocs.lastElement();
                        const firstIndex = this.rowIndex(firstDoc);
                        const curDoc = this.docs[firstIndex];
                        if (firstIndex > 0 && firstIndex < this.childDocs.length) {
                            const newDoc = this.docs[firstIndex - 1];
                            if (this._selectedDocs.includes(newDoc)) {
                                DocumentView.DeselectView(DocumentView.getFirstDocumentView(curDoc));
                                this.deselectCell(curDoc);
                            } else {
                                this.selectCell(newDoc, this._selectedCol, e.shiftKey, e.ctrlKey);
                                this.scrollToDoc(newDoc, {});
                            }
                        }
                        e.stopPropagation();
                        e.preventDefault();
                    }
                    break;
                case 'ArrowRight':
                    if (this._selectedCells) {
                        this._selectedCol = Math.min(this._colEles.length - 1, this._selectedCol + 1);
                    } else if (this._selectedDocs.length > 0) {
                        this.selectCell(this._selectedDocs[0], 0, false, false);
                    }
                    break;
                case 'ArrowLeft':
                    if (this._selectedCells) {
                        this._selectedCol = Math.max(0, this._selectedCol - 1);
                    } else if (this._selectedDocs.length > 0) {
                        this.selectCell(this._selectedDocs[0], 0, false, false);
                    }
                    break;
                case 'Backspace': {
                    undoable(() => {this._selectedDocs.forEach(d => this._containedDocs.includes(d) && this.removeDoc(d));}, 'delete schema row');
                    break;
                }
                case 'Escape': {
                    this.deselectAllCells();
                    break;
                }
                case 'P': {
                    break;
                }
                default:
            }
        }
    };

    addRow = (doc: Doc | Doc[]) => this.addDocument(doc);

    @undoBatch
    changeColumnKey = (index: number, newKey: string, defaultVal?: any) => {
        if (!this.documentKeys.includes(newKey)) this.addNewKey(newKey, defaultVal);

        const currKeys = this.columnKeys.slice(); // copy the column key array first, then change it.
        currKeys[index] = newKey;
        this.layoutDoc.schema_columnKeys = new List<string>(currKeys);
    };

    @undoBatch
    addColumn = (index: number = 0, key?: string, defaultVal?: any) => {
        if (key && !this.documentKeys.includes(key)) this.addNewKey(key, defaultVal);
        
        const newColWidth = this.tableWidth / (this.storedColumnWidths.length + 1);
        const currWidths = this.storedColumnWidths.slice();
        currWidths.splice(index, 0, newColWidth);
        const newDesiredTableWidth = currWidths.reduce((w, cw) => w + cw, 0);
        this.layoutDoc.schema_columnWidths = new List<number>(currWidths.map(w => (w / newDesiredTableWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth)));

        const currKeys = this.columnKeys.slice();
        if (!key) key = 'EmptyColumnKey' + Math.floor(Math.random() * 1000000000000000).toString();
        currKeys.splice(index, 0, key);
        this.changeColumnKey(index, 'EmptyColumnKey' + Math.floor(Math.random() * 1000000000000000).toString());
        this.layoutDoc.schema_columnKeys = new List<string>(currKeys);
    };

    @action
    addNewKey = (key: string, defaultVal: any) => {
        this.childDocs.forEach(doc => {
            doc[DocData][key] = defaultVal;
        });
    }

    @undoBatch
    removeColumn = (index: number) => {
        if (this.columnKeys.length === 1) return;
        if (this._columnMenuIndex === index) {
            this._headerRefs[index].toggleEditing(false);
            this.closeNewColumnMenu();
        }
        const currWidths = this.storedColumnWidths.slice();
        currWidths.splice(index, 1);
        const newDesiredTableWidth = currWidths.reduce((w, cw) => w + cw, 0);
        this.layoutDoc.schema_columnWidths = new List<number>(currWidths.map(w => (w / newDesiredTableWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth)));

        const currKeys = this.columnKeys.slice();
        currKeys.splice(index, 1);
        this.layoutDoc.schema_columnKeys = new List<string>(currKeys); 

        this._colEles.splice(index, 1);
    };

    @action
    startResize = (e: any, index: number, rightSide: boolean) => {
        this._displayColumnWidths = this.storedColumnWidths;
        setupMoveUpEvents(this, e, moveEv => this.resizeColumn(moveEv, index, rightSide), this.finishResize, emptyFunction);
    };

    @action
    resizeColumn = (e: PointerEvent, index: number, rightSide: boolean) => {
        if (this._displayColumnWidths) {
            let shrinking;
            let growing;

            let change = e.movementX;

            if (rightSide && (index !== this._displayColumnWidths.length - 1)) {
                growing = change < 0 ? index + 1: index;
                shrinking = change < 0 ? index : index + 1;
            } else if (index !== 0) {
                growing = change < 0 ? index : index - 1;
                shrinking = change < 0 ? index - 1 : index;
            }

            if (shrinking === undefined || growing === undefined) return true;

            change = Math.abs(change);
            if (this._displayColumnWidths[shrinking] - change < CollectionSchemaView._minColWidth) {
                change = this._displayColumnWidths[shrinking] - CollectionSchemaView._minColWidth;
            }

            this._displayColumnWidths[shrinking] -= change * this.ScreenToLocalBoxXf().Scale;
            this._displayColumnWidths[growing] += change * this.ScreenToLocalBoxXf().Scale;

            return false;
        }
        return true;
    };

    @action
    finishResize = () => {
        this.layoutDoc.schema_columnWidths = new List<number>(this._displayColumnWidths);
        this._displayColumnWidths = undefined;
    };

    @undoBatch
    moveColumn = (fromIndex: number, toIndex: number) => {
        if (this._selectedCol === fromIndex) this._selectedCol = toIndex;
        else if (toIndex === this._selectedCol) this._selectedCol = fromIndex; // keeps selected cell consistent

        const currKeys = this.columnKeys.slice();
        currKeys.splice(toIndex, 0, currKeys.splice(fromIndex, 1)[0]);
        this.layoutDoc.schema_columnKeys = new List<string>(currKeys);

        const currWidths = this.storedColumnWidths.slice();
        currWidths.splice(toIndex, 0, currWidths.splice(fromIndex, 1)[0]);
        this.layoutDoc.schema_columnWidths = new List<number>(currWidths);
    };

    @action
    dragColumn = (e: PointerEvent, index: number) => {
        this.closeNewColumnMenu();
        this._headerRefs.forEach(ref => ref.toggleEditing(false));
        this._draggedColIndex = index;
        this.setColDrag(true);  
        const dragData = new DragManager.ColumnDragData(index);
        const dragEles = [this._colEles[index]];
        this.childDocs.forEach(doc => dragEles.push(this._rowEles.get(doc).children[1].children[index]));
        DragManager.StartColumnDrag(dragEles, dragData, e.x, e.y);
        return true;
    };

    /**
     * Uses cursor x coordinate to calculate which index the column should be rendered/dropped in
     * @param mouseX cursor x coordinate
     * @returns column index
     */
    findColDropIndex = (mouseX: number) => {
        const xOffset: number = this._props.ScreenToLocalTransform().inverse().transformPoint(0,0)[0] + CollectionSchemaView._rowMenuWidth; 
        let index: number | undefined;
        this.displayColumnWidths.reduce((total, curr, i) => {
            if (total <= mouseX && total + curr >= mouseX) {
                if (mouseX <= total + curr) index = i;
                else index = i + 1;
            }
            return total + curr;
        }, xOffset);
        return index;
    };

    /**
     * Calculates the current index of dragged rows for dynamic rendering and drop logic.
     * @param mouseY user's cursor position relative to the viewport
     * @returns row index the dragged doc should be rendered/dropped in
     */
    findRowDropIndex = (mouseY: number): number => {
        const rowHeight = CollectionSchemaView._rowHeight;
        let index: number = 0;
        this.rowHeights.reduce((total, curr, i) => {
            if (total <= mouseY && total + curr >= mouseY) {
                if (mouseY <= total + curr) index = i;
                else index = i + 1;
            }
            return total + curr;
        }, rowHeight);

        // fix index if selected rows are dragged out of bounds
        let adjIndex = index - this._relCursorIndex;
        const maxY = this.rowHeights.reduce((total, curr) => total + curr, 0) + rowHeight;
        if (mouseY > maxY) adjIndex = this.childDocs.length - 1;
        else if (adjIndex <= 0) adjIndex = 0;

        return adjIndex;
    };

    @action
    setRelCursorIndex = (mouseY: number) => {
        this._mouseCoordinates.y = mouseY; // updates this.rowDropIndex computed value to overwrite the old cached value

        const rowHeight = CollectionSchemaView._rowHeight;
        const adjInitMouseY = mouseY - rowHeight - 100; // rowHeight: height of the column menu cells | 100: height of the top menu
        const yOffset = this._lowestSelectedIndex * rowHeight;

        const heights = this._selectedDocs.map(() => this.rowHeightFunc());
        let index: number = 0;
        heights.reduce((total, curr, i) => {
            if (total <= adjInitMouseY && total + curr >= adjInitMouseY) {
                if (adjInitMouseY <= total + curr) index = i;
                else index = i + 1;
            }
            return total + curr;
        }, yOffset);
        this._relCursorIndex = index;
    };

    highlightDraggedColumn = (index: number) =>
        this._colEles.forEach((colRef, i) => {
            const edgeStyle = i === index ? `solid 2px ${Colors.MEDIUM_BLUE}` : '';
            const sorted = i === this.columnKeys.indexOf(this.sortField);
            const cellEles = [
                colRef,
                ...this.docsWithDrag.docs 
                    .filter(doc => (i !== this._selectedCol || !this._selectedDocs.includes(doc)) && !sorted)
                    .map(doc => this._rowEles.get(doc).children[1].children[i]),
            ];
            cellEles.forEach(ele => {
                if (sorted || this.highlightedCells.includes(ele)) return;
                ele.style.borderTop = ele === cellEles[0] ? edgeStyle : '';
                ele.style.borderLeft = edgeStyle;
                ele.style.borderRight = edgeStyle;
                ele.style.borderBottom = ele === cellEles.slice(-1)[0] ? edgeStyle : '';
            });
        });

    removeDragHighlight = () => {
        this._colEles.forEach((colRef, i) => {
            const sorted = i === this.columnKeys.indexOf(this.sortField);
            if (sorted) return;

            colRef.style.borderLeft = '';
            colRef.style.borderRight = '';
            colRef.style.borderTop = '';

            this.childDocs.forEach(doc => {
                const cell = this._rowEles.get(doc).children[1].children[i];
                if (!(this._selectedDocs.includes(doc) && i === this._selectedCol) && !(this.highlightedCells.includes(cell)) && cell) {
                    cell.style.borderLeft = '';
                    cell.style.borderRight = '';
                    cell.style.borderBottom = '';
                }
            });
        });
    }

    /**
     * Applies a gradient highlight to a sorted column. The direction of the gradient depends
     * on whether the sort is ascending or descending.
     * @param field the column being sorted
     * @param descending whether the sort is descending or ascending; descending if true
     */
    highlightSortedColumn = (field?: string, descending?: boolean) => {
        let index = -1;
        const highlightColors: string[] = [];
        const rowCount: number = this._containedDocs.length + 1;
        if (field || this.sortField){
            index = this.columnKeys.indexOf(field || this.sortField);
            const increment: number = 110/rowCount;
            for (let i = 1; i <= rowCount; ++i){
                const adjColor = ClientUtils.lightenRGB(16, 66, 230, increment * i);
                highlightColors.push(`solid 2px rgb(${adjColor[0]}, ${adjColor[1]}, ${adjColor[2]})`);
            }
        }

        this._colEles.forEach((colRef, i) => {
            const highlight: boolean = i === index;
            const desc: boolean = descending || this.sortDesc;
            const cellEles = [
                colRef,
                ...this.docsWithDrag.docs 
                    .filter(doc => (i !== this._selectedCol || !this._selectedDocs.includes(doc)))
                    .map(doc => this._rowEles.get(doc).children[1].children[i]),
            ];
            const cellCount = cellEles.length;
            for (let ele = 0; ele < cellCount; ++ele){
                const currCell = cellEles[ele];
                if (this.highlightedCells.includes(currCell)) continue;
                const style = highlight ? desc ? `${highlightColors[cellCount - 1 - ele]}` : `${highlightColors[ele]}` : '';
                currCell.style.borderLeft = style;
                currCell.style.borderRight = style;
            }
            cellEles[0].style.borderTop = highlight ? desc ? `${highlightColors[cellCount - 1]}` : `${highlightColors[0]}` : '';
            if (!(this._selectedDocs.includes(this.docsWithDrag.docs[this.docsWithDrag.docs.length - 1]) && this._selectedCol === index) && !this.highlightedCells.includes(cellEles[cellCount - 1])) cellEles[cellCount - 1].style.borderBottom = highlight ? desc ? `${highlightColors[0]}` :  `${highlightColors[cellCount - 1]}` : ''; 
        });
        
    }

    /**
     * Gets the html element representing a cell so that styles can be applied on it.
     * @param doc the cell's row document
     * @param fieldKey the cell's column's field key
     * @returns the html element representing the cell at the given location
     */
    getCellElement = (doc: Doc, fieldKey: string) => {
        const index = this.columnKeys.indexOf(fieldKey);
        const cell = this._rowEles.get(doc).children[1].children[index];
        return cell;
    }

    /**
     * Given text in a cell, find references to other cells (for equations).
     * @param text the text in the cell
     * @returns the html cell elements referenced in the text. 
     */
    findCellRefs = (text: string) => {
        const pattern = /(this|d(\d+))\.(\w+)/g;
        interface Match { docRef: string; field: string; }

        const matches: Match[] = [];
        let match: RegExpExecArray | null;

        while ((match = pattern.exec(text)) !== null) {
            const docRef = match[1] === 'this' ? match[1] : match[2]; 
            matches.push({ docRef, field: match[3] });
        }

        const cells: Array<any> = [];
        matches.forEach((match: Match) => {
            const {docRef, field} = match;
            const docView = DocumentManager.Instance.DocumentViews[Number(docRef)];
            const doc = docView?.Document ?? undefined;
            if (this.columnKeys.includes(field) && this._containedDocs.includes(doc)) {cells.push([doc, field])}
        })

        return cells;
    }

    /**
     * Determines whether the rows above or below a given row have been
     * selected, so selection highlights don't overlap.
     * @param doc the document row to check
     * @returns a boolean tuple where 0 is the row above, and 1 is the row below
     */
    selectionOverlap = (doc: Doc): [boolean, boolean] => {
        const docs = this.docsWithDrag.docs;
        const index = this.rowIndex(doc);
        const selectedBelow: boolean = this._selectedDocs.includes(docs[index + 1]);
        const selectedAbove: boolean = this._selectedDocs.includes(docs[index - 1]);
        return [selectedAbove, selectedBelow];
    }

    @action
    removeCellHighlights = () => {
        this._highlightedCellsInfo.forEach(info => {
            const doc = info[0];
            const field = info[1];
            const cell = this.getCellElement(doc, field);
            if (this._selectedDocs.includes(doc) && this._selectedCol === this.columnKeys.indexOf(field)) {
                cell.style.border = `solid 2px ${Colors.MEDIUM_BLUE}`;
                if (this.selectionOverlap(doc)[0]) cell.style.borderTop = '';
                if (this.selectionOverlap(doc)[1]) cell.style.borderBottom = '';
            } else cell.style.border = '';
            cell.style.backgroundColor = '';});
        this._highlightedCellsInfo = [];
    }

    restoreCellHighlights = () => {
        this._highlightedCellsInfo.forEach(info => {
            const doc = info[0];
            const field = info[1];
            const key = `${doc[Id]}_${field}`;
            const cell = this.getCellElement(doc, field);
            const color = this._cellHighlightColors.get(key)[0];
            cell.style.borderTop = color;
            cell.style.borderLeft = color;
            cell.style.borderRight = color;
            cell.style.borderBottom = color;
        });
    }

    /**
     * Highlights cells based on equation text in the cell currently being edited. 
     * Does not highlight selected cells (that's done directly in SchemaTableCell).
     * @param text the equation
     */
    highlightCells = (text: string) => {
        this.removeCellHighlights();

        const cellsToHighlight = this.findCellRefs(text);
        this._highlightedCellsInfo = [...cellsToHighlight];

        for (let i = 0; i < this._highlightedCellsInfo.length; ++i) {
            const info = this._highlightedCellsInfo[i];
            const color = this._eqHighlightColors[i % 10];
            const colorStrings = [`solid 2px rgb(${color[0].r}, ${color[0].g}, ${color[0].b})`, `rgb(${color[1].r}, ${color[1].g}, ${color[1].b})`];
            const doc = info[0]; 
            const field = info[1];
            const key = `${doc[Id]}_${field}`;
            const cell = this.getCellElement(doc, field);
            this._cellHighlightColors.set(key, [colorStrings[0], colorStrings[1]]);
            cell.style.border = colorStrings[0];
            cell.style.backgroundColor = colorStrings[1];
        }
    }

    //Used in SchemaRowBox
    @action
    addRowRef = (doc: Doc, ref: HTMLDivElement) => this._rowEles.set(doc, ref);

    @action
    setColRef = (index: number, ref: HTMLDivElement) => {
        if (this._colEles.length <= index) {
            this._colEles.push(ref);
        } else {
            this._colEles[index] = ref;
        }
    };

    @action
    addDocToSelection = (doc: Doc, extendSelection: boolean) => {
        const rowDocView = DocumentView.getDocumentView(doc);
        if (rowDocView) DocumentView.SelectView(rowDocView, extendSelection);
    };

    @action
    clearSelection = () => {
        if (this._referenceSelectMode.enabled) return;
        DocumentView.DeselectAll();
        this.deselectAllCells();
    };


    selectRow = (doc: Doc, lastSelected: Doc) => {
        const index = this.rowIndex(doc);
        const lastSelectedRow = this.rowIndex(lastSelected);
        const startRow = Math.min(lastSelectedRow, index);
        const endRow = Math.max(lastSelectedRow, index);
        for (let i = startRow; i <= endRow; i++) {
            const currDoc = this.docsWithDrag.docs[i];
            if (!this._selectedDocs.includes(currDoc)) {
                this.selectCell(currDoc, this._selectedCol, false, true);
            }
        }
    };

    //Used in SchemaRowBox
    selectReference = (doc: Doc | undefined, col: number) => {
        if (!doc) return;
        const docIndex = DocumentView.getDocViewIndex(doc);
        const field = this.columnKeys[col];
        const refToAdd = `d${docIndex}.${field}`
        const editedField = this._referenceSelectMode.currEditing ? this._referenceSelectMode.currEditing as SchemaCellField : null;
        editedField?.insertText(refToAdd, true);
        editedField?.setupRefSelect(false);
        return;
    }

    @action
    selectCell = (doc: Doc, col: number, shiftKey: boolean, ctrlKey: boolean) => {
        this.closeNewColumnMenu();
        if (!shiftKey && !ctrlKey) this.clearSelection();
        !this._selectedCells && (this._selectedCells = []);
        !shiftKey && this._selectedCells.push(doc);
        const index = this.rowIndex(doc);

        if (!this) return;
        const lastSelected = Array.from(this._selectedDocs).lastElement();
        if (shiftKey && lastSelected && !this._selectedDocs.includes(doc)) this.selectRow(doc, lastSelected);
        else if (ctrlKey) {
            if (lastSelected && this._selectedDocs.includes(doc)) {
                DocumentView.DeselectView(DocumentView.getFirstDocumentView(doc));
                this.deselectCell(doc);
            } else this.addDocToSelection(doc, true);
        } else this.addDocToSelection(doc, false);
        this._selectedCol = col;

        if (this._lowestSelectedIndex === -1 || index < this._lowestSelectedIndex) this._lowestSelectedIndex = index;
    };

    @action
    deselectCell = (doc: Doc) => {
        this._selectedCells && (this._selectedCells = this._selectedCells.filter(d => d !== doc));
        if (this.rowIndex(doc) === this._lowestSelectedIndex) this._lowestSelectedIndex = Math.min(...this._selectedDocs.map(d => this.rowIndex(d)));
    };

    @action
    deselectAllCells = () => {
        this._selectedCells = [];
        this._lowestSelectedIndex = -1;
    };

    @computed
    get rowDropIndex() {
        const mouseY = this.ScreenToLocalBoxXf().transformPoint(this._mouseCoordinates.x, this._mouseCoordinates.y)[1];
        return this.findRowDropIndex(mouseY);
    }

    @action
    onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
        if (de.complete.columnDragData) {
            setTimeout(() => {this.setColDrag(false);});
            e.stopPropagation();
            return true;
        }

        const draggedDocs = de.complete.docDragData?.draggedDocuments;
        if (draggedDocs && super.onInternalDrop(e, de) && !this.sortField) {
            const docs = this.docsWithDrag.docs.slice();
            this.dataDoc[this.fieldKey ?? 'data'] = new List<Doc>([...docs]);
            this.clearSelection();
            draggedDocs.forEach(doc => {
                DocumentView.addViewRenderedCb(doc, dv => dv.select(true));
            });
            this._lowestSelectedIndex = Math.min(...(draggedDocs?.map(doc => this.rowIndex(doc)) ?? []));
            return true;
        }
        return false;
    };

    onExternalDrop = (e: React.DragEvent) => super.onExternalDrop(e, {}, docs => docs.map(doc => this.addDocument(doc)));
    onDividerDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, emptyFunction);

    @action
    onDividerMove = (e: PointerEvent) => {
        const nativeWidth = this._previewRef!.getBoundingClientRect();
        const minWidth = 40;
        const maxWidth = 1000;
        const movedWidth = this.ScreenToLocalBoxXf().transformDirection(nativeWidth.right - e.clientX, 0)[0];
        const width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth;
        this.layoutDoc.schema_previewWidth = width;
        return false;
    };

    menuCallback = (x: number, y: number) => {
        ContextMenu.Instance.clearItems();

        DocUtils.addDocumentCreatorMenuItems(this.addRow, this.addRow, x, y, true);

        ContextMenu.Instance.displayMenu(x, y, undefined, true);
    };

    focusDocument = (doc: Doc, options: FocusViewOptions) => {
        Doc.BrushDoc(doc);
        this.scrollToDoc(doc, options);
        return undefined;
    };

    scrollToDoc = (doc: Doc, options: FocusViewOptions) => {
        const found = this._tableContentRef && Array.from(this._tableContentRef.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]);
        if (found) {
            const rect = found.getBoundingClientRect();
            const localRect = this.ScreenToLocalBoxXf().transformBounds(rect.left, rect.top, rect.width, rect.height);
            if (localRect.y < this.rowHeightFunc() || localRect.y + localRect.height > this._props.PanelHeight()) {
                const focusSpeed = options.zoomTime ?? 50;
                smoothScroll(focusSpeed, this._tableContentRef!, localRect.y + this._tableContentRef!.scrollTop - this.rowHeightFunc(), options.easeFunc);
                return focusSpeed;
            }
        }
        return undefined;
    };

    @action
    setKey = (key: string, defaultVal?: any, index?: number) => {
        if (this.columnKeys.includes(key)) return;
    
        if (this._makeNewColumn) {
            this.addColumn(this.columnKeys.indexOf(key), key, defaultVal);
            this._makeNewColumn = false;
        } else this.changeColumnKey(this._columnMenuIndex! | index!, key, defaultVal);

        this.closeNewColumnMenu();
    };

    /**
     * Used in SchemaRowBox to set 
     * @param key 
     * @param value 
     * @returns 
     */
    setCellValues = (key: string, value: string) => {
        if (this._selectedCells.length === 1) this.docs.forEach(doc => !doc._lockedSchemaEditing && Doc.SetField(doc, key, value)); 
        else this._selectedCells.forEach(doc => !doc._lockedSchemaEditing && Doc.SetField(doc, key, value)); 
        return true;
    };

    @action
    openNewColumnMenu = (index: number, newCol: boolean) => {
        this.closeFilterMenu();

        this._makeNewColumn = false;
        this._columnMenuIndex = index;
        this._menuValue = '';
        this._menuKeys = this.documentKeys;
        this._newFieldWarning = '';
        this._makeNewColumn = newCol;
    };

    @action
    closeNewColumnMenu = () => {
        this._columnMenuIndex = undefined;
    };

    @action
    openFilterMenu = (index: number) => {
        this._filterColumnIndex = index;
        this._filterSearchValue = '';
    };

    @action
    closeFilterMenu = () => {
        this._filterColumnIndex = undefined;
    };

    @undoBatch
    setColumnSort = (field: string | undefined, desc: boolean = false) => {
        this.layoutDoc.sortField = field;
        this.layoutDoc.sortDesc = desc;
    };

    openContextMenu = (x: number, y: number, index: number) => {
        this.closeNewColumnMenu();
        this.closeFilterMenu();
        const cm = ContextMenu.Instance;
        cm.clearItems();

        const fieldSortedAsc = (this.sortField === this.columnKeys[index] && !this.sortDesc);
        const fieldSortedDesc = (this.sortField === this.columnKeys[index] && this.sortDesc);
        const revealOptions = cm.findByDescription('Sort column')
        const sortOptions: ContextMenuProps[] = revealOptions && revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : [];
        sortOptions.push({
            description: 'Sort A-Z',     
            event: () => {
                this.setColumnSort(undefined); 
                const field = this.columnKeys[index];
                this._containedDocs = this.sortDocs(field, false);
                setTimeout(() => {
                    this.highlightSortedColumn(field, false);
                    setTimeout(() => this.highlightSortedColumn(), 480); 
                }, 20);
            },     
            icon: 'arrow-down-a-z',});
        sortOptions.push({
            description: 'Sort Z-A',     
            event: () => {
                this.setColumnSort(undefined); 
                const field = this.columnKeys[index];
                this._containedDocs = this.sortDocs(field, true);
                setTimeout(() => {
                    this.highlightSortedColumn(field, true);
                    setTimeout(() => this.highlightSortedColumn(), 480); 
                }, 20);
            },     
            icon: 'arrow-up-z-a'});
        sortOptions.push({
            description: 'Persistent Sort A-Z',     
            event: () => {
                if (fieldSortedAsc){
                    this.setColumnSort(undefined);
                    this.highlightSortedColumn();
                } else {
                    this.sortDocs(this.columnKeys[index], false);
                    this.setColumnSort(this.columnKeys[index], false);
                }
            },     
            icon: fieldSortedAsc ? 'lock' : 'lock-open'}); // prettier-ignore
        sortOptions.push({
            description: 'Persistent Sort Z-A',     
            event: () => {
                if (fieldSortedDesc){
                    this.setColumnSort(undefined);
                    this.highlightSortedColumn();
                } else {
                    this.sortDocs(this.columnKeys[index], true);
                    this.setColumnSort(this.columnKeys[index], true);
                }
            },     
            icon: fieldSortedDesc ? 'lock' : 'lock-open'}); // prettier-ignore
    
        cm.addItem({
            description: `Change field`,
            event: () => this.openNewColumnMenu(index, false),
            icon: 'pencil-alt',
        });
        cm.addItem({
            description: 'Filter field',
            event: () => this.openFilterMenu(index),
            icon: 'filter',
        });
        cm.addItem({ 
            description: 'Sort column', 
            addDivider: false, 
            noexpand: true, 
            subitems: sortOptions, 
            icon: 'sort' 
        });
        cm.addItem({
            description: 'Add column to left',
            event: () => this.addColumn(index),
            icon: 'plus',
        });
        cm.addItem({
            description: 'Add column to right',
            event: () => this.addColumn(index + 1),
            icon: 'plus',
        });
        cm.addItem({
            description: 'Delete column',
            event: () => this.removeColumn(index),
            icon: 'trash',
        });
        cm.displayMenu(x, y, undefined, false);
    };

    //used in schemacolumnheader
    @action
    updateKeySearch = (val: string) => {
        this._menuKeys = this.documentKeys.filter(value => value.toLowerCase().includes(val.toLowerCase()));
    };

    getFieldFilters = (field: string) => StrListCast(this.Document._childFilters).filter(filter => filter.split(Doc.FilterSep)[0] === field);

    removeFieldFilters = (field: string) => {
        this.getFieldFilters(field).forEach(filter => Doc.setDocFilter(this.Document, field, filter.split(Doc.FilterSep)[1], 'remove'));
    };

    onFilterKeyDown = (e: React.KeyboardEvent) => {
        switch (e.key) {
            case 'Enter':
            case 'Escape':
                this.closeFilterMenu();
                break;
            default:
        }
    };

    @action
    updateFilterSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
        this._filterSearchValue = e.target.value;
    };

    onKeysPassiveWheel = (e: WheelEvent) => {
        // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
        if (!this._oldKeysWheel.scrollTop && e.deltaY <= 0) e.preventDefault();
        e.stopPropagation();
    };

    _oldKeysWheel: any;
    @computed get keysDropdown() {
        return (
            <div className="schema-key-search">
                <div
                    className="schema-key-list"
                    ref={r => {
                        this._oldKeysWheel?.removeEventListener('wheel', this.onKeysPassiveWheel);
                        this._oldKeysWheel = r;
                        r?.addEventListener('wheel', this.onKeysPassiveWheel, { passive: false });
                    }}>
                    {this._menuKeys.map(key => (
                        <div
                            className="schema-search-result"
                            onPointerDown={e => {
                                e.stopPropagation();
                                this.setKey(key);
                            }}>
                            <p>
                                <span className="schema-search-result-key">
                                    <b>{key}</b>
                                </span>
                                <span>: </span>
                                <span className="schema-search-result-desc">&nbsp;&nbsp;{this.fieldInfos.get(key)!.description}</span>
                            </p>
                        </div>
                    ))}
                </div>
            </div>
        );
    }

    @computed get renderColumnMenu() {
        const x = this._columnMenuIndex! === -1 ? 0 : this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._columnMenuIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth);
        return (
            <div className="schema-column-menu" style={{ left: x,  maxWidth: `${Math.max(this._colEles[this._columnMenuIndex ?? 0].offsetWidth, 150)}px` }}>
                {this.keysDropdown}
            </div>
        );
    }

    @computed get renderFilterOptions() {
        const keyOptions: string[] = [];
        const columnKey = this.columnKeys[this._filterColumnIndex!];
        const allDocs = DocListCast(this.dataDoc[this._props.fieldKey]);
        allDocs.forEach(doc => {
            const value = StrCast(doc[columnKey]);
            if (!keyOptions.includes(value) && value !== '' && (this._filterSearchValue === '' || value.includes(this._filterSearchValue))) {
                keyOptions.push(value);
            }
        });

        const filters = StrListCast(this.Document._childFilters);
        return keyOptions.map(key => {
            let bool = false;
            if (filters !== undefined) {
                const ind = filters.findIndex(filter => filter.split(Doc.FilterSep)[1] === key);
                const fields = ind === -1 ? undefined : filters[ind].split(Doc.FilterSep);
                bool = fields ? fields[2] === 'check' : false;
            }
            return (
                <div key={key} className="schema-filter-option">
                    <input 
                        type="checkbox"
                        onPointerDown={e => e.stopPropagation()}
                        onClick={e => e.stopPropagation()}
                        onChange={e => Doc.setDocFilter(this.Document, columnKey, key, e.target.checked ? 'check' : 'remove')}
                        checked={bool}
                    />
                    <span style={{ paddingLeft: 4 }}>{key}</span>
                </div>
            );
        });
    }

    @computed get renderFilterMenu() {
        const x = this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._filterColumnIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth);
        return (
            <div className="schema-filter-menu" style={{ left: x, maxWidth: `${Math.max(this._colEles[this._columnMenuIndex ?? 0].offsetWidth, 150)}px`}}>
                <input className="schema-filter-input" type="text" value={this._filterSearchValue} onKeyDown={this.onFilterKeyDown} onChange={this.updateFilterSearch} onPointerDown={e => e.stopPropagation()} />
                {this.renderFilterOptions}
                <div
                    className="schema-column-menu-button"
                    onPointerDown={action((e: any) => {
                        e.stopPropagation();
                        this.closeFilterMenu();
                    })}>
                    done
                </div>
            </div>
        );
    }

    @action setColDrag = (beingDragged: boolean) => {
        this._colBeingDragged = beingDragged;
        !beingDragged && this.removeDragHighlight();
    }

    @action updateMouseCoordinates = (e: React.PointerEvent<HTMLDivElement>) => {
        const prevX = this._mouseCoordinates.x;
        const prevY = this._mouseCoordinates.y;
        this._mouseCoordinates = { x: e.clientX, y: e.clientY, prevX: prevX, prevY: prevY };
    }

    @action
    onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
        if (DragManager.docsBeingDragged.length) {
            this.updateMouseCoordinates(e);
        }
        if (this._colBeingDragged) {
            this.updateMouseCoordinates(e);
            const newIndex = this.findColDropIndex(e.clientX);
            const direction: number = this._mouseCoordinates.x > this._mouseCoordinates.prevX ? 1 : 0;
            if (newIndex !== undefined && ((newIndex > this._draggedColIndex && direction === 1) || (newIndex < this._draggedColIndex && direction === 0))) {
                this.moveColumn(this._draggedColIndex, newIndex ?? this._draggedColIndex);
                this._draggedColIndex = newIndex !== undefined ? newIndex : this._draggedColIndex;
            }
            this.highlightSortedColumn(); //TODO: Make this more efficient
            this.restoreCellHighlights();
            !(this.sortField && this._draggedColIndex === this.columnKeys.indexOf(this.sortField)) && this.highlightDraggedColumn(this._draggedColIndex);
        }
    };

    /**
     * Gets docs contained by collections within the schema. Currently defunct.
     * @param doc 
     * @param displayed 
     * @returns 
     */
    // subCollectionDocs = (doc: Doc, displayed: boolean) => {
    //     const childDocs = DocListCast(doc[Doc.LayoutFieldKey(doc)]);
    //     let collections: Array<Doc> = [];
    //     if (displayed) collections = childDocs.filter(d => d.type === 'collection' && d._childrenSharedWithSchema);
    //     else collections = childDocs.filter(d => d.type === 'collection' && !d._childrenSharedWithSchema);
    //     let toReturn: Doc[] = [...childDocs];
    //     collections.forEach(d => toReturn = toReturn.concat(this.subCollectionDocs(d, displayed)));
    //     return toReturn;
    // }

    /**
     * Applies any filters active on the schema to filter out docs that don't match.
     */
    @computed get filteredDocs() {
        const childDocFilters = this.childDocFilters();
        const childFiltersByRanges = this.childDocRangeFilters();
        const searchDocs = this.searchFilterDocs();

        const docsforFilter: Doc[] = [];
        this._containedDocs.forEach(d => {
            // dragging facets
            const dragged = this._props.childFilters?.().some(f => f.includes(ClientUtils.noDragDocsFilter));
            if (dragged && SnappingManager.CanEmbed && DragManager.docsBeingDragged.includes(d)) return;
            let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), childFiltersByRanges, this.Document).length > 0;
            if (notFiltered) {
                notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, childFiltersByRanges, this.Document).length > 0;
                const fieldKey = Doc.LayoutFieldKey(d);
                const isAnnotatableDoc = d[fieldKey] instanceof List && !(d[fieldKey] as List<Doc>)?.some(ele => !(ele instanceof Doc));
                const docChildDocs = d[isAnnotatableDoc ? fieldKey + '_annotations' : fieldKey];
                const sidebarDocs = isAnnotatableDoc && d[fieldKey + '_sidebar'];
                if (docChildDocs !== undefined || sidebarDocs !== undefined) {
                    let subDocs = [...DocListCast(docChildDocs), ...DocListCast(sidebarDocs)];
                    if (subDocs.length > 0) {
                        let newarray: Doc[] = [];
                        notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, childFiltersByRanges, d).length);
                        while (subDocs.length > 0 && !notFiltered) {
                            newarray = [];
                            // eslint-disable-next-line no-loop-func
                            subDocs.forEach(t => {
                                const docFieldKey = Doc.LayoutFieldKey(t);
                                const isSubDocAnnotatable = t[docFieldKey] instanceof List && !(t[docFieldKey] as List<Doc>)?.some(ele => !(ele instanceof Doc));
                                notFiltered =
                                    notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !childFiltersByRanges.length) || DocUtils.FilterDocs([t], childDocFilters, childFiltersByRanges, d).length));
                                DocListCast(t[isSubDocAnnotatable ? docFieldKey + '_annotations' : docFieldKey]).forEach(newdoc => newarray.push(newdoc));
                                isSubDocAnnotatable && DocListCast(t[docFieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc));
                            });
                            subDocs = newarray;
                        }
                    }
                }
            }
            notFiltered && docsforFilter.push(d);
        });
        return docsforFilter;
    }

    /**
     * Returns all child docs of the schema and child docs of contained collections that satisfy applied filters.
     */
    @computed get docs() {
        //let docsFromChildren: Doc[] = [];

        // Functionality for adding child docs
        //const displayedCollections = this.childDocs.filter(d => d.type === 'collection' && d._childrenSharedWithSchema);
        // displayedCollections.forEach(d => {
        //     let docsNotAlreadyDisplayed = this.subCollectionDocs(d, true).filter(dc => !this._containedDocs.includes(dc));
        //     docsFromChildren = docsFromChildren.concat(docsNotAlreadyDisplayed);
        // });

        return this.filteredDocs;;
    }

    /**
     * Sorts docs first alphabetically and then numerically. 
     * @param field the column being sorted
     * @param desc whether the sort is ascending or descending
     * @param persistent whether the sort is applied persistently or is one-shot
     * @returns 
     */
    sortDocs = (field: string, desc: boolean, persistent?: boolean) => {
        const numbers: Doc[] = [];
        const strings: Doc[] = []; 

        this.docs.forEach(doc => {
            if (!isNaN(Number(Field.toString(doc[field] as FieldType)))) numbers.push(doc);
            else strings.push(doc);
        });

        const sortedNums = numbers.sort((numOne, numTwo) => {
            const numA = Number(Field.toString(numOne[field] as FieldType));
            const numB = Number(Field.toString(numTwo[field] as FieldType));
            return desc? numA - numB : numB - numA;
        });

        const collator = new Intl.Collator(undefined, {sensitivity: 'base'});
        let sortedStrings;
        if (!desc) {sortedStrings = strings.slice().sort((docA, docB) => collator.compare(Field.toString(docA[field] as FieldType), Field.toString(docB[field] as FieldType)));
        } else sortedStrings = strings.slice().sort((docB, docA) => collator.compare(Field.toString(docA[field] as FieldType), Field.toString(docB[field] as FieldType)));

        const sortedDocs = desc ? sortedNums.concat(sortedStrings) : sortedStrings.concat(sortedNums);
        if (!persistent) this._containedDocs = sortedDocs;
        return sortedDocs;
    }
    
    /**
     * Returns all docs minus those currently being dragged by the user.
     */
    @computed get docsWithDrag() {
        let docs = this.docs.slice();
        if (this.sortField){
            const field = StrCast(this.layoutDoc.sortField);
            const desc = BoolCast(this.layoutDoc.sortDesc); // is this an ascending or descending sort
            docs = this.sortDocs(field, desc, true);
        } else {
            const draggedDocs = this.isContentActive() ? DragManager.docsBeingDragged.filter(doc => !(doc.type === 'fonticonbox')) : [];
            docs = docs.filter(d => !draggedDocs.includes(d));
            docs.splice(this.rowDropIndex, 0, ...draggedDocs);
        }

        return { docs };
    }

    rowHeightFunc = () => (BoolCast(this.layoutDoc._schema_singleLine) ? CollectionSchemaView._rowSingleLineHeight : CollectionSchemaView._rowHeight);
    isContentActive = () => this._props.isSelected() || this._props.isContentActive();
    screenToLocal = () => this.ScreenToLocalBoxXf().translate(-this.tableWidth, 0);
    previewWidthFunc = () => this.previewWidth;
    onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
    displayedDocsFunc = () => this.docsWithDrag.docs;
    _oldWheel: any;
    render() {
        return (
            <div className="collectionSchemaView" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} 
            onDrop={this.onExternalDrop.bind(this)} 
            onPointerMove={e => this.onPointerMove(e)} 
            onPointerDown={() => {this.closeNewColumnMenu(); this.setColDrag(false)}}>
                <div ref={this._menuTarget} style={{ background: 'red', top: 0, left: 0, position: 'absolute', zIndex: 10000 }} />
                <div
                    className="schema-table"
                    style={{ width: `calc(100% - ${this.previewWidth}px)` }}
                    onWheel={e => this._props.isContentActive() && e.stopPropagation()}
                    ref={ele => {
                        // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
                        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
                        (this._oldWheel = ele)?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
                    }}>
                    <div className="schema-header-row" style={{ height: this.rowHeightFunc() }}>
                        <div className="row-menu" style={{ width: CollectionSchemaView._rowMenuWidth }}>
                            <IconButton
                            tooltip="Add a new key"
                            icon={ <FontAwesomeIcon icon="plus" size='lg'/>}
                            size={Size.XSMALL}
                            color={'black'}
                            onPointerDown={e =>
                                setupMoveUpEvents(
                                    this,
                                    e,
                                    returnFalse,
                                    emptyFunction,
                                    undoable(clickEv => {
                                        clickEv.stopPropagation();
                                        this.addColumn()
                                    }, 'add key to schema')
                                )
                            }
                            />
                        </div>
                        {this.columnKeys.map((key, index) => (
                            <SchemaColumnHeader
                                // eslint-disable-next-line react/no-array-index-key
                                //cleanupField={this.cleanupComputedField}
                                ref={r => r && this._headerRefs.push(r)}
                                keysDropdown={(this.keysDropdown)}
                                schemaView={this}
                                columnWidth={() => CollectionSchemaView._minColWidth} //TODO: update
                                Document={this.Document}
                                key={index}
                                columnIndex={index}
                                columnKeys={this.columnKeys}
                                columnWidths={this.displayColumnWidths}
                                setSort={this.setColumnSort}
                                rowHeight={this.rowHeightFunc}
                                removeColumn={this.removeColumn}
                                resizeColumn={this.startResize}
                                openContextMenu={this.openContextMenu}
                                dragColumn={this.dragColumn}
                                setColRef={this.setColRef}
                                isContentActive={this._props.isContentActive}
                            />
                        ))}
                    </div>
                    {this._columnMenuIndex !== undefined && this._columnMenuIndex !== -1 && this.renderColumnMenu}
                    {this._filterColumnIndex !== undefined && this.renderFilterMenu}
                    {
                        // eslint-disable-next-line no-use-before-define
                        <CollectionSchemaViewDocs
                            schema={this}
                            childDocs={this.displayedDocsFunc}
                            rowHeight={this.rowHeightFunc}
                            setRef={(ref: HTMLDivElement | null) => {
                                this._tableContentRef = ref;
                            }}
                        />
                    }
                    {this.layoutDoc.chromeHidden ? null : (
                        <div className="schema-add">
                            <EditableView
                                GetValue={returnEmptyString}
                                SetValue={undoable(value => (value ? this.addRow(Docs.Create.TextDocument(value, { title: value, _layout_autoHeight: true })) : false), 'add text doc')}
                                placeholder={"Type text to create note or ':' to create specific type"}
                                contents="+ New Node"
                                menuCallback={this.menuCallback}
                                height={CollectionSchemaView._newNodeInputHeight}
                            />
                        </div>
                    )}
                </div>
                {this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown} />}
                {this.previewWidth > 0 && (
                    <div
                        style={{ width: `${this.previewWidth}px` }}
                        ref={ref => {
                            this._previewRef = ref;
                        }}>
                        {Array.from(this._selectedDocs).lastElement() && (
                            <DocumentView
                                Document={Array.from(this._selectedDocs).lastElement()}
                                fitContentsToBox={returnTrue}
                                dontCenter="y"
                                onClickScriptDisable="always"
                                focus={emptyFunction}
                                defaultDoubleClick={returnIgnore}
                                renderDepth={this._props.renderDepth + 1}
                                rootSelected={this.rootSelected}
                                PanelWidth={this.previewWidthFunc}
                                PanelHeight={this._props.PanelHeight}
                                isContentActive={returnTrue}
                                isDocumentActive={returnFalse}
                                ScreenToLocalTransform={this.screenToLocal}
                                childFilters={this.childDocFilters}
                                childFiltersByRanges={this.childDocRangeFilters}
                                searchFilterDocs={this.searchFilterDocs}
                                styleProvider={DefaultStyleProvider}
                                containerViewPath={returnEmptyDoclist}
                                moveDocument={this._props.moveDocument}
                                addDocument={this.addRow}
                                removeDocument={this._props.removeDocument}
                                whenChildContentsActiveChanged={returnFalse}
                                addDocTab={this._props.addDocTab}
                                pinToPres={this._props.pinToPres}
                            />
                        )}
                    </div>
                )}
            </div>
        );
    }
}

interface CollectionSchemaViewDocProps {
    schema: CollectionSchemaView;
    index: number;
    doc: Doc;
    rowHeight: () => number;
}

@observer
class CollectionSchemaViewDoc extends ObservableReactComponent<CollectionSchemaViewDocProps> {
    constructor(props: any) {
        super(props);
        makeObservable(this);
    }

    tableWidthFunc = () => this._props.schema.tableWidth;
    screenToLocalXf = () => this._props.schema.ScreenToLocalBoxXf().translate(0, -this._props.rowHeight() - this._props.index * this._props.rowHeight());
    noOpacityStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => {
        if (property === StyleProp.Opacity) return 1;
        return DefaultStyleProvider(doc, props, property);
    };
    isRowContentActive = () => this._props.schema.isContentActive() || this._props.schema._props.isSelected() || this._props.schema._props.isAnyChildContentActive();
    render() {
        return (
            <DocumentView
                key={this._props.doc[Id]}
                // eslint-disable-next-line react/jsx-props-no-spreading
                {...this._props.schema._props}
                containerViewPath={this._props.schema.childContainerViewPath}
                LayoutTemplate={this._props.schema._props.childLayoutTemplate}
                LayoutTemplateString={SchemaRowBox.LayoutString(this._props.schema._props.fieldKey, this._props.index)}
                Document={this._props.doc}
                renderDepth={this._props.schema._props.renderDepth + 1}
                PanelWidth={this.tableWidthFunc}
                PanelHeight={this._props.rowHeight}
                styleProvider={this.noOpacityStyleProvider}
                waitForDoubleClickToClick={returnNever}
                defaultDoubleClick={returnIgnore}
                dragAction={dropActionType.move}
                onClickScriptDisable="always"
                focus={this._props.schema.focusDocument}
                childFilters={this._props.schema.childDocFilters}
                childFiltersByRanges={this._props.schema.childDocRangeFilters}
                searchFilterDocs={this._props.schema.searchFilterDocs}
                rootSelected={this._props.schema.rootSelected}
                ScreenToLocalTransform={this.screenToLocalXf}
                dragWhenActive
                isDocumentActive={this._props.schema._props.childDocumentsActive?.() ? this._props.schema._props.isDocumentActive : this._props.schema.isContentActive}
                isContentActive={this.isRowContentActive}
                whenChildContentsActiveChanged={this._props.schema._props.whenChildContentsActiveChanged}
                hideDecorations
                hideTitle
                hideDocumentButtonBar
                hideLinkAnchors
                fitWidth={returnTrue}
            />
        );
    }
}
interface CollectionSchemaViewDocsProps {
    schema: CollectionSchemaView;
    setRef: (ref: HTMLDivElement | null) => void;
    childDocs: () => Doc[];
    rowHeight: () => number;
}

@observer
class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsProps> {
    render() {
        return (
            <div className="schema-table-content" ref={this.props.setRef} style={{ height: `calc(100% - ${CollectionSchemaView._newNodeInputHeight + this.props.rowHeight()}px)` }}>
                {this.props.childDocs().map((doc: Doc, index: number) => (
                    <div key={doc[Id]} className="schema-row-wrapper" style={{ height: this.props.rowHeight() }}>
                        <CollectionSchemaViewDoc doc={doc} schema={this.props.schema} index={index} rowHeight={this.props.rowHeight} />
                    </div>
                ))}
            </div>
        );
    }
}