/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy 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 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy 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 Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.system;

import icy.gui.dialog.MessageDialog;
import icy.gui.frame.progress.FailedAnnounceFrame;
import icy.gui.plugin.PluginErrorReport;
import icy.main.Icy;
import icy.math.UnitUtil;
import icy.network.NetworkUtil;
import icy.plugin.PluginDescriptor;
import icy.plugin.PluginDescriptor.PluginIdent;
import icy.plugin.PluginLauncher;
import icy.plugin.PluginLoader;
import icy.plugin.interface_.PluginBundled;
import icy.util.ClassUtil;
import icy.util.StringUtil;

import java.io.File;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author Stephane
 */
public class IcyExceptionHandler implements UncaughtExceptionHandler
{
    private static final double ERROR_ANTISPAM_TIME = 15 * 1000;
    private static IcyExceptionHandler exceptionHandler = new IcyExceptionHandler();
    private static long lastErrorDialog = 0;
    private static long lastReport = 0;
    private static Set<String> reportedPlugin = new HashSet<String>();

    public static void init()
    {
        Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
    }

    /**
     * Display the specified Throwable message in error output.
     */
    public static void showErrorMessage(Throwable t, boolean printStackTrace)
    {
        showErrorMessage(t, printStackTrace, true);
    }

    /**
     * Display the specified Throwable message in console.<br>
     * If <i>error</i> is true the message is considerer as an error and then written in error
     * output.
     */
    public static void showErrorMessage(Throwable t, boolean printStackTrace, boolean error)
    {
        final String mess = getErrorMessage(t, printStackTrace);

        if (!StringUtil.isEmpty(mess))
        {
            if (error)
                System.err.println(mess);
            else
                System.out.println(mess);
        }
    }

    /**
     * Returns the formatted error message for the specified {@link Throwable}.<br>
     * If <i>printStackTrace</i> is <code>true</code> the stack trace is also returned in the
     * message.
     */
    public static String getErrorMessage(Throwable t, boolean printStackTrace)
    {
        String result = "";
        Throwable throwable = t;

        while (throwable != null)
        {
            result += throwable.toString() + "\n";

            if (printStackTrace)
            {
                try
                {
                    // sometime 'getStackTrace()' throws a weird AbstractMethodError exception
                    for (StackTraceElement element : throwable.getStackTrace())
                        result += "\tat " + element.toString() + "\n";
                }
                catch (Throwable t2)
                {
                    result += "Error while trying to get exception stack trace...\n";
                }
            }

            throwable = throwable.getCause();
            if (throwable != null)
                result += "Caused by :\n";
        }

        return result;
    }

    @Override
    public void uncaughtException(Thread t, Throwable e)
    {
        handleException(t, e, true);
    }

    /**
     * Handle the specified exception.<br>
     * It actually display a message or report dialog depending the exception type.
     */
    private static void handleException(Thread thread, PluginDescriptor plugin, String devId, Throwable t,
            boolean printStackStrace)
    {
        final long current = System.currentTimeMillis();
        final String errMess = (t.getMessage() != null) ? t.getMessage() : "";

        if (t instanceof IcyHandledException)
        {
            final String message = errMess + ((t.getCause() == null) ? "" : "\n" + t.getCause());

            // handle HandledException differently
            MessageDialog.showDialog(message, MessageDialog.ERROR_MESSAGE);
            // update last error dialog time
            lastErrorDialog = System.currentTimeMillis();

            // don't need the antispam for the IcyHandledException
            // if ((current - lastErrorDialog) > ERROR_ANTISPAM_TIME)
            // {
            // // handle HandledException differently
            // MessageDialog.showDialog(message, MessageDialog.ERROR_MESSAGE);
            // // update last error dialog time
            // lastErrorDialog = System.currentTimeMillis();
            // }
            // else
            // // spam --> write it in the console output instead
            // System.err.println(message + " (spam protection)");
        }
        else
        {
            String message = "";

            if (t instanceof OutOfMemoryError)
            {
                if (errMess.contains("Thread"))
                {
                    message = "Out of resource error: cannot create new thread.\n"
                            + "You should report this error as something goes wrong here !";
                }
                else
                {
                    message = "The task could not be completed because there is not enough memory !\n"
                            + "Try to increase the Maximum Memory parameter in Preferences.";
                }
            }

            if (!StringUtil.isEmpty(message))
                message += "\n";
            message += getErrorMessage(t, printStackStrace);

            // write message in console if wanted or if spam error message
            if ((t instanceof OutOfMemoryError) || printStackStrace
                    || ((current - lastErrorDialog) < ERROR_ANTISPAM_TIME))
            {
                if (plugin != null)
                    System.err.println("An error occured while plugin '" + plugin.getName() + "' was running :");
                else if (!StringUtil.isEmpty(devId))
                    System.err.println("An error occured while a plugin was running :");

                System.err.println(message);
            }

            // do report (anti spam protected)
            if ((current - lastErrorDialog) > ERROR_ANTISPAM_TIME)
            {
                final String title = t.toString();

                // handle the specific "not enough memory" differently
                if ((t instanceof OutOfMemoryError) && (!errMess.contains("Thread")))
                {
                    if (!Icy.getMainInterface().isHeadLess())
                        new FailedAnnounceFrame(
                                "Not enough memory to complete the process ! Try to increase the 'Max Memory' parameter in Preferences.",
                                30);
                }
                else
                    // just report the error
                    PluginErrorReport.report(plugin, devId, title, message);

                // update last error dialog time
                lastErrorDialog = System.currentTimeMillis();
            }
        }
    }

    /**
     * Handle the specified exception.<br>
     * It actually display a message or report dialog depending the exception type.
     */
    public static void handleException(PluginDescriptor pluginDesc, Throwable t, boolean printStackStrace)
    {
        handleException(null, pluginDesc, null, t, printStackStrace);
    }

    /**
     * Handle the specified exception.<br>
     * It actually display a message or report dialog depending the exception type.
     */
    public static void handleException(String devId, Throwable t, boolean printStackStrace)
    {
        handleException(null, null, devId, t, printStackStrace);
    }

    /**
     * Handle the specified exception.<br>
     * Try to find the origin plugin which thrown the exception.
     * It actually display a message or report dialog depending the exception type.
     */
    public static void handleException(Throwable t, boolean printStackStrace)
    {
        handleException((Thread) null, t, printStackStrace);
    }

    /**
     * Handle the specified exception.<br>
     * Try to find the origin plugin which thrown the exception.
     * It actually display a message or report dialog depending the exception type.
     */
    private static void handleException(Thread thread, Throwable t, boolean printStackStrace)
    {
        Throwable throwable = t;
        final List<PluginDescriptor> plugins = PluginLoader.getPlugins();

        while (throwable != null)
        {
            StackTraceElement[] stackTrace;

            try
            {
                // sometime 'getStackTrace()' throws a weird AbstractMethodError exception
                stackTrace = throwable.getStackTrace();
            }
            catch (Throwable t2)
            {
                stackTrace = new StackTraceElement[0];
            }

            // search plugin class (start from the end of stack trace)
            final PluginDescriptor plugin = findPluginFromStackTrace(plugins, stackTrace);

            // plugin found --> show the plugin report frame
            if (plugin != null)
            {
                // only send to last plugin raising the exception
                handleException(thread, plugin, null, t, printStackStrace);
                return;
            }

            // we did not find plugin class so we will search for plugin developer id
            final String devId = findDevIdFromStackTrace(stackTrace);

            if (devId != null)
            {
                handleException(thread, null, devId, t, printStackStrace);
                return;
            }

            throwable = throwable.getCause();
        }

        // general exception (no plugin information found)
        handleException(thread, null, null, t, printStackStrace);
    }

    /**
     * @deprecated Use {@link #handleException(PluginDescriptor, Throwable, boolean)} instead.
     */
    @Deprecated
    public static void handlePluginException(PluginDescriptor pluginDesc, Throwable t, boolean printStackStrace)
    {
        handleException(pluginDesc, t, printStackStrace);
    }

    private static PluginDescriptor findMatchingLocalPlugin(List<PluginDescriptor> plugins, String text)
    {
        String className = ClassUtil.getBaseClassName(text);

        // get the JAR file of this class
        final File file = ClassUtil.getFile(className);

        // found ?
        if (file != null)
        {
            // try to find plugin using the same JAR file (so
            for (PluginDescriptor p : plugins)
            {
                final String jarFileName = p.getJarFilename();

                if (!StringUtil.isEmpty(jarFileName))
                {
                    final File jarFile = new File(jarFileName);

                    // matching jar file --> return plugin
                    if (StringUtil.equals(file.getAbsolutePath(), jarFile.getAbsolutePath()))
                        return p;
                }
            }
        }

        // not found with first method so now we try on the class name
        while (!(StringUtil.equals(className, PluginLoader.PLUGIN_PACKAGE) || StringUtil.isEmpty(className)))
        {
            final PluginDescriptor plugin = findMatchingLocalPluginInternal(plugins, className);

            if (plugin != null)
                return plugin;

            // not found --> we test with parent package
            className = ClassUtil.getPackageName(className);
        }

        return null;
    }

    private static PluginDescriptor findMatchingLocalPluginInternal(List<PluginDescriptor> plugins, String text)
    {
        PluginDescriptor result = null;

        for (PluginDescriptor plugin : plugins)
        {
            if (plugin.getClassName().startsWith(text))
            {
                if (result != null)
                    return null;

                result = plugin;
            }
        }

        return result;
    }

    private static PluginDescriptor findPluginFromStackTrace(List<PluginDescriptor> plugins, StackTraceElement[] st)
    {
        for (StackTraceElement trace : st)
        {
            final String className = trace.getClassName();

            // plugin class ?
            if (className.startsWith(PluginLoader.PLUGIN_PACKAGE + "."))
            {
                // try to find a matching plugin
                final PluginDescriptor plugin = findMatchingLocalPlugin(plugins, className);

                // plugin found --> show the plugin report frame
                if (plugin != null)
                    return plugin;
            }
        }

        return null;
    }

    private static String findDevIdFromStackTrace(StackTraceElement[] st)
    {
        // we did not find plugin class so we will search for plugin developer id
        for (StackTraceElement trace : st)
        {
            final String className = trace.getClassName();

            // plugin class ?
            if (className.startsWith(PluginLoader.PLUGIN_PACKAGE + "."))
                // use plugin developer id (only send to last plugin raising the exception)
                return className.split("\\.")[1];
        }

        return null;
    }

    /**
     * Report an error log from a given plugin or developer id to Icy web site.
     * 
     * @param plugin
     *        The plugin responsible of the error or <code>null</code> if the error comes from the
     *        application or if we are not able to get the plugin descriptor.
     * @param devId
     *        The developer id of the plugin responsible of the error when the plugin descriptor was
     *        not found or <code>null</code> if the error comes from the application.
     * @param errorLog
     *        Error log to report.
     */
    public static void report(PluginDescriptor plugin, String devId, String errorLog)
    {
        final long current = System.currentTimeMillis();

        // avoid report spam
        if ((current - lastReport) < ERROR_ANTISPAM_TIME)
            return;

        final String icyId;
        final String javaId;
        final String osId;
        final String memory;
        String pluginId;
        String pluginDepsId;
        final Map<String, String> values = new HashMap<String, String>();

        values.put(NetworkUtil.ID_KERNELVERSION, Icy.version.toString());
        values.put(NetworkUtil.ID_JAVANAME, SystemUtil.getJavaName());
        values.put(NetworkUtil.ID_JAVAVERSION, SystemUtil.getJavaVersion());
        values.put(NetworkUtil.ID_JAVABITS, Integer.toString(SystemUtil.getJavaArchDataModel()));
        values.put(NetworkUtil.ID_OSNAME, SystemUtil.getOSName());
        values.put(NetworkUtil.ID_OSVERSION, SystemUtil.getOSVersion());
        values.put(NetworkUtil.ID_OSARCH, SystemUtil.getOSArch());

        icyId = "Icy Version " + Icy.version + "\n";
        javaId = SystemUtil.getJavaName() + " " + SystemUtil.getJavaVersion() + " ("
                + SystemUtil.getJavaArchDataModel() + " bit)\n";
        osId = "Running on " + SystemUtil.getOSName() + " " + SystemUtil.getOSVersion() + " (" + SystemUtil.getOSArch()
                + ")\n";
        memory = "Max java memory : " + UnitUtil.getBytesString(SystemUtil.getJavaMaxMemory()) + "\n";

        if (plugin != null)
        {
            final String className = plugin.getClassName();

            // we already reported error for this plugin --> avoid spaming
            if (reportedPlugin.contains(className))
                return;

            reportedPlugin.add(className);

            values.put(NetworkUtil.ID_PLUGINCLASSNAME, className);
            values.put(NetworkUtil.ID_PLUGINVERSION, plugin.getVersion().toString());
            pluginId = "Plugin " + plugin.toString();

            // determine origin plugin
            PluginDescriptor originPlugin = plugin;

            // bundled plugin ?
            if (plugin.isBundled())
            {
                try
                {
                    // get original plugin
                    originPlugin = PluginLoader.getPlugin(((PluginBundled) PluginLauncher.create(plugin))
                            .getMainPluginClassName());
                    // add bundle info
                    pluginId = "Bundled in " + originPlugin.toString();
                }
                catch (Throwable t)
                {
                    // miss bundle info
                    pluginId = "Bundled plugin (could not retrieve origin plugin)";
                }
            }

            pluginId += "\n\n";

            if (originPlugin.getRequired().size() > 0)
            {
                pluginDepsId = "Dependances:\n";
                for (PluginIdent ident : originPlugin.getRequired())
                {
                    final PluginDescriptor installed = PluginLoader.getPlugin(ident.getClassName());

                    if (installed == null)
                        pluginDepsId += "Class " + ident.getClassName() + " not found !\n";
                    else
                        pluginDepsId += "Plugin " + installed.toString() + " is correctly installed\n";
                }
                pluginDepsId += "\n";
            }
            else
                pluginDepsId = "";
        }
        else
        {
            values.put(NetworkUtil.ID_PLUGINCLASSNAME, "");
            values.put(NetworkUtil.ID_PLUGINVERSION, "");
            pluginId = "";
            pluginDepsId = "";
        }

        if (StringUtil.isEmpty(devId))
            values.put(NetworkUtil.ID_DEVELOPERID, devId);
        else
            values.put(NetworkUtil.ID_DEVELOPERID, "");

        values.put(NetworkUtil.ID_ERRORLOG, icyId + javaId + osId + memory + "\n" + pluginId + pluginDepsId + errorLog);

        // send report
        lastReport = current;
        NetworkUtil.report(values);
    }

    /**
     * Report an error log from a given plugin to Icy web site.
     * 
     * @param plugin
     *        The plugin responsible of the error or <code>null</code> if the error comes from the
     *        application.
     * @param errorLog
     *        Error log to report.
     */
    public static void report(PluginDescriptor plugin, String errorLog)
    {
        report(plugin, null, errorLog);
    }

    /**
     * Report an error log from the application to Icy web site.
     * 
     * @param errorLog
     *        Error log to report.
     */
    public static void report(String errorLog)
    {
        report(null, null, errorLog);
    }

    /**
     * Report an error log from a given plugin developer id to the Icy web site.
     * 
     * @param devId
     *        The developer id of the plugin responsible of the error or <code>null</code> if the
     *        error comes from the application.
     * @param errorLog
     *        Error log to report.
     */
    public static void report(String devId, String errorLog)
    {
        report(null, devId, errorLog);
    }

}