//!wrt $BSPEC:{"icn":"C:/local/cmirror/icon.png","cpr":"Copyright (C) Windows 96 Team 2022.","dsc":"CodeMirror Editor for Windows 96.","frn":"CodeMirror","ver":1}
/*
 * CodeMirror Windows 96 package (C) https://windows96.net 2021.
 * 
 * CodeMirror (C) http://codemirror.net
 *   See LICENSE.txt for CodeMirror license.
 */

const { Theme, MenuBar, MsgBoxSimple, DialogCreator, SaveFileDialog, OpenFileDialog } = w96.ui;

const langMap = [
    {
        label: "C++",
        id: "cpp",
        exts: [".cpp", ".cxx", ".h", ".hpp", ".c", ".hxx"]
    },
    {
        label: "CSS",
        id: "css",
        exts: [".css"]
    },
    {
        label: "HTML",
        id: "html",
        exts: [".html"]
    },
    {
        label: "JavaScript",
        id: "javascript",
        exts: [".js", ".mjs"]
    },
    {
        label: "JSON",
        id: "json",
        exts: [".json"]
    },
    {
        label: "Python",
        id: "python",
        exts: [".py"]
    },
    {
        label: "XML",
        id: "xml",
        exts: [".xml"]
    }
];

const SAVE_EXTENSIONS = [".txt", ...langMap.map(x=>x.exts).join().split(',')];

class CodeMirrorApplication extends WApplication {
    constructor() {
        super();

        this.blobURLs = [];

        this.currentDocument = {
            path: null,
            modified: false
        }

        this.currentLang = "javascript";

        this.preventWindowClosure = false;
        this.lastPath = "c:/user/documents";
    }

    /**
     * Converts file system path to blob URL.
     * @param {String} p The path to convert.
     */
    async fsToBlob(p) {
        if(!FS.exists(p))
            return;
        
        const previousBlob = this.blobURLs.find(x=>x.path == p);

        if(previousBlob)
            return previousBlob.url;

        let o = {
            path: p,
            url: URL.createObjectURL(await FS.toBlob(p))
        };

        this.blobURLs.push(o);

        return o.url;
    }

    /**
     * Displays an open file dialog.
     */
    displayOpenFileDialog() {
        this.preventWindowClosure = true;

        new OpenFileDialog(this.lastPath, SAVE_EXTENSIONS, async (v)=>{
            this.preventWindowClosure = false;

            if(!v)
                return;

            this.displayUnsavedPrompt(async()=>{
                this.lastPath = FSUtil.getParentPath(v);
                await this.loadDocument(v);
                this.mainwnd.activate();
            });
        }).show();
    }

    /**
     * Displays an unsaved file prompt and calls the specified callback.
     */
    displayUnsavedPrompt(callback) {
        if(this.currentDocument.modified) {
            DialogCreator.create({
                title: "CodeMirror",
                body: "This document has unsaved changes! Would you like to save them?",
                icon: "warning",
                buttons: [
                    {
                        "id": "save",
                        "text": "Save",
                        "action": async (e)=>{
                            // Disable all buttons
                            e.dlg.wnd.getBodyContainer().querySelectorAll(".w96-button").forEach((v)=>v.setAttribute("disabled", ""));

                            e.dlg.wnd.onclose = (e)=>e.canceled = true;                                    

                            await this.saveDocument(false, (result)=>{
                                e.dlg.wnd.onclose = null;

                                if(result)
                                    callback();
                                
                                e.dlg.close();
                            })
                        }
                    },
                    {
                        "id": "dontsave",
                        "text": "Don't Save",
                        "action": (e)=>{
                            e.dlg.close();
                            callback();
                        }
                    },
                    {
                        "id": "cancel",
                        "text": "Cancel",
                        "action": "$close"
                    }
                ],
                events: {
                    onshown: ()=>this.preventWindowClosure = true,
                    onclose: ()=>this.preventWindowClosure = false
                }
            })
        }
        else
            callback();
    }

    /**
     * Resolves a language by id.
     * @param {String} id The ID of the language to resolve.
     */
    resolveLang(id) {
        return langMap.find(x=>x.id == id);
    }

    /**
     * The main entry point.
     */
    async main(argv) {
        super.main(argv);
		
		argv.shift(); //shift args

        // Load the CodeMirror module.
        const idle = MsgBoxSimple.idleProgress("CodeMirror", "Loading core library...");
        const cmirror = await include("c:/local/cmirror/cm_wrt.js");
        idle.closeDialog();

        // Setup the main window

        const mainwnd = this.createWindow({
            title: "CodeMirror",
            body: `<div class="appbar"></div>
            <div class="cmirror-container"></div>
            <footer class="w96-footer">
                Language: JavaScript, Editing: [C:/]
            </footer>`,
            bodyClass: "codemirror-app",
            taskbar: true,
            initialWidth: 590,
            initialHeight: 470,
            icon: await this.fsToBlob("C:/local/cmirror/icon.png")
        }, true);

        mainwnd.onclose = (e)=>{
            if(this.preventWindowClosure)
                return e.canceled = true;

            if(this.currentDocument.modified) {
                e.canceled = true;

                this.displayUnsavedPrompt(()=>{
                    this.currentDocument.modified = false;
                    mainwnd.close();
                });
            }
        }

        // Setup the UI
        const body = mainwnd.getBodyContainer();
        body.appendChild(await w96.sys.loader.createStyleFromPath("C:/local/cmirror/styles.css"));
        
        const appbar = new MenuBar();

        appbar.addRoot("File", [
            {
                type: "normal",
                label: "New",
                onclick: ()=>{
                    this.displayUnsavedPrompt(()=>this.newDocument());
                }
            },
            {
                type: "separator"
            },
            {
                type: "normal",
                label: "Open",
                onclick: ()=>{
                    this.displayOpenFileDialog();
                }
            },
            {
                type: "normal",
                label: "Save",
                onclick: ()=>this.saveDocument()
            },
            {
                type: "normal",
                label: "Save As",
                onclick: ()=>this.saveDocument(true)
            },
            {
                type: "separator"
            },
            {
                type: "normal",
                label: "Exit",
                onclick: ()=>mainwnd.close()
            }
        ]);

        appbar.addRoot("Properties", [
            {
                type: "submenu",
                label: "Languages",
                items: [
                    ... langMap.map((x,i)=> x = {
                        type: "normal",
                        label: x.label,
                        onclick: ()=>{
                            this.cmirror.switchLang(langMap[i].id);
                            this.currentLang = langMap[i].id;
                            this.updateFooter();
                        }
                    })
                ]
            }
        ]);

        appbar.addRoot("Help", [
            {
                type: "normal",
                label: "About",
                onclick: ()=>MsgBoxSimple.info ("About CodeMirror", '<span class="bold-noaa">CodeMirror</span><br>Version 1.0-cm6<br><br>Powered by <a href="https://codemirror.net/6">CodeMirror Editor</a>', "OK").dlg.setSize(320, 140)
            }
        ]);

        body.querySelector(".appbar").replaceWith(appbar.getMenuDiv());

        // Setup the editor
        const editorContainer = body.querySelector(".cmirror-container");
        this.editor = cmirror.createEditor(editorContainer);

        // Create references
        this.cmirror = cmirror;

        mainwnd.show();
        this.mainwnd = mainwnd;

        // Setup keybinds
        this.setupKeybinds();

        // Hacky way to check for document modification
        this.editor.dom.addEventListener('keydown', (e)=>{
            if(((e.key.length == 1) || (e.key == "Backspace")) && (!e.ctrlKey))
                this.setModified(true);
        });

        if(argv[0] == null)
            this.newDocument();
        else {
            // Check if file exists.

            if(!FS.exists(argv[0])) {
                DialogCreator.create({
                    title: "CodeMirror",
                    icon: "error",
                    body: "The specified file does not exist. An empty document has been loaded instead."
                });

                this.newDocument();
                return;
            }

            try {
                this.loadDocument(argv[0]);
            } catch(e) {
                DialogCreator.create({
                    title: "CodeMirror",
                    icon: "error",
                    body: "The specified document failed to load: " + new String(e)
                });
            }
        }
    }

    /**
     * Sets up the keybinds.
     */
    setupKeybinds() {
        /** @type {HTMLDivElement} */
        const body = this.mainwnd.getBodyContainer();

        body.addEventListener('keydown', (e)=>{
            if(e.ctrlKey) {
                if(e.shiftKey) {
                    switch(e.code) {
                        case "KeyS":
                            e.preventDefault();
                            this.saveDocument(true);
                            break;
                    }
                } else {
                    switch(e.code) {
                        case "KeyS":
                            e.preventDefault();
                            this.saveDocument();
                            break;
                        case "KeyO":
                            e.preventDefault();
                            this.displayOpenFileDialog();
                            break;
                        case "KeyN":
                            e.preventDefault();
                            this.displayUnsavedPrompt(()=>this.newDocument());
                            break;
                    }
                }
            }
        });
    }

    /**
     * Creates a new document.
     */
    newDocument() {
        this.cmirror.setDocumentValue("");
        this.mainwnd.setTitle("Untitled - CodeMirror");

        this.currentDocument = {
            path: null,
            modified: false
        }

        this.updateFooter();
    }

    /**
     * Updates the document information footer.
     */
    updateFooter() {
        const footer = this.mainwnd.getBodyContainer().querySelector("footer");

        if(!this.currentDocument.path) 
            footer.innerText = `Language: ${this.resolveLang(this.currentLang).label}, Editing: [<Untitled>]`;
        else 
            footer.innerText = `Language: ${this.resolveLang(this.currentLang).label}, Editing: [${this.currentDocument.path}]`;
    }

    /**
     * Sets the document modification state.
     * @param {Boolean} v Whether the document was modified.
     */
    setModified(v) {
        this.currentDocument.modified = v;

        if(!this.currentDocument.path) {
            this.mainwnd.setTitle(`${v ? "*" : ""}Untitled - CodeMirror`);
        }
        else {
            this.mainwnd.setTitle(`${v ? "*" : ""}${FSUtil.fname(this.currentDocument.path)} - CodeMirror`);
        }
    }

    /**
     * Loads a document
     * @param {String} path The path of the document to load.
     */
    async loadDocument(path) {
        if(!FS.exists(path)) {
            DialogCreator.create({
                title: "Error",
                body: "The file path does not exist.",
                icon: "error"
            });
            return;
        }

        this.currentDocument = {
            modified: false,
            path: path
        }

        this.mainwnd.setTitle(`${FSUtil.fname(this.currentDocument.path)} - CodeMirror`);
        this.cmirror.setDocumentValue(await FS.readstr(path));

        // Set editor highlighting
        const ext = FSUtil.getExtension(path);
        
        this.setHlight(ext);

        this.updateFooter();
    }

    /**
     * Sets the highlighter for the specified file extension.
     */
    setHlight(ext) {
        for(let lang of langMap) {
            if(lang.exts.includes(ext)) {
                this.cmirror.switchLang(lang.id);
                this.currentLang = lang.id;
                this.updateFooter();
                return;
            }
        }
    }

    /**
     * Saves the document.
     * @param {Function} The function to use as a callback upon save completion.
     */
    async saveDocument(forceSaveAs = false, callback = ()=>{}) {
        if((this.currentDocument.path != null) && (!forceSaveAs)) {
            await FS.writestr(this.currentDocument.path, this.editor.state.doc.toString());
            this.setModified(false);
            return callback(true);
        }
        
        new SaveFileDialog(this.lastPath, SAVE_EXTENSIONS, async (v)=>{
            if(!v) {
                // Canceled.
                return callback(false);
            }

            // Save the file
            try {
                await FS.writestr(v, this.editor.state.doc.toString());
                this.currentDocument.path = v;
                this.lastPath = FSUtil.getParentPath(v);
                this.setModified(false);
                this.updateFooter();
                this.mainwnd.activate();
                this.setHlight(FSUtil.getExtension(v));
                return callback(true);
            } catch(e) {
                // Show error
                DialogCreator.create({
                    title: "CodeMirror",
                    icon: "error",
                    body: "Cannot save file: " + new String(e)
                });

                return callback(false);
            }
        }).show();
    }

    ontermination() {
        // Dereference components

        this.cmirror = null;
        this.editor = null;
        this.mainwnd = null;

        // Revoke all blobs.

        for(let blob of this.blobURLs) {
            URL.revokeObjectURL(blob.url);
        }
    }
}

return WApplication.execAsync(new CodeMirrorApplication(), this.boxedEnv.args);