VeLaDialog.java

/**
 * VStar: a statistical analysis tool for variable star data.
 * Copyright (C) 2010  AAVSO (http://www.aavso.org/)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 */
package org.aavso.tools.vstar.ui.vela;

import java.awt.Font;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JTextArea;

import org.aavso.tools.vstar.ui.dialog.ITextComponent;
import org.aavso.tools.vstar.ui.dialog.MessageBox;
import org.aavso.tools.vstar.ui.dialog.TextArea;
import org.aavso.tools.vstar.ui.dialog.TextAreaTabs;
import org.aavso.tools.vstar.ui.dialog.TextDialog;
import org.aavso.tools.vstar.ui.mediator.Mediator;
import org.aavso.tools.vstar.ui.resources.ResourceAccessor;
import org.aavso.tools.vstar.util.Pair;
import org.aavso.tools.vstar.util.locale.LocaleProps;
import org.aavso.tools.vstar.vela.AST;
import org.aavso.tools.vstar.vela.Operand;
import org.aavso.tools.vstar.vela.VeLaInterpreter;
import org.aavso.tools.vstar.vela.VeLaPrefs;

/**
 * A dialog in which to run VeLa code.
 */
@SuppressWarnings("serial")
public class VeLaDialog extends TextDialog {

    private static final String tabTextSeparator = "===---===";

    private static ITextComponent<String> codeTextArea;
    private static ITextComponent<String> resultTextArea;

    private static VeLaInterpreter vela;

    private static String code = "";

    private String path;

    private static List<ITextComponent<String>> createTextAreas() {
        codeTextArea = new TextArea("VeLa Code", code, 12, 42, false, true);
        addKeyListener();

        boolean diagnosticMode = VeLaPrefs.getDiagnosticMode();

        if (diagnosticMode) {
            resultTextArea = new TextAreaTabs(Arrays.asList("Output", "LISP AST", "DOT AST"), Arrays.asList("", "", ""),
                    15, 70, true, true, tabTextSeparator);
        } else {
            resultTextArea = new TextArea("Output", "", 12, 42, true, true);
        }

        Font font = codeTextArea.getUIComponent().getFont();
        codeTextArea.getUIComponent().setFont(new Font(Font.MONOSPACED, Font.PLAIN, font.getSize()));
        resultTextArea.getUIComponent().setFont(new Font(Font.MONOSPACED, Font.PLAIN, font.getSize()));

        return Arrays.asList(codeTextArea, resultTextArea);
    }

    public VeLaDialog(String title) {
        super(title, createTextAreas(), true, true);
        path = "Untitled";
    }

    public VeLaDialog(String title, String code) {
        this(title);
        VeLaDialog.code = code;
        codeTextArea.setValue(code);
    }

    public VeLaDialog() {
        this("VeLa");
    }

    /**
     * @return the most recently loaded/saved file path
     */
    public String getPath() {
        return path;
    }

    /**
     * Get the VeLa code.
     * 
     * @return A string containing the code.
     */
    public String getCode() {
        return codeTextArea.getStringValue();
    }

    @Override
    protected JPanel createButtonPane() {
        JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS));

        JButton cancelButton = new JButton(LocaleProps.get("CANCEL_BUTTON"));
        cancelButton.addActionListener(createCancelButtonListener());
        panel.add(cancelButton);

        JButton clearButton = new JButton(LocaleProps.get("CLEAR_BUTTON"));
        clearButton.addActionListener(e -> {
            resultTextArea.setValue("");
        });
        panel.add(clearButton);

        JButton runButton = new JButton(LocaleProps.get("RUN_BUTTON"));
        runButton.addActionListener(e -> {
            execute();
        });
        panel.add(runButton);

        JButton loadButton = new JButton(LocaleProps.get("LOAD_BUTTON"));
        loadButton.addActionListener(e -> {
            try {
                Pair<String, String> content = Mediator.getInstance().getVelaFileLoadDialog().readFileAsString(this,
                        null);
                if (content != null) {
                    codeTextArea.setValue(content.first);
                }
            } catch (Exception ex) {
                MessageBox.showErrorDialog(this, getTitle(), ex);
            }
        });

        panel.add(loadButton);

        JButton saveButton = new JButton(LocaleProps.get("SAVE_BUTTON"));
        saveButton.addActionListener(e -> {
            try {
                String content = codeTextArea.getValue();
                Mediator.getInstance().getVelaFileSaveDialog().writeStringToFile(this, content, null);
            } catch (Exception ex) {
                MessageBox.showErrorDialog(this, getTitle(), ex);
            }
        });
        panel.add(saveButton);

        JButton dismissButton = new JButton(LocaleProps.get("OK_BUTTON"));
        dismissButton.addActionListener(e -> {
            okAction();
        });
        panel.add(dismissButton);

        return panel;
    }

    // Helpers

    @Override
    protected void okAction() {
        super.okAction();
    }

    /**
     * Retain VeLa source across dialog invocations whenever the window is closed
     * (OK, Cancel, or window close), not only when the dismiss button runs
     * {@link #okAction()}.
     */
    @Override
    public void dispose() {
        persistCodeFromEditor();
        super.dispose();
    }

    private static void persistCodeFromEditor() {
        if (codeTextArea != null) {
            code = codeTextArea.getValue();
        }
    }

    private static void addKeyListener() {
        JTextArea area = (JTextArea) (codeTextArea.getUIComponent());
        area.addKeyListener(new KeyAdapter() {
            boolean escapeMode = false;

            @Override
            public void keyTyped(KeyEvent e) {
                char ch = e.getKeyChar();
                String newCh = null;

                if (escapeMode) {
                    switch (ch) {
                    case 'b':
                    case 'B':
                        // Boolean set
                        newCh = "\uD835\uDD39";
                        break;
                    case 'l':
                    case 'L':
                        // lambda
                        newCh = "\u03BB";
                        break;
                    case 'p':
                    case 'P':
                        // pi
                        newCh = "\u03C0";
                        break;
                    case 'r':
                    case 'R':
                        // real number set
                        newCh = "\u211D";
                        break;
                    case 'z':
                    case 'Z':
                        // integer number set
                        newCh = "\u2124";
                        break;
                    case '\\':
                        // backslash
                        newCh = "\\";
                        break;
                    }

                    e.consume();

                    int pos = area.getCaretPosition();
                    area.insert(newCh, pos);
                    escapeMode = false;

                } else if (ch == '\\') {
                    escapeMode = true;
                    e.consume();
                }
            }
        });
    }

    private void execute() {
        boolean diagnostic = VeLaPrefs.getDiagnosticMode();

        String text = codeTextArea.getValue();

        String output = "";
        String error = "";
        String lispAST = "";
        String dotAST = "";

        // Capture standard output and error
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        System.setOut(new PrintStream(outStream));

        ByteArrayOutputStream errStream = new ByteArrayOutputStream();
        System.setErr(new PrintStream(errStream));

        try {
            Mediator.getUI().setScriptingStatus(true);

            // Compile and execute the code.
            vela = new VeLaInterpreter(VeLaPrefs.getVerboseMode(), true, VeLaPrefs.getCodeDirsList());

            Pair<Optional<Operand>, AST> pair = vela.veLaToResultASTPair(text);

            Optional<Operand> result = pair.first;

            if (result.isPresent()) {
                AST ast = pair.second;
                if (diagnostic && ast != null) {
                    lispAST = ast.toString();
                    dotAST = ast.toFullDOT();
                }
            }

            // Any standard error or output to show?
            error = showOutput(errStream);
            output = showOutput(outStream);

            // Is there a result to show and no error?
            if (result.isPresent() && "".equals(error)) {
                output += result.get().toHumanReadableString();
            }
        } catch (Exception e) {
            // e.printStackTrace();

            // Show error in text area.
            String msg = e.getLocalizedMessage();
            if (msg != null) {
                error = msg;
            } else {
                error = "Error";
            }

            if (msg != null && !msg.equals(errStream.toString())) {
                // Any standard error to show in relation to this exception?
                // Don't repeat msg.
                error += showOutput(errStream);
            }
        } finally {
            // Reset standard output and error to console.
            System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out)));

            System.setErr(new PrintStream(new FileOutputStream(FileDescriptor.err)));

            Mediator.getUI().setScriptingStatus(false);
        }

        String result = output;
        if (!error.isEmpty()) {
            result += "\n";
            result += error;
        }

        resultTextArea.setValue(areaTabsPayload(VeLaPrefs.getDiagnosticMode(), result, lispAST, dotAST));
    }

    private String areaTabsPayload(boolean verbose, String... strings) {
        StringBuffer buf = new StringBuffer();

        for (String str : strings) {
            buf.append(str);
            if (verbose) {
                buf.append(tabTextSeparator);
            } else {
                buf.append("\n");
            }
        }

        return buf.toString().trim();
    }

    private String showOutput(ByteArrayOutputStream stream) {
        String str = "";

        if (stream.size() != 0) {
            str = stream.toString() + "\n";
        }

        return str;
    }
}