import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { returnAlways, returnTrue } from '../../../Utils';
import { Doc, Field, FieldResult } from '../../../fields/Doc';
import { List } from '../../../fields/List';
import { RichTextField } from '../../../fields/RichTextField';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
import { DocCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { Docs } from '../../documents/Documents';
import { SetupDrag } from '../../util/DragManager';
import { CompiledScript } from '../../util/Scripting';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ObservableReactComponent } from '../ObservableReactComponent';
import { DocumentIconContainer } from './DocumentIcon';
import { OpenWhere } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { ImageBox } from './ImageBox';
import './KeyValueBox.scss';
import { KeyValuePair } from './KeyValuePair';
import { FormattedTextBox } from './formattedText/FormattedTextBox';

export type KVPScript = {
    script: CompiledScript;
    type: 'computed' | 'script' | false;
    onDelegate: boolean;
};
@observer
export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
    public static LayoutString() {
        return FieldView.LayoutString(KeyValueBox, 'data');
    }
    constructor(props: any) {
        super(props);
        makeObservable(this);
    }

    private _mainCont = React.createRef<HTMLDivElement>();
    private _keyHeader = React.createRef<HTMLTableHeaderCellElement>();
    private _keyInput = React.createRef<HTMLInputElement>();
    private _valInput = React.createRef<HTMLInputElement>();

    componentDidMount() {
        this._props.setContentViewBox?.(this);
    }
    isKeyValueBox = returnTrue;
    able = returnAlways;
    layout_fitWidth = returnTrue;
    onClickScriptDisable = returnAlways;

    @observable private rows: KeyValuePair[] = [];
    @observable _splitPercentage = 50;

    get fieldDocToLayout() {
        return this._props.fieldKey ? DocCast(this._props.Document[this._props.fieldKey], DocCast(this._props.Document)) : this._props.Document;
    }

    @action
    onEnterKey = (e: React.KeyboardEvent): void => {
        if (e.key === 'Enter') {
            e.stopPropagation();
            if (this._keyInput.current?.value && this._valInput.current?.value && this.fieldDocToLayout) {
                if (KeyValueBox.SetField(this.fieldDocToLayout, this._keyInput.current.value, this._valInput.current.value)) {
                    this._keyInput.current.value = '';
                    this._valInput.current.value = '';
                    document.body.focus();
                }
            }
        }
    };
    /**
     * this compiles a string as a script after parsing off initial characters that determine script parameters
     * if the script starts with '=', then it will be stored on the delegate of the Doc, otherise on the data doc
     * if the script then starts with a ':=', then it will be treated as ComputedField,
     *                                  '$=', then it will just be a Script
     * @param value
     * @returns
     */
    public static CompileKVPScript(rawvalue: string): KVPScript | undefined {
        const onDelegate = rawvalue.startsWith('=');
        rawvalue = onDelegate ? rawvalue.substring(1) : rawvalue;
        const type: 'computed' | 'script' | false = rawvalue.startsWith(':=') ? 'computed' : rawvalue.startsWith('$=') ? 'script' : false;
        rawvalue = type ? rawvalue.substring(2) : rawvalue;
        rawvalue = rawvalue.replace(/.*\(\((.*)\)\)/, 'dashCallChat(_setCacheResult_, this, `$1`)');
        const value = ["'", '"', '`'].includes(rawvalue.length ? rawvalue[0] : '') || !isNaN(rawvalue as any) ? rawvalue : '`' + rawvalue + '`';

        var script = ScriptField.CompileScript(rawvalue, {}, true, undefined, DocumentIconContainer.getTransformer());
        if (!script.compiled) {
            script = ScriptField.CompileScript(value, {}, true, undefined, DocumentIconContainer.getTransformer());
        }
        return !script.compiled ? undefined : { script, type, onDelegate };
    }

    public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) {
        const { script, type, onDelegate } = kvpScript;
        //const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates
        const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : DocCast(doc.proto, doc);
        let field: Field | undefined;
        switch (type) {
            case 'computed': field = new ComputedField(script); break; // prettier-ignore
            case 'script': field = new ScriptField(script); break; // prettier-ignore
            default: {
                const _setCacheResult_ = (value: FieldResult) => {
                    field = value as Field;
                    if (setResult) setResult?.(value);
                    else target[key] = field;
                };
                const res = script.run({ this: Doc.Layout(doc), self: doc, _setCacheResult_ }, console.log);
                if (!res.success) {
                    if (key) target[key] = script.originalScript;
                    return false;
                }
                field === undefined && (field = res.result instanceof Array ? new List<any>(res.result) : res.result);
            }
        }
        if (!key) return false;
        if (Field.IsField(field, true) && (key !== 'proto' || field !== target)) {
            target[key] = field;
            return true;
        }
        return false;
    }

    @undoBatch
    public static SetField(doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) {
        const script = this.CompileKVPScript(value);
        if (!script) return false;
        return this.ApplyKVPScript(doc, key, script, forceOnDelegate, setResult);
    }

    onPointerDown = (e: React.PointerEvent): void => {
        if (e.buttons === 1 && this._props.isSelected()) {
            e.stopPropagation();
        }
    };
    onPointerWheel = (e: React.WheelEvent): void => e.stopPropagation();

    rowHeight = () => 30;

    @computed get createTable() {
        const doc = this.fieldDocToLayout;
        if (!doc) {
            return (
                <tr>
                    <td>Loading...</td>
                </tr>
            );
        }
        const realDoc = doc;

        const ids: { [key: string]: string } = {};
        const protos = Doc.GetAllPrototypes(doc);
        for (const proto of protos) {
            Object.keys(proto).forEach(key => {
                if (!(key in ids) && realDoc[key] !== ComputedField.undefined) {
                    ids[key] = key;
                }
            });
        }

        const rows: JSX.Element[] = [];
        let i = 0;
        const self = this;
        const keys = Object.keys(ids).slice();
        //for (const key of [...keys.filter(id => id !== 'layout' && !id.includes('_')).sort(), ...keys.filter(id => id === 'layout' || id.includes('_')).sort()]) {
        for (const key of keys.sort((a: string, b: string) => {
            const a_ = a.split('_')[0];
            const b_ = b.split('_')[0];
            if (a_ < b_) return -1;
            if (a_ > b_) return 1;
            if (a === a_) return -1;
            if (b === b_) return 1;
            return a === b ? 0 : a < b ? -1 : 1;
        })) {
            rows.push(
                <KeyValuePair
                    doc={realDoc}
                    addDocTab={this._props.addDocTab}
                    PanelWidth={this._props.PanelWidth}
                    PanelHeight={this.rowHeight}
                    ref={(function () {
                        let oldEl: KeyValuePair | undefined;
                        return (el: KeyValuePair) => {
                            if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1);
                            oldEl = el;
                            if (el) self.rows.push(el);
                        };
                    })()}
                    keyWidth={100 - this._splitPercentage}
                    rowStyle={'keyValueBox-' + (i++ % 2 ? 'oddRow' : 'evenRow')}
                    key={key}
                    keyName={key}
                />
            );
        }
        return rows;
    }
    @computed get newKeyValue() {
        return (
            <tr className="keyValueBox-valueRow">
                <td
                    className="keyValueBox-td-key"
                    onClick={e => {
                        this._keyInput.current!.select();
                        e.stopPropagation();
                    }}
                    style={{ width: `${100 - this._splitPercentage}%` }}>
                    <input style={{ width: '100%' }} ref={this._keyInput} type="text" placeholder="Key" />
                </td>
                <td
                    className="keyValueBox-td-value"
                    onClick={e => {
                        this._valInput.current!.select();
                        e.stopPropagation();
                    }}
                    style={{ width: `${this._splitPercentage}%` }}>
                    <input style={{ width: '100%' }} ref={this._valInput} type="text" placeholder="Value" onKeyDown={this.onEnterKey} />
                </td>
            </tr>
        );
    }

    @action
    onDividerMove = (e: PointerEvent): void => {
        const nativeWidth = this._mainCont.current!.getBoundingClientRect();
        this._splitPercentage = Math.max(0, 100 - Math.round(((e.clientX - nativeWidth.left) / nativeWidth.width) * 100));
    };
    @action
    onDividerUp = (e: PointerEvent): void => {
        document.removeEventListener('pointermove', this.onDividerMove);
        document.removeEventListener('pointerup', this.onDividerUp);
    };
    onDividerDown = (e: React.PointerEvent) => {
        e.stopPropagation();
        e.preventDefault();
        document.addEventListener('pointermove', this.onDividerMove);
        document.addEventListener('pointerup', this.onDividerUp);
    };

    getFieldView = () => {
        const rows = this.rows.filter(row => row.isChecked);
        if (rows.length > 1) {
            const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this._props.Document).title}`, _chromeHidden: true });
            for (const row of rows) {
                const field = this.createFieldView(DocCast(this._props.Document), row);
                field && Doc.AddDocToList(parent, 'data', field);
                row.uncheck();
            }
            return parent;
        }
        return rows.length ? this.createFieldView(DocCast(this._props.Document), rows.lastElement()) : undefined;
    };

    createFieldView = (templateDoc: Doc, row: KeyValuePair) => {
        const metaKey = row._props.keyName;
        const fieldTemplate = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc);
        fieldTemplate.title = metaKey;
        fieldTemplate.layout_fitWidth = true;
        fieldTemplate._xMargin = 10;
        fieldTemplate._yMargin = 10;
        fieldTemplate._width = 100;
        fieldTemplate._height = 40;
        fieldTemplate.layout = this.inferType(templateDoc[metaKey], metaKey);
        return fieldTemplate;
    };

    inferType = (data: FieldResult, metaKey: string) => {
        const options = { _width: 300, _height: 300, title: metaKey };
        if (data instanceof RichTextField || typeof data === 'string' || typeof data === 'number') {
            return FormattedTextBox.LayoutString(metaKey);
        } else if (data instanceof List) {
            if (data.length === 0) {
                return Docs.Create.StackingDocument([], options);
            }
            const first = DocCast(data[0]);
            if (!first || !first.data) {
                return Docs.Create.StackingDocument([], options);
            }
            switch (first.data.constructor) {
                case RichTextField:
                    return Docs.Create.TreeDocument([], options);
                case ImageField:
                    return Docs.Create.MasonryDocument([], options);
                default:
                    console.log(`Template for ${first.data.constructor} not supported!`);
                    return undefined;
            }
        } else if (data instanceof ImageField) {
            return ImageBox.LayoutString(metaKey);
        }
        return new Doc();
    };

    specificContextMenu = (e: React.MouseEvent): void => {
        const cm = ContextMenu.Instance;
        const open = cm.findByDescription('Change Perspective...');
        const openItems: ContextMenuProps[] = open && 'subitems' in open ? open.subitems : [];
        openItems.push({
            description: 'Default Perspective',
            event: () => {
                this._props.addDocTab(this._props.Document, OpenWhere.close);
                this._props.addDocTab(this.fieldDocToLayout, OpenWhere.addRight);
            },
            icon: 'image',
        });
        !open && cm.addItem({ description: 'Change Perspective...', subitems: openItems, icon: 'external-link-alt' });
    };

    render() {
        const dividerDragger =
            this._splitPercentage === 0 ? null : (
                <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this._splitPercentage}% - 5px), 0px)` }}>
                    <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} />
                </div>
            );

        return (
            <div className="keyValueBox-cont" onWheel={this.onPointerWheel} onContextMenu={this.specificContextMenu} ref={this._mainCont}>
                <table className="keyValueBox-table">
                    <tbody className="keyValueBox-tbody">
                        <tr className="keyValueBox-header">
                            <th className="keyValueBox-key" style={{ width: `${100 - this._splitPercentage}%` }} ref={this._keyHeader} onPointerDown={SetupDrag(this._keyHeader, this.getFieldView)}>
                                Key
                            </th>
                            <th className="keyValueBox-fields" style={{ width: `${this._splitPercentage}%` }}>
                                Fields
                            </th>
                        </tr>
                        {this.createTable}
                        {this.newKeyValue}
                    </tbody>
                </table>
                {dividerDragger}
            </div>
        );
    }
}
