import {debounce, Debouncer, Keymap, setIcon, TextFileView, ViewStateResult, WorkspaceLeaf} from "obsidian";
import PlantumlPlugin from "./main";
import {drawSelection, EditorView, highlightActiveLine, keymap} from "@codemirror/view";
import {Annotation, EditorState, Extension, Transaction} from "@codemirror/state";
import {highlightActiveLineGutter, lineNumbers} from "@codemirror/gutter";
import {highlightSelectionMatches, search} from "@codemirror/search";
import {history} from "@codemirror/history";
import {defaultKeymap, indentWithTab} from "@codemirror/commands";

export const VIEW_TYPE = "plantuml";

const views: EditorView[] = [];

const syncAnnotation = Annotation.define<boolean>();

function syncDispatch(from: number) {
    return (tr: Transaction) => {
        views[from].update([tr]);
        if (tr.changes && tr.annotation && !tr.changes.empty && !tr.annotation(syncAnnotation)) {
            for (let i = 0; i < views.length; i++) {
                if(i !== from) {
                    views[i].dispatch({
                        changes: tr.changes,
                        annotations: syncAnnotation.of(true)
                    })
                }
            }

        }

    }
}

export class PumlView extends TextFileView {
    editor: EditorView;
    previewEl: HTMLElement;
    sourceEl: HTMLElement;
    changeModeButton: HTMLElement;
    currentView: 'source' | 'preview';
    plugin: PlantumlPlugin;
    dispatchId = -1;
    debounced: Debouncer<any>;

    extensions: Extension[] = [
        highlightActiveLine(),
        highlightActiveLineGutter(),
        highlightSelectionMatches(),
        drawSelection(),
        keymap.of([...defaultKeymap, indentWithTab]),
        history(),
        search(),
        EditorView.updateListener.of(v => {
            if(v.docChanged) {
                this.requestSave();
                this.renderPreview();
            }
        })
    ]

    constructor(leaf: WorkspaceLeaf, plugin: PlantumlPlugin) {
        super(leaf);
        this.plugin = plugin;

        this.debounced = debounce(this.plugin.getProcessor().png, this.plugin.settings.debounce * 1000, true);

        this.sourceEl = this.contentEl.createDiv({cls: 'plantuml-source-view', attr: {'style': 'display: block'}});
        this.previewEl = this.contentEl.createDiv({cls: 'plantuml-preview-view', attr: {'style': 'display: none'}});

        const vault = (this.app.vault as any);

        if (vault.getConfig("showLineNumber")) {
            this.extensions.push(lineNumbers());
        }
        if(vault.getConfig("lineWrap")) {
            this.extensions.push(EditorView.lineWrapping);
        }

        this.editor = new EditorView({
            state: EditorState.create({
                extensions: this.extensions,
                doc: this.data,
            }),
            parent: this.sourceEl,
            dispatch: syncDispatch(views.length),
        });
        this.dispatchId = views.push(this.editor) - 1;

    }

    getViewType(): string {
        return VIEW_TYPE;
    }

    getState(): any {
        return super.getState();
    }

    setState(state: any, result: ViewStateResult): Promise<void> {
        // switch to preview mode
        if (state.mode === 'preview') {
            this.currentView = 'preview';
            setIcon(this.changeModeButton, 'pencil');
            this.changeModeButton.setAttribute('aria-label', 'Edit (Ctrl+Click to edit in new pane)');

            this.previewEl.style.setProperty('display', 'block');
            this.sourceEl.style.setProperty('display', 'none');
            this.renderPreview();
        }
        // switch to source mode
        else {
            this.currentView = 'source';
            setIcon(this.changeModeButton, 'lines-of-text');
            this.changeModeButton.setAttribute('aria-label', 'Preview (Ctrl+Click to open in new pane)');

            this.previewEl.style.setProperty('display', 'none');
            this.sourceEl.style.setProperty('display', 'block');
            //this.editor.refresh();
        }

        return super.setState(state, result);
    }

    async onload() {
        // add the action to switch between source and preview mode
        this.changeModeButton = this.addAction('lines-of-text', 'Preview (Ctrl+Click to open in new pane)', (evt) => this.switchMode(evt), 17);

        // undocumented: Get the current default view mode to switch to
        const defaultViewMode = (this.app.vault as any).getConfig('defaultViewMode');
        this.currentView = defaultViewMode;
        await this.setState({...this.getState(), mode: defaultViewMode}, {});
    }

    onunload() {
        views.remove(views[this.dispatchId]);
        this.editor.destroy();
    }

    // function to switch between source and preview mode
    async switchMode(arg: 'source' | 'preview' | MouseEvent) {
        let mode = arg;
        // if force mode not provided, switch to opposite of current mode
        if (!mode || mode instanceof MouseEvent) mode = this.currentView === 'source' ? 'preview' : 'source';

        if (arg instanceof MouseEvent) {
            if (Keymap.isModEvent(arg)) {
                this.app.workspace.duplicateLeaf(this.leaf).then(async () => {
                    const viewState = this.app.workspace.activeLeaf?.getViewState();
                    if (viewState) {
                        viewState.state = {...viewState.state, mode: mode};
                        await this.app.workspace.activeLeaf?.setViewState(viewState);
                    }
                });
            } else {
                await this.setState({...this.getState(), mode: mode}, {});
            }
        }
    }

    // get the data for save
    getViewData() : string {
        return this.editor.state.sliceDoc();
    }

    // load the data into the view
    async setViewData(data: string, clear: boolean) {
        this.data = data;
         if (clear) {
             this.editor.setState(EditorState.create({
                 doc: data,
                 extensions: this.extensions
             }));

         }else {
             this.editor.dispatch({
                 changes: {
                     from: 0,
                     to: this.editor.state.doc.length,
                     insert: data,
                 }
             })
         }
        // if we're in preview view, also render that
        if (this.currentView === 'preview') this.renderPreview();
    }

    // clear the editor, etc
    clear() {
        this.previewEl.empty();
        this.data = null;
    }

    getDisplayText() {
        if (this.file) return this.file.basename;
        else return "PlantUML (no file)";
    }

    canAcceptExtension(extension: string) {
        return extension == 'puml';
    }

    getIcon() {
        return "document-plantuml";
    }


    async renderPreview() {
        if(this.currentView !== "preview") return;
        this.previewEl.empty();
        const loadingHeader = this.previewEl.createEl("h1", {text: "Loading"});
        const previewDiv = this.previewEl.createDiv();


        this.debounced(this.getViewData(), previewDiv, null);
        loadingHeader.remove();
    }
}