/*
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
*/
package jpa.tools.swing;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Arrays;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JEditorPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;

/**
 * A dialog to display runtime error.
 * 
 * @author Pinaki Poddar
 *
 */
@SuppressWarnings("serial")
public class ErrorDialog extends JDialog {
    private static List<String> filters = Arrays.asList(
            "java.awt.", 
            "javax.swing.", 
            "sun.reflect.",
            "java.util.concurrent.");
    private static Dimension MESSAGE_SIZE    = new Dimension(600,200);
    private static Dimension STACKTRACE_SIZE = new Dimension(600,300);
    private static Dimension TOTAL_SIZE      = new Dimension(600,500);
    
    
    static String NEWLINE = "\r\n";
    static String INDENT  = "    ";
    
    private boolean      _showingDetails;
    private boolean      _isFiltering = true;
    private JComponent _message;
    private JComponent   _main;
    private JScrollPane  _details;
    private JTextPane    _stacktrace;
    private final Throwable _error;
    
    /**
     * Creates a modal dialog to display the given exception message.
     * 
     * @param t the exception to display
     */
    public ErrorDialog(Throwable t) {
        this(null, null, t);
    }

    public ErrorDialog(JComponent owner, Throwable t) {
        this(owner, null, t);
    }
    
    /**
     * Creates a modal dialog to display the given exception message.
     * 
     * @param owner if non-null, then the dialog is positioned (centered) w.r.t. this component
     * @param t the exception to display
     */
    public ErrorDialog(JComponent owner, Icon icon, Throwable t) {
        super();
        setTitle(t.getClass().getName());
        setModal(true);
        if (icon != null && icon instanceof ImageIcon) 
            setIconImage(((ImageIcon)icon).getImage());
        setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
        _error = t;
        _message = createErrorMessage(_error);
        _main    = createContent();
        getContentPane().add(_main);

        pack();
        SwingHelper.position(this, owner);
    }
    
    /**
     * Creates the display with the top-level exception message 
     * followed by a pane (that toggles) for detailed stack traces.
     *  
     * @param t a non-null exception
     */
    JComponent createContent() {
        final JButton showDetails = new JButton("Show Details >>");
        showDetails.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (_showingDetails) {
                     _main.remove(_details);
                     _main.validate();
                     _main.setPreferredSize(MESSAGE_SIZE);
                } else {
                    if (_details == null) {
                        _details = createDetailedMessage(_error);
                        StringBuilder buffer = new StringBuilder();
                        _stacktrace.setText(generateStackTrace(_error, buffer).toString());
                        _stacktrace.setBackground(_main.getBackground());
                        _stacktrace.setPreferredSize(STACKTRACE_SIZE);
                    }
                    _main.add(_details, BorderLayout.CENTER);
                    _main.validate();
                    _main.setPreferredSize(TOTAL_SIZE);
                }
                _showingDetails = !_showingDetails;
                showDetails.setText(_showingDetails ? "<< Hide Details" : "Show Details >>");
                ErrorDialog.this.pack();
            }
        });
        JPanel messagePanel = new JPanel();
      
        final JCheckBox filter = new JCheckBox("Filter stack traces");
        filter.setSelected(_isFiltering);
        filter.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e) {
                _isFiltering = filter.isSelected();
                StringBuilder buffer = new StringBuilder();
                _stacktrace.setText(generateStackTrace(_error, buffer).toString());
                _stacktrace.repaint();
            }
        });
        _message.setBackground(messagePanel.getBackground());
        JPanel buttonPanel = new JPanel();
        buttonPanel.add(Box.createHorizontalStrut(20));
        buttonPanel.add(showDetails);
        buttonPanel.add(filter);
        buttonPanel.add(Box.createHorizontalGlue());
        messagePanel.setLayout(new BorderLayout());
        messagePanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
        messagePanel.add(_message, BorderLayout.CENTER);
        messagePanel.add(buttonPanel, BorderLayout.SOUTH);
        messagePanel.setPreferredSize(MESSAGE_SIZE);
        
        JPanel main = new JPanel();
        main.setLayout(new BorderLayout());
        main.add(messagePanel, BorderLayout.NORTH);
        return main;
    }
    
    /**
     * Creates a non-editable widget to display the error message.
     * 
     */
    JComponent createErrorMessage(Throwable t) {
        String txt = t.getLocalizedMessage();
        JEditorPane message = new JEditorPane();
        message.setContentType("text/plain");
        message.setEditable(false);
        message.setText(txt);
        return message;
    }
    
    /**
     * Creates a non-editable widget to display the detailed stack trace.
     */
    JScrollPane createDetailedMessage(Throwable t) {
        _stacktrace = new JTextPane();
        _stacktrace.setEditable(false);
        JScrollPane pane = new JScrollPane(_stacktrace, 
                JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, 
                JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        
        return pane;
    }
    
    /**
     * Recursively print the stack trace on the given buffer.
     */    
    StringBuilder generateStackTrace(Throwable t, StringBuilder buffer) {
        buffer.append(t.getClass().getName() + ": " + t.getMessage() + NEWLINE);
        buffer.append(toString(t.getStackTrace()));
        Throwable cause = t.getCause();
        if (cause !=null && cause != t) {
            generateStackTrace(cause, buffer);
        }
        return buffer;
    }
    
    StringBuilder toString(StackTraceElement[] traces) {
        StringBuilder error = new StringBuilder();
        for (StackTraceElement e : traces) {
            if (!_isFiltering || !isSuppressed(e.getClassName())) {
                String str = e.toString();
                error.append(INDENT).append(str).append(NEWLINE);
            }
        }
        return error;
    }
    
    /**
     * Affirms if the error messages from the given class name is to be suppressed.
     */
    private boolean isSuppressed(String className) {
        for (String s : filters) {
            if (className.startsWith(s))
                return true;
        }
        return false;
    }
    
    public static void main(String[] args) {
        String m1 = "This is test error with very very very very very long line of error message that " 
            + " should not be in a single line. Another message string that shoul dbe split across word." +
            "The quick brown fox jumpled over the lazy dog";
        String m2 = "This is another test error with very long line of error message that " 
            + " should not be in a single line";
        Throwable nested = new NumberFormatException(m2);
        Throwable top = new IllegalArgumentException(m1, nested);
        new ErrorDialog(top).setVisible(true);
    }

}
