/*
 * Copyright (c) 2006, 2013, Oracle and/or its affiliates. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   - Neither the name of Oracle nor the names of its
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * This source code is provided to illustrate the usage of a given feature
 * or technique and has been deliberately simplified. Additional steps
 * required for a production-quality application, such as security checks,
 * input validation and proper error handling, might not be present in
 * this sample code.
 */

/*
 * This script creates a simple Notepad-like interface, which
 * serves as a simple script editor, runner.
 *
 * File dependency:
 *
 *    gui.js -> for basic GUI functions
 */

/*
 * globalThis is used for actionHelpGlobals() and showFrame().
 */
var globalThis = this;

/*
 * JavaImporter helps in avoiding pollution of JavaScript
 * global namespace. We can import multiple Java packages
 * with this and use the JavaImporter object with "with"
 * statement.
 */
var guiPkgs = new JavaImporter(java.awt, java.awt.event,
                               javax.swing, javax.swing.undo,
                               javax.swing.event, javax.swing.text);

// main entry point of the scriptpad application
var main = function() {
    function createEditor() {
        var c = new guiPkgs.JTextArea();
        c.setDragEnabled(true);
        c.setFont(new guiPkgs.Font("monospaced", guiPkgs.Font.PLAIN, 12));
        return c;
    }

    /*const*/ var titleSuffix = "- Scriptpad";
    /*const*/ var defaultTitle = "Untitled" + titleSuffix;

    // Scriptpad's main frame
    var frame;
    // Scriptpad's main editor
    var editor;

    // To track the current file name
    var curFileName = null;

    // To track whether the current document
    // has been modified or not
    var docChanged = false;

    // check and alert user for unsaved
    // but modified document
    function checkDocChanged() {
        if (docChanged) {
            // ignore zero-content untitled document
            if (curFileName == null &&
                editor.document.length == 0) {
                return;
            }

            if (confirm("Do you want to save the changes?",
                        "The document has changed")) {
                actionSave();
            }
        }
    }

    // set a document listener to track
    // whether that is modified or not
    function setDocListener() {
        var doc = editor.getDocument();
        docChanged = false;
        doc.addDocumentListener( new guiPkgs.DocumentListener() {
                                     equals: function(o) {
                                         return this === o; },
                                     toString: function() {
                                         return "doc listener"; },
                                     changeUpdate: function() {
                                         docChanged = true; },
                                     insertUpdate: function() {
                                         docChanged = true; },
                                     removeUpdate: function() {
                                         docChanged = true; }
                                 });
    }

    // menu action functions

    // "File" menu

    // create a "new" document
    function actionNew() {
        checkDocChanged();
        curFileName = null;
        editor.setDocument(new guiPkgs.PlainDocument());
        setDocListener();
        frame.setTitle(defaultTitle);
        editor.revalidate();
    }

    // open an existing file
    function actionOpen() {
        checkDocChanged();
        var f = fileDialog();
        if (f == null) {
            return;
        }

        if (f.isFile() && f.canRead()) {
            frame.setTitle(f.getName() + titleSuffix);
            editor.setDocument(new guiPkgs.PlainDocument());
            var progress = new guiPkgs.JProgressBar();
            progress.setMinimum(0);
            progress.setMaximum(f.length());
            var doc = editor.getDocument();
            var inp = new java.io.FileReader(f);
            var buff = java.lang.reflect.Array.newInstance(
                java.lang.Character.TYPE, 4096);
            var nch;
            while ((nch = inp.read(buff, 0, buff.length)) != -1) {
                doc.insertString(doc.getLength(),
                                 new java.lang.String(buff, 0, nch), null);
                progress.setValue(progress.getValue() + nch);
            }
            inp.close();
            curFileName = f.getAbsolutePath();
            setDocListener();
        } else {
            error("Can not open file: " + f,
                  "Error opening file: " + f);
        }
    }

    // open script from a URL
    function actionOpenURL() {
        checkDocChanged();
        var url = prompt("Address:");
        if (url == null) {
            return;
        }

        try {
            var u = new java.net.URL(url);
            editor.setDocument(new guiPkgs.PlainDocument());
            frame.setTitle(url + titleSuffix);
            var progress = new guiPkgs.JProgressBar();
            progress.setMinimum(0);
            progress.setIndeterminate(true);
            var doc = editor.getDocument();
            var inp = new java.io.InputStreamReader(u.openStream());
            var buff = java.lang.reflect.Array.newInstance(
                java.lang.Character.TYPE, 4096);
            var nch;
            while ((nch = inp.read(buff, 0, buff.length)) != -1) {
                doc.insertString(doc.getLength(),
                                 new java.lang.String(buff, 0, nch), null);
                progress.setValue(progress.getValue() + nch);
            }
            curFileName = null;
            setDocListener();
        } catch (e) {
            error("Error opening URL: " + e,
                  "Can not open URL: " + url);
        }
    }

    // factored out "save" function used by
    // save, save as menu actions
    function save(file) {
        var doc = editor.getDocument();
        frame.setTitle(file.getName() + titleSuffix);
        curFileName = file;
        var progress = new guiPkgs.JProgressBar();
        progress.setMinimum(0);
        progress.setMaximum(file.length());
        var out = new java.io.FileWriter(file);
        var text = new guiPkgs.Segment();
        text.setPartialReturn(true);
        var charsLeft = doc.getLength();
        var offset = 0;
        var min;

        while (charsLeft > 0) {
            doc.getText(offset, Math.min(4096, charsLeft), text);
            out.write(text.array, text.offset, text.count);
            charsLeft -= text.count;
            offset += text.count;
            progress.setValue(offset);
            java.lang.Thread.sleep(10);
        }

        out.flush();
        out.close();
        docChanged = false;
    }

    // file-save as menu
    function actionSaveAs() {
        var ret = fileDialog(null, true);
        if (ret == null) {
            return;
        }
        save(ret);
    }

    // file-save menu
    function actionSave() {
        if (curFileName) {
            save(new java.io.File(curFileName));
        } else {
            actionSaveAs();
        }
    }

    // exit from scriptpad
    function actionExit() {
        checkDocChanged();
        java.lang.System.exit(0);
    }

    // "Edit" menu

    // cut the currently selected text
    function actionCut() {
        editor.cut();
    }

    // copy the currently selected text to clipboard
    function actionCopy() {
        editor.copy();
    }

    // paste clipboard content to document
    function actionPaste() {
        editor.paste();
    }

    // select all the text in editor
    function actionSelectAll() {
        editor.selectAll();
    }

    // "Tools" menu

    // run the current document as JavaScript
    function actionRun() {
        var doc = editor.getDocument();
        var script = doc.getText(0, doc.getLength());
        var oldFile = engine.get(javax.script.ScriptEngine.FILENAME);
        try {
            if (engine == undefined) {
                var m = new javax.script.ScriptEngineManager();
                engine = m.getEngineByName("nashorn");
            }
            engine.put(javax.script.ScriptEngine.FILENAME, frame.title);
            engine.eval(script, context);
        } catch (e) {
            error(e, "Script Error");
            e.printStackTrace();
        } finally {
            engine.put(javax.script.ScriptEngine.FILENAME, oldFile);
        }
    }

    // "Examples" menu

    // show given script as new document
    function showScript(title, str) {
        actionNew();
        frame.setTitle("Example - " + title + titleSuffix);
        var doc = editor.document;
        doc.insertString(0, str, null);
    }

    // "hello world"
    function actionHello() {
        showScript(actionEval.title,
                   "alert('Hello, world');");
    }
    actionHello.title = "Hello, World";

    // eval the "hello world"!
    function actionEval() {
        showScript(actionEval.title,
                   "eval(\"alert('Hello, world')\");");
    }
    actionEval.title = "Eval";

    // show how to access Java static methods
    function actionJavaStatic() {
        showScript(arguments.callee.title,
                   "// Just use Java syntax\n" +
                   "var props = java.lang.System.getProperties();\n" +
                   "alert(props.get('os.name'));");
    }
    actionJavaStatic.title = "Java Static Calls";

    // show how to access Java classes, methods
    function actionJavaAccess() {
        showScript(arguments.callee.title,
                   "// just use new JavaClass();\n" +
                   "var fr = new javax.swing.JFrame();\n" +
                   "// call all public methods as in Java\n" +
                   "fr.setTitle('hello');\n" +
                   "fr.setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);\n" +
                   "fr.setSize(200, 200);\n" +
                   "fr.setVisible(true);");
    }
    actionJavaAccess.title = "Java Object Access";

    // show how to use Java bean conventions
    function actionJavaBean() {
        showScript(arguments.callee.title,
                   "var fr = new javax.swing.JFrame();\n" +
                   "fr.setSize(200, 200);\n" +
                   "// access public get/set methods as fields\n" +
                   "fr.defaultCloseOperation = javax.swing.WindowConstants.DISPOSE_ON_CLOSE;\n" +
                   "fr.title = 'hello';\n" +
                   "fr.visible = true;");
    }
    actionJavaBean.title = "Java Beans";

    // show how to implement Java interface
    function actionJavaInterface() {
        showScript(arguments.callee.title,
                   "// use Java anonymizer class-like syntax!\n" +
                   "var r = new java.lang.Runnable() {\n" +
                   "            run: function() {\n" +
                   "                    alert('hello');\n" +
                   "            }\n" +
                   "    };\n" +
                   "// use the above Runnable to create a Thread\n" +
                   "new java.lang.Thread(r).start();\n" +
                   "// For simple one method interfaces, just pass script function\n" +
                   "new java.lang.Thread(function() { alert('world'); }).start();");
    }
    actionJavaInterface.title = "Java Interfaces";

    // show how to import Java classes, packages
    function actionJavaImport() {
        showScript(arguments.callee.title,
                   "// use Java-like import *...\n" +
                   "//    importPackage(java.io);\n" +
                   "// or import a specific class\n" +
                   "//    importClass(java.io.File);\n" +
                   "// or better - import just within a scope!\n" +
                   "var ioPkgs = JavaImporter(java.io);\n" +
                   "with (ioPkgs) { alert(new File('.').absolutePath); }");
    }
    actionJavaImport.title = "Java Import";

    // "Help" menu

    /*
     * Shows a one liner help message for each
     * global function. Note that this function
     * depends on docString meta-data for each
     * function.
     */
    function actionHelpGlobals() {
        var names = new java.util.ArrayList();
        for (var i in globalThis) {
            var func = globalThis[i];
            if (typeof(func) == "function" &&
                ("docString" in func)) {
                names.add(i);
            }
        }
        java.util.Collections.sort(names);
        var helpDoc = new java.lang.StringBuffer();
        helpDoc.append("<table border='1'>");
        var itr = names.iterator();
        while (itr.hasNext()) {
            var name = itr.next();
            helpDoc.append("<tr><td>");
            helpDoc.append(name);
            helpDoc.append("</td><td>");
            helpDoc.append(globalThis[name].docString);
            helpDoc.append("</td></tr>");
        }
        helpDoc.append("</table>");

        var helpEditor = new guiPkgs.JEditorPane();
        helpEditor.setContentType("text/html");
        helpEditor.setEditable(false);
        helpEditor.setText(helpDoc.toString());

        var scroller = new guiPkgs.JScrollPane();
        var port = scroller.getViewport();
        port.add(helpEditor);

        var helpFrame = new guiPkgs.JFrame("Help - Global Functions");
        helpFrame.getContentPane().add("Center", scroller);
        helpFrame.setDefaultCloseOperation(guiPkgs.WindowConstants.DISPOSE_ON_CLOSE);
        helpFrame.pack();
        helpFrame.setSize(500, 600);
        helpFrame.setVisible(true);
    }

    // show a simple about message for scriptpad
    function actionAbout() {
        alert("Scriptpad\nVersion 1.1", "Scriptpad");
    }

    /*
     * This data is used to construct menu bar.
     * This way adding a menu is easier. Just add
     * top level menu or add an item to an existing
     * menu. "action" should be a function that is
     * called back on clicking the correponding menu.
     */
    var menuData = [
        {
            menu: "File",
            items: [
                { name: "New",  action: actionNew , accel: guiPkgs.KeyEvent.VK_N },
                { name: "Open...", action: actionOpen, accel: guiPkgs.KeyEvent.VK_O },
                { name: "Open URL...", action: actionOpenURL, accel: guiPkgs.KeyEvent.VK_U },
                { name: "Save", action: actionSave, accel: guiPkgs.KeyEvent.VK_S },
                { name: "Save As...", action: actionSaveAs },
                { name: "-" },
                { name: "Exit", action: actionExit, accel: guiPkgs.KeyEvent.VK_Q }
            ]
        },

        {
            menu: "Edit",
            items: [
                { name: "Cut", action: actionCut, accel: guiPkgs.KeyEvent.VK_X },
                { name: "Copy", action: actionCopy, accel: guiPkgs.KeyEvent.VK_C },
                { name: "Paste", action: actionPaste, accel: guiPkgs.KeyEvent.VK_V },
                { name: "-" },
                { name: "Select All", action: actionSelectAll, accel: guiPkgs.KeyEvent.VK_A }
            ]
        },

        {
            menu: "Tools",
            items: [
                { name: "Run", action: actionRun, accel: guiPkgs.KeyEvent.VK_R }
            ]
        },

        {
            menu: "Examples",
            items: [
                { name: actionHello.title, action: actionHello },
                { name: actionEval.title, action: actionEval },
                { name: actionJavaStatic.title, action: actionJavaStatic },
                { name: actionJavaAccess.title, action: actionJavaAccess },
                { name: actionJavaBean.title, action: actionJavaBean },
                { name: actionJavaInterface.title, action: actionJavaInterface },
                { name: actionJavaImport.title, action: actionJavaImport }
            ]
        },

        {
            menu: "Help",
            items: [
                { name: "Global Functions", action: actionHelpGlobals },
                { name: "-" },
                { name: "About Scriptpad", action: actionAbout }
            ]
        }
    ];

    function setMenuAccelerator(mi, accel) {
        var keyStroke = guiPkgs.KeyStroke.getKeyStroke(accel,
                                                       guiPkgs.Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false);
        mi.setAccelerator(keyStroke);
    }

    // create a menubar using the above menu data
    function createMenubar() {
        var mb = new guiPkgs.JMenuBar();
        for (var m in menuData) {
            var items = menuData[m].items;
            var menu = new guiPkgs.JMenu(menuData[m].menu);

            for (var i in items) {
                if (items[i].name.equals("-")) {
                    menu.addSeparator();
                } else {
                    var mi = new guiPkgs.JMenuItem(items[i].name);
                    var action = items[i].action;
                    mi.addActionListener(action);
                    var accel = items[i].accel;
                    if (accel) {
                        setMenuAccelerator(mi, accel);
                    }
                    menu.add(mi);
                }
	        }

	        mb.add(menu);
        }

        return mb;
    }

    // function to add a new menu item under "Tools" menu
    function addTool(menuItem, action, accel) {
        if (typeof(action) != "function") {
            return;
        }

        var toolsIndex = -1;
        // find the index of the "Tools" menu
        for (var i in menuData) {
            if (menuData[i].menu.equals("Tools")) {
                toolsIndex = i;
                break;
            }
        }
        if (toolsIndex == -1) {
            return;
        }
        var toolsMenu = frame.getJMenuBar().getMenu(toolsIndex);
        var mi = new guiPkgs.JMenuItem(menuItem);
        mi.addActionListener(action);
        if (accel) {
            setMenuAccelerator(mi, accel);
        }
        toolsMenu.add(mi);
    }

    // create Scriptpad frame
    function createFrame() {
        frame = new guiPkgs.JFrame();
        frame.setTitle(defaultTitle);
        frame.setBackground(guiPkgs.Color.lightGray);
        frame.getContentPane().setLayout(new guiPkgs.BorderLayout());

        // create notepad panel
        var notepad = new guiPkgs.JPanel();
        notepad.setBorder(guiPkgs.BorderFactory.createEtchedBorder());
        notepad.setLayout(new guiPkgs.BorderLayout());

        // create editor
        editor = createEditor();
        var scroller = new guiPkgs.JScrollPane();
        var port = scroller.getViewport();
        port.add(editor);

        // add editor to notepad panel
        var panel = new guiPkgs.JPanel();
        panel.setLayout(new guiPkgs.BorderLayout());
        panel.add("Center", scroller);
        notepad.add("Center", panel);

        // add notepad panel to frame
        frame.getContentPane().add("Center", notepad);

        // set menu bar to frame and show the frame
        frame.setJMenuBar(createMenubar());
        frame.setDefaultCloseOperation(guiPkgs.JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setSize(500, 600);
    }

    // show Scriptpad frame
    function showFrame() {
        // set global variable by the name "window"
        globalThis.window = frame;

        // open new document
        actionNew();

        frame.setVisible(true);
    }

    // create and show Scriptpad frame
    createFrame();
    showFrame();

    /*
     * Application object has two fields "frame", "editor"
     * which are current JFrame and editor and a method
     * called "addTool" to add new menu item to "Tools" menu.
     */
    return {
        frame: frame,
        editor: editor,
        addTool: addTool
    };
};

/*
 * Call the main and store Application object
 * in a global variable named "application".
 */
var application = main();

if (this.load == undefined) {
    function load(file) {
        var ioPkgs = new JavaImporter(java.io);
        with (ioPkgs) {
            var stream = new FileInputStream(file);
            var bstream = new BufferedInputStream(stream);
            var reader = new BufferedReader(new InputStreamReader(bstream));
            var oldFilename = engine.get(engine.FILENAME);
            engine.put(engine.FILENAME, file);
            try {
                engine.eval(reader, context);
            } finally {
                engine.put(engine.FILENAME, oldFilename);
            }
            stream.close();
        }
    }
    load.docString = "loads the given script file";
}

/*
 * Load user specific init file under home dir, if found.
 */
function loadUserInit() {
    var home = java.lang.System.getProperty("user.home");
    var f = new java.io.File(home, "scriptpad.js");
    if (f.exists()) {
        engine.eval(new java.io.FileReader(f));
    }
}

loadUserInit();
