import mermaid from 'mermaid';
import { action, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast } from '../../../fields/Doc';
import { List } from '../../../fields/List';
import { RichTextField } from '../../../fields/RichTextField';
import { DocCast, NumCast } from '../../../fields/Types';
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 { ViewBoxAnnotatableComponent } from '../DocComponent';
import { InkingStroke } from '../InkingStroke';
import './DiagramBox.scss';
import { FieldView, FieldViewProps } from './FieldView';

@observer
export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
    public static LayoutString(fieldKey: string) {
        return FieldView.LayoutString(DiagramBox, fieldKey);
    }
    private _ref: React.RefObject<HTMLDivElement> = React.createRef();
    private _dragRef = React.createRef<HTMLDivElement>();
    constructor(props: FieldViewProps) {
        super(props);
        makeObservable(this);
    }

    @observable inputValue = '';
    @observable loading = false;
    @observable errorMessage = '';
    @observable mermaidCode = '';

    @action handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        this.inputValue = e.target.value;
    };
    async componentDidMount() {
        this._props.setContentViewBox?.(this);
        mermaid.initialize({
            securityLevel: 'loose',
            startOnLoad: true,
            flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'cardinal' },
        });
        this.mermaidCode = 'asdasdasd';
        const docArray: Doc[] = DocListCast(this.Document.data);
        let mermaidCodeDoc = docArray.filter(doc => doc.type === 'rich text');
        mermaidCodeDoc = mermaidCodeDoc.filter(doc => (doc.text as RichTextField).Text === 'mermaidCodeTitle');
        if (mermaidCodeDoc[0]) {
            if (typeof mermaidCodeDoc[0].title === 'string') {
                console.log(mermaidCodeDoc[0].title);
                if (mermaidCodeDoc[0].title !== '') {
                    this.renderMermaidAsync(mermaidCodeDoc[0].title);
                }
            }
        }
        // this will create a text doc far away where the user cant to save the mermaid code, where it will then be accessed when flipped to the diagram box side
        // the code is stored in the title since it is much easier to change than in the text
        else {
            DocumentManager.Instance.AddViewRenderedCb(this.Document, docViewForYourCollection => {
                if (docViewForYourCollection && docViewForYourCollection.ComponentView) {
                    if (docViewForYourCollection.ComponentView.addDocument && docViewForYourCollection.ComponentView.removeDocument) {
                        const newDoc = Docs.Create.TextDocument('mermaidCodeTitle', { title: '', x: 9999 + NumCast(this.layoutDoc._width), y: 9999 });
                        docViewForYourCollection.ComponentView?.addDocument(newDoc);
                    }
                }
            });
        }
        console.log(this.Document.title);
        // this is so that ever time a new doc, text node or ink node, is created, this.createMermaidCode will run which will create a save
        reaction(
            () => DocListCast(this.Document.data),
            () => this.convertDrawingToMermaidCode(),
            { fireImmediately: true }
        );
    }
    renderMermaid = async (str: string) => {
        try {
            const { svg, bindFunctions } = await this.mermaidDiagram(str);
            return { svg, bindFunctions };
        } catch (error) {
            console.error('Error rendering mermaid diagram:', error);
            return { svg: '', bindFunctions: undefined };
        }
    };
    mermaidDiagram = async (str: string) => mermaid.render('graph' + Date.now(), str);

    async renderMermaidAsync(mermaidCode: string) {
        try {
            const { svg, bindFunctions } = await this.renderMermaid(mermaidCode);
            const dashDiv = document.getElementById('dashDiv' + this.Document.title);
            if (dashDiv) {
                dashDiv.innerHTML = svg;
                if (bindFunctions) {
                    bindFunctions(dashDiv);
                }
            }
        } catch (error) {
            console.error('Error rendering Mermaid:', error);
        }
    }
    @action handleRenderClick = () => {
        this.generateMermaidCode();
    };
    @action async generateMermaidCode() {
        console.log('Generating Mermaid Code');
        this.loading = true;
        let prompt = '';
        // let docArray: Doc[] = DocListCast(this.Document.data);
        // let mermaidCodeDoc = docArray.filter(doc => doc.type == 'rich text')
        // mermaidCodeDoc=mermaidCodeDoc.filter(doc=>(doc.text as RichTextField).Text=='mermaidCodeTitle')
        // if(mermaidCodeDoc[0]){
        //     console.log(mermaidCodeDoc[0].title)
        //     if(typeof mermaidCodeDoc[0].title=='string'){
        //         console.log(mermaidCodeDoc[0].title)
        //         if(mermaidCodeDoc[0].title!=""){
        //             prompt="Edit this code "+this.inputValue+": "+mermaidCodeDoc[0].title
        //             console.log("you have to see me")
        //         }
        //     }
        // }
        // else{
        prompt = 'Write this in mermaid code and only give me the mermaid code: ' + this.inputValue;
        console.log('there is no text save');
        // }
        const res = await gptAPICall(prompt, GPTCallType.MERMAID);
        this.loading = false;
        if (res === 'Error connecting with API.') {
            // If GPT call failed
            console.error('GPT call failed');
            this.errorMessage = 'GPT call failed; please try again.';
        } else if (res !== null) {
            // If GPT call succeeded, set htmlCode;;; TODO: check if valid html
            if (this.isValidCode(res)) {
                this.mermaidCode = res;
                console.log('GPT call succeeded:' + res);
                this.errorMessage = '';
            } else {
                console.error('GPT call succeeded but invalid html; please try again.');
                this.errorMessage = 'GPT call succeeded but invalid html; please try again.';
            }
        }
        this.renderMermaidAsync.call(this, this.removeWords(this.mermaidCode));
        this.loading = false;
    }
    isValidCode = (html: string) => true;
    removeWords(inputStrIn: string) {
        const inputStr = inputStrIn.replace('```mermaid', '');
        return inputStr.replace('```', '');
    }
    // method to convert the drawings on collection node side the mermaid code
    async convertDrawingToMermaidCode() {
        let mermaidCode = '';
        let diagramExists = false;
        if (this.Document.data instanceof List) {
            const docArray: Doc[] = DocListCast(this.Document.data);
            const rectangleArray = docArray.filter(doc => doc.title === 'rectangle' || doc.title === 'circle');
            const lineArray = docArray.filter(doc => doc.title === 'line' || doc.title === 'stroke');
            const textArray = docArray.filter(doc => doc.type === 'rich text');
            const timeoutPromise = () =>
                new Promise(resolve => {
                    setTimeout(resolve, 0);
                });
            await timeoutPromise();
            const inkStrokeArray = lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke);
            console.log(inkStrokeArray.length);
            console.log(lineArray.length);
            if (inkStrokeArray[0] && inkStrokeArray.length === lineArray.length) {
                mermaidCode = 'graph TD;';
                const inkingStrokeArray = inkStrokeArray.map(stroke => stroke?.ComponentView);
                for (let i = 0; i < rectangleArray.length; i++) {
                    const rectangle = rectangleArray[i];
                    for (let j = 0; j < lineArray.length; j++) {
                        const inkScaleX = (inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkScaleX;
                        const inkScaleY = (inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkScaleY;
                        const inkStrokeXArray = (inkingStrokeArray[j] as InkingStroke)
                            ?.inkScaledData()
                            .inkData.map(coord => coord.X)
                            .map(doc => doc * inkScaleX);
                        const inkStrokeYArray = (inkingStrokeArray[j] as InkingStroke)
                            ?.inkScaledData()
                            .inkData.map(coord => coord.Y)
                            .map(doc => doc * inkScaleY);
                        console.log(inkingStrokeArray.length);
                        console.log(lineArray.length);
                        // 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 minX: number = Math.min(...inkStrokeXArray);
                        const minY: number = Math.min(...inkStrokeYArray);
                        const startX = inkStrokeXArray[0] - minX + (lineArray[j]?.x as number);
                        const startY = inkStrokeYArray[0] - minY + (lineArray[j]?.y as number);
                        const endX = inkStrokeXArray[inkStrokeXArray.length - 1] - minX + (lineArray[j].x as number);
                        const endY = inkStrokeYArray[inkStrokeYArray.length - 1] - minY + (lineArray[j].y as number);
                        if (this.isPointInBox(rectangle, [startX, startY])) {
                            for (let k = 0; k < rectangleArray.length; k++) {
                                const rectangle2 = rectangleArray[k];
                                if (this.isPointInBox(rectangle2, [endX, endY]) && typeof rectangle.x === 'number' && typeof rectangle2.x === 'number') {
                                    diagramExists = true;
                                    const linkedDocs: Doc[] = LinkManager.Instance.getAllRelatedLinks(lineArray[j]).map(d => DocCast(LinkManager.getOppositeAnchor(d, lineArray[j])));
                                    console.log(linkedDocs.length);
                                    if (linkedDocs.length !== 0) {
                                        const linkedText = (linkedDocs[0].text as RichTextField).Text;
                                        mermaidCode += Math.abs(rectangle.x) + this.getTextInBox(rectangle, textArray) + '-->|' + linkedText + '|' + Math.abs(rectangle2.x) + this.getTextInBox(rectangle2, textArray) + ';';
                                    } else {
                                        mermaidCode += Math.abs(rectangle.x) + this.getTextInBox(rectangle, textArray) + '-->' + Math.abs(rectangle2.x) + this.getTextInBox(rectangle2, textArray) + ';';
                                    }
                                }
                            }
                        }
                    }
                }
                // this will save the text
                DocumentManager.Instance.AddViewRenderedCb(this.Document, docViewForYourCollection => {
                    if (docViewForYourCollection && docViewForYourCollection.ComponentView) {
                        if (docViewForYourCollection.ComponentView.addDocument && docViewForYourCollection.ComponentView.removeDocument) {
                            let docs: Doc[] = DocListCast(this.Document.data);
                            docs = docs.filter(doc => doc.type === 'rich text');
                            const mermaidCodeDoc = docs.filter(doc => (doc.text as RichTextField).Text === 'mermaidCodeTitle');
                            if (mermaidCodeDoc[0]) {
                                if (diagramExists) {
                                    mermaidCodeDoc[0].title = mermaidCode;
                                } else {
                                    mermaidCodeDoc[0].title = '';
                                }
                            }
                        }
                    }
                });
            }
        }
    }
    testInkingStroke = () => {
        if (this.Document.data instanceof List) {
            const docArray: Doc[] = DocListCast(this.Document.data);
            const lineArray = docArray.filter(doc => doc.title === 'line' || doc.title === 'stroke');
            setTimeout(() => {
                const inkStrokeArray = lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke);
                console.log(inkStrokeArray);
            });
        }
    };
    getTextInBox = (box: Doc, richTextArray: Doc[]): string => {
        for (let i = 0; i < richTextArray.length; i++) {
            const textDoc = richTextArray[i];
            if (typeof textDoc.x === 'number' && typeof textDoc.y === 'number' && typeof box.x === 'number' && typeof box.height === 'number' && typeof box.width === 'number' && typeof box.y === 'number') {
                if (textDoc.x > box.x && textDoc.x < box.x + box.width && textDoc.y > box.y && textDoc.y < box.y + box.height) {
                    if (box.title === 'rectangle') {
                        return '(' + ((textDoc.text as RichTextField)?.Text ?? '') + ')';
                    }
                    if (box.title === 'circle') {
                        return '((' + ((textDoc.text as RichTextField)?.Text ?? '') + '))';
                    }
                }
            }
        }
        return '( )';
    };
    isPointInBox = (box: Doc, line: number[]): boolean => {
        if (typeof line[0] === 'number' && typeof box.x === 'number' && typeof box.width === 'number' && typeof box.height === 'number' && typeof box.y === 'number' && typeof line[1] === 'number') {
            return line[0] < box.x + box.width && line[0] > box.x && line[1] > box.y && line[1] < box.y + box.height;
        }
        return false;
    };

    render() {
        return (
            <div ref={this._ref} className="DIYNodeBox">
                <div ref={this._dragRef} className="DIYNodeBox-wrapper">
                    <div className="search-bar">
                        <input type="text" value={this.inputValue} onChange={this.handleInputChange} />
                        <button type="button" onClick={this.handleRenderClick}>
                            Generate
                        </button>
                    </div>
                    <div className="content">
                        {this.mermaidCode ? (
                            <div id={'dashDiv' + this.Document.title} className="diagramBox" />
                        ) : (
                            <div>{this.loading ? <div className="loading-circle" /> : <div>{this.errorMessage ? this.errorMessage : 'Insert prompt to generate diagram'}</div>}</div>
                        )}
                    </div>
                </div>
            </div>
        );
    }
}

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