//BSD License  
//DXGfx® - http://www.eteractions.com
//Copyright (c) 2005
//by Guillaume Randon
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software
//and associated documentation files (the "Software"), to deal in the Software without restriction,
//including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
//and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
//subject to the following conditions:
//The above copyright notice and this permission notice shall be included in all copies or substantial
//portions of the Software.
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
//INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 
//AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
//DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Reflection;
using Microsoft.CSharp;
using System.CodeDom.Compiler;

namespace DXGfxLib
{
    /// <summary>
    /// With the addition of scripts to scene node, we needed a central class to perform compilation and other services for the
    /// Script class so that it becomes usable. This is the role of the script manager. This class keeps track of whether the
    /// script are up to date and everything. It manage the compilation of the scripts...
    /// 
    /// Important Note!! At the moment it is not possible to unload a specific assembly from an AppDomain although it is possible
    /// to fully unload an AppDomain. Each time script are modified an assembly is created on the fly. As they are never unloaded
    /// this means that sometime it might be good to restart the editor :). This is a bit ugly I confess. One option would be
    /// to have script run in a different app domain but this makes things much more complicated. Another option would be to
    /// have a loader for the editor which restart the editor from time to time.
    /// Need to double check if something was added with .Net 2.0 which would allow me to change the design of this. TODO.
    /// </summary>
    public class ScriptManager
    {
        /// <summary>
        /// Ok this is a Hashtable with all our scripts.
        /// </summary>
        private Hashtable scripts = null;

        /// <summary>
        /// A list of strings for all the assemblies we need for compilation.
        /// </summary>
        private string[] referencedAssemblies = null;

        /// <summary>
        /// We want only one instance of the script manager per app domain. We keep a reference to that unique instance here.
        /// </summary>
        private static ScriptManager globalInstance = null;

        /// <summary>
        /// The current version of the assembly that has been generated on the fly to contain scripts.
        /// </summary>
        private Assembly scriptsAssembly = null;

        /// <summary>
        /// Are our scripts ready to be executed or do we know that some scripts have been modified since last compilation.
        /// </summary>
        public bool scriptsAreReady = false;

        /// <summary>
        /// Potential errors that have happened during last compilation.
        /// </summary>
        public string lastCompilationErrorMessage = "";

        /// <summary>
        /// Constructor. As we want to authorise only one instance of this class per AppDomain we check if we have already created
        /// one and if we have we throw an exception here.
        /// </summary>
        public ScriptManager()
        {
            if (globalInstance != null)
            {
                throw new Exception("There is already one scriptManager we don't expect to have several active at the same time!");
            }
            else
            {
                globalInstance = this;
            }
            scripts = new Hashtable();
            BuildReferences();
        }

        /// <summary>
        /// This should dispose all resources owned by the script manager.
        /// </summary>
        public void Dispose()
        {
            globalInstance = null;
        }

        /// <summary>
        /// This static method will return a ref on our unique instance of ScriptManager in this appDomain.
        /// </summary>
        /// <returns></returns>
        public static ScriptManager GetGlobalInstance()
        {
            if (globalInstance == null)
            {
                throw new Exception("There is no scriptManager!");
            }
            return globalInstance;
        }

        /// <summary>
        /// When a Script is added to a scene node, the script needs to be registered to the ScriptManage if you want it to be compiled.
        /// At the moment this is done by the scene node code itself.
        /// If the script is already registerd it won't be registered twice you can safely call this method without creating havok.
        /// </summary>
        /// <param name="script"></param>
        public void RegisterScript(Script script)
        {
            if (!scripts.Contains(script.name))
            {
                scripts.Add(script.name, script);
            }
        }

        /// <summary>
        /// If a script has been updated by the user you should call this.
        /// </summary>
        /// <param name="script"></param>
        public void ScriptUpdated(Script script)
        {
            RegisterScript(script);
            scriptsAreReady = false;
        }

        /// <summary>
        /// When you don't want a script anymore you can unregister it.
        /// </summary>
        /// <param name="script"></param>
        public void UnRegisterScript(Script script)
        {
            scripts.Remove(script.name);
        }

        /// <summary>
        /// Ask the ScriptManager instance to compile all script if needed.
        /// </summary>
        public void GetReady()
        {
            if (!scriptsAreReady)
            {
                if (scriptsAssembly != null)
                {
                    // It is still not possible to unload an assembly.
                    // Putting the assembly in a different appdomain to unload it requires a lot of additional work.
                    // We do nothing here for now.
                }
                lastCompilationErrorMessage = "";
                scriptsAssembly = Compile(ref lastCompilationErrorMessage);
            }
        }

        /// <summary>
        /// This method will compile everything and return errors if any in the string errors.
        /// </summary>
        /// <param name="errors"></param>
        /// <returns></returns>
        public Assembly Compile(ref string errors)
        {
            // Obtains in-memory running instance of the C# compiler
            CSharpCodeProvider csc = new CSharpCodeProvider();
            ICodeCompiler icc = csc.CreateCompiler();

            // Set input params for the compiler
            CompilerParameters co = new CompilerParameters();


            foreach (string referencedAssembly in referencedAssemblies)
            {
                co.ReferencedAssemblies.Add(referencedAssembly);
            }

            IDictionaryEnumerator scriptEnum = scripts.GetEnumerator();
            string[] sources = new string[scripts.Count];
            int count = 0;

            string tempScriptsFolderName = "tmpScripts";
            while (scriptEnum.MoveNext())
            {
                Script script = scriptEnum.Value as Script;
                if (script != null)
                {
                    if (!Directory.Exists(tempScriptsFolderName))
                    {
                        Directory.CreateDirectory(tempScriptsFolderName);
                    }
                    string csFileName = tempScriptsFolderName + "\\" + StringUtil.GetName(script.fileName);
                    File.WriteAllText(csFileName, script.GetSrcForCompilation());
                    sources[count] = csFileName;
                    // We do recompile our script assembly so we need to reset all scripts
                    // as references to current instances of derived class are going to be invalid.
                    script.Reset();
                    count++;
                }
                else
                {
                    throw new Exception("Scripts Hashtable is corrupted!");
                }
            }

            co.GenerateInMemory = true;

            // Run the compiling process
            CompilerResults results = icc.CompileAssemblyFromFileBatch(co, sources);

            if (results.Errors.HasErrors)
            {
                scriptsAreReady = false;

                foreach (CompilerError error in results.Errors)
                {
                    errors = errors + error.ErrorNumber + " " + error.ErrorText + " " + error.FileName + " " + error.Line + "\n";
                }

                return null;
            }
            else
            {
                errors = "Compilation Ok";
                scriptsAreReady = true;
            }

            return results.CompiledAssembly;
        }

        /// <summary>
        /// This method build a list of string containing all the references we should need.
        /// </summary>
        public void BuildReferences()
        {
            string managedDirect3DXPath = Environment.GetEnvironmentVariable("windir") + @"\Microsoft.NET\DirectX for Managed Code\1.0.2911.0\";
            string managedDirect3DPath = Environment.GetEnvironmentVariable("windir") + @"\Microsoft.NET\DirectX for Managed Code\1.0.2902.0\";

            string binFolder = AppDomain.CurrentDomain.BaseDirectory;

            referencedAssemblies = new string[] {"system.dll", "system.data.dll", "system.drawing.dll", 
                "system.windows.forms.dll", "system.xml.dll", 
                binFolder + "DXCommon.dll",
                binFolder + "DXGfxLib.dll", 
                managedDirect3DPath + "Microsoft.DirectX.dll", 
                managedDirect3DPath + "Microsoft.DirectX.Direct3D.dll", 
                managedDirect3DXPath + "Microsoft.DirectX.Direct3DX.dll", 
                managedDirect3DPath + "Microsoft.DirectX.DirectInput.dll"};
        }

        /// <summary>
        /// This method return the list of references.
        /// </summary>
        /// <returns></returns>
        public string[] GetReferences()
        {
            return referencedAssemblies;
        }

        /// <summary>
        /// This allow us to set a new list of references to the ScriptManager is we want to.
        /// </summary>
        /// <param name="references"></param>
        public void SetReferences(string[] references)
        {
            referencedAssemblies = references;
        }

        /// <summary>
        /// This will create an instance of a class deriving from ExecutableBlock for us. This is central to how our scripts
        /// run.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public ExecutableBlock GetNewExecutableBlock(string name)
        {
            Type desiredType = scriptsAssembly.GetType("DXGfxLib." + name);
            ConstructorInfo ctor = desiredType.GetConstructor(new Type[] { });
            Object[] args = new Object[] { };
            Object obj = ctor.Invoke(args);

            ExecutableBlock newExecutableBlock = obj as ExecutableBlock;

            if (newExecutableBlock == null)
            {
                throw new Exception("Type doesn't derive from ExecutableBlock this is terribly wrong!!");
            }

            return newExecutableBlock;
        }
    }
}
