import mermaid from 'mermaid';
import { action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { RichTextField } from '../../../fields/RichTextField';
import { Cast, DocCast, NumCast } from '../../../fields/Types';
import { Gestures } from '../../../pen-gestures/GestureTypes';
import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
import { LinkManager } from '../../util/LinkManager';
import { undoable } from '../../util/UndoManager';
import { ViewBoxAnnotatableComponent } from '../DocComponent';
import { InkingStroke } from '../InkingStroke';
import './DiagramBox.scss';
import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';

@observer
export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
    public static LayoutString(fieldKey: string) {
        return FieldView.LayoutString(DiagramBox, fieldKey);
    }
    static isPointInBox = (box: Doc, pt: number[]): boolean => {
        if (typeof pt[0] === 'number' && typeof box.x === 'number' && typeof box.y === 'number' && typeof pt[1] === 'number') {
            return pt[0] < box.x + NumCast(box.width) && pt[0] > box.x && pt[1] > box.y && pt[1] < box.y + NumCast(box.height);
        }
        return false;
    };

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

    @observable _showCode = false;
    @observable _inputValue = '';
    @observable _generating = false;
    @observable _errorMessage = '';

    @computed get mermaidcode() {
        return Cast(this.Document[DocData].text, RichTextField, null)?.Text ?? '';
    }

    componentDidMount() {
        this._props.setContentViewBox?.(this);
        mermaid.initialize({
            securityLevel: 'loose',
            startOnLoad: true,
            flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'cardinal' },
        });
        // when a new doc/text/ink/shape is created in the freeform view, this generates the corresponding mermaid diagram code
        reaction(
            () => DocListCast(this.Document.data),
            docArray => docArray.length && this.convertDrawingToMermaidCode(docArray),
            { fireImmediately: true }
        );
    }
    renderMermaid = (str: string) => {
        try {
            return mermaid.render('graph' + Date.now(), str);
        } catch (error) {
            return { svg: '', bindFunctions: undefined };
        }
    };

    renderMermaidAsync = async (mermaidCode: string, dashDiv: HTMLDivElement) => {
        try {
            const { svg, bindFunctions } = await this.renderMermaid(mermaidCode);
            dashDiv.innerHTML = svg;
            bindFunctions?.(dashDiv);
        } catch (error) {
            console.error('Error rendering Mermaid:', error);
        }
    };

    setMermaidCode = undoable((res: string) => {
        this.Document[DocData].text = new RichTextField(
            JSON.stringify({
                doc: {
                    type: 'doc',
                    content: [
                        {
                            type: 'code_block',
                            content: [
                                { type: 'text', text: `^@mermaids\n` },
                                { type: 'text', text: this.removeWords(res) },
                            ],
                        },
                    ],
                },
                selection: { type: 'text', anchor: 1, head: 1 },
            }),
            res
        );
    }, 'set mermaid code');

    generateMermaidCode = action(() => {
        this._generating = true;
        const prompt = 'Write this in mermaid code and only give me the mermaid code: ' + this._inputValue;
        gptAPICall(prompt, GPTCallType.MERMAID).then(
            action(res => {
                this._generating = false;
                if (res === 'Error connecting with API.') {
                    this._errorMessage = 'GPT call failed; please try again.';
                }
                // If GPT call succeeded, set mermaid code on Doc which will trigger a rendering if _showCode is false
                else if (res && this.isValidCode(res)) {
                    this.setMermaidCode(res);
                    this._errorMessage = '';
                } else {
                    this._errorMessage = 'GPT call succeeded but invalid html; please try again.';
                }
            })
        );
    });
    isValidCode = (html: string) => (html ? true : false);
    removeWords = (inputStrIn: string) => inputStrIn.replace('```mermaid', '').replace(`^@mermaids`, '').replace('```', '');

    // method to convert the drawings on collection node side the mermaid code
    convertDrawingToMermaidCode = async (docArray: Doc[]) => {
        const rectangleArray = docArray.filter(doc => doc.title === Gestures.Rectangle || doc.title === Gestures.Circle);
        const lineArray = docArray.filter(doc => doc.title === Gestures.Line || doc.title === Gestures.Stroke);
        const textArray = docArray.filter(doc => doc.type === DocumentType.RTF);
        await new Promise(resolve => setTimeout(resolve));
        const inkStrokeArray = lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke);
        if (inkStrokeArray[0] && inkStrokeArray.length === lineArray.length) {
            let mermaidCode = `graph TD \n`;
            const inkingStrokeArray = inkStrokeArray.map(stroke => stroke?.ComponentView as InkingStroke).filter(stroke => stroke);
            for (const rectangle of rectangleArray) {
                for (const inkStroke of inkingStrokeArray) {
                    const inkData = inkStroke.inkScaledData();
                    const { inkScaleX, inkScaleY } = inkData;
                    const inkStrokeXArray = inkData.inkData.map(coord => coord.X * inkScaleX);
                    const inkStrokeYArray = inkData.inkData.map(coord => coord.Y * inkScaleY);
                    // need to minX and minY to since the inkStroke.x and.y is not relative to the doc. so I have to do some calcluations
                    const offX = Math.min(...inkStrokeXArray) - NumCast(inkStroke.Document.x);
                    const offY = Math.min(...inkStrokeYArray) - NumCast(inkStroke.Document.y);

                    const startX = inkStrokeXArray[0] - offX;
                    const startY = inkStrokeYArray[0] - offY;
                    const endX = inkStrokeXArray.lastElement() - offX;
                    const endY = inkStrokeYArray.lastElement() - offY;
                    if (DiagramBox.isPointInBox(rectangle, [startX, startY])) {
                        for (const rectangle2 of rectangleArray) {
                            if (DiagramBox.isPointInBox(rectangle2, [endX, endY])) {
                                const linkedDocs = LinkManager.Instance.getAllRelatedLinks(inkStroke.Document).map(d => DocCast(LinkManager.getOppositeAnchor(d, inkStroke.Document)));
                                const linkedDocText = Cast(linkedDocs[0]?.text, RichTextField, null)?.Text;
                                const linkText = linkedDocText ? `|${linkedDocText}|` : '';
                                mermaidCode += '   ' + Math.abs(NumCast(rectangle.x)) + this.getTextInBox(rectangle, textArray) + '-->' + linkText + Math.abs(NumCast(rectangle2.x)) + this.getTextInBox(rectangle2, textArray) + `\n`;
                            }
                        }
                    }
                }
                this.setMermaidCode(mermaidCode);
            }
        }
    };

    getTextInBox = (box: Doc, richTextArray: Doc[]) => {
        for (const textDoc of richTextArray) {
            if (DiagramBox.isPointInBox(box, [NumCast(textDoc.x), NumCast(textDoc.y)])) {
                switch (box.title) {
                        case Gestures.Rectangle: return '('  + ((textDoc.text as RichTextField)?.Text ?? '') +  ')';
                        case Gestures.Circle:    return '((' + ((textDoc.text as RichTextField)?.Text ?? '') + '))';
                        default:
                    } // prettier-ignore
            }
        }
        return '( )';
    };

    render() {
        return (
            <div className="DIYNodeBox">
                <div className="DIYNodeBox-searchbar">
                    <input type="text" value={this._inputValue} onKeyDown={action(e => e.key === 'Enter' && this.generateMermaidCode())} onChange={action(e => (this._inputValue = e.target.value))} />
                    <button type="button" onClick={this.generateMermaidCode}>
                        Gen
                    </button>
                    <input type="checkbox" onClick={action(() => (this._showCode = !this._showCode))} />
                </div>
                <div className="DIYNodeBox-content">
                    {this._showCode ? (
                        <FormattedTextBox {...this._props} fieldKey="text" />
                    ) : this._generating ? (
                        <div className="loading-circle" />
                    ) : (
                        <div className="diagramBox" ref={r => r && this.renderMermaidAsync.call(this, this.removeWords(this.mermaidcode), r)}>
                            {this._errorMessage || 'Type a prompt to generate a diagram'}
                        </div>
                    )}
                </div>
            </div>
        );
    }
}

Docs.Prototypes.TemplateMap.set(DocumentType.DIAGRAM, {
    layout: { view: DiagramBox, dataField: 'data' },
    options: {
        _height: 300, //
        _layout_fitWidth: true,
        _layout_nativeDimEditable: true,
        _layout_reflowVertical: true,
        _layout_reflowHorizontal: true,
        waitForDoubleClickToClick: 'always',
        systemIcon: 'BsGlobe',
    },
});
