/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package flash.tools.debugger.concrete;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.BindException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import flash.localization.LocalizationManager;
import flash.tools.debugger.AIRLaunchInfo;
import flash.tools.debugger.DebuggerLocalizer;
import flash.tools.debugger.DefaultDebuggerCallbacks;
import flash.tools.debugger.IDebuggerCallbacks;
import flash.tools.debugger.ILaunchNotification;
import flash.tools.debugger.ILauncher;
import flash.tools.debugger.IProgress;
import flash.tools.debugger.Player;
import flash.tools.debugger.Session;
import flash.tools.debugger.SessionManager2;
import flash.tools.debugger.VersionException;
import flash.util.URLHelper;

public class PlayerSessionManager implements SessionManager2
{
	private ServerSocket m_serverSocket;
	private HashMap<String, Object> m_prefs;
	private IDebuggerCallbacks m_debuggerCallbacks;
	private static LocalizationManager m_localizationManager;
	private Socket m_connectSocket;
	private boolean m_cancelConnect;
	
	static
	{
        // set up for localizing messages
        m_localizationManager = new LocalizationManager();
        m_localizationManager.addLocalizer( new DebuggerLocalizer("flash.tools.debugger.concrete.djapi.") ); //$NON-NLS-1$
	}

	public PlayerSessionManager()
	{
		m_debuggerCallbacks = new DefaultDebuggerCallbacks();

		m_serverSocket = null;
		m_connectSocket = null;
		m_cancelConnect = false;
		m_prefs = new HashMap<String, Object>();

		// manager
		setPreference(PREF_ACCEPT_TIMEOUT, 120000); // 2 minutes
		setPreference(PREF_URI_MODIFICATION, 1);
		setPreference(PREF_CONNECT_TIMEOUT, 120000); // 2 minutes
		setPreference(PREF_CONNECT_WAIT_INTERVAL, 250); // 0.25 seconds
		setPreference(PREF_CONNECT_RETRY_ATTEMPTS, -1); // Retry till timeout
		
		// session

		// response to requests
		setPreference(PREF_SOCKET_TIMEOUT, -1); // no timeout by default
		setPreference(PREF_RESPONSE_TIMEOUT, 750); // 0.75s
		setPreference(PREF_CONTEXT_RESPONSE_TIMEOUT, 1000); // 1s
		setPreference(PREF_GETVAR_RESPONSE_TIMEOUT, 1500); // 1.5s
		setPreference(PREF_SETVAR_RESPONSE_TIMEOUT, 5000); // 5s
		setPreference(PREF_SWFSWD_LOAD_TIMEOUT, 5000);  // 5s

		// wait for a suspend to occur after a halt
		setPreference(PREF_SUSPEND_WAIT, 7000);

		// invoke getters by default
		setPreference(PREF_INVOKE_GETTERS, 1);

		// hierarchical variables view
		setPreference(PREF_HIERARCHICAL_VARIABLES, 0);
	}

	/**
	 * Set preference 
	 * If an invalid preference is passed, it will be silently ignored.
	 */
	public void			setPreference(String pref, int value)	{ m_prefs.put(pref, Integer.valueOf(value)); }
	public void			setPreference(String pref, String value){ m_prefs.put(pref, value);	}
	public Set<String>	keySet()								{ return m_prefs.keySet(); }
	public Object		getPreferenceAsObject(String pref)		{ return m_prefs.get(pref); }

	/*
	 * @see flash.tools.debugger.SessionManager#getPreference(java.lang.String)
	 */
	public int getPreference(String pref)
	{
		int val = 0;
		Integer i = (Integer)m_prefs.get(pref);
		if (i == null)
			throw new NullPointerException();
		val = i.intValue();
		return val;
	}

	/*
	 * @see flash.tools.debugger.SessionManager#startListening()
	 */
	public void startListening() throws IOException 
	{
		if (m_serverSocket == null)
			m_serverSocket = new ServerSocket(DProtocol.DEBUG_PORT);
	}

	/*
	 * @see flash.tools.debugger.SessionManager#stopListening()
	 */
	public void stopListening() throws IOException
	{
		if (m_serverSocket != null)
		{
			m_serverSocket.close();
			m_serverSocket = null;
		}
	}

	/*
	 * @see flash.tools.debugger.SessionManager#isListening()
	 */
	public boolean isListening()
	{
		return (m_serverSocket == null) ? false : true;
	}

	private class LaunchInfo
	{
		private String m_uri;

		public LaunchInfo(String uri)
		{
			m_uri = uri;
		}

		public boolean isAbout()
		{
			return m_uri.startsWith("about:"); //$NON-NLS-1$
		}

		public boolean isHttpOrAbout()
		{
			return m_uri.startsWith("http:") || m_uri.startsWith("https:") || isAbout(); //$NON-NLS-1$ //$NON-NLS-2$
		}

		public boolean isWebPage()
		{
			return isHttpOrAbout() || m_uri.endsWith(".htm") || m_uri.endsWith(".html"); //$NON-NLS-1$ //$NON-NLS-2$
		}

		public boolean isWebBrowserNativeLaunch()
		{
			return isWebPage() && (m_debuggerCallbacks.getHttpExe() != null);
		}

		public boolean isPlayerNativeLaunch()
		{
			return m_uri.length() > 0 && !isWebPage() && (m_debuggerCallbacks.getPlayerExe() != null);
		}
		
		public boolean isAIRLaunch()
		{
			return m_uri.startsWith("file:") && (m_uri.endsWith("-app.xml") || m_uri.endsWith("application.xml")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		}
	}

	private enum OS {
		Mac,
		Windows,
		Unix
	}

	private OS getOS()
	{
		String osName = System.getProperty("os.name").toLowerCase(); //$NON-NLS-1$
		if (osName.startsWith("mac os x")) //$NON-NLS-1$
		{
			return OS.Mac;
		}
		else if (osName.startsWith("windows")) //$NON-NLS-1$
		{
			return OS.Windows;
		}
		else
		{
			return OS.Unix;
		}
	}

	/*
	 * @see flash.tools.debugger.SessionManager#launch(java.lang.String, flash.tools.debugger.AIRLaunchInfo, boolean, flash.tools.debugger.IProgress)
	 */
	public Session launch(String uri, AIRLaunchInfo airLaunchInfo, boolean forDebugging, IProgress waitReporter, ILaunchNotification launchNotification) throws IOException
	{
		String[] launchCommand = getLaunchCommand(uri, airLaunchInfo,forDebugging);

		// create the process and attach a thread to watch it during our accept phase
		Process proc = m_debuggerCallbacks.launchDebugTarget(launchCommand);

		ProcessListener processListener = startProcessListener(airLaunchInfo,forDebugging, launchNotification, launchCommand, proc,false); 
		PlayerSession session = null;

		if (forDebugging)
		{
			session = waitForConnection(uri, airLaunchInfo, waitReporter, proc, processListener);
		}

		return session;
	}

	private PlayerSession waitForConnection(String uri,
			AIRLaunchInfo airLaunchInfo, IProgress waitReporter, Process proc,
			ProcessListener pl) throws IOException {
		/* now wait for a connection */
		PlayerSession session = (PlayerSession)accept(pl, waitReporter);
		session.setProcess(proc);
		session.setLaunchUrl(uri);
		session.setAIRLaunchInfo(airLaunchInfo);
		return session;
	}

	/**
	 * Tweaks the launch URI if necessary, e.g. may append "?debug=true"
	 */
	private String tweakNativeLaunchUri(String uri, boolean forDebugging,
			LaunchInfo launchInfo) throws IOException, FileNotFoundException
	{
		// first let's see if it's an HTTP URL or not
		if (launchInfo.isHttpOrAbout())
		{
			boolean modify = (getPreference(PREF_URI_MODIFICATION) != 0);

			if (modify && forDebugging && !launchInfo.isAbout())
			{
				// escape spaces if we have any
				uri = URLHelper.escapeSpace(uri);

		        // be sure that ?debug=true is included in query string
				URLHelper urlHelper = new URLHelper(uri);
				Map<String, String> params = urlHelper.getParameterMap();
				params.put("debug", "true"); //$NON-NLS-1$ //$NON-NLS-2$
				urlHelper.setParameterMap(params);

				uri = urlHelper.getURL();
		    }
		}
		else
		{
			// ok, its not an http: type request therefore we should be able to see
			// it on the file system, right?  If not then it's probably not valid
			File f = null;
			if (uri.startsWith("file:")) //$NON-NLS-1$
			{
				try
				{
					f = new File(new URI(uri));
				}
				catch (URISyntaxException e)
				{
					IOException ioe = new IOException(e.getMessage());
					ioe.initCause(e);
					throw ioe;
				}
			}
			else
			{
				f = new File(uri);
			}

			if (f.exists()) {
				// Do not use getCanonicalPath() -- see FB-24595
				uri = f.getAbsolutePath();
			} else {
				throw new FileNotFoundException(uri);
			}
		}

		return uri;
	}

	/**
	 * Gets the arguments needed for launching a swf that needs to run
	 * in a web browser or the standalone player.
	 */
	private String[] getFlashLaunchArgs(String uri, LaunchInfo launchInfo) throws FileNotFoundException
	{
		String[] launchCommand;

		OS os = getOS();

		/**
		 * Various ways to launch this stupid thing.  If we have the exe
		 * values for the player, then we can launch it directly, monitor
		 * it and kill it when we die; otherwise we launch it through
		 * a command shell (cmd.exe, open, or bash) and our Process object
		 * dies right away since it spawned another process to run the
		 * Player within.
		 */
		if (os == OS.Mac)
		{
			if (launchInfo.isWebBrowserNativeLaunch())
			{
				File httpExe = m_debuggerCallbacks.getHttpExe();
				String[] customParams = m_debuggerCallbacks.getBrowserParameters(uri);
				if (customParams == null) {
					launchCommand = new String[] { "/usr/bin/open", "-a", httpExe.toString(), uri }; //$NON-NLS-1$ //$NON-NLS-2$
				}
				else {
					final int prependLen = 4;
					launchCommand = new String[customParams.length + prependLen ];
					launchCommand[0] = "/usr/bin/open"; //$NON-NLS-1$
					launchCommand[1] = "-a"; //$NON-NLS-1$
					launchCommand[2] = httpExe.toString();
					launchCommand[3] = "--args"; //$NON-NLS-1$
					for ( int i = 0; i < customParams.length; i++) {
						launchCommand[i + prependLen] = customParams[i];
					}
				}
			}
			else if (launchInfo.isPlayerNativeLaunch())
			{
				File playerExe = m_debuggerCallbacks.getPlayerExe();
				launchCommand = new String[] { "/usr/bin/open", "-a", playerExe.toString(), uri }; //$NON-NLS-1$ //$NON-NLS-2$
			}
			else
			{
				launchCommand = new String[] { "/usr/bin/open", uri }; //$NON-NLS-1$
			}
		}
		else // Windows or *nix
		{
			if (launchInfo.isWebBrowserNativeLaunch())
			{
				File httpExe = m_debuggerCallbacks.getHttpExe();
				String[] customParams = m_debuggerCallbacks.getBrowserParameters(uri);
				if (customParams == null) {
					if (os == OS.Windows)
						launchCommand = getWindowsBrowserLaunchArgs(httpExe, uri);
					else
						launchCommand = new String[] { httpExe.toString(), uri };
				}
				else {
					final int prependLen = 1;
					launchCommand = new String[customParams.length + prependLen];
					launchCommand[0] = httpExe.toString();
					for ( int i = 0; i < customParams.length; i++) {
						launchCommand[i + prependLen] = customParams[i];
					}
				}
			}
			else if (launchInfo.isPlayerNativeLaunch())
			{
				File playerExe = m_debuggerCallbacks.getPlayerExe();
				launchCommand = new String[] { playerExe.toString(), uri };
			}
			else
			{
				if (os == OS.Windows)
				{
					// We must quote all ampersands in the URL; if we don't, then
					// cmd.exe will interpret the ampersand as a command separator.
					uri = uri.replaceAll("&", "\"&\""); //$NON-NLS-1$ //$NON-NLS-2$

					launchCommand = new String[] { "cmd", "/c", "start", uri }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
				}
				else
				{
					String exeName;
					if (launchInfo.isWebPage())
						exeName = m_debuggerCallbacks.getHttpExeName();
					else
						exeName = m_debuggerCallbacks.getPlayerExeName();
					throw new FileNotFoundException(exeName);
				}
			}
		}
		return launchCommand;
	}

	

	/**
	 * Gets the arguments needed for launching a web browser on Windows.
	 */
	private String[] getWindowsBrowserLaunchArgs(File httpExe, String uri)
	{
		if (httpExe.getName().equalsIgnoreCase("chrome.exe")) //$NON-NLS-1$
		{
			// FB-16779: Adding "--disable-hang-monitor" to prevent Chrome
			// from warning us that a plug-inappears to be hung; it does
			// that when the user hits a breakpoint.
			return new String[] { httpExe.toString(), "--disable-hang-monitor", uri }; //$NON-NLS-1$
		}
		else if (httpExe.getName().equalsIgnoreCase("iexplore.exe")) //$NON-NLS-1$
		{
			boolean isIE8 = false;

			try
			{
				int[] ieVersion = m_debuggerCallbacks.getAppVersion(httpExe);
				if (ieVersion != null)
					isIE8 = (ieVersion[0] >= 8);
			} catch (IOException e) {
				// ignore
			}

			if (isIE8)
			{
				// FB-22107: Tell IE to keep using the new process we are
				// launching, rather than merging the new process into the
				// old one.  This allows us to terminate the new IE
				// debugging session.
				return new String[] { httpExe.toString(), "-noframemerging", uri }; //$NON-NLS-1$
			}
		}

		return new String[] { httpExe.toString(), uri };
	}

	/**
	 * Gets the arguments needed for launching a swf that needs to run
	 * in AIR.
	 */
	private String[] getAIRLaunchArgs(String uri, AIRLaunchInfo airLaunchInfo)
			throws IOException
	{
		List<String> cmdList = new LinkedList<String>();

		cmdList.add(airLaunchInfo.airDebugLauncher.getPath());

		if (airLaunchInfo.airRuntimeDir != null)
		{
			cmdList.add("-runtime"); //$NON-NLS-1$
			cmdList.add(airLaunchInfo.airRuntimeDir.getPath());
		}

		if (airLaunchInfo.airSecurityPolicy != null)
		{
			cmdList.add("-security-policy"); //$NON-NLS-1$
			cmdList.add(airLaunchInfo.airSecurityPolicy.getPath());
		}

		if (airLaunchInfo.airPublisherID != null && airLaunchInfo.airPublisherID.length() > 0)
		{
			cmdList.add("-pubid"); //$NON-NLS-1$
			cmdList.add(airLaunchInfo.airPublisherID);
		}

		if (airLaunchInfo.profile != null && airLaunchInfo.profile.length() > 0)
		{
			cmdList.add("-profile"); //$NON-NLS-1$
			cmdList.add(airLaunchInfo.profile);
		}
		
		if (airLaunchInfo.screenSize != null && airLaunchInfo.screenSize.length() > 0)
		{
			cmdList.add("-screensize"); //$NON-NLS-1$
			cmdList.add(airLaunchInfo.screenSize);
		}
		
		if (airLaunchInfo.dpi > 0)
		{
			//TODO: this is apparently only going to be used in AIR 2.5.
			//Figure out permanent solution when AIR 3.0 comes along.
			cmdList.add("-XscreenDPI"); //$NON-NLS-1$
			cmdList.add(String.valueOf(airLaunchInfo.dpi));
		}
		
		if (airLaunchInfo.versionPlatform != null && airLaunchInfo.versionPlatform.length() > 0)
		{
			cmdList.add("-XversionPlatform"); //$NON-NLS-1$
			cmdList.add(airLaunchInfo.versionPlatform);
		}
		
		if (airLaunchInfo.extDir != null && airLaunchInfo.extDir.length() > 0) {
			cmdList.add("-extdir"); //$NON-NLS-1$
			cmdList.add(airLaunchInfo.extDir);
		}
		
		if (airLaunchInfo.deviceExtDir != null && airLaunchInfo.deviceExtDir.length() > 0) {
			cmdList.add("-XdeviceExtDir"); //$NON-NLS-1$
			cmdList.add(airLaunchInfo.deviceExtDir);
		}
		
		// If it's a "file:" URL, then pass the actual filename; otherwise, use the URL
		// ok, its not an http: type request therefore we should be able to see
		// it on the file system, right?  If not then it's probably not valid
		File f = null;
		if (uri.startsWith("file:")) //$NON-NLS-1$
		{
			try
			{
				f = new File(new URI(uri));
				cmdList.add(f.getPath());
			}
			catch (URISyntaxException e)
			{
				IOException ioe = new IOException(e.getMessage());
				ioe.initCause(e);
				throw ioe;
			}
		}
		else
		{
			cmdList.add(uri);
		}

		if (airLaunchInfo.applicationContentRootDir != null)
		{
			cmdList.add(airLaunchInfo.applicationContentRootDir.getAbsolutePath());
		}

		List<String> args = null;
		if (airLaunchInfo.applicationArgumentsArray != null)
		{
			args = Arrays.asList(airLaunchInfo.applicationArgumentsArray);
		}
		else if (airLaunchInfo.applicationArguments != null)
		{
			args = splitArgs(airLaunchInfo.applicationArguments);
		}

		if (args != null && args.size() > 0)
		{
			cmdList.add("--"); //$NON-NLS-1$
			cmdList.addAll(args);
		}

		return cmdList.toArray(new String[cmdList.size()]);
	}

	/**
	 * This is annoying: We must duplicate the operating system's behavior
	 * with regard to splitting arguments.
	 * 
	 * @param arguments A single string of arguments that are intended to
	 * be passed to an AIR application.  The tricky part is that some
	 * of the arguments may be quoted, and if they are, then the quoting
	 * will be in a way that is specific to the current platform.  For
	 * example, on Windows, strings are quoted with the double-quote character
	 * ("); on Mac and Unix, strings can be quoted with either double-quote
	 * or single-quote.
	 * @return The equivalent
	 */
	private List<String> splitArgs(String arguments)
	{
		List<String> retval = new ArrayList<String>();

		arguments = arguments.trim();

		// Windows quotes only with double-quote; Mac and Unix also allow single-quote.
		boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("win"); //$NON-NLS-1$ //$NON-NLS-2$
		boolean isMacOrUnix = !isWindows;

		int i=0;
		while (i<arguments.length()) {
			char ch = arguments.charAt(i);
			if (ch == ' ' || ch == '\t') {
				// keep looping
				i++;
			} else if (ch == '"' || (isMacOrUnix && ch == '\'')) {
				char quote = ch;
				int nextQuote = arguments.indexOf(quote, i+1);
				if (nextQuote == -1) {
					retval.add(arguments.substring(i+1));
					return retval;
				} else {
					retval.add(arguments.substring(i+1, nextQuote));
					i = nextQuote+1;
				}
			} else {
				int startPos = i;
				while (i<arguments.length()) {
					ch = arguments.charAt(i);
					if (ch == ' ' || ch == '\t') {
						break;
					}
					i++;
				}
				retval.add(arguments.substring(startPos, i));
			}
		}

		return retval;
	}

	/*
	 * @see flash.tools.debugger.SessionManager#playerForUri(java.lang.String, flash.tools.debugger.AIRLaunchInfo)
	 */
	public Player playerForUri(String url, AIRLaunchInfo airLaunchInfo)
	{
		LaunchInfo launchInfo = new LaunchInfo(url);

		if (airLaunchInfo != null)
		{
			return new AIRPlayer(airLaunchInfo.airDebugLauncher);
		}
		else if (launchInfo.isAIRLaunch())
		{
			return new AIRPlayer(null);
		}
		else
		{
			// Find the Netscape plugin
			if (getOS() == OS.Mac)
			{
				if (!launchInfo.isWebBrowserNativeLaunch())
				{
					File playerExe = m_debuggerCallbacks.getPlayerExe();
					return new StandalonePlayer(playerExe);
				}
				File flashPlugin = new File("/Library/Internet Plug-Ins/Flash Player.plugin"); //$NON-NLS-1$
				return new NetscapePluginPlayer(m_debuggerCallbacks.getHttpExe(), flashPlugin);
			}
			else
			{
				if (launchInfo.isWebBrowserNativeLaunch())
				{
					File httpExe = m_debuggerCallbacks.getHttpExe();
					if (httpExe.getName().equalsIgnoreCase("iexplore.exe")) //$NON-NLS-1$
					{
						// IE on Windows: Find the ActiveX control
						String activeXFile = null;
						final String registryKey = "HKEY_CLASSES_ROOT\\CLSID\\{D27CDB6E-AE6D-11cf-96B8-444553540000}\\InprocServer32";  //$NON-NLS-1$
						//check if this is a 64-bit machine
						boolean is64Bit = (System.getenv("ProgramFiles(x86)") != null); //$NON-NLS-1$
						try
						{
							if (is64Bit) {
								//now we have to ensure that we only query the registry
								//that is 32 or 64-bit depending upon whether we are
								//launching 32 or 64-bit IE.
								if (httpExe.getCanonicalPath().contains("(x86)")) { //$NON-NLS-1$
									activeXFile = m_debuggerCallbacks.queryWindowsRegistry(registryKey, null, 1);
								}
								else { 
									activeXFile = m_debuggerCallbacks.queryWindowsRegistry(registryKey, null, 2);
								}	
							}
							else {
								activeXFile = m_debuggerCallbacks.queryWindowsRegistry(registryKey, null, 1);
							}
							
						}
						catch (IOException e)
						{
							// ignore
						}
						if (activeXFile == null)
							return null; // we couldn't find the player
						File file = new File(activeXFile);
						return new ActiveXPlayer(httpExe, file);
					}
					else
					{
						// Find the Netscape plugin
						File browserDir = httpExe.getParentFile();
	
						// Opera puts plugins under "program\plugins" rather than under "plugins"
						if (httpExe.getName().equalsIgnoreCase("opera.exe")) //$NON-NLS-1$
							browserDir = new File(browserDir, "program"); //$NON-NLS-1$
	
						File pluginsDir = new File(browserDir, "plugins"); //$NON-NLS-1$
						File flashPlugin = new File(pluginsDir, "NPSWF32.dll"); // WARNING, Windows-specific //$NON-NLS-1$

						// Bug FB-4691: The player is now installed via a registry key, not
						// in the "plugins" directory.
						//
						// Although Mozilla does not document this, the actual behavior of
						// the browser seems to be that it looks first in the "plugins" directory,
						// and then, if the file is not found there, it looks in the registry.
						// So, we mimic that behavior.
						if (!flashPlugin.exists())
						{
							File pathFromRegistry = getWindowsMozillaPlayerPathFromRegistry();

							if (pathFromRegistry != null)
								flashPlugin = pathFromRegistry;
						}
	
						return new NetscapePluginPlayer(httpExe, flashPlugin);
					}
				}
				else if (launchInfo.isPlayerNativeLaunch())
				{
					File playerExe = m_debuggerCallbacks.getPlayerExe();
					return new StandalonePlayer(playerExe);
				}
			}
		}

		return null;
	}

	/**
	 * Look in the Windows registry for the Mozilla version of the Flash player.
	 */
	private File getWindowsMozillaPlayerPathFromRegistry()
	{
		final String KEY = "\\SOFTWARE\\MozillaPlugins\\@adobe.com/FlashPlayer"; //$NON-NLS-1$
		final String PATH = "Path"; //$NON-NLS-1$

		// According to
		//
		//    http://developer.mozilla.org/en/docs/Plugins:_The_first_install_problem
		//
		// the MozillaPlugins key can be written to either HKEY_CURRENT_USER or
		// HKEY_LOCAL_MACHINE.  Unfortunately, as of this writing, Firefox
		// (version 2.0.0.2) doesn't actually work that way -- it only checks
		// HKEY_LOCAL_MACHINE, but not HKEY_CURRENT_USER.
		//
		// But in hopeful anticipation of a fix for that, we are going to check both
		// locations.  On current builds, that won't do any harm, because the
		// current Flash Player installer only writes to HKEY_LOCAL_MACHINE.  In the
		// future, if Mozilla gets fixed and then the Flash player installer gets
		// updated, then our code will already work correctly.
		//
		// Another quirk: In my opinion, it would be better for Mozilla to look first
		// in HKEY_CURRENT_USER, and then in HKEY_LOCAL_MACHINE.  However, according to
		//
		//    http://developer.mozilla.org/en/docs/Installing_plugins_to_Gecko_embedding_browsers_on_Windows
		//
		// they don't agree with that -- they want HKEY_LOCAL_MACHINE first.
		String[] roots = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" }; //$NON-NLS-1$ //$NON-NLS-2$
		for (int i=0; i<roots.length; ++i)
		{
			try
			{
				String path = m_debuggerCallbacks.queryWindowsRegistry(roots[i] + KEY, PATH, 1);
				if (path != null)
					return  new File(path);
			}
			catch (IOException e)
			{
				// ignore
			}
		}

		return null;
	}

	/*
	 * @see flash.tools.debugger.SessionManager#supportsLaunch()
	 */
	public boolean supportsLaunch()
	{
		return true;
	}

	/*
	 * @see flash.tools.debugger.SessionManager#accept(flash.tools.debugger.IProgress)
	 */
	public Session accept(IProgress waitReporter) throws IOException
	{
		return accept(null, waitReporter);
	}

	/**
	 * A private variation on <code>accept()</code> that also has an argument
	 * indicating that the process we are waiting for has terminated.
	 * 
	 * @param pl
	 *            Optional process listener. If non-null, this is used to detect
	 *            if a process that was launched has terminated unexpectedly.
	 *            For example, if launch() launches adl, but adl exits, then we
	 *            don't want to continue to wait for a socket connection.
	 */
	private Session accept(ProcessListener pl, IProgress waitReporter) throws IOException
	{
		// get timeout 
		int timeout = getPreference(PREF_ACCEPT_TIMEOUT);
		int totalTimeout = timeout;
		int iterateOn = 100;

		PlayerSession session = null;
		try
		{
			m_serverSocket.setSoTimeout(iterateOn);

			// Wait 100ms per iteration.  We have to do that so that we can report how long
			// we have been waiting.
			Socket s = null;
			while (s == null && !airAppTerminated(pl))
			{
				try
				{
					s = m_serverSocket.accept();
				}
				catch(IOException ste)
				{
					timeout -= iterateOn;
					if (timeout < 0 || m_serverSocket == null || m_serverSocket.isClosed())
						throw ste; // we reached the timeout, or someome called stopListening()
				}

				// Tell the progress monitor we've waited a little while longer,
				// so that the Eclipse progress bar can keep chugging along
				if (waitReporter != null)
					waitReporter.setProgress(totalTimeout - timeout, totalTimeout);
			}

			if (s == null && airAppTerminated(pl))
			{
				throw pl.createLaunchFailureException();
			}

			/* create a new session around this socket */
			session = PlayerSession.createFromSocketWithOptions(s, m_debuggerCallbacks, this);

			// transfer preferences
			session.setPreferences(m_prefs);
		}
		catch(NullPointerException npe)
		{
			throw new BindException(getLocalizationManager().getLocalizedTextString("serverSocketNotListening")); //$NON-NLS-1$
		}

		return session;
	}

	/**
	 * Returns true if the passed-in process listener is for an AIR application
	 * that has terminated. This is used by accept() in order to detect that it
	 * should give up listening on the socket.
	 * 
	 * The reason we can't do this for Flash player-based apps is that unlike
	 * AIR apps, the process that we launched sometimes acts as just sort of a
	 * "launcher" process that terminates quickly, and the actual Flash player
	 * is in some other process. For example, on Mac, we often invoke the "open"
	 * program to open a web browser; and on Windows, if you launch firefox.exe
	 * but it detects that there is already a running instance of firefox.exe,
	 * the new instance will just pass a message to the old instance, and then
	 * the new instance will terminate.
	 * 
	 * @param pl
	 *            a process listener, or <code>null</code>
	 * @return true if pl refers to an AIR app that has terminated.
	 */
	private boolean airAppTerminated(ProcessListener pl)
	{
		if (pl != null)
		{
			if (pl.isAIRApp())
			{
				if (pl.isProcessDead())
				{
					return true;
				}
			}
		}

		return false;
	}

	/*
	 * @see flash.tools.debugger.SessionManager#getDebuggerCallbacks()
	 */
	public IDebuggerCallbacks getDebuggerCallbacks()
	{
		return m_debuggerCallbacks;
	}

	/*
	 * @see flash.tools.debugger.SessionManager#setDebuggerCallbacks(flash.tools.debugger.IDebuggerCallbacks)
	 */
	public void setDebuggerCallbacks(IDebuggerCallbacks debuggerCallbacks)
	{
		m_debuggerCallbacks = debuggerCallbacks;
	}
	
	/**
	 * A private variation on <code>connect()</code> that also has an argument
	 * indicating that the process we are waiting for has terminated.
	 * 
	 * @param pl
	 *            Optional process listener. If non-null, this is used to detect
	 *            if a process that was launched has terminated unexpectedly.
	 *            For example, if launch() launches adl, but adl exits, then we
	 *            don't want to continue to wait for a socket connection.
	 */
	public Session connect(int port, IProgress waitReporter) throws IOException
	{
		final int waitTime = getPreference(PREF_CONNECT_WAIT_INTERVAL);
		final int maxRetryAttempts = getPreference(PREF_CONNECT_RETRY_ATTEMPTS);
		final int totalTimeout = getPreference(PREF_CONNECT_TIMEOUT);
		final long timeForConnectStart = System.currentTimeMillis();
		
		long elapsedTime = 0;
		int retryAttempts = -1;
		PlayerSession session = null;
		Socket s = null;		
		
		m_cancelConnect = false;		
		
		// Try to see if a connect happens till totalTimeout
		// If the connection was refused in between, retry
		// again after waitTime until totalTimeout is elapsed.
		// Retry mechanism is disabled if PREF_CONNECT_RETRY_ATTEMPTS
		// is 0.
		while (s == null)
		{
			try
			{
				InetSocketAddress localAddress = new InetSocketAddress(InetAddress.getByName(null), port);
				s = new Socket();
				//save the socket for canceling connect
				m_connectSocket = s;
				//connect to loopback address at the specified port
				s.connect(localAddress, totalTimeout);
			}
			catch(IOException ste)
			{
				if (ste instanceof SocketTimeoutException) {
					//if we timed out, abort connect
					abortConnect(ste);
				}

				safeCloseSocket(s);
				
				s = null;
				retryAttempts++;
				
				//if we should not retry, checkConnectTimeout 
				//throws an exception
				elapsedTime = checkConnectTimeout(waitTime, maxRetryAttempts,
						totalTimeout, retryAttempts, timeForConnectStart, ste);
			}

			// Tell the progress monitor we've waited a little while longer,
			// so that the Eclipse progress bar can keep chugging along
			if (waitReporter != null)
				waitReporter.setProgress((int)elapsedTime, totalTimeout);
			
			if (s != null) {
				/** If we connected, make sure that we get some response 
				 * back after sending the handshake. This is required because
				 * of the way port forwarding works. A connect will be successful
				 * if port forwarding is set up, but we won't get any response 
				 * unless the application is actually listening. 				
				 */				
				/* create a new session around this socket */
				session = PlayerSession.createFromSocketWithOptions(s, m_debuggerCallbacks, this);
				// transfer preferences
				session.setPreferences(m_prefs);
				try {
					session.bind();					
				}
				catch (VersionException ex) {
					session.unbind();
					safeCloseSocket(s);
					
					s = null;					
					retryAttempts++;
					
					/** The VersionException here is considered as an IOException
					 * because we do not know if there was even a valid application
					 * listening on the port. Once the port is forwarded, connect
					 * succeeds and we get a VersionException even if player is not
					 * listening on that port.
					 */
					elapsedTime = checkConnectTimeout(waitTime, maxRetryAttempts, 
							totalTimeout, retryAttempts, timeForConnectStart, 
							new IOException(ex.getLocalizedMessage()));
				}
			}
		}
		m_connectSocket = null;
		
		return session;
	}

	/**
	 * @param waitTime
	 * @param maxRetryAttempts
	 * @param totalTimeout
	 * @param retryAttempts
	 * @param startTime
	 * @param caughtException
	 * @return
	 * @throws IOException
	 */
	private long checkConnectTimeout(final int waitTime,
			final int maxRetryAttempts, final int totalTimeout,
			int retryAttempts, final long startTime, IOException caughtException)
			throws IOException {
		long elapsedTime;
		long endTime = System.currentTimeMillis();
		elapsedTime = endTime - startTime;
		
		// check if we should retry
		boolean retryFinished = (maxRetryAttempts != -1 && retryAttempts >= maxRetryAttempts);
		
		// check if we timed out or somebody called stopConnecting()
		if (retryFinished ||
				elapsedTime > totalTimeout || 
				m_cancelConnect ) {
			abortConnect(caughtException);
		}

		//wait a bit before retrying
		try {
			Thread.sleep(waitTime);
		} catch (InterruptedException e) {
			abortConnect(caughtException);
		}

		//check cancel before resuming
		if (m_cancelConnect ) {
			abortConnect(caughtException);
		}
		return elapsedTime;
	}

	/**
	 * @param ste
	 * @throws IOException
	 */
	private void abortConnect(IOException ste) throws IOException {
		m_connectSocket = null;
		m_cancelConnect = false;
		throw ste;
	}

	/**
	 * @param s
	 */
	private void safeCloseSocket(Socket s) {
		//clean up the socket
		if (s != null && !s.isClosed()) {
			try {
				s.close();
			}
			catch (IOException closeException) {
				//ignore
			}
		}
	}

	/*
	 * @see flash.tools.debugger.SessionManager#stopConnecting()
	 */
	public void stopConnecting() throws IOException
	{
		if (!m_cancelConnect) {
			m_cancelConnect = true;
			if (m_connectSocket != null)
			{			
				m_connectSocket.close();
				m_connectSocket = null;
			}
		}		
	}

	/*
	 * @see flash.tools.debugger.SessionManager#isConnecting()
	 */
	public boolean isConnecting()
	{
		return (m_connectSocket == null) ? false : true;
	}
	
	/**
	 * Returns the localization manager.  Use this for all localized strings.
	 */
	public static LocalizationManager getLocalizationManager()
	{
		return m_localizationManager;
	}

	@Override
	public Process launchForRun(String uri, AIRLaunchInfo airLaunchInfo,
			IProgress waitReporter, ILaunchNotification launchNotification)
			throws IOException {
		String[] launchCommand = getLaunchCommand(uri, airLaunchInfo, false);

		// create the process and attach a thread to watch it during our accept phase
		Process proc = m_debuggerCallbacks.launchDebugTarget(launchCommand);
		//forDebugging = false
		// If launching an AIR app, and forDebugging=false (meaning we are just running it,
		// not debugging it), start a background thread that will call the launchNotification
		// when the launch is complete.
		startProcessListener(airLaunchInfo, false,launchNotification,
				launchCommand, proc,true);
				
		return proc;
	}

	private ProcessListener startProcessListener(AIRLaunchInfo airLaunchInfo, boolean forDebugging,
			ILaunchNotification launchNotification, String[] launchCommand,
			Process proc, boolean isRunLaunch) {

		 ProcessListener processListener = new ProcessListener(launchCommand, proc, launchNotification, forDebugging, airLaunchInfo != null); // BUG FB-9874: launchNotifier added
		 processListener.setIsRunLaunch(isRunLaunch);

		// If launching an AIR app, and forDebugging=false (meaning we are just running it,
		// not debugging it), start a background thread that will call the launchNotification
		// when the launch is complete.
		if (!forDebugging && airLaunchInfo != null && launchNotification != null)
			processListener.startLaunchNotifier();

		return processListener;
	}

	private String[] getLaunchCommand(String uri, AIRLaunchInfo airLaunchInfo, boolean forDebugging)
			throws IOException, FileNotFoundException {

		String[] launchCommand;
		
		uri = uri.trim();

		if (airLaunchInfo == null)
		{
			LaunchInfo launchInfo = new LaunchInfo(uri);

			uri = tweakNativeLaunchUri(uri, forDebugging, launchInfo);

			launchCommand = getFlashLaunchArgs(uri, launchInfo);
		}
		else // else, AIR
		{
			launchCommand = getAIRLaunchArgs(uri, airLaunchInfo);
		}
		return launchCommand;
	}

	@Override
	public Process launchForRun(String uri, AIRLaunchInfo airLaunchInfo, IProgress waitReporter, ILaunchNotification launchNotification,
			ILauncher launcher) throws IOException {

		String[] launchCommand = getLaunchCommandForLauncher(uri, airLaunchInfo,false);

		// create the process and attach a thread to watch it during our accept phase
		Process proc = m_debuggerCallbacks.launchDebugTarget(launchCommand,launcher);
		//forDebugging = false
		// If launching an AIR app, and forDebugging=false (meaning we are just running it,
		// not debugging it), start a background thread that will call the launchNotification
		// when the launch is complete.
		startProcessListener(airLaunchInfo, false,launchNotification,
				launchCommand, proc, true);;
				
		return proc;
	}

	private String[] getLaunchCommandForLauncher(String uri, AIRLaunchInfo airLaunchInfo,boolean forDebugging)
			throws IOException, FileNotFoundException {
		String[] launchCommand;
		if (airLaunchInfo == null)
		{
			LaunchInfo launchInfo = new LaunchInfo(uri);

			uri = tweakNativeLaunchUri(uri, forDebugging, launchInfo);

			launchCommand = new String[]{uri};
		}
		else // else, AIR
		{
			launchCommand = getAIRLaunchArgs(uri, airLaunchInfo);
		}
		return launchCommand;
	}

	@Override
	public Session launch(String uri, AIRLaunchInfo airLaunchInfo, boolean forDebugging, IProgress waitReporter,
			ILaunchNotification launchNotification, ILauncher launcher) throws IOException {
		
		String[] launchCommand = getLaunchCommandForLauncher(uri, airLaunchInfo,forDebugging);

		// create the process and attach a thread to watch it during our accept phase
		Process proc = m_debuggerCallbacks.launchDebugTarget(launchCommand, launcher);

		ProcessListener processListener = startProcessListener(airLaunchInfo,forDebugging, launchNotification, launchCommand, proc,false); 
		PlayerSession session = null;

		if (forDebugging)
		{
			session = waitForConnection(uri, airLaunchInfo, waitReporter, proc, processListener);
			session.setLauncher(launcher);
		}

		return session;
	}
}
