import { IReactionDisposer, action, makeObservable, observable, reaction, runInAction } from "mobx";
import { ObservableReactComponent } from "../../ObservableReactComponent";
import { observer } from "mobx-react";
import { OverlayView } from "../../OverlayView";
import { DocumentIconContainer } from "../../nodes/DocumentIcon";
import React, { FormEvent } from "react";
import { FieldView, FieldViewProps } from "../../nodes/FieldView";
import { ObjectField } from "../../../../fields/ObjectField";
import { Doc } from "../../../../fields/Doc";
import { DocumentView } from "../../nodes/DocumentView";

export interface SchemaCellFieldProps {
    contents: any;
    fieldContents?: FieldViewProps;
    editing?: boolean;
    oneLine?: boolean;
    Document: Doc;
    highlightCells?: (text: string) => void;
    GetValue(): string | undefined;
    SetValue(value: string, shiftDown?: boolean, enterKey?: boolean): boolean;
    getCells: (text: string) => HTMLDivElement[] | [];
}

@observer
export class SchemaCellField extends ObservableReactComponent<SchemaCellFieldProps> {

    private _disposers: { [name: string]: IReactionDisposer } = {};
    private _inputref: HTMLDivElement | null = null;
    private _unrenderedContent: string = '';
    _overlayDisposer?: () => void;
    @observable _editing: boolean = false;
    @observable _displayedContent = '';

    constructor(props: SchemaCellFieldProps) {
        super(props);
        makeObservable(this);
        setTimeout(() => { 
            this._unrenderedContent = this._props.GetValue() ?? '';
            this.setContent(this._unrenderedContent);
        }, 0); //must be moved to end of batch or else other docs aren't loaded, so render as d-1 in function
    }

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

    componentDidMount(): void {
        this._unrenderedContent = this._props.GetValue() ?? '';
        this.setContent(this._unrenderedContent, true);
        this._disposers.editing = reaction(
            () => this._editing,
            editing => {
                if (editing) {
                    setTimeout(() => {
                        if (this._inputref?.innerText.startsWith('=') || this._inputref?.innerText.startsWith(':=')) {
                            this._overlayDisposer?.();
                            this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
                            this._props.highlightCells?.(this._unrenderedContent);
                            this.setContent(this._unrenderedContent, true);
                        } 
                    });
                } else {
                    this._overlayDisposer?.();
                    this._overlayDisposer = undefined;
                    this._props.highlightCells?.('');
                }
            },
            { fireImmediately: true }
        );
        this._disposers.fieldUpdate = reaction(
            () => this._props.GetValue(),
            fieldVal => {
                this._unrenderedContent = fieldVal ?? '';
                this.setContent(this._unrenderedContent);
            }
        )
    }

    componentDidUpdate(prevProps: Readonly<SchemaCellFieldProps>) {
        super.componentDidUpdate(prevProps);
        if (this._editing && this._props.editing === false) {
            this.finalizeEdit(false, true, false);
        } else
            runInAction(() => {
                if (this._props.editing !== undefined) this._editing = this._props.editing;
            });
    }

    componentWillUnmount(): void {
        this._overlayDisposer?.();
        Object.values(this._disposers).forEach(disposer => disposer?.());
        this.finalizeEdit(false, true, false);
    }

    makeSpans = (content: string) => {
        let chunkedText = content;

        const pattern = /(this|d(\d+))\.(\w+)/g;
        const matches: string[] = [];
        let match: RegExpExecArray | null;

        const cells: Map<string, HTMLDivElement> = new Map();

        while ((match = pattern.exec(content)) !== null) {
            const cell = this._props.getCells(match[0]);
            if (cell.length) {
                matches.push(match[0]);
                cells.set(match[0], cell[0])
            }
        }

        let matchNum = 0;
        matches.forEach((match: string) => {
            const adjMatch = match.replace('this.', `d${this.docIndex}`)
            chunkedText = chunkedText.replace(match, `<span style="color: ${cells.get(match)?.style.borderTop.replace('2px solid', '')}">${match}</span>`);
            ++matchNum;
        })

        return chunkedText;
    }

    @action 
    setContent = (content: string, restoreCursorPos?: boolean) => {
        console.log('content: ' + content)
        this._displayedContent = this.makeSpans(content);
        if (restoreCursorPos) { 
            const cursorPos = this.cursorPosition;
            setTimeout(() => this.restoreCursorPosition(cursorPos), 0);
        }
    }

    @action
    setIsFocused = (value: boolean) => {
        const wasFocused = this._editing;
        this._editing = value;
        return wasFocused !== this._editing;
    };

    get cursorPosition() {
        const selection = window.getSelection();
        if (!selection || selection.rangeCount === 0 || !this._inputref) return null; 
    
        const range = selection.getRangeAt(0);
        const adjRange = range.cloneRange();
    
        adjRange.selectNodeContents(this._inputref);
        adjRange.setEnd(range.startContainer, range.startOffset);

        console.log(adjRange.toString().length)

        return adjRange.toString().length; 
    }

    restoreCursorPosition = (position: number | null) => {
        const selection = window.getSelection();
        if (!selection || position === null || !this._inputref) return; 
    
        const range = document.createRange();
        range.setStart(this._inputref, 0);
        range.collapse(true);
    
        let currentPos = 0;
        const setRange = (nodes: NodeList) => {
            for (let i = 0; i < nodes.length; ++i) {
                const node = nodes[i];

                if (node.nodeType === Node.TEXT_NODE) { 
                    if (!node.textContent) return;
                    const nextPos = currentPos + node.textContent.length;
                    if (position <= nextPos) {
                        range.setStart(node, position - currentPos);
                        range.collapse(true);
                        selection.removeAllRanges();
                        selection.addRange(range);
                        return true;
                    }
                    currentPos = nextPos;

                } else if ((node.nodeType === Node.ELEMENT_NODE) && (setRange(node.childNodes))) return true;
            }
            return false;
        }

        setRange(this._inputref.childNodes);
    };

    shouldUpdate = (prevVal: string, currVal: string) => {
        if (this._props.getCells(currVal).length !== this._props.getCells(prevVal).length) return true;
        //if (contains self-ref pattern)
    };

    onChange = (e: FormEvent<HTMLDivElement>) => {
        const prevVal = this._unrenderedContent;
        const targVal = e.currentTarget.innerText;
        if (!(targVal.startsWith(':=') || targVal.startsWith('='))) {
            this._overlayDisposer?.();
            this._overlayDisposer = undefined;
        } else if (!this._overlayDisposer) {
            this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
        }
        this._unrenderedContent = targVal;
        this._props.highlightCells?.(targVal);
        if (this.shouldUpdate(prevVal, targVal)) {this.setContent(targVal, true)};
    };

    @action
    onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
        if (e.nativeEvent.defaultPrevented) return; // hack .. DashFieldView grabs native events, but react ignores stoppedPropagation and preventDefault, so we need to check it here
        switch (e.key) {
            case 'Tab':
                e.stopPropagation();
                this.finalizeEdit(e.shiftKey, false, false);
                break;
            case 'Backspace':
                e.stopPropagation();
                break;
            case 'Enter':
                e.stopPropagation();
                if (!e.ctrlKey) {
                    this.finalizeEdit(e.shiftKey, false, true);
                }
                break;
            case 'Escape':
                e.stopPropagation();
                this._editing = false;
                break;
            case 'ArrowUp': case 'ArrowDown': case 'ArrowLeft': case 'ArrowRight': //prettier-ignore
                e.stopPropagation();
                break;
            case ' ':
                e.stopPropagation();
                console.log('deteected')
                let cursorPos = 0;
                if (this.cursorPosition !== null) cursorPos = this.cursorPosition + 1;
                setTimeout(() => {
                    this.setContent(this._unrenderedContent);
                    setTimeout(() => this.restoreCursorPosition(cursorPos), 0);
                }
                , 0);
                break;
            case 'u': // for some reason 'u' otherwise exits the editor
                e.stopPropagation();
                break;
            case 'Shift': case 'Alt': case 'Meta': case 'Control': case ':': //prettier-ignore
                break;
            // eslint-disable-next-line no-fallthrough
            case 'P':
                break;
            default:
                break;
        }
    };


    @action
    onClick = (e?: React.MouseEvent) => {
        if (this._props.editing !== false) {
            e?.nativeEvent.stopPropagation();
            this._editing = true;
        }
    };

    @action
    finalizeEdit(shiftDown: boolean, lostFocus: boolean, enterKey: boolean) {
        if (this._props.SetValue(this._unrenderedContent, shiftDown, enterKey)) {
            this._editing = false;
        } else {
            this._editing = false;
            !lostFocus &&
                setTimeout(
                    action(() => {
                        this._editing = true;
                    }),
                    0
                );
        }
    }

    staticDisplay = () => {
        return <span className='editableView-static'>
        {
            // eslint-disable-next-line react/jsx-props-no-spreading
            this._props.fieldContents ? <FieldView {...this._props.fieldContents} /> : this.props.contents ? this._props.contents?.valueOf() : ''
        }
        </span>
    }

    renderEditor = () => {
        return (
            <div 
            contentEditable
            className='schemaField-editing'
            ref={r => { this._inputref = r; }}
            style={{ cursor: 'text', outline: 'none', overflow: 'auto', minHeight: `min(100%, ${(this._props.GetValue()?.split('\n').length || 1) * 15})`, minWidth: 20, }}
            onBlur={e => this.finalizeEdit(false, true, false)}
            autoFocus
            onInput={this.onChange}
            onKeyDown={this.onKeyDown}
            onPointerDown={e => e.stopPropagation}
            onClick={e => e.stopPropagation}
            onPointerUp={e => e.stopPropagation}
            onPointerMove={e => {e.stopPropagation(); e.preventDefault()}}
            dangerouslySetInnerHTML={{ __html: this._displayedContent }}
            >
            </div>
        );
    }

    render() {
        const gval = this._props.GetValue()?.replace(/\n/g, '\\r\\n');
        if ((this._editing && gval !== undefined)) {
            return <div className={`editableView-container-editing${this._props.oneLine ? '-oneLine' : ''}`}>{this.renderEditor()}</div>;
        } else return (
            this._props.contents instanceof ObjectField ? null : (
                <div
                    className={`editableView-container-editing${this._props.oneLine ? '-oneLine' : ''}`}
                    style={{
                        minHeight: '10px',
                        whiteSpace: this._props.oneLine ? 'nowrap' : 'pre-line',
                        width: '100%',
                    }}
                    onClick={this.onClick}>
                    {this.staticDisplay()}
                </div>
            )
        );
    }

}