/*
 * 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.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using libsecondlife;
using Nini.Config;
using Nwc.XmlRpc;
using OpenSim.Framework.Console;
using OpenSim.Framework.Servers;
using OpenSim.Region.Environment.Interfaces;
using OpenSim.Region.Environment.Scenes;

/*****************************************************
 *
 * XMLRPCModule
 * 
 * Module for accepting incoming communications from
 * external XMLRPC client and calling a remote data
 * procedure for a registered data channel/prim.
 * 
 * 
 * 1. On module load, open a listener port
 * 2. Attach an XMLRPC handler
 * 3. When a request is received:
 * 3.1 Parse into components: channel key, int, string
 * 3.2 Look up registered channel listeners
 * 3.3 Call the channel (prim) remote data method
 * 3.4 Capture the response (llRemoteDataReply)
 * 3.5 Return response to client caller
 * 3.6 If no response from llRemoteDataReply within
 *     RemoteReplyScriptTimeout, generate script timeout fault
 * 
 * Prims in script must:
 * 1. Open a remote data channel
 * 1.1 Generate a channel ID
 * 1.2 Register primid,channelid pair with module
 * 2. Implement the remote data procedure handler
 * 
 * llOpenRemoteDataChannel
 * llRemoteDataReply
 * remote_data(integer type, key channel, key messageid, string sender, integer ival, string sval)
 * llCloseRemoteDataChannel
 * 
 * **************************************************/

namespace OpenSim.Region.Environment.Modules
{
    public class XMLRPCModule : IRegionModule, IXMLRPC
    {
        private static readonly log4net.ILog m_log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        private object XMLRPCListLock = new object();
        private string m_name = "XMLRPCModule";
        private int RemoteReplyScriptWait = 300;
        private int RemoteReplyScriptTimeout = 9000;
        private int m_remoteDataPort = 0;
        private List<Scene> m_scenes = new List<Scene>();

        // <channel id, RPCChannelInfo>
        private Dictionary<LLUUID, RPCChannelInfo> m_openChannels;

        private Dictionary<LLUUID, RPCRequestInfo> m_rpcPending;
        private Dictionary<LLUUID, RPCRequestInfo> m_rpcPendingResponses;

        private Dictionary<LLUUID, SendRemoteDataRequest> m_pendingSRDResponses;

        public void Initialise(Scene scene, IConfigSource config)
        {
            try
            {
                m_remoteDataPort = config.Configs["Network"].GetInt("remoteDataPort", m_remoteDataPort);
            }
            catch (Exception)
            {
            }

            if (!m_scenes.Contains(scene))
            {
                m_scenes.Add(scene);

                scene.RegisterModuleInterface<IXMLRPC>(this);
            }
        }

        public void PostInitialise()
        {
            if (IsEnabled())
            {
                m_openChannels = new Dictionary<LLUUID, RPCChannelInfo>();
                m_rpcPending = new Dictionary<LLUUID, RPCRequestInfo>();
                m_rpcPendingResponses = new Dictionary<LLUUID, RPCRequestInfo>();
                m_pendingSRDResponses = new Dictionary<LLUUID, SendRemoteDataRequest>();

                // Start http server
                // Attach xmlrpc handlers
                m_log.Info("[REMOTE_DATA]: " +
                           "Starting XMLRPC Server on port " + m_remoteDataPort + " for llRemoteData commands.");
                BaseHttpServer httpServer = new BaseHttpServer((uint)m_remoteDataPort);
                httpServer.AddXmlRPCHandler("llRemoteData", XmlRpcRemoteData);
                httpServer.Start();
            }
        }

        public void Close()
        {
        }

        public string Name
        {
            get { return m_name; }
        }

        public bool IsSharedModule
        {
            get { return true; }
        }

        public bool IsEnabled()
        {
            return (m_remoteDataPort > 0);
        }

        /**********************************************
         * OpenXMLRPCChannel
         * 
         * Generate a LLUUID channel key and add it and
         * the prim id to dictionary <channelUUID, primUUID>
         * 
         * First check if there is a channel assigned for
         * this itemID.  If there is, then someone called
         * llOpenRemoteDataChannel twice.  Just return the
         * original channel.  Other option is to delete the
         * current channel and assign a new one.
         * 
         * ********************************************/

        public LLUUID OpenXMLRPCChannel(uint localID, LLUUID itemID)
        {
            LLUUID channel = new LLUUID();

            //Is a dupe?
            foreach (RPCChannelInfo ci in m_openChannels.Values)
            {
                if (ci.GetItemID().Equals(itemID))
                {
                    // return the original channel ID for this item
                    channel = ci.GetChannelID();
                    break;
                }
            }

            if (channel == LLUUID.Zero)
            {
                channel = LLUUID.Random();
                RPCChannelInfo rpcChanInfo = new RPCChannelInfo(localID, itemID, channel);
                lock (XMLRPCListLock)
                {
                    m_openChannels.Add(channel, rpcChanInfo);
                }
            }

            return channel;
        }

        // Delete channels based on itemID
        // for when a script is deleted
        public void DeleteChannels(LLUUID itemID)
        {

            if (m_openChannels != null)
            {
                ArrayList tmp = new ArrayList();

                lock (XMLRPCListLock)
                {
                    foreach (RPCChannelInfo li in m_openChannels.Values)
                    {
                        if (li.GetItemID().Equals(itemID))
                        {
                            tmp.Add(itemID);
                        }
                    }

                    System.Collections.IEnumerator tmpEnumerator = tmp.GetEnumerator();
                    while ( tmpEnumerator.MoveNext() )
                        m_openChannels.Remove((LLUUID)tmpEnumerator.Current);
                }

            }

        }

        /**********************************************
         * Remote Data Reply
         * 
         * Response to RPC message
         * 
         *********************************************/

        public void RemoteDataReply(string channel, string message_id, string sdata, int idata)
        {
            RPCRequestInfo rpcInfo;
            LLUUID message_key = new LLUUID(message_id);

            if (m_rpcPendingResponses.TryGetValue(message_key, out rpcInfo))
            {
                rpcInfo.SetStrRetval(sdata);
                rpcInfo.SetIntRetval(idata);
                rpcInfo.SetProcessed(true);
                m_rpcPendingResponses.Remove(message_key);
            }
        }

        /**********************************************
         * CloseXMLRPCChannel
         * 
         * Remove channel from dictionary
         * 
         *********************************************/

        public void CloseXMLRPCChannel(LLUUID channelKey)
        {
            if (m_openChannels.ContainsKey(channelKey))
                m_openChannels.Remove(channelKey);
        }


        public XmlRpcResponse XmlRpcRemoteData(XmlRpcRequest request)
        {
            XmlRpcResponse response = new XmlRpcResponse();

            Hashtable requestData = (Hashtable)request.Params[0];
            bool GoodXML = (requestData.Contains("Channel") && requestData.Contains("IntValue") &&
                            requestData.Contains("StringValue"));

            if (GoodXML)
            {
                LLUUID channel = new LLUUID((string)requestData["Channel"]);
                RPCChannelInfo rpcChanInfo;
                if (m_openChannels.TryGetValue(channel, out rpcChanInfo))
                {
                    string intVal = (string)requestData["IntValue"];
                    string strVal = (string)requestData["StringValue"];

                    RPCRequestInfo rpcInfo;

                    lock (XMLRPCListLock)
                    {
                        rpcInfo =
                            new RPCRequestInfo(rpcChanInfo.GetLocalID(), rpcChanInfo.GetItemID(), channel, strVal,
                                               intVal);
                        m_rpcPending.Add(rpcInfo.GetMessageID(), rpcInfo);
                    }

                    int timeoutCtr = 0;

                    while (!rpcInfo.IsProcessed() && (timeoutCtr < RemoteReplyScriptTimeout))
                    {
                        Thread.Sleep(RemoteReplyScriptWait);
                        timeoutCtr += RemoteReplyScriptWait;
                    }
                    if (rpcInfo.IsProcessed())
                    {
                        Hashtable param = new Hashtable();
                        param["StringValue"] = rpcInfo.GetStrRetval();
                        param["IntValue"] = Convert.ToString(rpcInfo.GetIntRetval());

                        ArrayList parameters = new ArrayList();
                        parameters.Add(param);

                        response.Value = parameters;
                        rpcInfo = null;
                    }
                    else
                    {
                        response.SetFault(-1, "Script timeout");
                        rpcInfo = null;
                    }
                }
                else
                {
                    response.SetFault(-1, "Invalid channel");
                }
            }

            return response;
        }

        public bool hasRequests()
        {
            lock (XMLRPCListLock)
            {
                if (m_rpcPending != null)
                    return (m_rpcPending.Count > 0);
                else
                    return false;
            }
        }

        public RPCRequestInfo GetNextCompletedRequest()
        {
            if (m_rpcPending != null)
            {
                lock (XMLRPCListLock)
                {
                    foreach (LLUUID luid in m_rpcPending.Keys)
                    {
                        RPCRequestInfo tmpReq;

                        if (m_rpcPending.TryGetValue(luid, out tmpReq))
                        {

                            if (!tmpReq.IsProcessed()) return tmpReq;
                        }
                    }
                }
            }
            return null;
        }

        public void RemoveCompletedRequest(LLUUID id)
        {
            lock (XMLRPCListLock)
            {
                RPCRequestInfo tmp;
                if (m_rpcPending.TryGetValue(id, out tmp))
                {
                    m_rpcPending.Remove(id);
                    m_rpcPendingResponses.Add(id, tmp);
                }
                else
                {
                    Console.WriteLine("UNABLE TO REMOVE COMPLETED REQUEST");
                }
            }
        }

        public LLUUID SendRemoteData(uint localID, LLUUID itemID, string channel, string dest, int idata, string sdata)
        {

            SendRemoteDataRequest req = new SendRemoteDataRequest(
                    localID, itemID, channel, dest, idata, sdata
                );
            m_pendingSRDResponses.Add(req.GetReqID(), req);
            return req.process();

        }

        public SendRemoteDataRequest GetNextCompletedSRDRequest()
        {
            if (m_pendingSRDResponses != null)
            {
                lock (XMLRPCListLock)
                {
                    foreach (LLUUID luid in m_pendingSRDResponses.Keys)
                    {
                        SendRemoteDataRequest tmpReq;

                        if (m_pendingSRDResponses.TryGetValue(luid, out tmpReq))
                        {
                            if (tmpReq.finished)
                                return tmpReq;
                        }
                    }
                }
            }
            return null;
        }

        public void RemoveCompletedSRDRequest(LLUUID id)
        {
            lock (XMLRPCListLock)
            {
                SendRemoteDataRequest tmpReq;
                if (m_pendingSRDResponses.TryGetValue(id, out tmpReq))
                {
                    m_pendingSRDResponses.Remove(id);
                }
            }
        }

        public void CancelSRDRequests(LLUUID itemID)
        {
            if (m_pendingSRDResponses != null)
            {
                lock (XMLRPCListLock)
                {
                    foreach (SendRemoteDataRequest li in m_pendingSRDResponses.Values)
                    {
                        if (li.m_itemID.Equals(itemID))
                            m_pendingSRDResponses.Remove(li.GetReqID());
                    }
                }
            }
        }
    }

    /**************************************************************
     * 
     * Class RPCRequestInfo
     * 
     * Holds details about incoming requests until they are picked
     * from the queue by LSLLongCmdHandler
     * ***********************************************************/

    public class RPCRequestInfo
    {
        private string m_StrVal;
        private string m_IntVal;
        private bool m_processed;
        private string m_respStr;
        private int m_respInt;
        private uint m_localID;
        private LLUUID m_ItemID;
        private LLUUID m_MessageID;
        private LLUUID m_ChannelKey;

        public RPCRequestInfo(uint localID, LLUUID itemID, LLUUID channelKey, string strVal, string intVal)
        {
            m_localID = localID;
            m_StrVal = strVal;
            m_IntVal = intVal;
            m_ItemID = itemID;
            m_ChannelKey = channelKey;
            m_MessageID = LLUUID.Random();
            m_processed = false;
            m_respStr = String.Empty;
            m_respInt = 0;
        }

        public bool IsProcessed()
        {
            return m_processed;
        }

        public LLUUID GetChannelKey()
        {
            return m_ChannelKey;
        }

        public void SetProcessed(bool processed)
        {
            m_processed = processed;
        }

        public void SetStrRetval(string resp)
        {
            m_respStr = resp;
        }

        public string GetStrRetval()
        {
            return m_respStr;
        }
        public void SetIntRetval(int resp)
        {
            m_respInt = resp;
        }

        public int GetIntRetval()
        {
            return m_respInt;
        }
        public uint GetLocalID()
        {
            return m_localID;
        }

        public LLUUID GetItemID()
        {
            return m_ItemID;
        }

        public string GetStrVal()
        {
            return m_StrVal;
        }

        public int GetIntValue()
        {
            return int.Parse(m_IntVal);
        }

        public LLUUID GetMessageID()
        {
            return m_MessageID;
        }
    }

    public class RPCChannelInfo
    {
        private LLUUID m_itemID;
        private uint m_localID;
        private LLUUID m_ChannelKey;

        public RPCChannelInfo(uint localID, LLUUID itemID, LLUUID channelID)
        {
            m_ChannelKey = channelID;
            m_localID = localID;
            m_itemID = itemID;
        }

        public LLUUID GetItemID()
        {
            return m_itemID;
        }

        public LLUUID GetChannelID()
        {
            return m_ChannelKey;
        }

        public uint GetLocalID()
        {
            return m_localID;
        }

    }

    public class SendRemoteDataRequest
    {

        public LLUUID reqID;
        public string destURL;
        public string channel;
        public string sdata;
        public int idata;
        public bool finished;
        public string response_sdata;
        public int response_idata;
        public XmlRpcRequest request;
        private Thread httpThread;
        public LLUUID m_itemID;
        public uint m_localID;
        private static readonly log4net.ILog m_log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        public SendRemoteDataRequest(uint localID, LLUUID itemID, string channel, string dest, int idata, string sdata)
        {

            this.channel = channel;
            this.destURL = dest;
            this.idata = idata;
            this.sdata = sdata;
            m_itemID = itemID;
            m_localID = localID;

            reqID = LLUUID.Random();

        }

        public LLUUID process()
        {
            httpThread = new Thread(SendRequest);
            httpThread.Name = "HttpRequestThread";
            httpThread.Priority = ThreadPriority.BelowNormal;
            httpThread.IsBackground = true;
            finished = false;
            httpThread.Start();
            OpenSim.Framework.ThreadTracker.Add(httpThread);

            return reqID;

        }

        /*
         * TODO: More work on the response codes.  Right now
         * returning 200 for success or 499 for exception
         */

        public void SendRequest()
        {
            Hashtable param = new Hashtable();

            // Check if channel is an LLUUID
            // if not, use as method name
            LLUUID parseUID;
            string mName = "llRemoteData";
            if( (channel != null) && (channel != "") )
                if( !LLUUID.TryParse(channel, out parseUID) )
                    mName = channel;
                else
                    param["Channel"] = channel;

            param["StringValue"] = sdata;
            param["IntValue"] = Convert.ToString(idata);

            ArrayList parameters = new ArrayList();
            parameters.Add(param);
            XmlRpcRequest req = new XmlRpcRequest(mName, parameters);
            try
            {
                XmlRpcResponse resp = req.Send(destURL, 30000);
                if (resp != null)
                {
                    Hashtable respParms;
                    if(resp.Value.GetType().Equals(Type.GetType("System.Collections.Hashtable"))) {
                        respParms = (Hashtable)resp.Value;
                    }
                    else {
                        ArrayList respData = (ArrayList)resp.Value;
                        respParms = (Hashtable)respData[0];
                    }
                    if (respParms != null)
                    {
                        if (respParms.Contains("StringValue"))
                        {
                            sdata = (string)respParms["StringValue"];
                        }
                        if (respParms.Contains("IntValue"))
                        {
                            idata = Convert.ToInt32((string)respParms["IntValue"]);
                        }
                        if (respParms.Contains("faultString"))
                        {
                            sdata = (string)respParms["faultString"];
                        }
                        if (respParms.Contains("faultCode"))
                        {
                            idata = Convert.ToInt32(respParms["faultCode"]);
                        }
                    }
                }
            }
            catch (System.Net.WebException we)
            {
                sdata = we.Message;
                m_log.Warn("[SendRemoteDataRequest]: Request failed");
                m_log.Warn(we.StackTrace);
            }

            finished = true;
        }

        public void Stop()
        {
            try
            {
                httpThread.Abort();
            }
            catch (Exception)
            {
            }
        }

        public LLUUID GetReqID()
        {
            return reqID;
        }
    }
}
