using System;
using System.CodeDom.Compiler;
using System.Reflection;

namespace Icarus.Scripting
{
	/// <summary>
	/// This singleton class manages scripts. When a script is requested, the local register of recently compiled scripts will be checked, and if the script is already there, then it will be run. Otherwise, the script will be loaded from the hard drive and compiled and then executed.
	/// </summary>
	public class ScriptManager
	{
		#region Private Variables
		/// <summary>
		/// Singleton Implementation.
		/// </summary>
		private static ScriptManager mg_Instance = new ScriptManager();

		/// <summary>
		/// These are the parameters that will be used to compile scripts.
		/// </summary>
		private CompilerParameters m_CompilerParameters;

		/// <summary>
		/// Determines whether the recompile is forced or not.
		/// </summary>
		private bool m_ForceRecompile;

		/// <summary>
		/// The language that will be used to compile the scripts.
		/// </summary>
		private Language m_CurrentLanguage;
		#endregion

		/// <summary>
		/// Singleton Implementation.
		/// </summary>
		private ScriptManager() 
		{
			this.ForceRecompile = false;
			this.CurrentLanguage = Language.CSharp;

			InitializeCompilerParameters();
		}

		#region Fields
		/// <summary>
		/// Singleton Implementation.
		/// </summary>
		public static ScriptManager Instance 
		{
			get { return mg_Instance; }
		}

		/// <summary>
		/// Gets or sets the force recompile property.
		/// If it is true, then every time a script is requested, it will be recompiled.
		/// If it is false, then the first time the script will be recompiled, but the consecutive times it will be referenced without recompilation.
		/// </summary>
		public bool ForceRecompile 
		{
			get { return m_ForceRecompile; }
			set { m_ForceRecompile = value; }
		}

		/// <summary>
		/// Gets or sets the CurrentLanguage property. This is the language that will be used to compile the scripts.
		/// </summary>
		public Language CurrentLanguage 
		{
			get { return m_CurrentLanguage; }
			set { m_CurrentLanguage = value; }
		}
		#endregion

		/// <summary>
		/// Initializes the Compiler Parameters with some default values.
		/// </summary>
		private void InitializeCompilerParameters() 
		{
			m_CompilerParameters = new CompilerParameters();

			m_CompilerParameters.GenerateExecutable = false;
			m_CompilerParameters.GenerateInMemory = true;
			m_CompilerParameters.IncludeDebugInformation = false;

			//Add all the referenced assemblies here:
			Assembly[] myAssemblies = AppDomain.CurrentDomain.GetAssemblies();
			foreach (Assembly a in myAssemblies) 
			{
				m_CompilerParameters.ReferencedAssemblies.Add(a.Location);
			}
		}

		/// <summary>
		/// Compiles and executes the script passed in. CurrentLanguage scripting language is used to compile the script.
		/// </summary>
		/// <param name="scriptText">The text of the script to run.</param>
		/// <param name="args">The arguments to pass to the main function of the script, if so desired.</param>
		/// <returns>If there were any compilation errors (and thus the script was not run) they are returned. Otherwise, returns null, which signifies a successful run.</returns>
		public CompilerErrorCollection ExecuteScript(string scriptText, params string[] args) 
		{
			//Wrap the script in the appropriate template:
			string wrapper = ScriptingProvider.Instance.GetExecutableTemplateFor(this.CurrentLanguage);
			string wrappedScript = string.Format(wrapper, scriptText);

			//Set up the specific compiler parameters:
			m_CompilerParameters.GenerateInMemory = true;
			m_CompilerParameters.GenerateExecutable = true;

			//Compile the code:
			ICodeCompiler compiler = ScriptingProvider.Instance.GetCodeCompilerFor(this.CurrentLanguage);
			CompilerResults results = compiler.CompileAssemblyFromSource(m_CompilerParameters, wrappedScript);

			//If it's all good, execute:
			if (results.Errors == null || results.Errors.Count <= 0)
			{
				try 
				{
					results.CompiledAssembly.EntryPoint.Invoke(null, new object[]{ args });
				} 
				catch (Exception e) 
				{
					//Go down to the most important exception
					while (e.InnerException != null) 
					{
						e = e.InnerException;
					}

					//Print 'er out:
					Console.Error.WriteLine("The script threw a {0}:", e.GetType().Name);
					Console.Error.WriteLine("Error Message: {0}", e.Message);
					Console.Error.WriteLine("\t{0}", e);
					Console.Error.WriteLine("");
				}

				return null;
			}
			else
			{
				return results.Errors;
			}
		}

		/// <summary>
		/// Compiles and executes the file whose name is passed in. The language in which the script is written is deduced from the extension of the file.
		/// </summary>
		/// <param name="filename">The fully qualified name of the file to execute.</param>
		/// <param name="args">Optional arguments to pass to the main function of the script.</param>
		/// <returns>If there were any compilation errors, they are returned.</returns>
		public CompilerErrorCollection ExecuteFile(string filename, params string[] args)
		{
			//Figure out what language to use:
			string[] fileparts = filename.Split('.');
			string extension = fileparts[fileparts.Length - 1];
			Language lang = ScriptingProvider.Instance.GetLanguageForExtension(extension);

			//Set up the specific compiler parameters:
			m_CompilerParameters.GenerateInMemory = true;
			m_CompilerParameters.GenerateExecutable = true;

			//Compile the code:
			ICodeCompiler compiler = ScriptingProvider.Instance.GetCodeCompilerFor(lang);
			CompilerResults results = compiler.CompileAssemblyFromFile(m_CompilerParameters, filename);

			//If it's all good, execute:
			if (results.Errors == null || results.Errors.Count <= 0)
			{
				results.CompiledAssembly.EntryPoint.Invoke(null, new object[]{ args });

				return null;
			}
			else
			{
				return results.Errors;
			}
		}
	}
}
