import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { CompileScript, Transformer, ts } from '../util/Scripting';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
import { SnappingManager } from '../util/SnappingManager';
import { undoable } from '../util/UndoManager';
import { ObservableReactComponent } from './ObservableReactComponent';
import { OverlayView } from './OverlayView';
import './ScriptingRepl.scss';
import { DocumentIconContainer } from './nodes/DocumentIcon';
import { DocumentView } from './nodes/DocumentView';
import { returnFalse, setupMoveUpEvents } from '../../ClientUtils';
import { emptyFunction } from '../../Utils';
import { ObjectField } from '../../fields/ObjectField';
import { RefField } from '../../fields/RefField';
import { Doc, FieldResult, FieldType, Opt } from '../../fields/Doc';

interface replValueProps {
    scrollToBottom: () => void;
    value: Opt<FieldResult | Promise<RefField | undefined>>;
    name?: string;
}
@observer
export class ScriptingValueDisplay extends ObservableReactComponent<replValueProps> {
    constructor(props: replValueProps) {
        super(props);
        makeObservable(this);
    }

    render() {
        const val = this._props.value instanceof Doc && this._props.name ? this._props.value[this._props.name] : this._props.value;
        const title = (name: string) => (
            <>
                {this._props.name ? <b>{this._props.name} : </b> : <> </>}
                {name}
            </>
        );
        if (typeof val === 'object') {
            // eslint-disable-next-line no-use-before-define
            return <ScriptingObjectDisplay scrollToBottom={this._props.scrollToBottom} value={val} name={this._props.name} />;
        }
        if (typeof val === 'function') {
            return <div className="scriptingObject-leaf">{title('[Function]')}</div>;
        }
        return <div className="scriptingObject-leaf">{title(String(val))}</div>;
    }
}
interface ReplProps {
    scrollToBottom: () => void;
    value: Opt<FieldResult | Promise<RefField | undefined>>;
    name?: string;
}
@observer
export class ScriptingObjectDisplay extends ObservableReactComponent<ReplProps> {
    @observable collapsed = true;

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

    @action
    toggle = () => {
        this.collapsed = !this.collapsed;
        this._props.scrollToBottom();
    };

    render() {
        const val = this._props.value;
        const proto = Object.getPrototypeOf(val);
        const name = (proto && proto.constructor && proto.constructor.name) || String(val);
        const title = (
            <>
                {this.props.name ? <b>{this._props.name} : </b> : null}
                {name}
            </>
        );
        if (val === undefined) return '--undefined--';
        if (val instanceof Promise) return '...Promise...';
        if (this.collapsed) {
            return (
                <div className="scriptingObject-collapsed">
                    <span onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.toggle)} className="scriptingObject-icon scriptingObject-iconCollapsed">
                        <FontAwesomeIcon icon="caret-right" size="sm" />
                    </span>
                    {title} (+{Object.keys(val).length})
                </div>
            );
        }
        return (
            <div className="scriptingObject-open">
                <div>
                    <span onClick={this.toggle} className="scriptingObject-icon">
                        <FontAwesomeIcon icon="caret-down" size="sm" />
                    </span>
                    {title}
                </div>
                <div className="scriptingObject-fields">
                    {Object.keys(val).map(key => (
                        <ScriptingValueDisplay name={key} key={key} value={this._props.value} scrollToBottom={this._props.scrollToBottom} />
                    ))}
                </div>
            </div>
        );
    }
}

@observer
export class ScriptingRepl extends ObservableReactComponent<object> {
    constructor(props: object) {
        super(props);
        makeObservable(this);
    }

    @observable private commands: { command: string; result: unknown }[] = [];
    private commandsHistory: string[] = [];

    @observable private commandString: string = '';
    private commandBuffer: string = '';

    @observable private historyIndex: number = -1;

    private commandsRef = React.createRef<HTMLDivElement>();

    getTransformer = (): Transformer => ({
        transformer: context => {
            const knownVars: { [name: string]: number } = {};
            const usedDocuments: number[] = [];
            ScriptingGlobals.getGlobals().forEach((global: string) => {
                knownVars[global] = 1;
            });
            return root => {
                function visit(nodeIn: ts.Node) {
                    if (ts.isIdentifier(nodeIn)) {
                        if (ts.isParameter(nodeIn.parent)) {
                            knownVars[nodeIn.text] = 1;
                        }
                    }
                    const node = ts.visitEachChild(nodeIn, visit, context);

                    if (ts.isIdentifier(node)) {
                        const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node;
                        const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node;
                        if (ts.isParameter(node.parent)) {
                            // delete knownVars[node.text];
                        } else if (isntPropAccess && isntPropAssign && !(node.text in knownVars) && !(node.text in globalThis)) {
                            const match = node.text.match(/d([0-9]+)/);
                            if (match) {
                                const m = parseInt(match[1]);
                                usedDocuments.push(m);
                            } else {
                                return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('args'), node);
                                //  ts.createPropertyAccess(ts.createIdentifier('args'), node);
                            }
                        }
                    }

                    return node;
                }
                return ts.visitNode(root, visit);
            };
        },
    });

    @action
    onKeyDown = (e: React.KeyboardEvent) => {
        let stopProp = true;
        switch (e.key) {
            case 'Enter': {
                e.stopPropagation();
                const docGlobals: { [name: string]: FieldType } = {};
                DocumentView.allViews().forEach((dv, i) => {
                    docGlobals[`d${i}`] = dv.Document;
                });
                const globals = ScriptingGlobals.makeMutableGlobalsCopy(docGlobals);
                const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: 'any' }, transformer: this.getTransformer(), globals });
                if (!script.compiled) {
                    this.commands.push({ command: this.commandString, result: script.errors });
                    this.maybeScrollToBottom();
                    return;
                }
                const result = undoable(() => script.run({}, err => this.commands.push({ command: this.commandString, result: err as string })), 'run:' + this.commandString)();
                if (result.success) {
                    this.commands.push({ command: this.commandString, result: result.result });
                    this.commandsHistory.push(this.commandString);

                    this.commandString = '';
                    this.commandBuffer = '';
                    this.historyIndex = -1;
                }

                this.maybeScrollToBottom();
                break;
            }
            case 'ArrowUp': {
                if (this.historyIndex < this.commands.length - 1) {
                    this.historyIndex++;
                    if (this.historyIndex === 0) {
                        this.commandBuffer = this.commandString;
                    }
                    this.commandString = this.commandsHistory[this.commands.length - 1 - this.historyIndex];
                }
                break;
            }
            case 'ArrowDown': {
                if (this.historyIndex >= 0) {
                    this.historyIndex--;
                    if (this.historyIndex === -1) {
                        this.commandString = this.commandBuffer;
                        this.commandBuffer = '';
                    } else {
                        this.commandString = this.commandsHistory[this.commands.length - 1 - this.historyIndex];
                    }
                }
                break;
            }
            default:
                stopProp = false;
                break;
        }

        if (stopProp) {
            e.stopPropagation();
            e.preventDefault();
        }
    };

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

    private shouldScroll: boolean = false;
    private maybeScrollToBottom = () => {
        const ele = this.commandsRef.current;
        if (ele && Math.abs(Math.ceil(ele.scrollTop) - (ele.scrollHeight - ele.offsetHeight)) < 2) {
            this.shouldScroll = true;
            this.forceUpdate();
        }
    };

    private scrollToBottom() {
        const ele = this.commandsRef.current;
        ele?.scroll({ behavior: 'smooth', top: ele.scrollHeight });
    }

    componentDidUpdate(prevProps: Readonly<object>) {
        super.componentDidUpdate(prevProps);
        if (this.shouldScroll) {
            this.shouldScroll = false;
            setTimeout(() => this.scrollToBottom(), 0);
        }
    }

    overlayDisposer?: () => void;
    onFocus = () => {
        this.overlayDisposer?.();
        this.overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
    };

    onBlur = () => this.overlayDisposer?.();

    render() {
        return (
            <div className="scriptingRepl-outerContainer">
                <div className="scriptingRepl-commandsContainer" style={{ background: SnappingManager.userBackgroundColor }} ref={this.commandsRef}>
                    {this.commands.map(({ command, result }, i) => (
                        <div className="scriptingRepl-resultContainer" style={{ background: SnappingManager.userBackgroundColor }} key={i}>
                            <div className="scriptingRepl-commandString" style={{ background: SnappingManager.userBackgroundColor }}>
                                {command || <br />}
                            </div>
                            <div className="scriptingRepl-commandResult" style={{ background: SnappingManager.userBackgroundColor }}>
                                <ScriptingValueDisplay scrollToBottom={this.maybeScrollToBottom} value={result as ObjectField | RefField} />
                            </div>
                        </div>
                    ))}
                </div>
                <input
                    className="scriptingRepl-commandInput"
                    style={{ background: SnappingManager.userBackgroundColor }} //
                    onFocus={this.onFocus}
                    onBlur={this.onBlur}
                    value={this.commandString}
                    onChange={this.onChange}
                    onKeyDown={this.onKeyDown}
                />
            </div>
        );
    }
}
