/*******************************************************************************
 * Copyright (c) MOBAC developers
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
package mobac.utilities;

import java.awt.AWTEvent;
import java.awt.BorderLayout;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.io.PrintWriter;
import java.io.StringWriter;

import javax.swing.JCheckBox;
import javax.swing.JEditorPane;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.UIManager;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;

import mobac.program.Logging;
import mobac.program.ProgramInfo;
import mobac.program.interfaces.ExceptionExtendedInfo;

import org.apache.log4j.Logger;

import com.sleepycat.je.ExceptionEvent;
import com.sleepycat.je.ExceptionListener;

public class GUIExceptionHandler implements Thread.UncaughtExceptionHandler, ExceptionListener {

	private static final GUIExceptionHandler instance = new GUIExceptionHandler();

	private static final Logger log = Logger.getLogger(GUIExceptionHandler.class);

	private static final double MB_DIV = 1024d * 1024d;

	private static final String URL_BUGTRACKER = "http://sourceforge.net/p/mobac/bugs/";

	static {
		Thread.setDefaultUncaughtExceptionHandler(instance);
	}

	public static void registerForCurrentThread() {
		Thread t = Thread.currentThread();
		log.trace("Registering MOBAC exception handler for thread \"" + t.getName() + "\" [" + t.getId() + "]");
		t.setUncaughtExceptionHandler(instance);
	}

	public static GUIExceptionHandler getInstance() {
		return instance;
	}

	private GUIExceptionHandler() {
		super();
	}

	/**
	 * Implementation for {@link com.sleepycat.je.ExceptionListener}
	 */
	public void exceptionThrown(ExceptionEvent paramExceptionEvent) {
		Exception e = paramExceptionEvent.getException();
		log.error("Exception in tile store: " + paramExceptionEvent.toString(), e);
		showExceptionDialog(e);
	}

	/**
	 * Implementation for {@link Thread.UncaughtExceptionHandler}
	 */
	public void uncaughtException(Thread t, Throwable e) {
		processException(t, e);
	}

	public static void processException(Throwable e) {
		processException(Thread.currentThread(), e);
	}

	public static void processFatalExceptionSimpleDialog(String dialogMessage, Throwable e) {
		log.error(dialogMessage + ": " + e.getMessage(), e);
		String[] options = { I18nUtils.localizedStringForKey("Exit"),
				I18nUtils.localizedStringForKey("dlg_download_show_error_report") };
		int a = JOptionPane.showOptionDialog(null, dialogMessage, I18nUtils.localizedStringForKey("Error"), 0,
				JOptionPane.ERROR_MESSAGE, null, options, options[0]);
		if (a == 1)
			GUIExceptionHandler.showExceptionDialog(e);
		System.exit(1);
	}

	public static void processException(Thread thread, Throwable t) {
		log.error("Uncaught exception: " + t, t);
		showExceptionDialog(thread, t, null);
	}

	public static void processException(Thread thread, Throwable t, AWTEvent newEvent) {
		String eventText = newEvent.toString();
		log.error("Uncaught exception on processing event " + eventText, t);
		if (eventText.length() > 100) {
			String[] parts = eventText.split(",");
			StringWriter sw = new StringWriter(eventText.length() + 20);
			sw.write(parts[0]);
			int len = parts[0].length();
			for (int i = 1; i < parts.length; i++) {
				String s = parts[i];
				if (s.length() + len > 80) {
					sw.write("\n\t");
					len = 0;
				}
				sw.write(s);
			}
			eventText = "Event: " + sw.toString();
		}
		showExceptionDialog(thread, t, eventText);
	}

	public static String prop(String key) {
		String s = System.getProperty(key);
		if (s != null)
			return s;
		else
			return "";
	}

	public static void showExceptionDialog(Throwable t) {
		showExceptionDialog(null, Thread.currentThread(), t, null);
	}

	public static void showExceptionDialog(String message, Throwable t) {
		showExceptionDialog(message, Thread.currentThread(), t, null);
	}

	public static void showExceptionDialog(Thread thread, Throwable t, String additionalInfo) {
		showExceptionDialog(null, thread, t, additionalInfo);
	}

	public static synchronized void showExceptionDialog(String message, Thread thread, Throwable t,
			String additionalInfo) {
		try {
			if (t != null) {
				// Recursive check - in case the Exception was thrown in a Component.paint() method
				// this may lead to an infinite recursion with one message dialog per recursion loop
				String thisClassName = GUIExceptionHandler.class.getName();
				for (StackTraceElement ste : t.getStackTrace()) {
					if (ste.getClassName().startsWith(thisClassName)
							&& "showExceptionDialog".equals(ste.getMethodName())) {
						log.error("Recursive error loop detected - aborting");
						return;
					}
				}
			}

			StringBuilder sb = new StringBuilder(2048);
			if (message != null)
				sb.append("Error message: " + message + "\n");
			sb.append("Version: " + ProgramInfo.getCompleteTitle());
			sb.append("\nPlatform: " + prop("os.name") + " (" + prop("os.version") + ")");
			String windowManager = System.getProperty("sun.desktop");
			if (windowManager != null)
				sb.append(" (" + windowManager + ")");

			String dist = OSUtilities.getLinuxDistributionName();
			if (dist != null)
				sb.append("\nDistribution name: " + dist);

			sb.append("\nJava VM: " + prop("java.vm.name") + " (" + prop("java.runtime.version") + ")");
			Runtime r = Runtime.getRuntime();
			sb.append(String.format("\nMax heap size: %3.2f MiB", r.maxMemory() / MB_DIV));
			// sb.append("\nMapsources rev: " + MapSourcesUpdater.getCurrentMapSourcesRev());

			sb.append("\nCPU cores: " + Runtime.getRuntime().availableProcessors());

			if (thread != null)
				sb.append("\n\nThread: " + thread.getName());

			if (additionalInfo != null)
				sb.append("\n\n" + additionalInfo);

			if (t instanceof ExceptionExtendedInfo) {
				ExceptionExtendedInfo ei = (ExceptionExtendedInfo) t;
				sb.append("\n");
				sb.append(ei.getExtendedInfo());
			}

			String guiText;
			String dialogTitle;
			JPanel panel = new JPanel(new BorderLayout());
			if (t != null) {
				sb.append("\n\nError hierarchy:");
				Throwable tmp = t;
				while (tmp != null) {
					sb.append("\n  " + tmp.getClass().getSimpleName() + ": " + tmp.getMessage());
					tmp = tmp.getCause();
				}

				String exceptionName = t.getClass().getSimpleName();
				dialogTitle = "Unexpected Exception: " + exceptionName;
				StringWriter stack = new StringWriter();
				t.printStackTrace(new PrintWriter(stack));
				sb.append("\n\n#############################################################\n\n");
				sb.append(stack.getBuffer().toString());
				sb.append("\n#############################################################");

				guiText = "An unexpected exception occurred (" + exceptionName + ")<br>"
						+ "<p>Please create a ticket in the bug tracker " + "on <a href=\"" + URL_BUGTRACKER
						+ "\">SourceForge.net</a><br>"
						+ "<b>Please include a detailed description of your performed actions <br>"
						+ "before the error occurred.</b></p>Be sure to include the following information:";
			} else {
				dialogTitle = "System report";
				guiText = "This is a user generated system report.<br>"
						+ "<p>It can be used for providing detailed version information <br>"
						+ "in case a new ticket in the bug tracker is created " + "on <a href=\"" + URL_BUGTRACKER
						+ "\">SourceForge.net</a><br>"
						+ "<b>Please include a detailed description of your problem <br>"
						+ "and what steps are necessary to reproduce the problem.</b>" + "</p>";
			}
			JEditorPane text = new JEditorPane("text/html", "");
			text.setOpaque(true);
			text.setBackground(UIManager.getColor("JFrame.background"));
			text.setEditable(false);
			text.addHyperlinkListener(new HyperlinkListener() {

				public void hyperlinkUpdate(HyperlinkEvent e) {
					if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED)
						return;
					try {
						Desktop.getDesktop().browse(e.getURL().toURI());
					} catch (Exception e1) {
						log.error("", e1);
					}
				}
			});
			panel.add(text, BorderLayout.NORTH);
			try {
				StringSelection contents = new StringSelection(sb.toString());
				ClipboardOwner owner = new ClipboardOwner() {
					public void lostOwnership(Clipboard clipboard, Transferable contents) {
					}
				};
				Toolkit.getDefaultToolkit().getSystemClipboard().setContents(contents, owner);
				guiText += "<p>(The following text has already been copied to your clipboard.)</p>";
			} catch (RuntimeException x) {
				log.error("", x);
			}
			text.setText("<html>" + guiText + "</html>");

			JCheckBox quickMOBACcb = new JCheckBox("Do not continue and quit program by pressing OK");
			JTextArea info = new JTextArea(sb.toString(), 20, 60);
			info.setCaretPosition(0);
			info.setEditable(false);
			info.setMinimumSize(new Dimension(200, 150));
			panel.add(new JScrollPane(info), BorderLayout.CENTER);
			panel.add(quickMOBACcb, BorderLayout.SOUTH);
			panel.setMinimumSize(new Dimension(700, 300));
			panel.validate();
			JOptionPane.showMessageDialog(null, panel, dialogTitle, JOptionPane.ERROR_MESSAGE);
			if (quickMOBACcb.isSelected()) {
				log.warn("User selected to quit MOBAC after an exception");
				System.exit(1);
			}
		} catch (Exception e1) {
			e1.printStackTrace();
		}
	}

	public static void installToolkitEventQueueProxy() {

		boolean isLikeJava7 = Float.parseFloat(System.getProperty("java.specification.version")) > 1.6;

		EventQueueProxy eventQueueProxy = new EventQueueProxy();
		// Bug handling. See
		// http://stackoverflow.com/questions/3158254/how-to-replace-the-awt-eventqueue-with-own-implementation
		try {
			if (isLikeJava7)
				EventQueue.invokeAndWait(eventQueueProxy);
			else
				eventQueueProxy.run();
		} catch (Exception e) {
			log.error("", e);
		}
	}

	/**
	 * Catching all Runtime Exceptions in Swing
	 * 
	 * http://ruben42.wordpress.com/2009/03/30/catching-all-runtime-exceptions-in-swing/
	 */
	protected static class EventQueueProxy extends EventQueue implements Runnable {

		/**
		 * Installs the {@link EventQueueProxy}
		 */
		@Override
		public void run() {
			EventQueue queue = Toolkit.getDefaultToolkit().getSystemEventQueue();
			queue.push(this);
		}

		protected void dispatchEvent(AWTEvent newEvent) {
			try {
				super.dispatchEvent(newEvent);
			} catch (Throwable e) {
				if (e instanceof ArrayIndexOutOfBoundsException) {
					StackTraceElement[] st = e.getStackTrace();
					if (st.length > 0) {
						if ("sun.font.FontDesignMetrics".equals(st[0].getClassName())) {
							log.error("Ignored JRE bug exception " + e.getMessage() + " caused by : " + st[0]);
							// This is a known JRE bug - we just ignore it
							return;
						}
					}
				}
				GUIExceptionHandler.processException(Thread.currentThread(), e);
			}
		}
	}

	public static void main(String[] args) {
		for (;;) {
			try {
				Logging.configureConsoleLogging();
				UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
				throw new RuntimeException("Test", new Exception("Inner"));
			} catch (Exception e) {
				showExceptionDialog("Test 123", e);
			} catch (Error e) {
				showExceptionDialog(e);
			}
			break;
		}
	}

}
