/*
 * 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.Threading;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Reflection;
using System.Timers;
using System.Xml;
using OpenMetaverse;
using Nwc.XmlRpc;
using Nini.Config;
using OpenSim.Framework;
using OpenSim.Framework.Console;
using OpenSim.Framework.Servers;
using OpenSim.Framework.Communications;
using OpenSim.Region.Environment.Scenes;

namespace OpenSim.ApplicationPlugins.Rest
{
    public abstract class RestPlugin : IApplicationPlugin
    {
        #region properties

        protected static readonly log4net.ILog  m_log =
            log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

        private IConfig        _config;       // Configuration source: Rest Plugins
        private IConfig        _pluginConfig; // Configuration source: Plugin specific
        private OpenSimBase    _app;          // The 'server'
        private BaseHttpServer _httpd;        // The server's RPC interface
        private string         _prefix;       // URL prefix below
                                              // which all REST URLs
                                              // are living
        private StringWriter  _sw = null;
        private RestXmlWriter _xw = null;

        private string _godkey;
        private int    _reqk;

        [ThreadStaticAttribute]
        private static string  _threadRequestID = String.Empty;

        /// <summary>
        /// Return an ever increasing request ID for logging
        /// </summary>
        protected string RequestID
        {
            get { return _reqk++.ToString(); }
            set { _reqk = Convert.ToInt32(value); }
        }

        /// <summary>
        /// Thread-constant message IDs for logging.
        /// </summary>
        protected string MsgID
        {
            get { return String.Format("[REST-{0}] #{1}", Name, _threadRequestID); }
            set { _threadRequestID = value; }
        }

        /// <summary>
        /// Returns true if Rest Plugins are enabled.
        /// </summary>
        public bool PluginsAreEnabled
        {
            get { return null != _config; }
        }

        /// <summary>
        /// Returns true if specific Rest Plugin is enabled.
        /// </summary>
        public bool IsEnabled
        {
            get
            {
                return (null != _pluginConfig) && _pluginConfig.GetBoolean("enabled", false);
            }
        }

        /// <summary>
        /// OpenSimMain application
        /// </summary>
        public OpenSimBase App
        {
            get { return _app; }
        }

        /// <summary>
        /// RPC server
        /// </summary>
        public BaseHttpServer HttpServer
        {
            get { return _httpd; }
        }

        /// <summary>
        /// URL prefix to use for all REST handlers
        /// </summary>
        public string Prefix
        {
            get { return _prefix; }
        }

        /// <summary>
        /// Access to GOD password string
        /// </summary>
        protected string GodKey
        {
            get { return _godkey; }
        }

        /// <summary>
        /// Configuration of the plugin
        /// </summary>
        public IConfig Config
        {
            get { return _pluginConfig; }
        }

        /// <summary>
        /// Name of the plugin
        /// </summary>
        public abstract string Name { get; }

        /// <summary>
        /// Return the config section name
        /// </summary>
        public abstract string ConfigName { get; }

        public XmlTextWriter XmlWriter
        {
            get {
                if (null == _xw)
                {
                    _sw = new StringWriter();
                    _xw = new RestXmlWriter(_sw);
                    _xw.Formatting = Formatting.Indented;
                }
                return _xw; }
        }

        public string XmlWriterResult
        {
            get
            {
                _xw.Flush();
                _xw.Close();
                _xw = null;

                return _sw.ToString();
            }
        }
        #endregion properties


        #region methods
        // TODO: required by IPlugin, but likely not at all right
        string m_version = "0.0";

        public string Version { get { return m_version; } }

        public void Initialise() 
        { 
            m_log.Info("[RESTPLUGIN]: " + Name + " cannot be default-initialized!");
            throw new PluginNotInitialisedException (Name);
        }

        /// <summary>
        /// This method is called by OpenSimMain immediately after loading the
        /// plugin and after basic server setup,  but before running any server commands.
        /// </summary>
        /// <remarks>
        /// Note that entries MUST be added to the active configuration files before
        /// the plugin can be enabled.
        /// </remarks>
        public virtual void Initialise(OpenSimBase openSim)
        {
            RequestID = "0";
            MsgID = RequestID;

            try
            {
                if ((_config = openSim.ConfigSource.Source.Configs["RestPlugins"]) == null)
                {
                    m_log.WarnFormat("{0} Rest Plugins not configured", MsgID);
                    return;
                }

                if (!_config.GetBoolean("enabled", false))
                {
                    m_log.WarnFormat("{0} Rest Plugins are disabled", MsgID);
                    return;
                }

                _app = openSim;
                _httpd = openSim.HttpServer;

                // Retrieve GOD key value, if any.
                _godkey = _config.GetString("god_key", String.Empty);

                // Retrive prefix if any.
                _prefix = _config.GetString("prefix", "/admin");

                // Get plugin specific config
                _pluginConfig = openSim.ConfigSource.Source.Configs[ConfigName];

                m_log.InfoFormat("{0} Rest Plugins Enabled", MsgID);
            }
            catch (Exception e)
            {
                // we can safely ignore this, as it just means that
                // the key lookup in Configs failed, which signals to
                // us that noone is interested in our services...they
                // don't know what they are missing out on...
                // NOTE: Under the present OpenSim implementation it is
                // not possible for the openSim pointer to be null. However
                // were the implementation to be changed, this could
                // result in a silent initialization failure. Harmless
                // except for lack of function and lack of any
                // diagnostic indication as to why. The same is true if
                // the HTTP server reference is bad.
                // We should at least issue a message...
                m_log.WarnFormat("{0} Initialization failed: {1}", MsgID, e.Message);
                m_log.DebugFormat("{0} Initialization failed: {1}", MsgID, e.ToString());
            }
        }

        private List<RestStreamHandler> _handlers               = new List<RestStreamHandler>();
        private Dictionary<string, IHttpAgentHandler> _agents   = new Dictionary<string, IHttpAgentHandler>();

        /// <summary>
        /// Add a REST stream handler to the underlying HTTP server.
        /// </summary>
        /// <param name="httpMethod">GET/PUT/POST/DELETE or
        /// similar</param>
        /// <param name="path">URL prefix</param>
        /// <param name="method">RestMethod handler doing the actual work</param>
        public virtual void AddRestStreamHandler(string httpMethod, string path, RestMethod method)
        {
            if (!IsEnabled) return;

            if (!path.StartsWith(_prefix))
            {
                path = String.Format("{0}{1}", _prefix, path);
            }

            RestStreamHandler h = new RestStreamHandler(httpMethod, path, method);
            _httpd.AddStreamHandler(h);
            _handlers.Add(h);

            m_log.DebugFormat("{0} Added REST handler {1} {2}", MsgID, httpMethod, path);
        }

        /// <summary>
        /// Add a powerful Agent handler to the underlying HTTP
        /// server.
        /// </summary>
        /// <param name="agentName">name of agent handler</param>
        /// <param name="handler">agent handler method</param>
        /// <returns>false when the plugin is disabled or the agent
        /// handler could not be added. Any generated exceptions are
        /// allowed to drop through to the caller, i.e. ArgumentException.
        /// </returns>
        public bool AddAgentHandler(string agentName, IHttpAgentHandler handler)
        {
            if (!IsEnabled) return false;
            _agents.Add(agentName, handler);
            return _httpd.AddAgentHandler(agentName, handler);
        }

        /// <summary>
        /// Remove a powerful Agent handler from the underlying HTTP
        /// server.
        /// </summary>
        /// <param name="agentName">name of agent handler</param>
        /// <param name="handler">agent handler method</param>
        /// <returns>false when the plugin is disabled or the agent
        /// handler could not be removed. Any generated exceptions are
        /// allowed to drop through to the caller, i.e. KeyNotFound.
        /// </returns>
        public bool RemoveAgentHandler(string agentName, IHttpAgentHandler handler)
        {
            if (!IsEnabled) return false;
            if (_agents[agentName] == handler)
            {
                _agents.Remove(agentName);
                return _httpd.RemoveAgentHandler(agentName, handler);
            }
            return false;
        }

        /// <summary>
        /// Check whether the HTTP request came from god; that is, is
        /// the god_key as configured in the config section supplied
        /// via X-OpenSim-Godkey?
        /// </summary>
        /// <param name="request">HTTP request header</param>
        /// <returns>true when the HTTP request came from god.</returns>
        protected bool IsGod(OSHttpRequest request)
        {
            string[] keys = request.Headers.GetValues("X-OpenSim-Godkey");
            if (null == keys) return false;

            // we take the last key supplied
            return keys[keys.Length-1] == _godkey;
        }

        /// <summary>
        /// Checks wether the X-OpenSim-Password value provided in the
        /// HTTP header is indeed the password on file for the avatar
        /// specified by the UUID
        /// </summary>
        protected bool IsVerifiedUser(OSHttpRequest request, UUID uuid)
        {
            // XXX under construction
            return false;
        }

        /// <summary>
        /// Clean up and remove all handlers that were added earlier.
        /// </summary>
        public virtual void Close()
        {
            foreach (RestStreamHandler h in _handlers)
            {
                _httpd.RemoveStreamHandler(h.HttpMethod, h.Path);
            }
            _handlers = null;
            foreach (KeyValuePair<string,IHttpAgentHandler> h in _agents)
            {
                _httpd.RemoveAgentHandler(h.Key,h.Value);
            }
            _agents   = null;
        }

        public virtual void Dispose()
        {
            Close();
        }

        /// <summary>
        /// Return a failure message.
        /// </summary>
        /// <param name="method">origin of the failure message</param>
        /// <param name="message">failure message</param>
        /// <remarks>This should probably set a return code as
        /// well. (?)</remarks>
        protected string Failure(OSHttpResponse response, OSHttpStatusCode status,
                                 string method, string format, params string[] msg)
        {
            string m = String.Format(format, msg);

            response.StatusCode = (int)status;
            response.StatusDescription = m;

            m_log.ErrorFormat("{0} {1} failed: {2}", MsgID, method, m);
            return String.Format("<error>{0}</error>", m);
        }

        /// <summary>
        /// Return a failure message.
        /// </summary>
        /// <param name="method">origin of the failure message</param>
        /// <param name="e">exception causing the failure message</param>
        /// <remarks>This should probably set a return code as
        /// well. (?)</remarks>
        public string Failure(OSHttpResponse response, OSHttpStatusCode status,
                              string method, Exception e)
        {
            string m = String.Format("exception occurred: {0}", e.Message);

            response.StatusCode = (int)status;
            response.StatusDescription = m;

            m_log.DebugFormat("{0} {1} failed: {2}", MsgID, method, e.ToString());
            m_log.ErrorFormat("{0} {1} failed: {2}", MsgID, method, e.Message);

            return String.Format("<error>{0}</error>", e.Message);
        }
        #endregion methods
    }
}