/*
 * 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.Net;
using System.Net.Sockets;
using System.Reflection;
using log4net;
using Mono.Addins;
using Nwc.XmlRpc;
using OpenSim.Framework;
using OpenSim.Framework.Servers;

[assembly : Addin("RegionProxy", "0.1")]
[assembly : AddinDependency("OpenSim", "0.5")]

namespace OpenSim.ApplicationPlugins.RegionProxy
{
    /* This module has an interface to OpenSim clients that is constant, and is responsible for relaying
     * messages to and from clients to the region objects. Since the region objects can be duplicated and
     * moved dynamically, the proxy provides methods for changing and adding regions. If more than one region
     * is associated with a client port, then the message will be broadcasted to all those regions.
     *
     * The client interface port may be blocked. While being blocked, all messages from the clients will be
     * stored in the proxy. Once the interface port is unblocked again, all stored messages will be resent
     * to the regions. This functionality is used when moving or cloning an region to make sure that no messages
     * are sent to the region while it is being reconfigured.
     *
     * The proxy opens a XmlRpc interface with these public methods:
     * - AddPort
     * - AddRegion
     * - ChangeRegion
     * - BlockClientMessages
     * - UnblockClientMessages
     */

    [Extension("/OpenSim/Startup")]
    public class RegionProxyPlugin : IApplicationPlugin
    {
        private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
        private BaseHttpServer command_server;
        private ProxyServer proxy;

        #region IApplicationPlugin Members

        public void Initialise(OpenSimBase openSim)
        {
            m_log.Info("Starting proxy");
            string proxyURL = openSim.ConfigSource.Configs["Network"].GetString("proxy_url", "");
            if (proxyURL.Length == 0) return;

            uint port = (uint) Int32.Parse(proxyURL.Split(new char[] {':'})[2]);
            command_server = new BaseHttpServer(port);
            command_server.Start();
            command_server.AddXmlRPCHandler("AddPort", AddPort);
            command_server.AddXmlRPCHandler("AddRegion", AddRegion);
            command_server.AddXmlRPCHandler("DeleteRegion", DeleteRegion);
            command_server.AddXmlRPCHandler("ChangeRegion", ChangeRegion);
            command_server.AddXmlRPCHandler("BlockClientMessages", BlockClientMessages);
            command_server.AddXmlRPCHandler("UnblockClientMessages", UnblockClientMessages);
            command_server.AddXmlRPCHandler("Stop", Stop);

            proxy = new ProxyServer(m_log);
        }

        public void Close()
        {
        }

        #endregion

        private XmlRpcResponse Stop(XmlRpcRequest request)
        {
            try
            {
                proxy.Stop();
            }
            catch (Exception e)
            {
                m_log.Error("[PROXY]" + e.Message);
                m_log.Error("[PROXY]" + e.StackTrace);
            }
            return new XmlRpcResponse();
        }

        private XmlRpcResponse AddPort(XmlRpcRequest request)
        {
            try
            {
                int clientPort = (int) request.Params[0];
                int regionPort = (int) request.Params[1];
                string regionUrl = (string) request.Params[2];
                proxy.AddPort(clientPort, regionPort, regionUrl);
            }
            catch (Exception e)
            {
                m_log.Error("[PROXY]" + e.Message);
                m_log.Error("[PROXY]" + e.StackTrace);
            }
            return new XmlRpcResponse();
        }

        private XmlRpcResponse AddRegion(XmlRpcRequest request)
        {
            try
            {
                int currentRegionPort = (int) request.Params[0];
                string currentRegionUrl = (string) request.Params[1];
                int newRegionPort = (int) request.Params[2];
                string newRegionUrl = (string) request.Params[3];
                proxy.AddRegion(currentRegionPort, currentRegionUrl, newRegionPort, newRegionUrl);
            }
            catch (Exception e)
            {
                m_log.Error("[PROXY]" + e.Message);
                m_log.Error("[PROXY]" + e.StackTrace);
            }
            return new XmlRpcResponse();
        }

        private XmlRpcResponse ChangeRegion(XmlRpcRequest request)
        {
            try
            {
                int currentRegionPort = (int) request.Params[0];
                string currentRegionUrl = (string) request.Params[1];
                int newRegionPort = (int) request.Params[2];
                string newRegionUrl = (string) request.Params[3];
                proxy.ChangeRegion(currentRegionPort, currentRegionUrl, newRegionPort, newRegionUrl);
            }
            catch (Exception e)
            {
                m_log.Error("[PROXY]" + e.Message);
                m_log.Error("[PROXY]" + e.StackTrace);
            }
            return new XmlRpcResponse();
        }

        private XmlRpcResponse DeleteRegion(XmlRpcRequest request)
        {
            try
            {
                int currentRegionPort = (int) request.Params[0];
                string currentRegionUrl = (string) request.Params[1];
                proxy.DeleteRegion(currentRegionPort, currentRegionUrl);
            }
            catch (Exception e)
            {
                m_log.Error("[PROXY]" + e.Message);
                m_log.Error("[PROXY]" + e.StackTrace);
            }
            return new XmlRpcResponse();
        }

        private XmlRpcResponse BlockClientMessages(XmlRpcRequest request)
        {
            try
            {
                string regionUrl = (string) request.Params[0];
                int regionPort = (int) request.Params[1];
                proxy.BlockClientMessages(regionUrl, regionPort);
            }
            catch (Exception e)
            {
                m_log.Error("[PROXY]" + e.Message);
                m_log.Error("[PROXY]" + e.StackTrace);
            }
            return new XmlRpcResponse();
        }

        private XmlRpcResponse UnblockClientMessages(XmlRpcRequest request)
        {
            try
            {
                string regionUrl = (string) request.Params[0];
                int regionPort = (int) request.Params[1];
                proxy.UnblockClientMessages(regionUrl, regionPort);
            }
            catch (Exception e)
            {
                m_log.Error("[PROXY]" + e.Message);
                m_log.Error("[PROXY]" + e.StackTrace);
            }
            return new XmlRpcResponse();
        }
    }


    public class ProxyServer
    {
        protected readonly ILog m_log;
        protected ProxyMap proxy_map = new ProxyMap();
        protected AsyncCallback receivedData;
        protected bool running;

        public ProxyServer(ILog log)
        {
            m_log = log;
            running = false;
            receivedData = new AsyncCallback(OnReceivedData);
        }

        public void BlockClientMessages(string regionUrl, int regionPort)
        {
            EndPoint client = proxy_map.GetClient(new IPEndPoint(IPAddress.Parse(regionUrl), regionPort));
            ProxyMap.RegionData rd = proxy_map.GetRegionData(client);
            rd.isBlocked = true;
        }

        public void UnblockClientMessages(string regionUrl, int regionPort)
        {
            EndPoint client = proxy_map.GetClient(new IPEndPoint(IPAddress.Parse(regionUrl), regionPort));
            ProxyMap.RegionData rd = proxy_map.GetRegionData(client);

            rd.isBlocked = false;
            while (rd.storedMessages.Count > 0)
            {
                StoredMessage msg = (StoredMessage) rd.storedMessages.Dequeue();
                //m_log.Verbose("[PROXY]"+"Resending blocked message from {0}", msg.senderEP);
                SendMessage(msg.buffer, msg.length, msg.senderEP, msg.sd);
            }
        }

        public void AddRegion(int oldRegionPort, string oldRegionUrl, int newRegionPort, string newRegionUrl)
        {
            //m_log.Verbose("[PROXY]"+"AddRegion {0} {1}", oldRegionPort, newRegionPort);
            EndPoint client = proxy_map.GetClient(new IPEndPoint(IPAddress.Parse(oldRegionUrl), oldRegionPort));
            ProxyMap.RegionData data = proxy_map.GetRegionData(client);
            data.regions.Add(new IPEndPoint(IPAddress.Parse(newRegionUrl), newRegionPort));
        }

        public void ChangeRegion(int oldRegionPort, string oldRegionUrl, int newRegionPort, string newRegionUrl)
        {
            //m_log.Verbose("[PROXY]"+"ChangeRegion {0} {1}", oldRegionPort, newRegionPort);
            EndPoint client = proxy_map.GetClient(new IPEndPoint(IPAddress.Parse(oldRegionUrl), oldRegionPort));
            ProxyMap.RegionData data = proxy_map.GetRegionData(client);
            data.regions.Clear();
            data.regions.Add(new IPEndPoint(IPAddress.Parse(newRegionUrl), newRegionPort));
        }

        public void DeleteRegion(int oldRegionPort, string oldRegionUrl)
        {
            m_log.InfoFormat("[PROXY]" + "DeleteRegion {0} {1}", oldRegionPort, oldRegionUrl);
            EndPoint regionEP = new IPEndPoint(IPAddress.Parse(oldRegionUrl), oldRegionPort);
            EndPoint client = proxy_map.GetClient(regionEP);
            ProxyMap.RegionData data = proxy_map.GetRegionData(client);
            data.regions.Remove(regionEP);
        }

        public void AddPort(int clientPort, int regionPort, string regionUrl)
        {
            running = true;

            //m_log.Verbose("[PROXY]"+"AddPort {0} {1}", clientPort, regionPort);
            IPEndPoint clientEP = new IPEndPoint(IPAddress.Parse("127.0.0.1"), clientPort);
            proxy_map.Add(clientEP, new IPEndPoint(IPAddress.Parse(regionUrl), regionPort));

            ServerData sd = new ServerData();
            sd.clientEP = new IPEndPoint(clientEP.Address, clientEP.Port);

            OpenPort(sd);
        }

        protected void OpenPort(ServerData sd)
        {
            // sd.clientEP must be set before calling this function

            ClosePort(sd);

            try
            {
                m_log.InfoFormat("[PROXY] Opening special UDP socket on {0}", sd.clientEP);
                sd.serverIP = new IPEndPoint(IPAddress.Parse("0.0.0.0"), ((IPEndPoint) sd.clientEP).Port);
                sd.server = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
                sd.server.Bind(sd.serverIP);

                sd.senderEP = new IPEndPoint(IPAddress.Parse("0.0.0.0"), 0);
                //receivedData = new AsyncCallback(OnReceivedData);
                sd.server.BeginReceiveFrom(sd.recvBuffer, 0, sd.recvBuffer.Length, SocketFlags.None, ref sd.senderEP, receivedData, sd);
            }
            catch (Exception e)
            {
                m_log.ErrorFormat("[PROXY] Failed to (re)open socket {0}", sd.clientEP);
                m_log.Error("[PROXY]" + e.Message);
                m_log.Error("[PROXY]" + e.StackTrace);
            }
        }

        protected static void ClosePort(ServerData sd)
        {
            // Close the port if it exists and is open
            if (sd.server == null) return;

            try
            {
                sd.server.Shutdown(SocketShutdown.Both);
                sd.server.Close();
            }
            catch (Exception)
            {
            }
        }

        public void Stop()
        {
            running = false;
            m_log.InfoFormat("[PROXY] Stopping the proxy server");
        }


        protected virtual void OnReceivedData(IAsyncResult result)
        {
            if (!running) return;

            ServerData sd = (ServerData) result.AsyncState;
            sd.senderEP = new IPEndPoint(IPAddress.Parse("0.0.0.0"), 0);

            try
            {
                int numBytes = sd.server.EndReceiveFrom(result, ref sd.senderEP);
                if (numBytes > 0)
                {
                    SendMessage(sd.recvBuffer, numBytes, sd.senderEP, sd);
                }
            }
            catch (Exception e)
            {
//                OpenPort(sd); // reopen the port just in case
                m_log.ErrorFormat("[PROXY] EndReceiveFrom failed in {0}", sd.clientEP);
                m_log.Error("[PROXY]" + e.Message);
                m_log.Error("[PROXY]" + e.StackTrace);
            }

            WaitForNextMessage(sd);
        }

        protected void WaitForNextMessage(ServerData sd)
        {
            bool error = true;
            while (error)
            {
                error = false;
                try
                {
                    sd.server.BeginReceiveFrom(sd.recvBuffer, 0, sd.recvBuffer.Length, SocketFlags.None, ref sd.senderEP, receivedData, sd);
                }
                catch (Exception e)
                {
                    error = true;
                    m_log.ErrorFormat("[PROXY] BeginReceiveFrom failed, retrying... {0}", sd.clientEP);
                    m_log.Error("[PROXY]" + e.Message);
                    m_log.Error("[PROXY]" + e.StackTrace);
                    OpenPort(sd);
                }
            }
        }

        protected void SendMessage(byte[] buffer, int length, EndPoint senderEP, ServerData sd)
        {
            int numBytes = length;

            //m_log.ErrorFormat("[PROXY] Got message from {0} in thread {1}, size {2}", senderEP, sd.clientEP, numBytes);
            EndPoint client = proxy_map.GetClient(senderEP);

            if (client != null)
            {
                try
                {
                    client = PacketPool.DecodeProxyMessage(buffer, ref numBytes);
                    try
                    {
                        // This message comes from a region object, forward it to the its client
                        sd.server.SendTo(buffer, numBytes, SocketFlags.None, client);
                        //m_log.InfoFormat("[PROXY] Sending region message from {0} to {1}, size {2}", senderEP, client, numBytes);
                    }
                    catch (Exception e)
                    {
                        OpenPort(sd); // reopen the port just in case
                        m_log.ErrorFormat("[PROXY] Failed sending region message from {0} to {1}", senderEP, client);
                        m_log.Error("[PROXY]" + e.Message);
                        m_log.Error("[PROXY]" + e.StackTrace);
                        return;
                    }
                }
                catch (Exception e)
                {
                    OpenPort(sd); // reopen the port just in case
                    m_log.ErrorFormat("[PROXY] Failed decoding region message from {0}", senderEP);
                    m_log.Error("[PROXY]" + e.Message);
                    m_log.Error("[PROXY]" + e.StackTrace);
                    return;
                }
            }
            else
            {
                // This message comes from a client object, forward it to the the region(s)
                PacketPool.EncodeProxyMessage(buffer, ref numBytes, senderEP);
                ProxyMap.RegionData rd = proxy_map.GetRegionData(sd.clientEP);
                foreach (EndPoint region in rd.regions)
                {
                    if (rd.isBlocked)
                    {
                        rd.storedMessages.Enqueue(new StoredMessage(buffer, length, numBytes, senderEP, sd));
                    }
                    else
                    {
                        try
                        {
                            sd.server.SendTo(buffer, numBytes, SocketFlags.None, region);
                            //m_log.InfoFormat("[PROXY] Sending client message from {0} to {1}", senderEP, region);
                        }
                        catch (Exception e)
                        {
                            OpenPort(sd); // reopen the port just in case
                            m_log.ErrorFormat("[PROXY] Failed sending client message from {0} to {1}", senderEP, region);
                            m_log.Error("[PROXY]" + e.Message);
                            m_log.Error("[PROXY]" + e.StackTrace);
                            return;
                        }
                    }
                }
            }
        }

        #region Nested type: ProxyMap

        protected class ProxyMap
        {
            private Dictionary<EndPoint, RegionData> map;

            public ProxyMap()
            {
                map = new Dictionary<EndPoint, RegionData>();
            }

            public void Add(EndPoint client, EndPoint region)
            {
                if (map.ContainsKey(client))
                {
                    map[client].regions.Add(region);
                }
                else
                {
                    RegionData regions = new RegionData();
                    map.Add(client, regions);
                    regions.regions.Add(region);
                }
            }

            public RegionData GetRegionData(EndPoint client)
            {
                return map[client];
            }

            public EndPoint GetClient(EndPoint region)
            {
                foreach (KeyValuePair<EndPoint, RegionData> pair in map)
                {
                    if (pair.Value.regions.Contains(region))
                    {
                        return pair.Key;
                    }
                }
                return null;
            }

            #region Nested type: RegionData

            public class RegionData
            {
                public bool isBlocked = false;
                public List<EndPoint> regions = new List<EndPoint>();
                public Queue storedMessages = new Queue();
            }

            #endregion
        }

        #endregion

        #region Nested type: ServerData

        protected class ServerData
        {
            public EndPoint clientEP;
            public byte[] recvBuffer = new byte[4096];
            public EndPoint senderEP;
            public Socket server;
            public IPEndPoint serverIP;

            public ServerData()
            {
                server = null;
            }
        }

        #endregion

        #region Nested type: StoredMessage

        protected class StoredMessage
        {
            public byte[] buffer;
            public int length;
            public ServerData sd;
            public EndPoint senderEP;

            public StoredMessage(byte[] buffer, int length, int maxLength, EndPoint senderEP, ServerData sd)
            {
                this.buffer = new byte[maxLength];
                this.length = length;
                for (int i = 0; i < length; i++) this.buffer[i] = buffer[i];
                this.senderEP = senderEP;
                this.sd = sd;
            }
        }

        #endregion
    }
}