import React = require('react');
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable, ObservableMap, observe, trace } from 'mobx';
import { observer } from 'mobx-react';
import { computedFn } from 'mobx-utils';
import { Doc, DocListCast, Field, NumListCast, StrListCast } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
import { listSpec } from '../../../../fields/Schema';
import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
import { emptyFunction, returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils';
import { Docs, DocumentOptions, DocUtils, FInfo } from '../../../documents/Documents';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager } from '../../../util/DragManager';
import { SelectionManager } from '../../../util/SelectionManager';
import { undoable, undoBatch } from '../../../util/UndoManager';
import { ContextMenu } from '../../ContextMenu';
import { EditableView } from '../../EditableView';
import { Colors } from '../../global/globalEnums';
import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView';
import { KeyValueBox } from '../../nodes/KeyValueBox';
import { DefaultStyleProvider } from '../../StyleProvider';
import { CollectionSubView } from '../CollectionSubView';
import './CollectionSchemaView.scss';
import { SchemaColumnHeader } from './SchemaColumnHeader';
import { SchemaRowBox } from './SchemaRowBox';

export enum ColumnType {
    Number,
    String,
    Boolean,
    Date,
    Image,
    RTF,
    Enumeration,
    Any,
}

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 _closestDropIndex: number = 0;
    private _previewRef: HTMLDivElement | null = null;
    private _makeNewColumn: boolean = false;
    private _documentOptions: DocumentOptions = new DocumentOptions();
    private _tableContentRef: HTMLDivElement | null = null;
    private _menuTarget = React.createRef<HTMLDivElement>();

    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 = 30;
    public fieldInfos = new ObservableMap<string, FInfo>();

    @observable _menuKeys: string[] = [];
    @observable _rowEles: ObservableMap = new ObservableMap<Doc, HTMLDivElement>();
    @observable _colEles: HTMLDivElement[] = [];
    @observable _displayColumnWidths: number[] | undefined;
    @observable _columnMenuIndex: number | undefined;
    @observable _newFieldWarning: string = '';
    @observable _makeNewField: boolean = false;
    @observable _newFieldDefault: any = 0;
    @observable _newFieldType: ColumnType = ColumnType.Number;
    @observable _menuValue: string = '';
    @observable _filterColumnIndex: number | undefined;
    @observable _filterSearchValue: string = '';
    @observable _selectedCell: [Doc, number] | undefined;

    // target HTMLelement portal for showing a popup menu to edit cell values.
    public get MenuTarget() {
        return this._menuTarget.current;
    }

    @computed get _selectedDocs() {
        return SelectionManager.Docs().filter(doc => Doc.AreProtosEqual(DocCast(doc.embedContainer), this.rootDoc));
    }

    @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 Cast(this.layoutDoc.schema_columnKeys, listSpec('string'), 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 displayColumnWidths() {
        return this._displayColumnWidths ?? this.storedColumnWidths;
    }

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

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

    @action
    componentDidMount() {
        this.props.setContentView?.(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.rootDoc[this.fieldKey ?? 'data'] as List<Doc>,
            change => {
                switch (change.type as any) {
                    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(''))))));
                        break;
                    case 'update': //let oldValue = change.oldValue; // fill this in if the entire child list will ever be reassigned with a new list
                }
            },
            true
        );
    }

    componentWillUnmount() {
        this._keysDisposer?.();
        document.removeEventListener('keydown', this.onKeyDown);
    }

    rowIndex = (doc: Doc) => this.sortedDocs.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.sortedDocs.docs[lastIndex];
                        if (lastIndex >= 0 && lastIndex < this.childDocs.length - 1) {
                            !e.shiftKey && this.clearSelection();
                            const newDoc = this.sortedDocs.docs[lastIndex + 1];
                            if (this._selectedDocs.includes(newDoc)) {
                                SelectionManager.DeselectView(DocumentManager.Instance.getFirstDocumentView(curDoc));
                            } else {
                                this.addDocToSelection(newDoc, e.shiftKey, lastIndex + 1);
                                this._selectedCell && (this._selectedCell[0] = newDoc);
                                this.scrollToDoc(newDoc, {});
                            }
                        }
                        e.stopPropagation();
                        e.preventDefault();
                    }
                    break;
                case 'ArrowUp':
                    {
                        const firstDoc = this._selectedDocs.lastElement();
                        const firstIndex = this.rowIndex(firstDoc);
                        const curDoc = this.sortedDocs.docs[firstIndex];
                        if (firstIndex > 0 && firstIndex < this.childDocs.length) {
                            !e.shiftKey && this.clearSelection();
                            const newDoc = this.sortedDocs.docs[firstIndex - 1];
                            if (this._selectedDocs.includes(newDoc)) SelectionManager.DeselectView(DocumentManager.Instance.getFirstDocumentView(curDoc));
                            else {
                                this.addDocToSelection(newDoc, e.shiftKey, firstIndex - 1);
                                this._selectedCell && (this._selectedCell[0] = newDoc);
                                this.scrollToDoc(newDoc, {});
                            }
                        }
                        e.stopPropagation();
                        e.preventDefault();
                    }
                    break;
                case 'ArrowRight':
                    if (this._selectedCell) {
                        this._selectedCell[1] = Math.min(this._selectedCell[1] + 1, this.columnKeys.length - 1);
                    } else if (this._selectedDocs.length > 0) {
                        this.selectCell(this._selectedDocs[0], 0);
                    }
                    break;
                case 'ArrowLeft':
                    if (this._selectedCell) {
                        this._selectedCell[1] = Math.max(this._selectedCell[1] - 1, 0);
                    } else if (this._selectedDocs.length > 0) {
                        this.selectCell(this._selectedDocs[0], 0);
                    }
                    break;
                case 'Backspace': {
                    this.removeDocument(this._selectedDocs);
                    break;
                }
                case 'Escape': {
                    this.deselectCell();
                }
            }
        }
    };

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

    addRow = (doc: Doc | Doc[]) => {
        const result: boolean = this.addDocument(doc);
        return result;
    };

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

        let currKeys = [...this.columnKeys];
        currKeys[index] = newKey;
        this.layoutDoc.schema_columnKeys = new List<string>(currKeys);
    };

    @undoBatch
    @action
    addColumn = (key: string, defaultVal?: any) => {
        if (!this.documentKeys.includes(key)) {
            this.addNewKey(key, defaultVal);
        }

        const newColWidth = this.tableWidth / (this.storedColumnWidths.length + 1);
        const currWidths = this.storedColumnWidths.slice();
        currWidths.splice(0, 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)));

        let currKeys = this.columnKeys.slice();
        currKeys.splice(0, 0, key);
        this.layoutDoc.schema_columnKeys = new List<string>(currKeys);
    };

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

    @undoBatch
    @action
    removeColumn = (index: number) => {
        if (this.columnKeys.length === 1) return;
        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)));

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

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

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

            let change = e.movementX;

            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.props.ScreenToLocalTransform().Scale;
            this._displayColumnWidths[growing] += change * this.props.ScreenToLocalTransform().Scale;

            return false;
        }
        return true;
    };

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

    @undoBatch
    @action
    moveColumn = (fromIndex: number, toIndex: number) => {
        let currKeys = this.columnKeys.slice();
        currKeys.splice(toIndex, 0, currKeys.splice(fromIndex, 1)[0]);
        this.layoutDoc.schema_columnKeys = new List<string>(currKeys);

        let 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) => {
        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);

        document.removeEventListener('pointermove', this.highlightDropColumn);
        document.addEventListener('pointermove', this.highlightDropColumn);
        let stopHighlight = (e: PointerEvent) => {
            document.removeEventListener('pointermove', this.highlightDropColumn);
            document.removeEventListener('pointerup', stopHighlight);
        };
        document.addEventListener('pointerup', stopHighlight);

        return true;
    };

    findDropIndex = (mouseX: number) => {
        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;
        }, CollectionSchemaView._rowMenuWidth);
        return index;
    };

    @action
    highlightDropColumn = (e: PointerEvent) => {
        e.stopPropagation();
        const mouseX = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];
        const index = this.findDropIndex(mouseX);
        this._colEles.forEach((colRef, i) => {
            let leftStyle = '';
            let rightStyle = '';
            if (i + 1 === index) rightStyle = `solid 12px ${Colors.MEDIUM_BLUE}`;
            if (i === index && i === 0) leftStyle = `solid 12px ${Colors.MEDIUM_BLUE}`;
            colRef.style.borderLeft = leftStyle;
            colRef.style.borderRight = rightStyle;
            this.childDocs.forEach(doc => {
                this._rowEles.get(doc).children[1].children[i].style.borderLeft = leftStyle;
                this._rowEles.get(doc).children[1].children[i].style.borderRight = rightStyle;
            });
        });
    };

    @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, index: number) => {
        const rowDocView = DocumentManager.Instance.getDocumentView(doc);
        if (rowDocView) SelectionManager.SelectView(rowDocView, extendSelection);
    };

    @action
    clearSelection = () => SelectionManager.DeselectAll();

    selectRows = (rootDoc: Doc, lastSelected: Doc) => {
        const index = this.rowIndex(rootDoc);
        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.sortedDocs.docs[i];
            if (!this._selectedDocs.includes(currDoc)) this.addDocToSelection(currDoc, true, i);
        }
    };

    @action
    selectCell = (doc: Doc, index: number) => (this._selectedCell = [doc, index]);

    @action
    deselectCell = () => (this._selectedCell = undefined);

    sortedSelectedDocs = () => this.sortedDocs.docs.filter(doc => this._selectedDocs.includes(doc));

    setDropIndex = (index: number) => (this._closestDropIndex = index);

    @action
    onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
        if (de.complete.columnDragData) {
            const mouseX = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y)[0];
            const index = this.findDropIndex(mouseX);
            this.moveColumn(de.complete.columnDragData.colIndex, index ?? de.complete.columnDragData.colIndex);

            this._colEles.forEach((colRef, i) => {
                colRef.style.borderLeft = '';
                colRef.style.borderRight = '';
                this.childDocs.forEach(doc => {
                    this._rowEles.get(doc).children[1].children[i].style.borderLeft = '';
                    this._rowEles.get(doc).children[1].children[i].style.borderRight = '';
                });
            });

            e.stopPropagation();
            return true;
        }
        const draggedDocs = de.complete.docDragData?.draggedDocuments;
        if (draggedDocs && super.onInternalDrop(e, de) && !this.sortField) {
            const pushedDocs = this.childDocs.filter((doc, index) => index >= this._closestDropIndex && !draggedDocs.includes(doc));
            const pushedAndDraggedDocs = [...pushedDocs, ...draggedDocs];
            const removed = this.childDocs.slice().filter(doc => !pushedAndDraggedDocs.includes(doc));
            this.dataDoc[this.fieldKey ?? 'data'] = new List<Doc>([...removed, ...draggedDocs, ...pushedDocs]);
            this.clearSelection();
            draggedDocs.forEach(doc => {
                const draggedView = DocumentManager.Instance.getFirstDocumentView(doc);
                if (draggedView) DocumentManager.Instance.RemoveView(draggedView);
                DocumentManager.Instance.AddViewRenderedCb(doc, dv => dv.select(true));
            });
            return true;
        }
        return false;
    };

    @action
    onExternalDrop = async (e: React.DragEvent): Promise<void> => {
        super.onExternalDrop(e, {}, undoBatch(action(docus => docus.map((doc: Doc) => this.addDocument(doc)))));
    };

    onDividerDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, emptyFunction);

    @action
    onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => {
        const nativeWidth = this._previewRef!.getBoundingClientRect();
        const minWidth = 40;
        const maxWidth = 1000;
        const movedWidth = this.props.ScreenToLocalTransform().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: DocFocusOptions) => {
        Doc.BrushDoc(doc);
        this.scrollToDoc(doc, options);
        return undefined;
    };

    scrollToDoc = (doc: Doc, options: DocFocusOptions) => {
        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.props.ScreenToLocalTransform().transformBounds(rect.left, rect.top, rect.width, rect.height);
            if (localRect.y < this.rowHeightFunc() || localRect.y + localRect.height > this.props.PanelHeight()) {
                let focusSpeed = options.zoomTime ?? 50;
                smoothScroll(focusSpeed, this._tableContentRef!, localRect.y + this._tableContentRef!.scrollTop - this.rowHeightFunc(), options.easeFunc);
                return focusSpeed;
            }
        }
    };

    @computed get fieldDefaultInput() {
        switch (this._newFieldType) {
            case ColumnType.Number:
                return <input type="number" name="" id="" value={this._newFieldDefault ?? 0} onPointerDown={e => e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.value))} />;
            case ColumnType.Boolean:
                return (
                    <>
                        <input type="checkbox" name="" id="" value={this._newFieldDefault} onPointerDown={e => e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.checked))} />
                        {this._newFieldDefault ? 'true' : 'false'}
                    </>
                );
            case ColumnType.String:
                return <input type="text" name="" id="" value={this._newFieldDefault ?? ''} onPointerDown={e => e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.value))} />;
        }
    }

    onSearchKeyDown = (e: React.KeyboardEvent) => {
        switch (e.key) {
            case 'Enter':
                this._menuKeys.length > 0 && this._menuValue.length > 0 ? this.setKey(this._menuKeys[0]) : action(() => (this._makeNewField = true))();
                break;
            case 'Escape':
                this.closeColumnMenu();
                break;
        }
    };

    @action
    setKey = (key: string, defaultVal?: any) => {
        if (this._makeNewColumn) {
            this.addColumn(key, defaultVal);
        } else {
            this.changeColumnKey(this._columnMenuIndex!, key, defaultVal);
        }
        this.closeColumnMenu();
    };

    setColumnValues = (key: string, value: string) => {
        let success: boolean = true;
        this.childDocs.forEach(doc => success && KeyValueBox.SetField(doc, key, value));
        return success;
    };

    @action
    openColumnMenu = (index: number, newCol: boolean) => {
        this._makeNewColumn = false;
        this._columnMenuIndex = index;
        this._menuValue = '';
        this._menuKeys = this.documentKeys;
        this._makeNewField = false;
        this._newFieldWarning = '';
        this._makeNewField = false;
        this._makeNewColumn = newCol;
    };

    @action
    closeColumnMenu = () => (this._columnMenuIndex = undefined);

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

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

    openContextMenu = (x: number, y: number, index: number) => {
        this.closeColumnMenu();
        this.closeFilterMenu();
        ContextMenu.Instance.clearItems();
        ContextMenu.Instance.addItem({
            description: 'Change field',
            event: () => this.openColumnMenu(index, false),
            icon: 'pencil-alt',
        });
        ContextMenu.Instance.addItem({
            description: 'Filter field',
            event: () => this.openFilterMenu(index),
            icon: 'filter',
        });
        ContextMenu.Instance.addItem({
            description: 'Delete column',
            event: () => this.removeColumn(index),
            icon: 'trash',
        });
        ContextMenu.Instance.displayMenu(x, y, undefined, false);
    };

    @action
    updateKeySearch = (e: React.ChangeEvent<HTMLInputElement>) => {
        this._menuValue = e.target.value;
        this._menuKeys = this.documentKeys.filter(value => value.toLowerCase().includes(this._menuValue.toLowerCase()));
    };

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

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

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

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

    @computed get newFieldMenu() {
        return (
            <div className="schema-new-key-options">
                <div className="schema-key-type-option">
                    <input
                        type="radio"
                        name="newFieldType"
                        checked={this._newFieldType == ColumnType.Number}
                        onChange={action(() => {
                            this._newFieldType = ColumnType.Number;
                            this._newFieldDefault = 0;
                        })}
                    />
                    number
                </div>
                <div className="schema-key-type-option">
                    <input
                        type="radio"
                        name="newFieldType"
                        checked={this._newFieldType == ColumnType.Boolean}
                        onChange={action(() => {
                            this._newFieldType = ColumnType.Boolean;
                            this._newFieldDefault = false;
                        })}
                    />
                    boolean
                </div>
                <div className="schema-key-type-option">
                    <input
                        type="radio"
                        name="newFieldType"
                        checked={this._newFieldType == ColumnType.String}
                        onChange={action(() => {
                            this._newFieldType = ColumnType.String;
                            this._newFieldDefault = '';
                        })}
                    />
                    string
                </div>
                <div className="schema-key-default-val">value: {this.fieldDefaultInput}</div>
                <div className="schema-key-warning">{this._newFieldWarning}</div>
                <div
                    className="schema-column-menu-button"
                    onPointerDown={action(e => {
                        if (this.documentKeys.includes(this._menuValue)) {
                            this._newFieldWarning = 'Field already exists';
                        } else if (this._menuValue.length === 0) {
                            this._newFieldWarning = 'Field cannot be an empty string';
                        } else {
                            this.setKey(this._menuValue, this._newFieldDefault);
                        }
                    })}>
                    done
                </div>
            </div>
        );
    }

    onPassiveWheel = (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._oldWheel.scrollTop && e.deltaY <= 0) e.preventDefault();
        e.stopPropagation();
    };
    _oldWheel: any;
    @computed get keysDropdown() {
        return (
            <div className="schema-key-search">
                <div
                    className="schema-column-menu-button"
                    onPointerDown={action(e => {
                        e.stopPropagation();
                        this._makeNewField = true;
                    })}>
                    + new field
                </div>
                <div
                    className="schema-key-list"
                    ref={r => {
                        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
                        this._oldWheel = r;
                        r?.addEventListener('wheel', this.onPassiveWheel, { 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">
                                    {key}
                                    {this.fieldInfos.get(key)!.fieldType ? ', ' : ''}
                                </span>
                                <span className="schema-search-result-type" style={{ color: this.fieldInfos.get(key)!.readOnly ? 'red' : 'inherit' }}>
                                    {this.fieldInfos.get(key)!.fieldType}
                                </span>
                            </p>
                            <p className="schema-search-result-desc">{this.fieldInfos.get(key)!.description}</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, minWidth: CollectionSchemaView._minColWidth }}>
                <input className="schema-key-search-input" type="text" value={this._menuValue} onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} />
                {this._makeNewField ? this.newFieldMenu : this.keysDropdown}
                <div
                    className="schema-column-menu-button"
                    onPointerDown={action(e => {
                        e.stopPropagation();
                        this.closeColumnMenu();
                    })}>
                    cancel
                </div>
            </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(':')[1] === key);
                const fields = ind === -1 ? undefined : filters[ind].split(':');
                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={action(e => {
                            if (e.target.checked) {
                                Doc.setDocFilter(this.props.Document, columnKey, key, 'check');
                            } else {
                                Doc.setDocFilter(this.props.Document, columnKey, key, '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, minWidth: CollectionSchemaView._minColWidth }}>
                <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 => {
                        e.stopPropagation();
                        this.closeFilterMenu();
                    })}>
                    done
                </div>
            </div>
        );
    }

    @computed get sortedDocs() {
        trace();
        const field = StrCast(this.layoutDoc.sortField);
        const desc = BoolCast(this.layoutDoc.sortDesc);
        const docs = !field
            ? this.childDocs
            : [...this.childDocs].sort((docA, docB) => {
                  const aStr = Field.toString(docA[field] as Field);
                  const bStr = Field.toString(docB[field] as Field);
                  var out = 0;
                  if (aStr < bStr) out = -1;
                  if (aStr > bStr) out = 1;
                  if (desc) out *= -1;
                  return out;
              });
        return { docs };
    }
    rowHeightFunc = () => (BoolCast(this.layoutDoc._schema_singleLine) ? CollectionSchemaView._rowSingleLineHeight : CollectionSchemaView._rowHeight);
    sortedDocsFunc = () => this.sortedDocs;
    isContentActive = () => this.props.isSelected() || this.props.isContentActive();
    screenToLocal = () => this.props.ScreenToLocalTransform().translate(-this.tableWidth, 0);
    previewWidthFunc = () => this.previewWidth;
    render() {
        return (
            <div className="collectionSchemaView" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} onDrop={this.onExternalDrop.bind(this)}>
                <div ref={this._menuTarget} style={{ background: 'red', top: 0, left: 0, position: 'absolute', zIndex: 10000 }}></div>
                <div
                    className="schema-table"
                    onWheel={e => this.props.isContentActive() && e.stopPropagation()}
                    ref={r => {
                        // prevent wheel events from passively propagating up through containers
                        r?.addEventListener('wheel', (e: WheelEvent) => {}, { passive: false });
                    }}>
                    <div className="schema-header-row" style={{ height: this.rowHeightFunc() }}>
                        <div className="row-menu" style={{ width: CollectionSchemaView._rowMenuWidth }}>
                            <div className="schema-header-button" onPointerDown={e => (this._columnMenuIndex === -1 ? this.closeColumnMenu() : this.openColumnMenu(-1, true))}>
                                <FontAwesomeIcon icon="plus" />
                            </div>
                        </div>
                        {this.columnKeys.map((key, index) => (
                            <SchemaColumnHeader
                                key={index}
                                columnIndex={index}
                                columnKeys={this.columnKeys}
                                columnWidths={this.displayColumnWidths}
                                sortField={this.sortField}
                                sortDesc={this.sortDesc}
                                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.renderColumnMenu}
                    {this._filterColumnIndex !== undefined && this.renderFilterMenu}
                    <CollectionSchemaViewDocs schema={this} childDocs={this.sortedDocsFunc} rowHeight={this.rowHeightFunc} setRef={(ref: HTMLDivElement | null) => (this._tableContentRef = ref)} />
                    <EditableView
                        GetValue={returnEmptyString}
                        SetValue={undoable(value => (value ? this.addRow(Docs.Create.TextDocument(value, { title: value, _layout_autoHeight: true })) : false), 'add text doc')}
                        placeholder={"Type ':' for commands"}
                        contents={'+ New Node'}
                        menuCallback={this.menuCallback}
                        height={CollectionSchemaView._newNodeInputHeight}
                    />
                </div>
                {this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown}></div>}
                {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()}
                                DataDoc={undefined}
                                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}
                                docViewPath={returnEmptyDoclist}
                                moveDocument={this.props.moveDocument}
                                addDocument={this.addRow}
                                removeDocument={this.props.removeDocument}
                                whenChildContentsActiveChanged={returnFalse}
                                addDocTab={this.props.addDocTab}
                                pinToPres={this.props.pinToPres}
                                bringToFront={returnFalse}
                            />
                        )}
                    </div>
                )}
            </div>
        );
    }
}

interface CollectionSchemaViewDocsProps {
    schema: CollectionSchemaView;
    setRef: (ref: HTMLDivElement | null) => void;
    childDocs: () => { docs: Doc[] };
    rowHeight: () => number;
}

@observer
class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsProps> {
    tableWidthFunc = () => this.props.schema.tableWidth;
    childScreenToLocal = computedFn((index: number) => () => this.props.schema.props.ScreenToLocalTransform().translate(0, -this.props.rowHeight() - index * this.props.rowHeight()));
    render() {
        return (
            <div className="schema-table-content" ref={this.props.setRef} style={{ height: `calc(100% - ${CollectionSchemaView._newNodeInputHeight + this.props.rowHeight()}px)` }}>
                {this.props.childDocs().docs.map((doc: Doc, index: number) => {
                    const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField ? undefined : this.props.schema.props.DataDoc;
                    return (
                        <div className="schema-row-wrapper" style={{ height: this.props.rowHeight() }}>
                            <DocumentView
                                key={doc[Id]}
                                {...this.props.schema.props}
                                LayoutTemplate={this.props.schema.props.childLayoutTemplate}
                                LayoutTemplateString={SchemaRowBox.LayoutString(this.props.schema.props.fieldKey)}
                                Document={doc}
                                DataDoc={dataDoc}
                                yPadding={index}
                                renderDepth={this.props.schema.props.renderDepth + 1}
                                PanelWidth={this.tableWidthFunc}
                                PanelHeight={this.props.rowHeight}
                                styleProvider={DefaultStyleProvider}
                                waitForDoubleClickToClick={returnNever}
                                defaultDoubleClick={returnIgnore}
                                dragAction="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.childScreenToLocal(index)}
                                bringToFront={emptyFunction}
                                isDocumentActive={this.props.schema.props.childDocumentsActive?.() ? this.props.schema.props.isDocumentActive : this.props.schema.isContentActive}
                                isContentActive={emptyFunction}
                                whenChildContentsActiveChanged={this.props.schema.props.whenChildContentsActiveChanged}
                                hideDecorations={true}
                                hideTitle={true}
                                hideDocumentButtonBar={true}
                                hideLinkAnchors={true}
                                layout_fitWidth={returnTrue}
                                scriptContext={this}
                            />
                        </div>
                    );
                })}
            </div>
        );
    }
}
