/*
* Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of the License "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description: 
*
*/
package com.nokia.cpp.internal.api.utils.core;

import org.eclipse.core.runtime.*;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.osgi.framework.Bundle;

import com.nokia.cpp.utils.core.noexport.Messages;
import com.nokia.cpp.utils.core.noexport.UtilsCorePlugin;

import java.io.PrintStream;
import java.util.*;

/**
 * Utilities for plugins.
 * 
 * All of these take a Plugin parameter, which usually requires a call in the
 * form:
 * <p>
 * <code>
 * Logging.<i>method</i>(MyPlugin.getDefault(), ...);
 * </code>
 * </p>
 * 
 * If this becomes unwieldly, implement a wrapper method in your plugin class to
 * access these methods natively:
 * <p>
 * 
 * <pre>
 * 
 *  class MyPlugin extends Plugin {
 *  ...
 *    void &lt;i&gt;method&lt;/i&gt;(...) {
 *       Logging.&lt;i&gt;method&lt;/i&gt;(getDefault(), ...);
 *    }
 *  ...
 *  }
 *  
 * </pre>
 * 
 * </p>
 * <p>
 * Then call as:
 * 
 * <pre>
 * 
 *  MyPlugin.&lt;i&gt;method&lt;/i&gt;(...); 
 *  
 * </pre>
 * 
 * </p>
 * 
 */
public class Logging {

    /** Tell whether Logging.timeStart() / Logging.timeEnd() / Logging.timeTask() show results
     *  Can't be final because it's modified from unit tests. */
    static public boolean SHOW_TIMINGS = false;
    
    /** List of ILogListener */
    static private List<ILogListener> listeners = new ArrayList<ILogListener>(0);
    
    /** Target for log messages when platform is not running
        This can't be final, it's set from tests */
    static public PrintStream consoleLog = System.err;
    /** Always dump log messages to console?  If false, only log when platform not running.
     * Can't be final because it's modified from unit tests. */
    static public boolean alwaysLogToConsole = false;
    
    static public void addListener(ILogListener listener) {
        listeners.add(listener);
    }
    
    static public void removeListener(ILogListener listener) {
        listeners.remove(listener);
    }
    
    /**
     * Create an IStatus instance eminating from the given plugin
     * which includes detailed info about the plugin vendor, version, etc.
     * Every IStatus generated by this class includes this information.
     * 
     * Adapted from Java Developer's Guide to Eclipse, Ch. 28
     * 
     * @param plugin
     *            the offending plugin
     * @param severity
     *            the severity (IStatus.ERROR, IStatus.WARNING, IStatus.INFO)
     * @param message
     *            the localized error string, or null to use
     *            thr.getLocalizedMessage()
     * @param thr
     *            the throwable, or null
     * @return new IStatus object
     */
    static public IStatus newStatus(Plugin plugin, int severity,
            String message, Throwable thr) {
        if (message == null) {
			if (thr != null) {
				message = thr.getLocalizedMessage();
			}
			if (message == null) {
				message = thr.getClass().getName();
			}
        }
            
        MultiStatus vitalInfoStatus;
        
        if (plugin == null || plugin.getBundle() == null) {
            return new Status(severity, "???", 0,
                    message, thr);
        }
        
        Bundle bundle = plugin.getBundle();
        String symbolicName = bundle.getSymbolicName();
        String bundleName = "" + bundle.getHeaders().get("Bundle-Name"); //$NON-NLS-1$
        String bundleVendor = "" + bundle.getHeaders().get("Bundle-Vendor"); //$NON-NLS-1$
        String bundleVersion = "" + bundle.getHeaders().get("Bundle-Version"); //$NON-NLS-1$

        vitalInfoStatus = new MultiStatus(symbolicName, severity,
                message, thr);

        // Put new info in separate status containers to force newlines in
        // dialog
        vitalInfoStatus.add(new Status(severity, symbolicName, 0, 
                "Plug-in Vendor: " + bundleVendor, null)); //$NON-NLS-1$
        vitalInfoStatus.add(new Status(severity, symbolicName, 0,
                "Plug-in Name: " + bundleName, null)); //$NON-NLS-1$
        vitalInfoStatus.add(new Status(severity, symbolicName, 0,
                "Plug-in ID: "  + symbolicName, null)); //$NON-NLS-1$
        vitalInfoStatus.add(new Status(severity, symbolicName, 0,
                "Plug-in Version: " + bundleVersion, null)); //$NON-NLS-1$

        return vitalInfoStatus;
    }

    /**
     * Create an IStatus instance eminating from the given plugin.
     * 
     * @param plugin
     *            the offending plugin
     * @param severity
     *            the severity (IStatus.ERROR,WARNING,INFO)
     * @param message
     *            the localized error string
     * @return new IStatus object
     */
    static public IStatus newStatus(Plugin plugin, int severity, String message) {
        if (message == null)
            message = ""; //$NON-NLS-1$

        return newStatus(plugin, severity, message, null);
    }

    
    /**
     * Create an IStatus instance wrapping an unwanted exception eminating from
     * the given plugin.
     * 
     * @param plugin
     *            the offending plugin
     * @param thr
     *            the throwable, or null
     * @return new IStatus object
     */
    static public IStatus newStatus(Plugin plugin, Throwable thr) {
        return newStatus(plugin, IStatus.ERROR, thr.getLocalizedMessage(), thr);
    }

    /**
     * Create an IStatus instance without plugin context information
     * (e.g. for logging) 
     * 
     * @param plugin
     *            the plugin
     * @param severity
     *            the severity (IStatus.ERROR,WARNING,INFO)
     * @param message
     *            the localized error string
     * @param thr
     *            the throwable, or null
     * 
     * @return new IStatus object
     */
    static public IStatus newSimpleStatus(Plugin plugin, int severity, String message, Throwable thr) {
        return new Status(severity, plugin.getBundle().getSymbolicName(), 
                0, message, thr);
    }

    /**
     * Create an IStatus instance occurring presumably a number of steps up the
     * call chain (e.g. library code, for which the current plugin is not
     * known). We report the class name from the stack trace stored in the
     * exception.  (Eclipse doesn't have a mechanism for looking up the plugin
     * from a class (multiple plugins can have the same class).)
     * 
     * @param depth
     *            stack depth relative to thr(0 = caller that created thr, 1 =
     *            caller's caller...)
     * @param severity
     *            the severity (IStatus.ERROR,WARNING,INFO)
     * @param message
     *            the localized error string
     * @param thr
     *            the throwable (if null, assume depth refers to caller)
     * 
     * @return new IStatus object
     */
    static public IStatus newSimpleStatus(int depth, int severity, String message, Throwable thr) {
        StackTraceElement els[];
        if (thr != null)
            els = thr.getStackTrace();
        else {
            els = new Exception().getStackTrace();
            depth++;
        }
        
        if (depth >= els.length)
            depth = els.length - 1;

        String klass = els[depth].getClassName();
        return new Status(severity, klass, 0, message, thr);
    }

    /**
     * Create an IStatus instance occurring presumably a number of steps up the
     * call chain (e.g. library code, for which the current plugin is not
     * known). We report the class name from the stack trace stored in the
     * exception.  (Eclipse doesn't have a mechanism for looking up the plugin
     * from a class (multiple plugins can have the same class).)
     * 
     * @param depth
     *            stack depth relative to thr(0 = caller that created thr, 1 =
     *            caller's caller...)
     * @param thr
     *            the throwable, or null
     * 
     * @return new IStatus object
     */
    static public IStatus newSimpleStatus(int depth, Throwable thr) {
        return newSimpleStatus(depth, IStatus.ERROR, thr.getLocalizedMessage(), thr);
    }

    /**
     * Tell if plugin configured for debugging and a debug option is enabled
     * 
     * @param plugin
     *            the offending plugin
     * @param option
     *            The debug option to test. The plugin's symbolic name is
     *            prepended to the option name, e.g.
     *            "com.nokia.myplugin"+"/"+option
     * @return true if debugging enabled and option enabled (e.g. defined and
     *         set to "true")
     */
    static public boolean isDebugOptionEnabled(Plugin plugin, String option) {
        if (plugin == null || !plugin.isDebugging())
            return false;
        String filter = Platform.getDebugOption(plugin.getBundle()
                .getSymbolicName()
                + "/" + option); //$NON-NLS-1$
        return (filter != null && "true".equalsIgnoreCase(filter)); //$NON-NLS-1$
    }

    /**
     * Send a message to the Error Log or stdout if plugin
     * not defined or not loaded.
     * 
     * @param plugin
     *            the calling plugin
     * @param status
     *            the status object
     *            
     */
    static public void log(Plugin plugin, IStatus status) {

        for (Iterator<ILogListener> iter = listeners.iterator(); iter.hasNext();) {
            ILogListener listener = (ILogListener) iter.next();
            listener.logging(status, plugin != null ? plugin.getBundle().getSymbolicName() : null);
        } 
        
        boolean logToConsole = alwaysLogToConsole;
        if (plugin != null && plugin.getLog() != null) {
            plugin.getLog().log(status);
        } else {
            logToConsole = true;
        }
        
        if (consoleLog != null && logToConsole) {
            consoleLog.println(status.getMessage());
            if (status.getPlugin() != null)
                consoleLog.println(Messages.getString("Logging.FromPlugin")+status.getPlugin()); //$NON-NLS-1$
            if (status.getException() != null) {
                consoleLog.println(Messages.getString("Logging.DueToException")); //$NON-NLS-1$
                status.getException().printStackTrace();
            }
        }
    }

    /**
     * Send a message to the Error Log or stdout 
     * if a given debug option is enabled.
     * 
     * @param plugin
     *            the calling plugin, or null
     * @param option
     *            the debug option to test. The plugin name is prepended to the
     *            option name, e.g. "com.nokia.myplugin"+"/"+option
     * @param status
     *            the status object
     * @see Logging#isDebugOptionEnabled(Plugin, String)
     */
    static public void logIf(Plugin plugin, String option, IStatus status) {
        if (!isDebugOptionEnabled(plugin, option))
            return;
        log(plugin, status);
    }

    /**
     * Utility to display an error dialog reporting an IStatus.
     * 
     * @param title
     *          title of dialog (if null, use "Error")
     * @param message
     *            concise error message (if null, use status message or status'
     *            wrapped exception message)
     * @param status
     *            the offending status
     */
    static public void showErrorDialog(String title, String message, final IStatus status) {
    	showErrorDialog(null, title, message, status);
    }
    
    /**
     * Utility to display an error dialog reporting an IStatus.
     * 
     * @param shell the parent shell
     * @param title
     *          title of dialog (if null, use "Error")
     * @param message
     *            concise error message (if null, use status message or status'
     *            wrapped exception message)
     * @param status
     *            the offending status
     */
    static public void showErrorDialog(final Shell shell, String title, String message, final IStatus status) {
    	if (!Platform.isRunning())
    		return;
    	
        if (title == null)
            title = Messages.getString("Logging.ErrorTitle"); //$NON-NLS-1$
        
        if (message == null) {
            // this leads to a lot of redundant text
            /*if (status != null) {
                message = status.getMessage();
                if (message == null) {
                    if (status.getException() != null) {
                        message = status.getException().getLocalizedMessage();
                        if (message == null)
                            message = Messages.getString("Logging.NoMessage"); //$NON-NLS-1$
                    }
                }
            }*/
            message = Messages.getString("Logging.GenericErrorMessage"); //$NON-NLS-1$
        }

        final String title_ = title;
        final String message_ = message;
        
        // yes, syncExec(): this is presumably an important error
        // that shouldn't wait for cascading failures to complete
        Display.getDefault().syncExec(new Runnable() {
            public void run() {
                ErrorDialog.openError(shell, title_, message_, status);
            }
        });
    }
    // stack of start times (n.b.: Stack is a Vector and is synchronized)
    private static Stack<Long> timeStack = new Stack<Long>();
    // stack of end times (n.b.: Stack is a Vector and is synchronized)
    private static Stack<String> timeLabelStack = new Stack<String>();
    
    private static long lastTime;
    
    /**
     * Start timing a task, reported to console if Logging#SHOW_TIMINGS is set.
     * Up to 16 timed tasks can be nested at time.
     * @param label
     */
    public synchronized static void timeStart(String label) {
    	long currentTime = System.currentTimeMillis(); 
        if (SHOW_TIMINGS) {
        	if (currentTime > lastTime + 333 && lastTime != 0) {
                IStatus status = newSimpleStatus(1, IStatus.INFO, "missing time: " + (currentTime - lastTime) + " ms", null); //$NON-NLS-1$
                log(UtilsCorePlugin.getDefault(), status);
        	}
            IStatus status = newSimpleStatus(1, IStatus.INFO, label + " start...", null); //$NON-NLS-1$
            log(UtilsCorePlugin.getDefault(), status);
        }
        timeLabelStack.push(label);
        timeStack.push(currentTime);
    	lastTime = currentTime;
    }

    /**
     * Stop timing a task, reporting results to console if Logging#SHOW_TIMINGS is set
     * @return time elapsed in ms
     */
    public synchronized static long timeEnd() {
    	lastTime = System.currentTimeMillis();
        long elapsed = lastTime - timeStack.pop();
        String label = timeLabelStack.pop();
        if (SHOW_TIMINGS) {
            IStatus status = newSimpleStatus(1, IStatus.INFO, label + " end: " + elapsed + " ms", null); //$NON-NLS-1$ //$NON-NLS-2$
            log(UtilsCorePlugin.getDefault(), status);
        }
        return elapsed;
    }
    
    /**
     * Time a task and report results if Logging#SHOW_TIMINGS is set.
     * @param task
     * @return elapsed time in milliseconds
     */
    public synchronized static long timeTask(ITimedTask task) {
        timeStart(task.getLabel());
        long elapsed = 0;
        try {
            task.run();
        } finally {
            elapsed = timeEnd();
        }
        return elapsed;
    }
}
