/**
 * Copyright (c) 2005-2008 Aptana, Inc.
 *
 * 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. If redistributing this code,
 * this entire header must remain intact.
 */
package org.eclipse.eclipsemonkey.lang.javascript;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.eclipse.core.runtime.Platform;
import org.eclipse.eclipsemonkey.utils.StringUtils;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.console.ConsolePlugin;
import org.eclipse.ui.console.IConsole;
import org.eclipse.ui.console.MessageConsoleStream;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.osgi.framework.Bundle;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
 * Provides global functions to JavaScript environment
 * 
 * @author Paul Colton
 * @author Kevin Lindsey
 */
public class JavaScriptGlobal extends ScriptableObject
{
	/**
	 * The "location" property name
	 */
	public static final String LOCATION_PROPERTY = "location";	//$NON-NLS-1$
	
	/**
	 * The "classLoader" property name
	 */
	public static final String CLASS_LOADER_PROPERTY = "classLoader";	//$NON-NLS-1$
	
	private static final String INCLUDES_PROPERTY = "includes";	//$NON-NLS-1$
	private static final long serialVersionUID = -8969608471837413334L;
	
	private static JavaScriptConsole _console;
	private static MessageConsoleStream _consoleStream;
	
	private JavaScriptPrintStream _err;
	private Map _runningSetTimeouts = new HashMap();
	private int _setTimeoutIndex;
	
	private static ImageDescriptor scriptingDescriptor = JavaScriptPlugin.getImageDescriptor("icons/js_file.gif");

	/**
	 * Provides global functions to the JavaScript environment
	 * 
	 * @param cx
	 *            The currently active script context
	 */
	public JavaScriptGlobal(Context cx)
	{
		// create unsealed standard objects
		cx.initStandardObjects(this, false);
	
		// create global properties
		this.createAllProperties();
	}
	
	/**
	 * @see org.mozilla.javascript.ScriptableObject#getClassName()
	 */
	public String getClassName()
	{
		return "JavaScriptGlobal";
	}
	
	/**
	 * Returns a reference to the current console, initializing it if it's not created
	 * 
	 * @return A console stream
	 */
	public static MessageConsoleStream getConsoleStream()
	{
		if (_console == null)
		{
			_console = new JavaScriptConsole("Scripting Console", scriptingDescriptor); //$NON-NLS-1$
			_consoleStream = _console.newMessageStream();

			PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable()
			{
				public void run()
				{
					_consoleStream.setColor(PlatformUI.getWorkbench().getDisplay().getSystemColor(SWT.COLOR_BLUE));
				}
			});

			ConsolePlugin.getDefault().getConsoleManager().addConsoles(new IConsole[] { _console });
			_consoleStream.println("Aptana JavaScript Scripting Console Started");
		}

		return _consoleStream;
	}
	
	/**
	 * Return a list of all property names. All names return by this method must appear within the definition of this
	 * class. Sub-classes should override this method to augment the return value to include functions defined within
	 * the sub-class itself.
	 * 
	 * @return Returns a list of function property names to add to this global
	 */
	protected String[] getFunctionPropertyNames()
	{
		return new String[] {
			"alert", //$NON-NLS-1$
			"clearTimeout", //$NON-NLS-1$
			"confirm", //$NON-NLS-1$
			"execute", //$NON-NLS-1$
			"getProperty", //$NON-NLS-1$
			"include", //$NON-NLS-1$
			"loadBundle", //$NON-NLS-1$
			"parseXML", //$NON-NLS-1$
			"prompt", //$NON-NLS-1$
			"runOnUIThread", //$NON-NLS-1$
			"setClassLoader", //$NON-NLS-1$
			"setTimeout", //$NON-NLS-1$
		};
	}
	
	/**
	 * Get a list of the full paths of all files in the Active Library view
	 * 
	 * @param property
	 *            The system property name to retrieve
	 * @return Returns a string array of full paths
	 */
	public String getProperty(String property)
	{
		String result = null;
	
		if (property != null && property.length() > 0)
		{
			result = System.getProperty(property, "undefined"); //$NON-NLS-1$
		}
	
		return result;
	}
	
	/**
	 * Get all text from the given input stream
	 * 
	 * @param stream
	 *            The input stream
	 * @return Returns all text from the input stream
	 * @throws IOException
	 */
	protected static String getText(InputStream stream) throws IOException
	{
		// create output buffer
		StringWriter sw = new StringWriter();

		// read contents into a string buffer
		try
		{
			// get buffered reader
			InputStreamReader isr = new InputStreamReader(stream);
			BufferedReader reader = new BufferedReader(isr);

			// create temporary buffer
			char[] buf = new char[1024];

			// fill buffer
			int numRead = reader.read(buf);

			// keep reading until the end of the stream
			while (numRead != -1)
			{
				// output temp buffer to output buffer
				sw.write(buf, 0, numRead);

				// fill buffer
				numRead = reader.read(buf);
			}
		}
		finally
		{
			if (stream != null)
			{
				stream.close();
			}
		}

		// return string buffer's content
		return sw.toString();
	}

	/**
	 * Execute a command-line in the system shell
	 * 
	 * @param cx
	 * @param thisObj
	 * @param args
	 *            The string command to execute
	 * @param funObj
	 * @return Returns an object array where the first object is the return code, the second object is the text from
	 *         stdout, and the third object is the text from stderr
	 */
	public static Scriptable execute(Context cx, Scriptable thisObj, Object[] args, Function funObj)
	{
		Scriptable result = null;
	
		if (args.length > 0)
		{
			String command = args[0].toString();
			String input = StringUtils.EMPTY;
	
			if (args.length > 1)
			{
				input = args[1].toString();
			}
	
			Process p = null;
			int retCode = 0;
			String stdout = StringUtils.EMPTY;
			String stderr = StringUtils.EMPTY;
	
			try
			{
				p = Runtime.getRuntime().exec(command);
				p.getOutputStream().write(input.getBytes());
				p.getOutputStream().flush();
				p.getOutputStream().close();
				retCode = p.waitFor();
				// retCode = p.exitValue();
				stdout = getText(p.getInputStream());
				stderr = getText(p.getErrorStream());
	
			}
			catch (IOException e)
			{
				if (p != null)
				{
					retCode = p.exitValue();
				}
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}
	
			// create Object
			Scriptable scope = ScriptableObject.getTopLevelScope(thisObj);
	
			result = cx.newObject(scope, "Object"); //$NON-NLS-1$
	
			// set property values
			result.put("code", result, new Integer(retCode)); //$NON-NLS-1$
			result.put("stdout", result, stdout); //$NON-NLS-1$
			result.put("stderr", result, stderr); //$NON-NLS-1$
		}
	
		// return result
		return result;
	}

	/**
	 * Popup an alert box with the specified message
	 * 
	 * @param message
	 *            The message to display in an alert dialog
	 */
	public void alert(final String message)
	{
		final Display currentDisplay = Display.getCurrent();

		if (currentDisplay != null)
		{
			currentDisplay.syncExec(new Runnable()
			{
				public void run()
				{
					Shell shell = currentDisplay.getActiveShell();

					if (shell != null)
					{
						MessageDialog.openWarning(shell, "Aptana Scripting Alert", message); //$NON-NLS-1$
					}
				}
			});
		}
	}

	/**
	 * Stop a setTimeout from firing its associated function
	 * 
	 * @param timeoutId
	 *            The setTimeout id returned when setTimeout was invoked. If the id is not recognized, this method does
	 *            nothing
	 */
	public void clearTimeout(int timeoutId)
	{
		synchronized (this._runningSetTimeouts)
		{
			Integer id = new Integer(timeoutId);

			if (this._runningSetTimeouts.containsKey(id))
			{
				this._runningSetTimeouts.remove(id);
			}
		}
	}

	/**
	 * Popup an confirm box with the specified message
	 * 
	 * @param message
	 *            The message to display at the confirmation prompt
	 * @return boolean
	 */
	public boolean confirm(final String message)
	{
		/**
		 * inner class for result
		 */
		class Answer
		{
			public boolean result = false;
		}

		// create instance of inner class
		final Answer a = new Answer();

		// get reply from user
		final Display currentDisplay = Display.getCurrent();

		if (currentDisplay != null)
		{
			currentDisplay.syncExec(new Runnable()
			{
				public void run()
				{
					Shell shell = currentDisplay.getActiveShell();

					if (shell != null)
					{
						a.result = MessageDialog.openConfirm(shell, "Aptana Scripting Confirm", message); //$NON-NLS-1$
					}
				}
			});
		}

		return a.result;
	}

	/**
	 * Create all properties for this global instance
	 */
	protected void createAllProperties()
	{
		this.createProperties();
		this.createFunctionProperties();
	}

	/**
	 * Create all function properties for this global. Sub-classes can override this method to modify how function
	 * properties are added to global; however, most sub-classes will need only to override getFunctionPropertyNames to
	 * include the additional function names provided by that instance.
	 */
	protected void createFunctionProperties()
	{
		String[] propertyNames = this.getFunctionPropertyNames();

		if (propertyNames != null)
		{
			this.defineFunctionProperties(propertyNames, this.getClass(), READONLY | PERMANENT);
		}
	}

	/**
	 * Create all non-function properties for this global. Sub-classes should override this method to add their own
	 * non-function properties to this global
	 */
	protected void createProperties()
	{
		// create standard error stream, cache, and add to global
		this._err = new JavaScriptPrintStream(this, System.err);
		this.defineProperty("err", _err, READONLY | PERMANENT); //$NON-NLS-1$

		// get standard out stream and add to global
		MessageConsoleStream stream = getConsoleStream();
		this.defineProperty("out", stream, READONLY | PERMANENT); //$NON-NLS-1$
	}

	/**
	 * Load a script into a currently executing library
	 * 
	 * @param cx
	 *            The scripting context
	 * @param thisObj
	 *            The object that activated this function call
	 * @param args
	 *            The arguments passed to this function call
	 * @param funObj
	 *            The function object that invoked this method
	 */
	public static void include(Context cx, Scriptable thisObj, Object[] args, Function funObj)
	{
		if (args.length > 0)
		{
			String libraryName = Context.toString(args[0]);
			java.io.File library = new java.io.File(libraryName);

			if (library.exists() == false)
			{
				String location = Context.toString(thisObj.get(LOCATION_PROPERTY, thisObj));
				java.io.File parent = new java.io.File(location).getParentFile();
				String name = parent.getAbsolutePath() + java.io.File.separator + libraryName;

				library = new java.io.File(name);
			}

			if (library.exists())
			{
				String absolutePath = library.getAbsolutePath();
				Scriptable includes;
				
				if (thisObj.has(INCLUDES_PROPERTY, thisObj))
				{
					includes = (Scriptable) thisObj.get(INCLUDES_PROPERTY, thisObj);
				}
				else
				{
					includes = cx.newObject(thisObj);
					
					((ScriptableObject) thisObj).defineProperty(INCLUDES_PROPERTY, includes, READONLY | PERMANENT);
				}
				
				// only compile and execute script if we haven't already
				if (includes.has(absolutePath, includes) == false)
				{
					try
					{
						// grab the script's source
						String source = JavaScriptGlobal.getText(new FileInputStream(library));
	
						// compile the script
						Script script = cx.compileString(source, absolutePath, 1, null);
	
						// tag as loaded
						includes.put(absolutePath, includes, "");
						
						// exec
						script.exec(cx, thisObj);
					}
					catch (IOException e)
					{
						// I/O error reading library
					}
				}
			}
			else
			{
				// cannot locate include
			}
		}
		else
		{
			// no include file defined
		}
	}
	
	/**
	 * Load the specified bundle and add it to the active JS class loader
	 * 
	 * @param cx
	 *            The scripting context
	 * @param thisObj
	 *            The object that activated this function call
	 * @param args
	 *            The arguments passed to this function call
	 * @param funObj
	 *            The function object that invoked this method
	 */
	public static void loadBundle(Context cx, Scriptable thisObj, Object[] args, Function funObj)
	{
		if (args.length > 0)
		{
			String bundleName = Context.toString(args[0]);
			Bundle bundle = Platform.getBundle(bundleName);
	
			if (bundle == null)
			{
				throw new RuntimeException("Global_Bundle_Not_Found: " + bundleName);
			}
	
			ClassLoader c = cx.getApplicationClassLoader();
	
			if (c instanceof JavaScriptClassLoader)
			{
				JavaScriptClassLoader classLoader = (JavaScriptClassLoader) c;
				classLoader.addBundle(bundle);
			}
			else
			{
				throw new RuntimeException("JavaScriptClassLoader not the application Classloader.");
			}
		}
	}
	
	/**
	 * Parse the specified XML string and return a W3C DOM as a result
	 * 
	 * @param xml
	 *            The source XML string
	 * @return Document The resulting Document element or null
	 */
	public Document parseXML(String xml)
	{
		Document result = null;

		if (xml != null && xml.length() > 0)
		{
			try
			{
				// get the document builder factory
				DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

				// create the document builder
				DocumentBuilder builder = factory.newDocumentBuilder();

				// create a stream from our XML string
				InputStream in = new ByteArrayInputStream(xml.getBytes());

				// parse XML to create our document element
				result = builder.parse(in);
			}
			catch (ParserConfigurationException e)
			{
				e.printStackTrace();
			}
			catch (SAXException e)
			{
				e.printStackTrace();
			}
			catch (IOException e)
			{
				e.printStackTrace();
			}
		}

		return result;
	}

	/**
	 * Popup an prompt box with the specified message
	 * 
	 * @param cx
	 *            The scripting context
	 * @param thisObj
	 *            The object that activated this function call
	 * @param args
	 *            The message to display in the prompt dialog
	 * @param funObj
	 *            The function object that invoked this method
	 * @return boolean
	 */
	public static Object prompt(Context cx, Scriptable thisObj, Object[] args, Function funObj)
	{
		final Display currentDisplay = Display.getCurrent();
		String messageArg = "Prompt";
		String defaultValueArg = "";
	
		if (args.length > 0)
		{
			messageArg = Context.toString(args[0]);
		}
		if (args.length > 1)
		{
			defaultValueArg = Context.toString(args[1]);
		}
	
		/**
		 * Answer
		 */
		class Answer
		{
			public Object result = "";
		}
	
		final String message = messageArg;
		final String defaultValue = defaultValueArg;
		final Answer a = new Answer();
	
		if (currentDisplay != null)
		{
			currentDisplay.syncExec(new Runnable()
			{
	
				public void run()
				{
					Shell shell = currentDisplay.getActiveShell();
	
					if (shell != null)
					{
						InputDialog dialog = new InputDialog(null, "Aptana Scripting Prompt", message, defaultValue, null); //$NON-NLS-1$
						int dialogResult = dialog.open();
						if (dialogResult == Window.OK)
						{
							a.result = dialog.getValue();
						}
						else
						{
							a.result = Context.getUndefinedValue();
						}
					}
				}
			});
		}
	
		return a.result;
	}
	
	/**
	 * Execute the specified function on the UI thread
	 * 
	 * @param cx
	 *            The scripting context
	 * @param thisObj
	 *            The object that activated this function call
	 * @param args
	 *            The message to display in the prompt dialog
	 * @param funObj
	 *            The function object that invoked this method
	 */
	public static void runOnUIThread(Context cx, Scriptable thisObj, Object[] args, Function funObj)
	{
		if (args.length > 0 && args[0] instanceof Function)
		{
			Function f = (Function) args[0];
	
			// get display
			final IWorkbench workbench = PlatformUI.getWorkbench();
			Display display = workbench.getDisplay();
	
			// execute callback in the correct thread
			display.asyncExec(new JavaScriptThread(f.getParentScope(), f, new Object[0]));
		}
	}
	
	/**
	 * setClassLoader
	 * 
	 * @param cx 
	 * @param thisObj 
	 * @param args 
	 * @param funObj 
	 * @return new class loader or class loader passed into method
	 */
	public static ClassLoader setClassLoader(Context cx, Scriptable thisObj, Object[] args, Function funObj)
	{
		ClassLoader result = null;
		
		if (args.length > 0)
		{
			Object arg = args[0];
			
			if (arg instanceof ClassLoader)
			{
				result = cx.getApplicationClassLoader();
				
				cx.setApplicationClassLoader((ClassLoader) arg);
			}
		}
//		else
//		{
//			result = cx.getApplicationClassLoader();
//			
//			cx.setApplicationClassLoader(new JavaScriptClassLoader());
//		}
		
		return result;
	}
	
	/**
	 * setTimeout
	 * 
	 * @param function
	 *            The function to invoke once this timer completes
	 * @param timeout
	 *            The amount of time to wait in milliseconds before firing the specified function
	 * @return int Returns a handle to this specific timeout instance. This handle can be used in clearTimeout to cancel
	 *         the timer before it fires
	 */
	public int setTimeout(final Function function, final int timeout)
	{
		final JavaScriptGlobal self = this;
		final int timeoutIndex = self._setTimeoutIndex++;
	
		Thread thread = new Thread(new Runnable()
		{
			public void run()
			{
				boolean active = true;
	
				try
				{
					// sleep
					Thread.sleep(timeout);
	
					// check if this was canceled while sleeping
					synchronized (self._runningSetTimeouts)
					{
						active = self._runningSetTimeouts.containsKey(new Integer(timeoutIndex));
					}
	
					// call the associated function if this is still active
					if (active)
					{
						// get display
						final IWorkbench workbench = PlatformUI.getWorkbench();
	
						if (workbench == null)
						{
							return;
						}
	
						Display display = workbench.getDisplay();
	
						if (display == null || display.isDisposed())
						{
							return;
						}
	
						// execute callback in the correct thread
						display.asyncExec(new Runnable()
						{
							public void run()
							{
								Scriptable scope = function.getParentScope();
								Context cx = Context.enter();
	
								function.call(cx, scope, scope, new Object[0]);
	
								Context.exit();
	
								self.clearTimeout(timeoutIndex);
							}
						});
					}
				}
				catch (InterruptedException e)
				{
					e.printStackTrace();
				}
			}
		}, "Aptana Scripting JavaScript setTimeout"); //$NON-NLS-1$
	
		// save reference to this thread to indicate that it should run. Note that clearTimeout will remove this entry
		// and if that is done before the thread wakes, then the action in the thread will not execute
		this._runningSetTimeouts.put(new Integer(timeoutIndex), thread);
	
		thread.setDaemon(true);
	
		// start the thread
		thread.start();
	
		// return the timeout index in case the thread wants to abort this timeout
		return timeoutIndex;
	}
}
