package org.limewire.ui.support;

import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Dialog.ModalityType;
import java.awt.event.ActionEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.WindowConstants;

import org.limewire.core.api.support.LocalClientInfo;
import org.limewire.core.api.support.LocalClientInfoFactory;
import org.limewire.core.impl.support.LocalClientInfoImpl;
import org.limewire.io.IOUtils;
import org.limewire.ui.swing.action.UrlAction;
import org.limewire.ui.swing.components.HyperlinkButton;
import org.limewire.ui.swing.components.LimeJDialog;
import org.limewire.ui.swing.components.MultiLineLabel;
import org.limewire.ui.swing.settings.BugSettings;
import org.limewire.ui.swing.util.GuiUtils;
import org.limewire.ui.swing.util.I18n;
import org.limewire.util.FileUtils;
import org.limewire.util.GenericsUtils;
import org.limewire.util.NotImplementedException;

import com.google.inject.Inject;
import com.limegroup.gnutella.util.LimeWireUtils;

/**
 * Interface for reporting bugs.
 * This can do any of the following:
 * <ul>
 *  <li>Allow the user to copy the bug & email it.</li>
 *  <li>Suppress the bug entirely.</li>
 *  </ul>
 */
public final class BugManager {

    @Inject private static volatile LocalClientInfoFactory localClientInfoFactory;
    
    /**
     * The width of the internal error dialog box.
     */
    private final int DIALOG_BOX_WIDTH = 300;
    /**
     * The height of the internal error dialog box.
     */
    private final int DIALOG_BOX_HEIGHT = 100;
    
    /**
     * A mapping of stack traces (String) to next allowed time (long)
     * that the bug can be reported.
     *
     * Used only if reporting the bug to the servlet.
     */
    private final Map<String, Long> BUG_TIMES = Collections.synchronizedMap(new HashMap<String, Long>());
    
    /**
     * A lock to be used when writing to the logfile, if the log is to be
     * recorded locally.
     */
    private final Object WRITE_LOCK = new Object();
    
    /**
     * A separator between bug reports.
     */
    // saved to local file system only, so use platform encoding
    private final byte[] SEPARATOR = "-----------------\n".getBytes();
    
    /**
     * The next time we're allowed to send any bug.
     *
     * Used only if reporting the bug to the servlet.
     */
    private volatile long _nextAllowedTime = 0;
    
    /**
     * The number of bug dialogs currently showing.
     */
    private volatile int _dialogsShowing = 0;
    
    /**
     * The maximum number of dialogs we're allowed to show.
     */
    private final int MAX_DIALOGS = 3;
    
    /**
     * Whether or not we have dirty data after the last save.
     */
    private boolean dirty = false;
    
    public BugManager() {
        loadOldBugs();
    }
    
    /**
     * Shuts down the BugManager.
     */
    public void shutdown() {
        writeBugsToDisk();
    }
    
    /**
     * Handles a single bug report.
     * If bug is a ThreadDeath, rethrows it.
     * If the user wants to ignore all bugs, this effectively does nothing.
     * The the server told us to stop reporting this (or any) bug(s) for
     * awhile, this effectively does nothing.
     * Otherwise, it will either send the bug directly to the servlet
     * or ask the user to review it before sending.
     */
    public void handleBug(Throwable bug, String threadName, String detail) {
        if( bug instanceof ThreadDeath ) // must rethrow.
            throw (ThreadDeath)bug;
        
        // Try to dispatch the bug to a friendly handler.
        if (bug instanceof IOException
                && IOUtils.handleException((IOException) bug, IOUtils.ErrorType.GENERIC)) {
            return; // handled already.
        }   
        
        bug.printStackTrace();
        
        // Build the LocalClientInfo out of the info ...
        LocalClientInfo info;
        if(localClientInfoFactory == null) {
            info = new LocalClientInfoImpl(bug, threadName, detail, false, new NoSessionInfo());
        } else {
            info = localClientInfoFactory.createLocalClientInfo(bug, threadName, detail, false);
        }

        if( BugSettings.LOG_BUGS_LOCALLY.getValue() ) {
            logBugLocally(info);
        }

        if (_dialogsShowing < MAX_DIALOGS ) {
            reviewBug(info, bug instanceof NotImplementedException);
        }
    }
    
    /**
     * Logs the bug report to a local file.
     * If the file reaches a certain size it is erased.
     */
    private void logBugLocally(LocalClientInfo info) {
        File f = BugSettings.BUG_LOG_FILE.get();
        FileUtils.setWriteable(f);
        OutputStream os = null;
        try {
            synchronized(WRITE_LOCK) {
                if ( f.length() > BugSettings.MAX_BUGFILE_SIZE.getValue() )
                    f.delete();
                os = new BufferedOutputStream(
                        new FileOutputStream(f.getPath(), true));
                // saved to local file system, use default encoding
                os.write((new Date().toString() + "\n").getBytes());
                os.write(info.toBugReport().getBytes());
                os.write(SEPARATOR);
                os.flush();
            }
        } catch(IOException ignored) {
        } finally {
            IOUtils.close(os);
        }
    }
    
    /**
     * Loads bugs from disk.
     */
    private void loadOldBugs() {
        ObjectInputStream in = null;
        File f = BugSettings.BUG_INFO_FILE.get();
        try {
            // Purposely not a ConverterObjectInputStream --
            // we never want to read old version's bug info.
            in = new ObjectInputStream(
                    new BufferedInputStream(
                        new FileInputStream(f)));
            String version = (String)in.readObject();
            long nextTime = in.readLong();
            if( version.equals(LimeWireUtils.getLimeWireVersion()) ) {
                Map<String, Long> bugs = GenericsUtils.scanForMap(
                        in.readObject(), String.class, Long.class,
                        GenericsUtils.ScanMode.REMOVE);
                // Only load them if we're continuing to use the same version
                // This way bugs for newer versions get reported.
                // We could check to make sure this is a newer version,
                // but it's not all that necessary.
                _nextAllowedTime = nextTime;
                long now = System.currentTimeMillis();
                for(Map.Entry<String, Long> entry : bugs.entrySet()) {
                    // Only insert those whose times haven't expired.
                    Long allowed = entry.getValue();
                    if( allowed != null && now < allowed.longValue() )
                        BUG_TIMES.put(entry.getKey(), allowed);
                }
            } else {
                // Otherwise, we're using a different version than the last time.
                // Unset 'discard all bugs'.
            	BugSettings.SHOW_BUGS.setValue(true);
            }
        } catch(Throwable t) {
            // ignore errors from disk.
        } finally {
            IOUtils.close(in);
        }
                    
    }
    
    /**
     * Write bugs out to disk.
     */
    private void writeBugsToDisk() {
        synchronized(WRITE_LOCK) {
            if(!dirty)
                return;
            
            ObjectOutputStream out = null;
            try {
                File f = BugSettings.BUG_INFO_FILE.get();
                out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(f)));
                String version = LimeWireUtils.getLimeWireVersion();
                out.writeObject(version);
                out.writeLong(_nextAllowedTime);
                out.writeObject(BUG_TIMES);
                out.flush();
            } catch(Exception e) {
                // oh well, no biggie if we couldn't write to disk.
            } finally {
                IOUtils.close(out);
            }
            
            dirty = false;
        }
    }
    
    /**
     * Displays a message to the user informing them an internal error
     * has occurred.
     */
    private void reviewBug(final LocalClientInfo info, boolean notImplemented) {
        _dialogsShowing++;
        
	// How does this line work?  I assume the colon separates title bar from dialogue box.  
	// If it doesn't, this needs to be fixed.  Of course, that's going to fuck up the i18n...
        String title = notImplemented ? I18n.tr("Error") : I18n.tr("An internal error has occurred.");
        final JDialog DIALOG = new LimeJDialog(GuiUtils.getMainFrame(), title, ModalityType.APPLICATION_MODAL);
        final Dimension DIALOG_DIMENSION = new Dimension(DIALOG_BOX_WIDTH, DIALOG_BOX_HEIGHT);
        DIALOG.setSize(DIALOG_DIMENSION);
        DIALOG.setResizable(false);
        DIALOG.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);


        // make sure number of current dialogs gets decremented when user closes it 
        DIALOG.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent we) {
                _dialogsShowing--;
            }
        });

        JPanel mainPanel = new JPanel();
        mainPanel.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
        mainPanel.setLayout(new GridBagLayout());

        String msg;
        if(notImplemented) {
            msg = I18n.tr("An error has occurred: this function has not yet been implemented.  To continue using WireShare, close this window; for more information, click \'Show Bug\'.");
        } else {
            msg = I18n.tr("An internal error has occurred.  To continue using WireShare, close this window; for more information, click \'Show Bug\'.");
        }

        MultiLineLabel label = new MultiLineLabel(msg, 500);
        JPanel labelPanel = new JPanel();
        labelPanel.setLayout(new GridBagLayout());
        GridBagConstraints constraints = new GridBagConstraints();
        constraints.gridx = 0;
        constraints.gridy = 0;
        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridheight = GridBagConstraints.REMAINDER;
        constraints.weightx = 1.0;
        constraints.weighty = 1.0;        
        labelPanel.add(label, constraints); 

        final JPanel bugSpecificsPanel = new JPanel();
        bugSpecificsPanel.setLayout(new GridBagLayout());
        bugSpecificsPanel.setVisible(false);

        // the component with the bug stacktrace
        JTextArea showBug = new JTextArea(info.toBugReport());
        showBug.setColumns(50);
        showBug.setEditable(false);
        showBug.setCaretPosition(0);
        showBug.setLineWrap(true);
        showBug.setWrapStyleWord(true);
        JScrollPane showBugScroller = new JScrollPane(showBug);
        showBugScroller.setBorder(BorderFactory.createEtchedBorder());
        showBugScroller.setPreferredSize( new Dimension(500, 200) );

        constraints = new GridBagConstraints();
        constraints.gridx = 0;
        constraints.gridy = 0;
        bugSpecificsPanel.add(showBugScroller, constraints);

        final String defaultDesc = I18n.tr("What were you doing before WireShare ran into this problem?");
        final JTextArea userCommentsTextArea = new JTextArea(defaultDesc);
        userCommentsTextArea.setLineWrap(true);
        userCommentsTextArea.setWrapStyleWord(true);

        // When the user clicks anywhere in the text field, it highlights the whole text
        // so that user could just type over it without having to delete it manually
        userCommentsTextArea.addFocusListener(new FocusAdapter() {
             @Override
            public void focusGained(FocusEvent e) {
                 if(userCommentsTextArea.getText().equals(defaultDesc)) {
                    userCommentsTextArea.selectAll();
                 }
             }
        });
        JScrollPane userCommentsScrollPane = new JScrollPane(userCommentsTextArea);
        userCommentsScrollPane.setBorder(BorderFactory.createEtchedBorder());
        userCommentsScrollPane.setPreferredSize( new Dimension(500, 60) );

        final HyperlinkButton showHideBugLink = new HyperlinkButton(I18n.tr("Show Bug"));
        showHideBugLink.addActionListener(new AbstractAction() {
            boolean panelVisible = false;

            @Override
            public void actionPerformed(ActionEvent e) {
                if (panelVisible) {
                    bugSpecificsPanel.setVisible(false);
                    showHideBugLink.setText(I18n.tr("Show Bug"));
                    DIALOG.pack();
                } else {
                    bugSpecificsPanel.setVisible(true);
                    showHideBugLink.setText(I18n.tr("Hide Bug"));
                }
                DIALOG.pack();
                panelVisible = !panelVisible;
            }
        });

        HyperlinkButton helpLink =
            new HyperlinkButton(I18n.tr("Ask for help on the forums"));
        String url = "http://www.gnutellaforums.com/";
        helpLink.addActionListener(new UrlAction(url));

        constraints = new GridBagConstraints();
        constraints.gridx = 0;
        constraints.gridy = 0;
        constraints.gridwidth = 2;
        mainPanel.add(labelPanel, constraints);

        constraints = new GridBagConstraints();
        constraints.gridx = 0;
        constraints.gridy = 1;
        constraints.insets = new Insets(10, 0, 0, 0);
        constraints.anchor = GridBagConstraints.LINE_START;
        mainPanel.add(showHideBugLink, constraints);
        
        constraints = new GridBagConstraints();
        constraints.gridx = 1;
        constraints.gridy = 1;
        constraints.insets = new Insets(10, 0, 0, 0);
        constraints.anchor = GridBagConstraints.LINE_END;
        mainPanel.add(helpLink, constraints);

        constraints = new GridBagConstraints();
        constraints.gridx = 0;
        constraints.gridy = 2;
        constraints.gridwidth = 2;
        constraints.fill = GridBagConstraints.BOTH;
        constraints.weightx = 1.0;
        constraints.weighty = 1.0;
        constraints.insets = new Insets(30, 0, 6, 0);
        mainPanel.add(bugSpecificsPanel, constraints);
        
        mainPanel.validate();
        DIALOG.getContentPane().add(mainPanel);
        DIALOG.pack();
        DIALOG.setLocationRelativeTo(GuiUtils.getMainFrame());

        try {
            DIALOG.setVisible(true);
        } catch(InternalError ie) {
            //happens occasionally, ignore.
        } catch(ArrayIndexOutOfBoundsException npe) {
            //happens occasionally, ignore.
        }
    }   
}