/*
 * Copyright (c) Contributors, http://opensimulator.org/
 * See CONTRIBUTORS.TXT for a full list of copyright holders.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the OpenSim Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/* Original code: Tedd Hansen */
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;
using libsecondlife;
using OpenSim.Framework;
using OpenSim.Grid.ScriptEngine.DotNetEngine.Compiler;
using OpenSim.Grid.ScriptEngine.DotNetEngine.Compiler.LSL;
using OpenSim.Region.Environment.Scenes;

namespace OpenSim.Grid.ScriptEngine.DotNetEngine
{
    /// <summary>
    /// Loads scripts
    /// Compiles them if necessary
    /// Execute functions for EventQueueManager (Sends them to script on other AppDomain for execution)
    /// </summary>
    [Serializable]
    public class ScriptManager
    {
        private static readonly log4net.ILog m_log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        #region Declares

        private Thread scriptLoadUnloadThread;
        private int scriptLoadUnloadThread_IdleSleepms = 100;
        private Queue<LoadStruct> loadQueue = new Queue<LoadStruct>();
        private Queue<UnloadStruct> unloadQueue = new Queue<UnloadStruct>();

        private struct LoadStruct
        {
            public uint localID;
            public LLUUID itemID;
            public string script;
        }

        private struct UnloadStruct
        {
            public uint localID;
            public LLUUID itemID;
        }

        // Object<string, Script<string, script>>
        // IMPORTANT: Types and MemberInfo-derived objects require a LOT of memory.
        // Instead use RuntimeTypeHandle, RuntimeFieldHandle and RunTimeHandle (IntPtr) instead!
        internal Dictionary<uint, Dictionary<LLUUID, LSL_BaseClass>> Scripts =
            new Dictionary<uint, Dictionary<LLUUID, LSL_BaseClass>>();

        public Scene World
        {
            get { return m_scriptEngine.World; }
        }

        #endregion

        #region Object init/shutdown

        private ScriptEngine m_scriptEngine;

        public ScriptManager(ScriptEngine scriptEngine)
        {
            m_scriptEngine = scriptEngine;
            AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
            scriptLoadUnloadThread = new Thread(ScriptLoadUnloadThreadLoop);
            scriptLoadUnloadThread.Name = "ScriptLoadUnloadThread";
            scriptLoadUnloadThread.IsBackground = true;
            scriptLoadUnloadThread.Priority = ThreadPriority.BelowNormal;
            scriptLoadUnloadThread.Start();
        }

        ~ScriptManager()
        {
            // Abort load/unload thread
            try
            {
                if (scriptLoadUnloadThread != null)
                {
                    if (scriptLoadUnloadThread.IsAlive == true)
                    {
                        scriptLoadUnloadThread.Abort();
                        scriptLoadUnloadThread.Join();
                    }
                }
            }
            catch
            {
            }
        }

        #endregion

        #region Load / Unload scripts (Thread loop)

        private void ScriptLoadUnloadThreadLoop()
        {
            try
            {
                while (true)
                {
                    if (loadQueue.Count == 0 && unloadQueue.Count == 0)
                        Thread.Sleep(scriptLoadUnloadThread_IdleSleepms);

                    if (loadQueue.Count > 0)
                    {
                        LoadStruct item = loadQueue.Dequeue();
                        _StartScript(item.localID, item.itemID, item.script);
                    }

                    if (unloadQueue.Count > 0)
                    {
                        UnloadStruct item = unloadQueue.Dequeue();
                        _StopScript(item.localID, item.itemID);
                    }
                }
            }
            catch (ThreadAbortException tae)
            {
                string a = tae.ToString();
                a = "";
                // Expected
            }
        }

        #endregion

        #region Helper functions

        private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            //Console.WriteLine("ScriptManager.CurrentDomain_AssemblyResolve: " + args.Name);
            return Assembly.GetExecutingAssembly().FullName == args.Name ? Assembly.GetExecutingAssembly() : null;
        }

        #endregion

        #region Internal functions to keep track of script

        internal Dictionary<LLUUID, LSL_BaseClass>.KeyCollection GetScriptKeys(uint localID)
        {
            if (Scripts.ContainsKey(localID) == false)
                return null;

            Dictionary<LLUUID, LSL_BaseClass> Obj;
            Scripts.TryGetValue(localID, out Obj);

            return Obj.Keys;
        }

        internal LSL_BaseClass GetScript(uint localID, LLUUID itemID)
        {
            if (Scripts.ContainsKey(localID) == false)
                return null;

            Dictionary<LLUUID, LSL_BaseClass> Obj;
            Scripts.TryGetValue(localID, out Obj);
            if (Obj.ContainsKey(itemID) == false)
                return null;

            // Get script
            LSL_BaseClass Script;
            Obj.TryGetValue(itemID, out Script);

            return Script;
        }

        internal void SetScript(uint localID, LLUUID itemID, LSL_BaseClass Script)
        {
            // Create object if it doesn't exist
            if (Scripts.ContainsKey(localID) == false)
            {
                Scripts.Add(localID, new Dictionary<LLUUID, LSL_BaseClass>());
            }

            // Delete script if it exists
            Dictionary<LLUUID, LSL_BaseClass> Obj;
            Scripts.TryGetValue(localID, out Obj);
            if (Obj.ContainsKey(itemID) == true)
                Obj.Remove(itemID);

            // Add to object
            Obj.Add(itemID, Script);
        }

        internal void RemoveScript(uint localID, LLUUID itemID)
        {
            // Don't have that object?
            if (Scripts.ContainsKey(localID) == false)
                return;

            // Delete script if it exists
            Dictionary<LLUUID, LSL_BaseClass> Obj;
            Scripts.TryGetValue(localID, out Obj);
            if (Obj.ContainsKey(itemID) == true)
                Obj.Remove(itemID);
        }

        #endregion

        #region Start/Stop/Reset script

        /// <summary>
        /// Fetches, loads and hooks up a script to an objects events
        /// </summary>
        /// <param name="itemID"></param>
        /// <param name="localID"></param>
        public void StartScript(uint localID, LLUUID itemID, string Script)
        {
            LoadStruct ls = new LoadStruct();
            ls.localID = localID;
            ls.itemID = itemID;
            ls.script = Script;
            loadQueue.Enqueue(ls);
        }

        /// <summary>
        /// Disables and unloads a script
        /// </summary>
        /// <param name="localID"></param>
        /// <param name="itemID"></param>
        public void StopScript(uint localID, LLUUID itemID)
        {
            UnloadStruct ls = new UnloadStruct();
            ls.localID = localID;
            ls.itemID = itemID;
            unloadQueue.Enqueue(ls);
        }

        public void ResetScript(uint localID, LLUUID itemID)
        {
            string script = GetScript(localID, itemID).SourceCode;
            StopScript(localID, itemID);
            StartScript(localID, itemID, script);
        }

        private void _StartScript(uint localID, LLUUID itemID, string Script)
        {
            //IScriptHost root = host.GetRoot();
            Console.WriteLine("ScriptManager StartScript: localID: " + localID + ", itemID: " + itemID);

            // We will initialize and start the script.
            // It will be up to the script itself to hook up the correct events.
            string ScriptSource = "";

            SceneObjectPart m_host = World.GetSceneObjectPart(localID);

            try
            {
                // Create a new instance of the compiler (currently we don't want reuse)
                Compiler.LSL.Compiler LSLCompiler = new Compiler.LSL.Compiler();
                // Compile (We assume LSL)
                ScriptSource = LSLCompiler.CompileFromLSLText(Script);
                //Console.WriteLine("Compilation of " + FileName + " done");
                // * Insert yield into code
                ScriptSource = ProcessYield(ScriptSource);


#if DEBUG
                long before;
                before = GC.GetTotalMemory(true);
#endif

                LSL_BaseClass CompiledScript;
                CompiledScript = m_scriptEngine.m_AppDomainManager.LoadScript(ScriptSource);

#if DEBUG
                Console.WriteLine("Script " + itemID + " occupies {0} bytes", GC.GetTotalMemory(true) - before);
#endif

                CompiledScript.SourceCode = ScriptSource;
                // Add it to our script memstruct
                SetScript(localID, itemID, CompiledScript);

                // We need to give (untrusted) assembly a private instance of BuiltIns
                //  this private copy will contain Read-Only FullitemID so that it can bring that on to the server whenever needed.


                LSL_BuiltIn_Commands LSLB = new LSL_BuiltIn_Commands(m_scriptEngine, m_host, localID, itemID);

                // Start the script - giving it BuiltIns
                CompiledScript.Start(LSLB);

                // Fire the first start-event
                m_scriptEngine.m_EventQueueManager.AddToScriptQueue(localID, itemID, "state_entry", new object[] {});
            }
            catch (Exception e)
            {
                //m_scriptEngine.m_log.Error("[ScriptEngine]: Error compiling script: " + e.ToString());
                try
                {
                    // DISPLAY ERROR INWORLD
                    string text = "Error compiling script:\r\n" + e.Message.ToString();
                    if (text.Length > 1500)
                        text = text.Substring(0, 1500);
                    World.SimChat(Helpers.StringToField(text), ChatTypeEnum.Say, 0, m_host.AbsolutePosition, m_host.Name, m_host.UUID);
                }
                catch (Exception e2)
                {
                    m_scriptEngine.m_log.Error("[ScriptEngine]: Error displaying error in-world: " + e2.ToString());
                }
            }
        }

        private void _StopScript(uint localID, LLUUID itemID)
        {
            // Stop script
            Console.WriteLine("Stop script localID: " + localID + " LLUID: " + itemID.ToString());


            // Stop long command on script
            m_scriptEngine.m_LSLLongCmdHandler.RemoveScript(localID, itemID);

            LSL_BaseClass LSLBC = GetScript(localID, itemID);
            if (LSLBC == null)
                return;

            // TEMP: First serialize it
            //GetSerializedScript(localID, itemID);


            try
            {
                // Get AppDomain
                AppDomain ad = LSLBC.Exec.GetAppDomain();
                // Tell script not to accept new requests
                GetScript(localID, itemID).Exec.StopScript();
                // Remove from internal structure
                RemoveScript(localID, itemID);
                // Tell AppDomain that we have stopped script
                m_scriptEngine.m_AppDomainManager.StopScript(ad);
            }
            catch (Exception e)
            {
                Console.WriteLine("Exception stopping script localID: " + localID + " LLUID: " + itemID.ToString() +
                                  ": " + e.ToString());
            }
        }

        private string ProcessYield(string FileName)
        {
            // TODO: Create a new assembly and copy old but insert Yield Code
            //return TempDotNetMicroThreadingCodeInjector.TestFix(FileName);
            return FileName;
        }

        #endregion

        #region Perform event execution in script

        /// <summary>
        /// Execute a LL-event-function in Script
        /// </summary>
        /// <param name="localID">Object the script is located in</param>
        /// <param name="itemID">Script ID</param>
        /// <param name="FunctionName">Name of function</param>
        /// <param name="args">Arguments to pass to function</param>
        internal void ExecuteEvent(uint localID, LLUUID itemID, string FunctionName, object[] args)
        {
            // Execute a function in the script
            //m_scriptEngine.m_log.Info("[ScriptEngine]: Executing Function localID: " + localID + ", itemID: " + itemID + ", FunctionName: " + FunctionName);
            LSL_BaseClass Script = m_scriptEngine.m_ScriptManager.GetScript(localID, itemID);
            if (Script == null)
                return;

            // Must be done in correct AppDomain, so leaving it up to the script itself
            Script.Exec.ExecuteEvent(FunctionName, args);
        }

        #endregion

        #region Script serialization/deserialization

        public void GetSerializedScript(uint localID, LLUUID itemID)
        {
            // Serialize the script and return it
            // Should not be a problem
            FileStream fs = File.Create("SERIALIZED_SCRIPT_" + itemID);
            BinaryFormatter b = new BinaryFormatter();
            b.Serialize(fs, GetScript(localID, itemID));
            fs.Close();
        }

        public void PutSerializedScript(uint localID, LLUUID itemID)
        {
            // Deserialize the script and inject it into an AppDomain

            // How to inject into an AppDomain?
        }

        #endregion
    }
}
