/* eslint-disable jsx-a11y/alt-text */
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable no-use-before-define */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Popup, Size, Type } from 'browndash-components';
import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import { extname } from 'path';
import * as React from 'react';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import Select from 'react-select';
import { ClientUtils, StopEvent, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../../ClientUtils';
import { emptyFunction } from '../../../../Utils';
import { DateField } from '../../../../fields/DateField';
import { Doc, DocListCast, Field, IdToDoc } from '../../../../fields/Doc';
import { RichTextField } from '../../../../fields/RichTextField';
import { ColumnType } from '../../../../fields/SchemaHeaderField';
import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast, toList } from '../../../../fields/Types';
import { ImageField } from '../../../../fields/URLField';
import { FInfo, FInfoFieldType } from '../../../documents/Documents';
import { dropActionType } from '../../../util/DropActionTypes';
import { SnappingManager } from '../../../util/SnappingManager';
import { Transform } from '../../../util/Transform';
import { undoBatch, undoable } from '../../../util/UndoManager';
import { EditableView } from '../../EditableView';
import { ObservableReactComponent } from '../../ObservableReactComponent';
import { DefaultStyleProvider, returnEmptyDocViewList } from '../../StyleProvider';
import { Colors } from '../../global/globalEnums';
import { DocumentView } from '../../nodes/DocumentView';
import { FieldViewProps } from '../../nodes/FieldView';
import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
import { CollectionSchemaView, FInfotoColType } from './CollectionSchemaView';
import './CollectionSchemaView.scss';
import { SchemaColumnHeader } from './SchemaColumnHeader';
import { ContextMenu } from '../../ContextMenu';
import { SchemaCellField } from './SchemaCellField';

export interface SchemaTableCellProps {
    Document: Doc;
    col: number;
    deselectCell: () => void;
    selectCell: (doc: Doc, col: number, shift: boolean, ctrl: boolean) => void;
    selectedCells: () => Doc[] | undefined;
    selectedCol: () => number;
    fieldKey: string;
    maxWidth?: () => number;
    columnWidth: () => number;
    rowHeight: () => number;
    padding?: number; // default is 5 -- see scss
    isRowActive: () => boolean | undefined;
    getFinfo: (fieldKey: string) => FInfo | undefined;
    setColumnValues: (field: string, value: string) => boolean;
    oneLine?: boolean; // whether all input should fit on one line vs allowing textare multiline inputs
    allowCRs?: boolean; // allow carriage returns in text input (othewrise CR ends the edit)
    finishEdit?: () => void; // notify container that edit is over (eg. to hide view in DashFieldView)
    options?: string[];
    menuTarget: HTMLDivElement | null;
    transform: () => Transform;
    autoFocus?: boolean; // whether to set focus on creation, othwerise wait for a click
    rootSelected?: () => boolean;
    rowSelected: () => boolean;
    isolatedSelection: [boolean, boolean];
    highlightCells: (text: string) => void;
}

function selectedCell(props: SchemaTableCellProps) {
    return (
        props.isRowActive() &&
        props.selectedCol() === props.col && 
        props.selectedCells()?.filter(d => d === props.Document)?.length
    );
}

@observer
export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellProps> {

    @observable _highlighted: boolean = false;

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

    get docIndex(){return DocumentView.getDocViewIndex(this._props.Document);} // prettier-ignore

    get isDefault(){return SchemaColumnHeader.isDefaultField(this._props.fieldKey);} // prettier-ignore

    get lockedInteraction(){return (this.isDefault || this._props.Document._lockedSchemaEditing);} // prettier-ignore

    get backgroundColor(){
        if (this.lockedInteraction){
            if (this._props.rowSelected()) return '#B0D1E7'
            else return '#F5F5F5'
        }   
        return ''
    }

    static addFieldDoc = (docs: Doc | Doc[] /* , where: OpenWhere */) => {
        DocumentView.FocusOrOpen(toList(docs)[0]);
        return true;
    };
    public static renderProps(props: SchemaTableCellProps) {
        const { Document, fieldKey, getFinfo, columnWidth, isRowActive } = props;
        let protoCount = 0;
        let doc: Doc | undefined = Document;
        while (doc) {
            if (Object.keys(doc).includes(fieldKey.replace(/^_/, ''))) break; 
            protoCount++;
            doc = DocCast(doc.proto);
        }
        const color = protoCount === 0 || (fieldKey.startsWith('_') && Document[fieldKey] === undefined) ? 'black' : 'blue'; // color of text in cells
        const textDecoration = '';
        const fieldProps: FieldViewProps = {
            childFilters: returnEmptyFilter,
            childFiltersByRanges: returnEmptyFilter,
            docViewPath: returnEmptyDocViewList,
            searchFilterDocs: returnEmptyDoclist,
            styleProvider: DefaultStyleProvider,
            isSelected: returnFalse,
            setHeight: returnFalse,
            select: emptyFunction,
            dragAction: dropActionType.move,
            renderDepth: 1,
            noSidebar: true,
            isContentActive: returnFalse,
            whenChildContentsActiveChanged: emptyFunction,
            ScreenToLocalTransform: Transform.Identity,
            focus: emptyFunction,
            addDocTab: SchemaTableCell.addFieldDoc,
            pinToPres: returnZero,
            Document: DocCast(Document.rootDocument, Document),
            fieldKey: fieldKey,
            PanelWidth: columnWidth,
            PanelHeight: props.rowHeight,
            rootSelected: props.rootSelected,
        };
        const readOnly = getFinfo(fieldKey)?.readOnly ?? false;
        const cursor = !readOnly ? 'text' : 'default';
        const pointerEvents: 'all' | 'none' = !readOnly && isRowActive() ? 'all' : 'none';
        return { color, textDecoration, fieldProps, cursor, pointerEvents };
    }
        
    adjustedHighlight = (field: string) => {
        const pattern = /\bthis.\b/g;  
        const modField = field.replace(pattern, `d${this.docIndex}.`);
        this._props.highlightCells(modField);
    }

    // parses a field from the "idToDoc(####)" format to DocumentId (d#) format for readability
    cleanupField = (field: string) => {
        const idPattern = /idToDoc\((.*?)\)/g;
        let modField = field.slice();
        let matches;
        let results = new Array<[id: string, func: string]>();
        while ((matches = idPattern.exec(field)) !== null) {results.push([matches[0], matches[1].replace(/"/g, '')]); }
        results.forEach((idFuncPair) => {modField = modField.replace(idFuncPair[0], 'd' + (DocumentView.getDocViewIndex(IdToDoc(idFuncPair[1]))).toString());})
        if (modField.charAt(modField.length - 1) === ';') modField = modField.substring(0, modField.length - 1);
        
        const selfRefPattern = `d${this.docIndex}.${this._props.fieldKey}`
        const selfRefRegX = RegExp(selfRefPattern, 'g');
        if (selfRefRegX.exec(modField) !== null) { return 'Invalid'}

        return modField;
    }

    @computed get defaultCellContent() {
        const { color, textDecoration, fieldProps, pointerEvents } = SchemaTableCell.renderProps(this._props);

        return (
            <div
                className="schemacell-edit-wrapper"
                // onContextMenu={}
                style={{
                    color,
                    textDecoration,
                    width: '100%',
                    pointerEvents: this.lockedInteraction ? 'none' : pointerEvents,
                }}>
                <EditableView
                    highlightCells={this.adjustedHighlight}
                    ref={r => selectedCell(this._props) && this._props.autoFocus && r?.setIsFocused(true)}
                    oneLine={this._props.oneLine}
                    contents={undefined}
                    fieldContents={fieldProps}
                    editing={selectedCell(this._props) ? undefined : false}
                    GetValue={() => this.cleanupField(Field.toKeyValueString(fieldProps.Document, this._props.fieldKey, SnappingManager.MetaKey))}
                    SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => {
                        if (shiftDown && enterKey) {
                            this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value);
                            this._props.finishEdit?.();
                            return true;
                        }
                        const hasNoLayout =  Doc.IsDataProto(fieldProps.Document) ? true : undefined; // the "delegate" is a a data document so never write to it's proto
                        const ret = Doc.SetField(fieldProps.Document, this._props.fieldKey.replace(/^_/, ''), value, hasNoLayout);
                        this._props.finishEdit?.();
                        return ret;
                    }, 'edit schema cell')}
                />
            </div>
        );
    }

    get getCellType() {
        const columnTypeStr = this._props.getFinfo(this._props.fieldKey)?.fieldType;
        const cellValue = this._props.Document[this._props.fieldKey];

        if (cellValue instanceof ImageField) return ColumnType.Image;
        if (cellValue instanceof DateField) return ColumnType.Date;
        if (cellValue instanceof RichTextField) return ColumnType.RTF;
        if (typeof cellValue === 'number') return ColumnType.Any;
        if (typeof cellValue === 'string' && columnTypeStr !== FInfoFieldType.enumeration) return ColumnType.Any;
        if (typeof cellValue === 'boolean') return ColumnType.Boolean;
        if (columnTypeStr && columnTypeStr in FInfotoColType) return FInfotoColType[columnTypeStr];

        return ColumnType.Any;
    }

    get content() {
        // prettier-ignore
        switch (this.getCellType) {
            case ColumnType.Image:       return <SchemaImageCell {...this._props} />;
            case ColumnType.Boolean:     return <SchemaBoolCell {...this._props} />;
            case ColumnType.RTF:         return <SchemaRTFCell {...this._props} />;
            case ColumnType.Enumeration: return <SchemaEnumerationCell {...this._props} options={this._props.getFinfo(this._props.fieldKey)?.values?.map(val => Field.toString(val))} />;
            case ColumnType.Date:        return <SchemaDateCell {...this._props} />;
            default:                     return this.defaultCellContent;
        }
    }

    @computed get borderColor() {
        const sides: Array<string | undefined> = [];
        sides[0] = selectedCell(this._props) ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined; // left
        sides[1] = selectedCell(this._props) ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined; // right
        sides[2] = (!this._props.isolatedSelection[0] && selectedCell(this._props)) ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined; // top
        sides[3] = (!this._props.isolatedSelection[1] && selectedCell(this._props)) ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined; // bottom
        return sides;
    }

    render() {
        return (
            <div
                className="schema-table-cell"
                onContextMenu={e => StopEvent(e)}
                onPointerDown={action(e => {
                    const shift: boolean = e.shiftKey;
                    const ctrl: boolean = e.ctrlKey;
                    if (this._props.isRowActive?.()) {
                        if (selectedCell(this._props) && ctrl) {
                            this._props.selectCell(this._props.Document, this._props.col, shift, ctrl);
                            e.stopPropagation();
                        } else !selectedCell(this._props) && this._props.selectCell(this._props.Document, this._props.col, shift, ctrl);
                    }
                    console.log(this._props.Document.title);
                })}
                style={{ padding: this._props.padding, 
                maxWidth: this._props.maxWidth?.(), 
                width: this._props.columnWidth() || undefined, 
                borderLeft: this.borderColor[0],
                borderRight: this.borderColor[1],
                borderTop: this.borderColor[2],
                borderBottom: this.borderColor[3],
                backgroundColor: this.backgroundColor}}>
                {this.isDefault ? '' : this.content}
            </div>
        );
    }
}

// mj: most of this is adapted from old schema code so I'm not sure what it does tbh
@observer
export class SchemaImageCell extends ObservableReactComponent<SchemaTableCellProps> {
    constructor(props: any) {
        super(props);
        makeObservable(this);
    }

    @observable _previewRef: HTMLImageElement | undefined = undefined;

    choosePath(url: URL) {
        if (url.protocol === 'data') return url.href; // if the url ises the data protocol, just return the href
        if (url.href.indexOf(window.location.origin) === -1) return ClientUtils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver
        if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href; // Why is this here — good question

        const ext = extname(url.href);
        return url.href.replace(ext, '_s' + ext);
    }

    get url() {
        const field = Cast(this._props.Document[this._props.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc
        const alts = DocListCast(this._props.Document[this._props.fieldKey + '_alternates']); // retrieve alternate documents that may be rendered as alternate images
        const altpaths = alts
            .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url)
            .filter(url => url)
            .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
        const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
        // If there is a path, follow it; otherwise, follow a link to a default image icon
        const url = paths.length ? paths : [ClientUtils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')];
        return url[0];
    }

    @action
    showHoverPreview = (e: React.PointerEvent) => {
        this._previewRef = document.createElement('img');
        document.body.appendChild(this._previewRef);
        const ext = extname(this.url);
        this._previewRef.src = this.url.replace('_s' + ext, '_m' + ext);
        this._previewRef.style.position = 'absolute';
        this._previewRef.style.left = e.clientX + 10 + 'px';
        this._previewRef.style.top = e.clientY + 10 + 'px';
        this._previewRef.style.zIndex = '1000';
    };

    @action
    moveHoverPreview = (e: React.PointerEvent) => {
        if (!this._previewRef) return;
        this._previewRef.style.left = e.clientX + 10 + 'px';
        this._previewRef.style.top = e.clientY + 10 + 'px';
    };

    @action
    removeHoverPreview = () => {
        if (!this._previewRef) return;
        document.body.removeChild(this._previewRef);
    };

    render() {
        const aspect = Doc.NativeAspect(this._props.Document); // aspect ratio
        // let width = Math.max(75, this._props.columnWidth); // get a with that is no smaller than 75px
        // const height = Math.max(75, width / aspect); // get a height either proportional to that or 75 px
        const height = this._props.rowHeight() ? this._props.rowHeight() - (this._props.padding || 6) * 2 : undefined;
        const width = height ? height * aspect : undefined; // increase the width of the image if necessary to maintain proportionality

        return <img src={this.url} width={width || undefined} height={height} style={{}} draggable="false" onPointerEnter={this.showHoverPreview} onPointerMove={this.moveHoverPreview} onPointerLeave={this.removeHoverPreview} />;
    }
}

@observer
export class SchemaDateCell extends ObservableReactComponent<SchemaTableCellProps> {
    constructor(props: any) {
        super(props);
        makeObservable(this);
    }

    @observable _pickingDate: boolean = false;
    @computed get date(): DateField {
        // if the cell is a date field, cast then contents to a date. Otherrwwise, make the contents undefined.
        return DateCast(this._props.Document[this._props.fieldKey]);
    }

    handleChange = undoable((date: Date | null) => {
        // const script = CompileScript(date.toString(), { requiredType: "Date", addReturn: true, params: { this: Doc.name } });
        // if (script.compiled) {
        //     this.applyToDoc(this._document, this._props.row, this._props.col, script.run);
        // } else {
        // ^ DateCast is always undefined for some reason, but that is what the field should be set to
        date && (this._props.Document[this._props.fieldKey] = new DateField(date));
        // }
    }, 'date change');

    render() {
        const { pointerEvents } = SchemaTableCell.renderProps(this._props);
        return (
            <>
                <div style={{ pointerEvents: 'none' }}>
                    <DatePicker dateFormat="Pp" selected={this.date?.date ?? Date.now()} onChange={emptyFunction} />
                </div>
                {pointerEvents === 'none' ? null : (
                    <Popup
                        icon={<FontAwesomeIcon size="sm" icon="caret-down" />}
                        size={Size.XSMALL}
                        type={Type.TERT}
                        color={SnappingManager.userColor}
                        background={SnappingManager.userBackgroundColor}
                        popup={
                            <div style={{ width: 'fit-content', height: '200px' }}>
                                <DatePicker open dateFormat="Pp" selected={this.date?.date ?? Date.now()} onChange={this.handleChange} />
                            </div>
                        }
                    />
                )}
            </>
        );
    }
}
@observer
export class SchemaRTFCell extends ObservableReactComponent<SchemaTableCellProps> {
    constructor(props: any) {
        super(props);
        makeObservable(this);
    }

    // if the text box blurs and none of its contents are focused(), then the edit finishes
    selectedFunc = () => !!selectedCell(this._props);
    render() {
        const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props);
        fieldProps.isContentActive = this.selectedFunc;
        return (
            <div className="schemaRTFCell" style={{ fontStyle: selectedCell(this._props) ? undefined : 'italic', color, textDecoration, cursor, pointerEvents }}>
                {selectedCell(this._props) ? <FormattedTextBox {...fieldProps} autoFocus onBlur={() => this._props.finishEdit?.()} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))}
            </div>
        );
    }
}
@observer
export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProps> {
    constructor(props: any) {
        super(props);
        makeObservable(this);
    }

    render() {
        const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props);
        return (
            <div className="schemaBoolCell" style={{ display: 'flex', color, textDecoration, cursor, pointerEvents }}>
                <input
                    style={{ marginRight: 4 }}
                    type="checkbox"
                    checked={BoolCast(this._props.Document[this._props.fieldKey])}
                    onChange={undoBatch((value: React.ChangeEvent<HTMLInputElement> | undefined) => {
                        if ((value?.nativeEvent as any).shiftKey) {
                            this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + (value?.target?.checked.toString() ?? ''));
                        } else Doc.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + (value?.target?.checked.toString() ?? ''));
                    })}
                />
                <EditableView
                    contents={undefined}
                    fieldContents={fieldProps}
                    editing={selectedCell(this._props) ? undefined : false}
                    GetValue={() => Field.toKeyValueString(this._props.Document, this._props.fieldKey)}
                    SetValue={undoBatch((value: string, shiftDown?: boolean, enterKey?: boolean) => {
                        if (shiftDown && enterKey) {
                            this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value);
                            this._props.finishEdit?.();
                            return true;
                        }
                        const set = Doc.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(this._props.Document) ? true : undefined);
                        this._props.finishEdit?.();
                        return set;
                    })}
                />
            </div>
        );
    }
}
@observer
export class SchemaEnumerationCell extends ObservableReactComponent<SchemaTableCellProps> {
    constructor(props: any) {
        super(props);
        makeObservable(this);
    }

    render() {
        const { color, textDecoration, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props);
        const options = this._props.options?.map(facet => ({ value: facet, label: facet }));
        return (
            <div className="schemaSelectionCell" style={{ color, textDecoration, cursor, pointerEvents }}>
                <div style={{ width: '100%' }}>
                    <Select
                        styles={{
                            container: base => ({
                                ...base,
                                height: 20,
                            }),
                            control: base => ({
                                ...base,
                                height: 20,
                                minHeight: 20,
                            }),
                            placeholder: base => ({
                                ...base,
                                top: '40%',
                            }),
                            input: base => ({
                                ...base,
                                height: 20,
                                minHeight: 20,
                                margin: 0,
                            }),
                            indicatorsContainer: base => ({
                                ...base,
                                height: 20,
                                transform: 'scale(0.75)',
                            }),
                            menuPortal: base => ({
                                ...base,
                                left: 0,
                                top: 0,
                                transform: `translate(${this._props.transform().TranslateX}px, ${this._props.transform().TranslateY}px)`,
                                width: Number(base.width) * this._props.transform().Scale,
                                zIndex: 9999,
                            }),
                        }}
                        menuPortalTarget={this._props.menuTarget}
                        menuPosition="absolute"
                        placeholder={StrCast(this._props.Document[this._props.fieldKey], 'select...')}
                        options={options}
                        isMulti={false}
                        onChange={val => Doc.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), `"${val?.value ?? ''}"`)}
                    />
                </div>
            </div>
        );
    }
}