import javax.swing.JFrame;
import javax.swing.JTextArea;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.text.Document;
import javax.swing.event.DocumentListener;
import javax.swing.event.DocumentEvent;

import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Dimension;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

import java.io.File;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import drafts.com.sun.star.script.framework.runtime.XScriptContext;
import bsh.Interpreter;

public class OOBeanShellDebugger implements OOScriptDebugger, ActionListener, DocumentListener {

    private JFrame frame;
    private JTextArea ta;
    private GlyphGutter gg;
    private XScriptContext context;
    private int currentPosition = -1;
    private int linecount;
    private Interpreter sessionInterpreter;
    private Thread execThread = null;
    private String filename = null;

    /* Entry point for script execution */
    public void go(XScriptContext context, String filename) {
        if (filename != null && filename != "") {
            try {
                FileInputStream fis = new FileInputStream(filename);
                this.filename = filename;
                go(context, fis);
            }
            catch (IOException ioe) {
                JOptionPane.showMessageDialog(frame,
                    "Error loading file: " + ioe.getMessage(),
                    "Error", JOptionPane.ERROR_MESSAGE);
            }
        }
    }

    /* Entry point for script execution */
    public void go(XScriptContext context, InputStream in) {
        this.context = context;
        initUI();

        if (in != null) {
            try {
                loadFile(in);
            }
            catch (IOException ioe) {
                JOptionPane.showMessageDialog(frame,
                    "Error loading stream: " + ioe.getMessage(),
                    "Error", JOptionPane.ERROR_MESSAGE);
            }
        }
    }

    public void loadFile(InputStream in) throws IOException {

        /* Remove ourselves as a DocumentListener while loading the file
           so we don't get a storm of DocumentEvents during loading */
        ta.getDocument().removeDocumentListener(this);

        byte[] contents = new byte[1024];
        int len = 0, pos = 0;

        while ((len = in.read(contents, 0, 1024)) != -1) {
            ta.insert(new String(contents, 0, len), pos);
            pos += len;
        }

        try {
            in.close();
        }
        catch (IOException ignore) {
        }

        /* Update the GlyphGutter and add back the DocumentListener */
        gg.update();
        ta.getDocument().addDocumentListener(this);
    }

    private void initUI() {
        frame = new JFrame("BeanShell Debug Window");
        ta = new JTextArea();
        ta.setRows(15);
        ta.setColumns(40);
        ta.setLineWrap(false);
        linecount = ta.getLineCount();

        gg = new GlyphGutter(this);

        final JScrollPane sp = new JScrollPane();
        sp.setViewportView(ta);
        sp.setRowHeaderView(gg);

        ta.getDocument().addDocumentListener(this);
        String[] labels = {"Run", "Clear", "Save", "Close"};
        JPanel p = new JPanel();
        p.setLayout(new FlowLayout());

        for (int i = 0; i < labels.length; i++) {
            JButton b = new JButton(labels[i]);
            b.addActionListener(this);
            p.add(b);

            if (labels[i].equals("Save") && filename == null) {
                b.setEnabled(false);
            }
        }

        frame.getContentPane().add(sp, "Center");
        frame.getContentPane().add(p, "South");
        frame.pack();
        frame.show();
    }

    /* Implementation of DocumentListener interface */
    public void insertUpdate(DocumentEvent e) {
        doChanged(e);
    }

    public void removeUpdate(DocumentEvent e) {
        doChanged(e);
    }

    public void changedUpdate(DocumentEvent e) {
        doChanged(e);
    }

    /* If the number of lines in the JTextArea has changed then update the
       GlyphGutter */
    public void doChanged(DocumentEvent e) {
        if (linecount != ta.getLineCount()) {
            gg.update();
            linecount = ta.getLineCount();
        }
    }

    private void startExecution() {
        execThread = new Thread() {
            public void run() {
                Interpreter interpreter = new Interpreter();
                interpreter.getNameSpace().clear();

                // reset position and repaint gutter so no red arrow appears
                currentPosition = -1;
                gg.repaint();

                try {
                    interpreter.set("context", context);
                    interpreter.eval(ta.getText());
                }
                catch (bsh.EvalError err) {
                    currentPosition = err.getErrorLineNumber() - 1;
                    try {
                        // scroll to line of the error
                        int line = ta.getLineStartOffset(currentPosition);
                        Rectangle rect = ta.modelToView(line);
                        ta.scrollRectToVisible(rect);
                    }
                    catch (Exception e) {
                        // couldn't scroll to line, do nothing
                    }
                    gg.repaint();

                    JOptionPane.showMessageDialog(frame, "Error at line " +
                        String.valueOf(err.getErrorLineNumber()) +
                        "\n\n: " + err.getErrorText(),
                        "Error", JOptionPane.ERROR_MESSAGE);
                }
                catch (Exception e) {
                    JOptionPane.showMessageDialog(frame,
                        "Error: " + e.getMessage(),
                        "Error", JOptionPane.ERROR_MESSAGE);
                }
            }
        };    
        execThread.start();
    }

    private void promptForSaveName() {
        JFileChooser chooser = new JFileChooser();
        chooser.setFileFilter(new javax.swing.filechooser.FileFilter() {
            public boolean accept(File f) {
                if (f.isDirectory() || f.getName().endsWith(".bsh")) {
                    return true;
                }
                return false;
            }

            public String getDescription() {
                return ("BeanShell files: *.bsh");
            }
        });

        int ret = chooser.showSaveDialog(frame);

        if (ret == JFileChooser.APPROVE_OPTION) {
            filename = chooser.getSelectedFile().getAbsolutePath();
            if (!filename.endsWith(".bsh")) {
                filename += ".bsh";
            }
        }

    }

    private void saveTextArea() {
        if (filename == null) {
            promptForSaveName();
        }

        FileOutputStream fos = null;
        if (filename != null) {
            try {
                File f = new File(filename);
                fos = new FileOutputStream(f);
                String s = ta.getText();
                fos.write(s.getBytes(), 0, s.length());
            }
            catch (IOException ioe) {
                JOptionPane.showMessageDialog(frame,
                    "Error saving file: " + ioe.getMessage(),
                    "Error", JOptionPane.ERROR_MESSAGE);
            }
            finally {
                if (fos != null) {
                    try {
                        fos.close();
                    }
                    catch (IOException ignore) {
                    }
                }
            }
        }
    }

    public void actionPerformed(ActionEvent e) {
        if (e.getActionCommand().equals("Run")) {
            startExecution();
        }
        else if (e.getActionCommand().equals("Close")) {
            frame.dispose();
        }
        else if (e.getActionCommand().equals("Save")) {
            saveTextArea();
        }
        else if (e.getActionCommand().equals("Clear")) {
            ta.setText("");
        }
    }

    public JTextArea getTextArea() {
        return ta;
    }

    public int getCurrentPosition() {
        return currentPosition;
    }
}

class GlyphGutter extends JComponent {

    private OOBeanShellDebugger debugger;
    private final String DUMMY_STRING = "99";

    GlyphGutter(OOBeanShellDebugger debugger) {
        this.debugger = debugger;
        update();
    }

    public void update() {
        JTextArea textArea = debugger.getTextArea();
        Font font = textArea.getFont();
        setFont(font);

        FontMetrics metrics = getFontMetrics(font);
        int h = metrics.getHeight();
        int lineCount = textArea.getLineCount() + 1;

        String dummy = Integer.toString(lineCount);
        if (dummy.length() < 2) {
            dummy = DUMMY_STRING;
        }

        Dimension d = new Dimension();
        d.width = metrics.stringWidth(dummy) + 16;
        d.height = lineCount * h + 100;
        setPreferredSize(d);
        setSize(d);
    }

    public void paintComponent(Graphics g) {
        JTextArea textArea = debugger.getTextArea();

        Font font = textArea.getFont();
        g.setFont(font);

        FontMetrics metrics = getFontMetrics(font);
        Rectangle clip = g.getClipBounds();

        g.setColor(getBackground());
        g.fillRect(clip.x, clip.y, clip.width, clip.height);

        int ascent = metrics.getMaxAscent();
        int h = metrics.getHeight();
        int lineCount = textArea.getLineCount() + 1;

        int startLine = clip.y / h;
        int endLine = (clip.y + clip.height) / h + 1;
        int width = getWidth();
        if (endLine > lineCount) {
            endLine = lineCount;
        }

        for (int i = startLine; i < endLine; i++) {
            String text;
            text = Integer.toString(i + 1) + " ";
            int w = metrics.stringWidth(text);
            int y = i * h;
            g.setColor(Color.blue);
            g.drawString(text, 0, y + ascent);
            int x = width - ascent;

            // if currentPosition is not -1 then a red arrow will be drawn
            if (i == debugger.getCurrentPosition()) {
                drawArrow(g, ascent, x, y);
            }
        }
    }

    private void drawArrow(Graphics g, int ascent, int x, int y) {
        Polygon arrow = new Polygon();
        int dx = x;
        y += ascent - 10;
        int dy = y;
        arrow.addPoint(dx, dy + 3);
        arrow.addPoint(dx + 5, dy + 3);
        for (x = dx + 5; x <= dx + 10; x++, y++) {
            arrow.addPoint(x, y);
        }
        for (x = dx + 9; x >= dx + 5; x--, y++) {
            arrow.addPoint(x, y);
        }
        arrow.addPoint(dx + 5, dy + 7);
        arrow.addPoint(dx, dy + 7);

        g.setColor(Color.red);
        g.fillPolygon(arrow);
        g.setColor(Color.black);
        g.drawPolygon(arrow);
    }
};

