/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
 * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
 * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package edu.mit.csail.sdg.alloy4whole;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.border.EmptyBorder;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.BoxView;
import javax.swing.text.Element;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.text.StyledEditorKit;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;

import edu.mit.csail.sdg.alloy4.OurAntiAlias;
import edu.mit.csail.sdg.alloy4.OurUtil;

/**
 * This helper method is used by SimpleGUI; only the AWT Event Thread may call
 * methods in this class.
 */

final class SwingLogPanel {

    /**
     * Try to wrap the input to about 60 characters per line; however, if a token is
     * too long, we won't break it.
     */
    private static void linewrap(StringBuilder sb, String msg) {
        StringTokenizer tokenizer = new StringTokenizer(msg, "\r\n\t ");
        final int max = 60;
        int now = 0;
        while (tokenizer.hasMoreTokens()) {
            String x = tokenizer.nextToken();
            if (now + 1 + x.length() > max) {
                if (now > 0) {
                    sb.append('\n');
                }
                sb.append(x);
                now = x.length();
            } else {
                if (now > 0) {
                    now++;
                    sb.append(' ');
                }
                sb.append(x);
                now = now + x.length();
            }
        }
    }

    /**
     * This field buffers previous calls to log() so that we can write them out
     * later in a single Swing call (If there is nothing buffered, this field can be
     * an empty list or even null).
     */
    private final List<String>       batch          = new ArrayList<String>();

    /**
     * The newly created JTextPane object that will display the log; null if this
     * log has been destroyed.
     */
    private JTextPane                log;

    /** The style to use when writing regular messages. */
    private final Style              styleRegular;

    /** The style to use when writing bold messages. */
    private final Style              styleBold;

    /** The style to use when writing red messages. */
    private final Style              styleRed;

    /**
     * This stores the JLabels used for displaying hyperlinks.
     */
    private final List<JLabel>       links          = new ArrayList<JLabel>();

    /**
     * When the window gains focus, we'll call handler.run(ev_logFocused); When a
     * hyperlink is clicked, we'll call handler.run(evs_visualize, linkURL).
     */
    private final SimpleGUI          handler;

    /**
     * The current length of the log, not counting any "red" error message at the
     * end of the log.
     */
    private int                      lastSize       = 0;

    /** The current font name. */
    private String                   fontName;

    /** The current font size. */
    private int                      fontSize;

    /**
     * The color to use for hyperlinks when the mouse is not hovering over it.
     */
    private static final Color       linkColor      = new Color(0.3f, 0.3f, 0.9f);

    /**
     * The color to use for a hyperlink when the mouse is hovering over it.
     */
    private static final Color       hoverColor     = new Color(0.9f, 0.3f, 0.3f);

    /**
     * This stores a default ViewFactory that will handle the View requests that we
     * don't care to override.
     */
    private static final ViewFactory defaultFactory = (new StyledEditorKit()).getViewFactory();

    /**
     * Constructs a new JTextPane logger, and put it inside an existing JScrollPane.
     *
     * @param parent - the existing JScrollPane to insert the JTextPane into
     * @param fontName - the font name to use
     * @param fontSize - the font size to use
     * @param background - the background color to use
     * @param regular - the color to use for regular messages
     * @param red - the color to use for red messages
     * @param handler - the SimpleGUI parent
     */
    public SwingLogPanel(final JScrollPane parent, String fontName, int fontSize, final Color background, final Color regular, final Color red, final SimpleGUI handler) {
        this.handler = handler;
        this.fontName = fontName;
        this.fontSize = fontSize;
        this.log = OurUtil.make(OurAntiAlias.pane(null), Color.BLACK, background, new EmptyBorder(1, 1, 1, 1), new Font(fontName, Font.PLAIN, fontSize));
        // This customized StyledEditorKit prevents line-wrapping up to 30000
        // pixels wide.
        // 30000 is a good number; value higher than about 32768 will cause
        // errors.
        this.log.setEditorKit(new StyledEditorKit() {

            private static final long serialVersionUID = 0;

            @Override
            public final ViewFactory getViewFactory() {
                return new ViewFactory() {

                    @Override
                    public final View create(Element x) {
                        if (!AbstractDocument.SectionElementName.equals(x.getName()))
                            return defaultFactory.create(x);
                        return new BoxView(x, View.Y_AXIS) {

                            @Override
                            public final float getMinimumSpan(int axis) {
                                return super.getPreferredSpan(axis);
                            }

                            @Override
                            public final void layout(int width, int height) {
                                int x = 0;
                                int dec = 20;
                                int w = 30000 + dec;
                                while (x++ < 10) {
                                    try {
                                        super.layout(w - (int) Math.pow(2, x - 1) * dec, height);
                                        break;
                                    } catch (Exception e) {
                                        // System.out.println("---------error
                                        // for ww = " + ww);
                                    }
                                }
                            }
                        };
                    }
                };
            }
        });
        log.setEditable(false);
        log.addFocusListener(new FocusListener() {

            @Override
            public final void focusGained(FocusEvent e) {
                if (handler != null)
                    handler.notifyFocusLost();
            }

            @Override
            public final void focusLost(FocusEvent e) {}
        });
        StyledDocument doc = log.getStyledDocument();
        styleRegular = doc.addStyle("regular", null);
        StyleConstants.setFontFamily(styleRegular, fontName);
        StyleConstants.setFontSize(styleRegular, fontSize);
        StyleConstants.setForeground(styleRegular, regular);
        styleBold = doc.addStyle("bold", styleRegular);
        StyleConstants.setBold(styleBold, true);
        styleRed = doc.addStyle("red", styleBold);
        StyleConstants.setForeground(styleRed, red);
        parent.setViewportView(log);
        parent.setBackground(background);
    }

    /** Write a horizontal separator into the log window. */
    public void logDivider() {
        if (log == null)
            return;
        clearError();
        StyledDocument doc = log.getStyledDocument();
        Style dividerStyle = doc.addStyle("bar", styleRegular);
        JPanel jpanel = new JPanel();
        jpanel.setBackground(Color.LIGHT_GRAY);
        jpanel.setPreferredSize(new Dimension(300, 1)); // 300 is arbitrary,
                                                       // since it will
                                                       // auto-stretch
        StyleConstants.setComponent(dividerStyle, jpanel);
        reallyLog(".", dividerStyle); // Any character would do; "." will be
                                     // replaced by the JPanel
        reallyLog("\n\n", styleRegular);
        log.setCaretPosition(doc.getLength());
        lastSize = doc.getLength();
    }

    /** Write a clickable link into the log window. */
    public void logLink(final String link, final String linkDestination) {
        if (log == null || link.length() == 0)
            return;
        if (linkDestination == null || linkDestination.length() == 0) {
            log(link);
            return;
        }
        clearError();
        StyledDocument doc = log.getStyledDocument();
        Style linkStyle = doc.addStyle("link", styleRegular);
        final JLabel label = OurUtil.make(OurAntiAlias.label(link), new Font(fontName, Font.BOLD, fontSize), linkColor);
        label.setAlignmentY(0.8f);
        label.setMaximumSize(label.getPreferredSize());
        label.addMouseListener(new MouseListener() {

            @Override
            public final void mousePressed(MouseEvent e) {
                if (handler != null)
                    handler.doVisualize(linkDestination);
            }

            @Override
            public final void mouseClicked(MouseEvent e) {}

            @Override
            public final void mouseReleased(MouseEvent e) {}

            @Override
            public final void mouseEntered(MouseEvent e) {
                label.setForeground(hoverColor);
            }

            @Override
            public final void mouseExited(MouseEvent e) {
                label.setForeground(linkColor);
            }
        });
        StyleConstants.setComponent(linkStyle, label);
        links.add(label);
        reallyLog(".", linkStyle); // Any character would do; the "." will be
                                  // replaced by the JLabel
        log.setCaretPosition(doc.getLength());
        lastSize = doc.getLength();
    }

    /** Write "msg" in regular style. */
    public void log(String msg) {
        if (log != null && msg.length() > 0)
            batch.add(msg);
    }

    /** Write "msg" in bold style. */
    public void logBold(String msg) {
        if (msg.length() > 0) {
            clearError();
            reallyLog(msg, styleBold);
        }
    }

    private void reallyLog(String text, Style style) {
        if (log == null || text.length() == 0)
            return;
        int i = text.lastIndexOf('\n'), j = text.lastIndexOf('\r');
        if (i >= 0 && i < j) {
            i = j;
        }
        StyledDocument doc = log.getStyledDocument();
        try {
            if (i < 0) {
                doc.insertString(doc.getLength(), text, style);
            } else {
                // Performs intelligent caret positioning
                doc.insertString(doc.getLength(), text.substring(0, i + 1), style);
                log.setCaretPosition(doc.getLength());
                if (i < text.length() - 1) {
                    doc.insertString(doc.getLength(), text.substring(i + 1), style);
                }
            }
        } catch (BadLocationException e) {
            // Harmless
        }
        if (style != styleRed) {
            lastSize = doc.getLength();
        }
    }

    /** Write "msg" in red style (with automatic line wrap). */
    public void logRed(String msg) {
        if (log == null || msg == null || msg.length() == 0)
            return;
        StringBuilder sb = new StringBuilder();
        while (msg.length() > 0) {
            int i = msg.indexOf('\n');
            if (i >= 0) {
                linewrap(sb, msg.substring(0, i));
                sb.append('\n');
                msg = msg.substring(i + 1);
            } else {
                linewrap(sb, msg);
                break;
            }
        }
        clearError();
        reallyLog(sb.toString(), styleRed);
    }

    /**
     * Write "msg" in regular style (with automatic line wrap).
     */
    public void logIndented(String msg) {
        if (log == null || msg.length() == 0)
            return;
        StringBuilder sb = new StringBuilder();
        while (msg.length() > 0) {
            int i = msg.indexOf('\n');
            if (i >= 0) {
                linewrap(sb, msg.substring(0, i));
                sb.append('\n');
                msg = msg.substring(i + 1);
            } else {
                linewrap(sb, msg);
                break;
            }
        }
        clearError();
        reallyLog(sb.toString(), styleRegular);
    }

    /** Set the font name. */
    public void setFontName(String fontName) {
        if (log == null)
            return;
        this.fontName = fontName;
        log.setFont(new Font(fontName, Font.PLAIN, fontSize));
        StyleConstants.setFontFamily(styleRegular, fontName);
        StyleConstants.setFontFamily(styleBold, fontName);
        StyleConstants.setFontFamily(styleRed, fontName);
        StyleConstants.setFontSize(styleRegular, fontSize);
        StyleConstants.setFontSize(styleBold, fontSize);
        StyleConstants.setFontSize(styleRed, fontSize);
        // Changes all existing text
        StyledDocument doc = log.getStyledDocument();
        Style temp = doc.addStyle("temp", null);
        StyleConstants.setFontFamily(temp, fontName);
        StyleConstants.setFontSize(temp, fontSize);
        doc.setCharacterAttributes(0, doc.getLength(), temp, false);
        // Changes all existing hyperlinks
        Font newFont = new Font(fontName, Font.BOLD, fontSize);
        for (JLabel link : links) {
            link.setFont(newFont);
        }
    }

    /** Set the font size. */
    public void setFontSize(int fontSize) {
        if (log == null)
            return;
        this.fontSize = fontSize;
        setFontName(this.fontName);
    }

    /** Set the background color. */
    public void setBackground(Color background) {
        if (log == null)
            return;
        log.setBackground(background);
    }

    /** Query the current length of the log. */
    int getLength() {
        if (log == null)
            return 0;
        clearError();
        return log.getStyledDocument().getLength();
    }

    /**
     * Truncate the log to the given length; if the log is shorter than the number
     * given, then nothing happens.
     */
    void setLength(int newLength) {
        if (log == null)
            return;
        clearError();
        StyledDocument doc = log.getStyledDocument();
        int n = doc.getLength();
        if (n <= newLength)
            return;
        try {
            doc.remove(newLength, n - newLength);
        } catch (BadLocationException e) {
            // Harmless
        }
        if (lastSize > doc.getLength()) {
            lastSize = doc.getLength();
        }
    }

    /**
     * This method copies the currently selected text in the log (if any) into the
     * clipboard.
     */
    public void copy() {
        if (log == null)
            return;
        log.copy();
    }

    /** Removes any messages writtin in "red" style. */
    public void clearError() {
        if (log == null)
            return;
        // Since this class always removes "red" messages prior to writing
        // anything,
        // that means if there are any red messages, they will always be at the
        // end of the JTextPane.
        StyledDocument doc = log.getStyledDocument();
        int n = doc.getLength();
        if (n > lastSize) {
            try {
                doc.remove(lastSize, n - lastSize);
            } catch (BadLocationException e) {}
        }
        if (batch.size() > 0) {
            for (String msg : batch) {
                reallyLog(msg, styleRegular);
            }
            batch.clear();
        }
    }

    /**
     * Commits all outstanding writes (if the messages are buffered).
     */
    public void flush() {
        if (log == null)
            return;
        if (batch.size() > 0)
            clearError();
    }
}