/*******************************************************************************
 * Copyright (c) 2000, 2010 QNX Software Systems and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     QNX Software Systems - Initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.debug.mi.core;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import com.ibm.icu.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import org.eclipse.cdt.debug.core.CDebugCorePlugin;
import org.eclipse.cdt.debug.core.ICDTLaunchConfigurationConstants;
import org.eclipse.cdt.debug.core.ICDebugConfiguration;
import org.eclipse.cdt.debug.core.ICDebugConstants;
import org.eclipse.cdt.debug.mi.core.cdi.Session;
import org.eclipse.cdt.debug.mi.core.command.CLITargetAttach;
import org.eclipse.cdt.debug.mi.core.command.CommandFactory;
import org.eclipse.cdt.debug.mi.core.command.MIStackListFrames;
import org.eclipse.cdt.debug.mi.core.command.MITargetSelect;
import org.eclipse.cdt.debug.mi.core.command.factories.CommandFactoryManager;
import org.eclipse.cdt.debug.mi.core.output.MIInfo;
import org.eclipse.cdt.utils.pty.PTY;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Plugin;
import org.eclipse.core.runtime.Preferences;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.osgi.framework.BundleContext;

/**
 * GDB/MI Plugin.
 */
public class MIPlugin extends Plugin {

	/**
	 * The plug-in identifier of the Java core support
	 * (value <code>"org.eclipse.jdt.core"</code>).
	 */
	public static final String PLUGIN_ID = "org.eclipse.cdt.debug.mi.core" ; //$NON-NLS-1$

	/**
	 * Simple identifier constant (value <code>"commandFactories"</code>)
	 * for the "gdb/mi command factories" extension point.
	 * 
	 * @since 3.1
	 */
	public static final String EXTENSION_POINT_COMMAND_FACTORIES = "commandFactories"; //$NON-NLS-1$	

	//The shared instance.
	private static MIPlugin plugin;

	// GDB command
	private static final String GDB = "gdb"; //$NON-NLS-1$

	private static final int INTERNAL_ERROR = 42;

	/**
	 * Has tracing for this plug-in been turned on? 
	 * @since 7.0
	 */
	public static final boolean DEBUG = "true".equals( //$NON-NLS-1$
            Platform.getDebugOption("org.eclipse.cdt.debug.mi.core/debug")); //$NON-NLS-1$
	/**
	 * The singleton command factory manager.
	 */
	private CommandFactoryManager fCommandFactoryManager;

	private static ResourceBundle fgResourceBundle;
	static {
		try {
			fgResourceBundle = ResourceBundle.getBundle("org.eclipse.cdt.debug.mi.core.MIPluginResources"); //$NON-NLS-1$
		} catch (MissingResourceException x) {
			fgResourceBundle = null;
		}
	}
	/**
	 * The constructor
	 * @see org.eclipse.core.runtime.Plugin#Plugin()
	 */
	public MIPlugin() {
		super();
		plugin = this;
	}

	/**
	 * Returns the singleton.
	 */
	public static MIPlugin getDefault() {
		return plugin;
	}

	/**
	 * Method createMISession.
	 * @param Process
	 * @param PTY
	 * @param int
	 * @param int
	 * @throws MIException
	 * @return MISession
	 * 
	 * @deprecated
	 */
	@Deprecated
	public MISession createMISession(MIProcess process, IMITTY pty, int timeout, int type, int launchTimeout, String miVersion, IProgressMonitor monitor) throws MIException {
		return new MISession(process, pty, type, timeout, launchTimeout, miVersion, monitor);
	}

	/**
	 * Method createMISession.
	 * @param Process
	 * @param PTY
	 * @param type
	 * @throws MIException
	 * @return MISession
	 * 
	 * @deprecated
	 */
	@Deprecated
	public MISession createMISession(MIProcess process, IMITTY pty, int type, String miVersion, IProgressMonitor monitor) throws MIException {
		MIPlugin miPlugin = getDefault();
		Preferences prefs = miPlugin.getPluginPreferences();
		int timeout = prefs.getInt(IMIConstants.PREF_REQUEST_TIMEOUT);
		int launchTimeout = prefs.getInt(IMIConstants.PREF_REQUEST_LAUNCH_TIMEOUT);
		return createMISession(process, pty, timeout, type, launchTimeout, miVersion, monitor);
	}

	private MISession createMISession0(int type, MIProcess process, CommandFactory commandFactory, IMITTY pty, int timeout) throws MIException {
		return new MISession(process, pty, type, commandFactory, timeout);
	}

	/**
	 * Method createCSession; Create an new PTY instance and launch gdb in mi for local debug.
	 * 
	 * @param program
	 * @return ICDISession
	 * @throws MIException
	 * 
	 * @deprecated use <code>createSession</code>
	 */
	@Deprecated
	public Session createCSession(String gdb, String miVersion, File program, File cwd, String gdbinit, IProgressMonitor monitor) throws IOException, MIException {
		IMITTY pty = null;
		boolean failed = false;

		try {
			PTY pseudo = new PTY();
			pty = new MITTYAdapter(pseudo);
		} catch (IOException e) {
			// Should we not print/log this ?
		}

		try {
			return createCSession(gdb, miVersion, program, cwd, gdbinit, pty, monitor);
		} catch (IOException exc) {
			failed = true;
			throw exc;
		} catch (MIException exc) {
			failed = true;
			throw exc;
		} finally {
			if (failed) {
				// Shutdown the pty console.
				if (pty != null) {
					try {
						OutputStream out = pty.getOutputStream();
						if (out != null) {
							out.close();
						}
						InputStream in = pty.getInputStream();
						if (in != null) {
							in.close();
						}
					} catch (IOException e) {
					}
				}
			}
		}
	}

	/**
	 * Method createCSession; lauch gdb in mi mode for local debugging
	 * @param program
	 * @return ICDISession
	 * @throws IOException
	 * 
	 * @deprecated use <code>createSession</code>
	 */
	@Deprecated
	public Session createCSession(String gdb, String miVersion, File program, File cwd, String gdbinit, IMITTY pty, IProgressMonitor monitor) throws IOException, MIException {
		if (gdb == null || gdb.length() == 0) {
			gdb =  GDB;
		}
		
		String commandFile = (gdbinit != null && gdbinit.length() > 0) ? "--command="+gdbinit : "--nx"; //$NON-NLS-1$ //$NON-NLS-2$

		if (monitor == null) {
			monitor = new NullProgressMonitor();
		}

		String[] args;
		if (pty != null) {
			if (program == null) {
				args = new String[] {gdb, "--cd="+cwd.getAbsolutePath(), commandFile, "-q", "-nw", "-tty", pty.getSlaveName(), "-i", miVersion}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
			} else {
				args = new String[] {gdb, "--cd="+cwd.getAbsolutePath(), commandFile, "-q", "-nw", "-tty", pty.getSlaveName(), "-i", miVersion, program.getAbsolutePath()}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
			}
		} else {
			if (program == null) {
				args = new String[] {gdb, "--cd="+cwd.getAbsolutePath(), commandFile, "-q", "-nw", "-i", miVersion}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
			} else {
				args = new String[] {gdb, "--cd="+cwd.getAbsolutePath(), commandFile, "-q", "-nw", "-i", miVersion, program.getAbsolutePath()}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
			}
		}

		int launchTimeout = MIPlugin.getDefault().getPluginPreferences().getInt(IMIConstants.PREF_REQUEST_LAUNCH_TIMEOUT);		
		MIProcess pgdb = new MIProcessAdapter(args, launchTimeout, monitor);

		if (MIPlugin.DEBUG) {
			StringBuffer sb = new StringBuffer();
			for (int i = 0; i < args.length; ++i) {
				sb.append(args[i]);
				sb.append(' ');
			}
			MIPlugin.getDefault().debugLog(sb.toString());
		}
		
		MISession session;
		try {
			session = createMISession(pgdb, pty, MISession.PROGRAM, miVersion, monitor);
		} catch (MIException e) {
			pgdb.destroy();
			throw e;
		}
		// Try to detect if we have been attach/connected via "target remote localhost:port"
		// or "attach" and set the state to be suspended.
		try {
			CommandFactory factory = session.getCommandFactory();
			MIStackListFrames frames = factory.createMIStackListFrames();
			session.postCommand(frames);
			MIInfo info = frames.getMIInfo();
			if (info == null) {
				pgdb.destroy();
				throw new MIException(getResourceString("src.common.No_answer")); //$NON-NLS-1$
			}
			//@@@ We have to manually set the suspended state since we have some stackframes
			session.getMIInferior().setSuspended();
			session.getMIInferior().update();
		} catch (MIException e) {
			// If an exception is thrown that means ok
			// we did not attach/connect to any target.
		}
		return new Session(session, false);
	}

	/**
	 * Method createCSession; Post mortem debug with a core file.
	 * @param program
	 * @param core
	 * @return ICDISession
	 * @throws IOException
	 *
	 * @deprecated use <code>createSession</code>
	 */
	@Deprecated
	public Session createCSession(String gdb, String miVersion, File program, File core, File cwd, String gdbinit, IProgressMonitor monitor) throws IOException, MIException {
		if (gdb == null || gdb.length() == 0) {
			gdb =  GDB;
		}
		
		String commandFile = (gdbinit != null && gdbinit.length() > 0) ? "--command="+gdbinit : "--nx"; //$NON-NLS-1$ //$NON-NLS-2$
		
		if (monitor == null) {
			monitor = new NullProgressMonitor();
		}

		String[] args;
		if (program == null) {
			args = new String[] {gdb, "--cd="+cwd.getAbsolutePath(), commandFile, "--quiet", "-nw", "-i", miVersion, "-c", core.getAbsolutePath()}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
		} else {
			args = new String[] {gdb, "--cd="+cwd.getAbsolutePath(), commandFile, "--quiet", "-nw", "-i", miVersion, "-c", core.getAbsolutePath(), program.getAbsolutePath()}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
		}

		int launchTimeout = MIPlugin.getDefault().getPluginPreferences().getInt(IMIConstants.PREF_REQUEST_LAUNCH_TIMEOUT);		
		MIProcess pgdb = new MIProcessAdapter(args, launchTimeout, monitor);
		
		if (MIPlugin.DEBUG) {
			StringBuffer sb = new StringBuffer();
			for (int i = 0; i < args.length; ++i) {
				sb.append(args[i]);
				sb.append(' ');
			}
			MIPlugin.getDefault().debugLog(sb.toString());
		}
		
		MISession session;
		try {
			session = createMISession(pgdb, null, MISession.CORE, miVersion, monitor);
			//@@@ We have to manually set the suspended state when doing post-mortem
			session.getMIInferior().setSuspended();
		} catch (MIException e) {
			pgdb.destroy();
			throw e;
		}
		return new Session(session);
	}

	/**
	 * Method createCSession; remote debuging by selectin  a target.
	 * @param program
	 * @param pid
	 * @return ICDISession
	 * @throws IOException
	 * 
	 * @deprecated use <code>createSession</code>
	 */
	@Deprecated
	public Session createCSession(String gdb, String miVersion, File program, int pid, String[] targetParams, File cwd, String gdbinit, IProgressMonitor monitor) throws IOException, MIException {
		if (gdb == null || gdb.length() == 0) {
			gdb =  GDB;
		}

		String commandFile = (gdbinit != null && gdbinit.length() > 0) ? "--command="+gdbinit : "--nx"; //$NON-NLS-1$ //$NON-NLS-2$

		if (monitor == null) {
			monitor = new NullProgressMonitor();
		}

		String[] args;
		if (program == null) {
			args = new String[] {gdb, "--cd="+cwd.getAbsolutePath(), commandFile, "--quiet", "-nw", "-i", miVersion}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
		} else {
			args = new String[] {gdb, "--cd="+cwd.getAbsolutePath(), commandFile, "--quiet", "-nw", "-i", miVersion, program.getAbsolutePath()}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
		}

		int launchTimeout = MIPlugin.getDefault().getPluginPreferences().getInt(IMIConstants.PREF_REQUEST_LAUNCH_TIMEOUT);		
		MIProcess pgdb = new MIProcessAdapter(args, launchTimeout, monitor);
		
		if (MIPlugin.getDefault().isDebugging()) {
			StringBuffer sb = new StringBuffer();
			for (int i = 0; i < args.length; ++i) {
				sb.append(args[i]);
				sb.append(' ');
			}
			MIPlugin.getDefault().debugLog(sb.toString());
		}
		
		MISession session;
		try {
			session = createMISession(pgdb, null, MISession.ATTACH, miVersion, monitor);
		} catch (MIException e) {
			pgdb.destroy();
			throw e;
		}
		CommandFactory factory = session.getCommandFactory();
		try {
			if (targetParams != null && targetParams.length > 0) {
				MITargetSelect target = factory.createMITargetSelect(targetParams);
				session.postCommand(target);
				MIInfo info = target.getMIInfo();
				if (info == null) {
					throw new MIException(getResourceString("src.common.No_answer")); //$NON-NLS-1$
				}
			}
			if (pid > 0) {
				CLITargetAttach attach = factory.createCLITargetAttach(pid);
				session.postCommand(attach);
				MIInfo info = attach.getMIInfo();
				if (info == null) {
					throw new MIException(getResourceString("src.common.No_answer")); //$NON-NLS-1$
				}
				session.getMIInferior().setInferiorPID(pid);
				// @@@ for attach we nee to manually set the connected state
				// attach does not send the ^connected ack
				session.getMIInferior().setConnected();
			}
		} catch (MIException e) {
			if(session != null)
				session.terminate();
			
			pgdb.destroy();
			throw e;
		}
		//@@@ We have to manually set the suspended state when we attach
		session.getMIInferior().setSuspended();
		session.getMIInferior().update();
		return new Session(session, true);
	}

	/**
	 * Starts a process by executing the following command:
	 * gdb -q -nw -i <mi_version>(extracted from the command factory) 
	 *     -tty<pty_name> (if <code>usePTY</code> is <code>true</code>) 
	 *     extraArgs program (if <code>program</code> is not <code>null</code>)  
	 * 
	 * @param sessionType the type of debugging session: 
	 *                    <code>MISession.PROGRAM</code>, 
	 *                    <code>MISession.ATTACH</code> 
	 *                    or <code>MISession.CORE</code>
	 * @param gdb the name of the gdb file
	 * @param factory the command set supported by gdb
	 * @param program a program to debug or <code>null</code>
	 * @param extraArgs arguments to pass to gdb
	 * @param usePty whether to use pty or not
	 * @param monitor a progress monitor
	 * @return an instance of <code>ICDISession</code>
	 * @throws IOException
	 * @throws MIException
	 */
	public Session createSession(int sessionType, String gdb, CommandFactory factory, File program, String[] extraArgs, boolean usePty, IProgressMonitor monitor) throws IOException, MIException {
		if (monitor == null) {
			monitor = new NullProgressMonitor();
		}
		
		if (gdb == null || gdb.length() == 0) {
			gdb =  GDB;
		}

		IMITTY pty = null;

		if (usePty) {
			try {
				PTY pseudo = new PTY();
				pty = new MITTYAdapter(pseudo);
			} catch (IOException e) {
				// Should we not print/log this ?
			}
		}

		ArrayList argList = new ArrayList(extraArgs.length + 8);
		argList.add(gdb);
		argList.add("-q"); //$NON-NLS-1$
		argList.add("-nw"); //$NON-NLS-1$
		argList.add("-i"); //$NON-NLS-1$
		argList.add(factory.getMIVersion());
		if (pty != null) {
			argList.add("-tty"); //$NON-NLS-1$
			argList.add(pty.getSlaveName());
		}
		argList.addAll(Arrays.asList(extraArgs));
		if (program != null) {
			argList.add(program.getAbsolutePath());
		}
		String[] args = (String[])argList.toArray(new String[argList.size()]);
		int launchTimeout = MIPlugin.getDefault().getPluginPreferences().getInt(IMIConstants.PREF_REQUEST_LAUNCH_TIMEOUT);		

		MISession miSession = null;
		MIProcess pgdb = null;
		boolean failed = false;
		try {
			pgdb = factory.createMIProcess(args, launchTimeout, monitor);
	
			if (MIPlugin.DEBUG) {
				StringBuffer sb = new StringBuffer();
				for (int i = 0; i < args.length; ++i) {
					sb.append(args[i]);
					sb.append(' ');
				}
				MIPlugin.getDefault().debugLog(sb.toString());
			}
		
			miSession = createMISession0(sessionType, pgdb, factory, pty, getCommandTimeout());
		} catch (MIException e) {
			failed = true;
			throw e;
		} catch(IOException e ) {
			failed = true;
			throw e;
		} finally {
			if (failed) {
				// Kill gdb
				if ( pgdb != null )
					pgdb.destroy();
				// Shutdown the pty console.
				if (pty != null) {
					try {
						OutputStream out = pty.getOutputStream();
						if (out != null) {
							out.close();
						}
						InputStream in = pty.getInputStream();
						if (in != null) {
							in.close();
						}
					} catch (IOException e) {
					}
				}
			}
		}
		
		return new Session(miSession);
	}

	/**
	 * Convenience method which returns the unique identifier of this plugin.
	 */
	public static String getUniqueIdentifier() {
		return PLUGIN_ID;
	}

	public void debugLog(String message) {
		if (getDefault().isDebugging()) {			
			// Time stamp
			message = MessageFormat.format( "[{0}] {1}", new Object[] { new Long( System.currentTimeMillis() ), message } ); //$NON-NLS-1$
			// This is to verbose for a log file, better use the console.
			//	getDefault().getLog().log(StatusUtil.newStatus(Status.ERROR, message, null));
			// ALERT:FIXME: For example for big buffers say 4k length,
			// the console will simply blows taking down eclipse.
			// This seems only to happen in Eclipse-gtk and Eclipse-motif
			// on GNU/Linux, so we break the lines in smaller chunks.
			while (message.length() > 100) {
				String partial = message.substring(0, 100);
				message = message.substring(100);
				System.out.println(partial + "\\"); //$NON-NLS-1$
			}
			if (message.endsWith("\n")) { //$NON-NLS-1$
				System.out.print(message);
			} else {
				System.out.println(message);
			}
		}
	}
	public static String getResourceString(String key) {
		try {
			return fgResourceBundle.getString(key);
		} catch (MissingResourceException e) {
			return '!' + key + '!';
		} catch (NullPointerException e) {
			return '#' + key + '#';
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.core.runtime.Plugin#startup()
	 */
	@Override
	public void start(BundleContext context) throws Exception {
		super.start(context);
		ICDebugConfiguration dc = CDebugCorePlugin.getDefault().getDefaultDefaultDebugConfiguration();
		if (dc == null) {
			CDebugCorePlugin.getDefault().getPluginPreferences().setDefault(ICDebugConstants.PREF_DEFAULT_DEBUGGER_TYPE, "org.eclipse.cdt.debug.mi.core.CDebuggerNew"); //$NON-NLS-1$
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.core.runtime.Plugin#shutdown()
	 */
	@Override
	public void stop(BundleContext context) throws Exception {
		savePluginPreferences();
		super.stop(context);
	}

	public static int getCommandTimeout() {
		Preferences prefs = getDefault().getPluginPreferences();
		return prefs.getInt(IMIConstants.PREF_REQUEST_TIMEOUT);
	}

	public static int getLaunchTimeout() {
		Preferences prefs = plugin.getPluginPreferences();
		return prefs.getInt(IMIConstants.PREF_REQUEST_LAUNCH_TIMEOUT);
	}

	public static String getMIVersion( ILaunchConfiguration config ) {
		String miVersion = ""; //$NON-NLS-1$
		try {
			miVersion = config.getAttribute( IMILaunchConfigurationConstants.ATTR_DEBUGGER_PROTOCOL, "" ); //$NON-NLS-1$
		}
		catch( CoreException e ) {
		}
		if ( miVersion.length() == 0 ) {
			try {
				miVersion = config.getAttribute( ICDTLaunchConfigurationConstants.ATTR_DEBUGGER_PROTOCOL, "mi" ); //$NON-NLS-1$
			}
			catch( CoreException e ) {
				miVersion = "mi"; //$NON-NLS-1$
			}
		}
		return miVersion;
	}

	public static String getCommandFactory( ILaunchConfiguration config ) {
		String commandFactory = ""; //$NON-NLS-1$
		try {
			commandFactory = config.getAttribute( IMILaunchConfigurationConstants.ATTR_DEBUGGER_COMMAND_FACTORY, "" ); //$NON-NLS-1$
		}
		catch( CoreException e ) {
		}
		return commandFactory;
	}

	public CommandFactoryManager getCommandFactoryManager() {
		if ( fCommandFactoryManager == null ) {
			fCommandFactoryManager = new CommandFactoryManager();
		}
		return fCommandFactoryManager;
	}

	/**
	 * Log internal error
	 * @param string - error message
	 */
	public static void log(String string) {
		log(new Status( IStatus.ERROR, getUniqueIdentifier(), string));
	}
	/**
	 * Logs the specified status with this plug-in's log.
	 * 
	 * @param status
	 *            status to log
	 */
	public static void log( IStatus status ) {
		getDefault().getLog().log( status );
	}

	/**
	 * Logs an internal error with the specified throwable
	 * 
	 * @param e
	 *            the exception to be logged
	 */
	public static void log( Throwable e ) {
		log( new Status( IStatus.ERROR, getUniqueIdentifier(), INTERNAL_ERROR, "Internal Error", e ) ); //$NON-NLS-1$
	}
}
