/*
 * 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 OpenSimulator 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.Generic;
using System.Reflection;
using OpenSim.Framework.Servers;
using OpenSim.Framework.Servers.HttpServer;

namespace OpenSim.ApplicationPlugins.Rest.Inventory
{
    /// <remarks>
    /// The class signature reveals the roles that RestHandler plays.
    ///
    /// [1] It is a sub-class of RestPlugin. It inherits and extends
    ///     the functionality of this class, constraining it to the
    ///     specific needs of this REST implementation. This relates
    ///     to the plug-in mechanism supported by OpenSim, the specifics
    ///     of which are mostly hidden by RestPlugin.
    /// [2] IRestHandler describes the interface that this class
    ///     exports to service implementations. This is the services
    ///     management interface.
    /// [3] IHttpAgentHandler describes the interface that is exported
    ///     to the BaseHttpServer in support of this particular HTTP
    ///     processing model. This is the request interface of the
    ///     handler.
    /// </remarks>

    public class RestHandler : RestPlugin, IRestHandler, IHttpAgentHandler
    {
        // Handler tables: both stream and REST are supported. The path handlers and their
        // respective allocators are stored in separate tables.

        internal Dictionary<string,RestMethodHandler>   pathHandlers   = new Dictionary<string,RestMethodHandler>();
        internal Dictionary<string,RestMethodAllocator> pathAllocators = new Dictionary<string,RestMethodAllocator>();
        internal Dictionary<string,RestStreamHandler>   streamHandlers = new Dictionary<string,RestStreamHandler>();

        #region local static state

        private static bool  handlersLoaded = false;
        private static List<Type>  classes  = new List<Type>();
        private static List<IRest> handlers = new List<IRest>();
        private static Type[]         parms = new Type[0];
        private static Object[]       args  = new Object[0];

        /// <summary>
        /// This static initializer scans the ASSEMBLY for classes that
        /// export the IRest interface and builds a list of them. These
        /// are later activated by the handler. To add a new handler it
        /// is only necessary to create a new services class that implements
        /// the IRest interface, and recompile the handler. This gives
        /// all of the build-time flexibility of a modular approach
        /// while not introducing yet-another module loader. Note that
        /// multiple assembles can still be built, each with its own set
        /// of handlers. Examples of services classes are RestInventoryServices
        /// and RestSkeleton.
        /// </summary>

        static RestHandler()
        {
            Module[] mods = Assembly.GetExecutingAssembly().GetModules();

            foreach (Module m in mods)
            {
                Type[] types = m.GetTypes();
                foreach (Type t in types)
                {
                    try
                    {
                        if (t.GetInterface("IRest") != null)
                        {
                            classes.Add(t);
                        }
                    }
                    catch (Exception)
                    {
                        Rest.Log.WarnFormat("[STATIC-HANDLER]: #0 Error scanning {1}", t);
                        Rest.Log.InfoFormat("[STATIC-HANDLER]: #0 {1} is not included", t);
                    }
                }
            }
        }

        #endregion local static state

        #region local instance state

        /// <summary>
        /// This routine loads all of the handlers discovered during
        /// instance initialization.
        /// A table of all loaded and successfully constructed handlers
        /// is built, and this table is then used by the constructor to
        /// initialize each of the handlers in turn.
        /// NOTE: The loading process does not automatically imply that
        /// the handler has registered any kind of an interface, that
        /// may be (optionally) done by the handler either during
        /// construction, or during initialization.
        ///
        /// I was not able to make this code work within a constructor
        /// so it is isolated within this method.
        /// </summary>

        private void LoadHandlers()
        {
            lock (handlers)
            {
                if (!handlersLoaded)
                {
                    ConstructorInfo ci;
                    Object          ht;

                    foreach (Type t in classes)
                    {
                        try
                        {
                            ci = t.GetConstructor(parms);
                            ht = ci.Invoke(args);
                            handlers.Add((IRest)ht);
                        }
                        catch (Exception e)
                        {
                            Rest.Log.WarnFormat("{0} Unable to load {1} : {2}", MsgId, t, e.Message);
                        }
                    }
                    handlersLoaded = true;
                }
            }
        }

        #endregion local instance state

        #region overriding properties

        // These properties override definitions
        // in the base class.

        // Name is used to differentiate the message header.

        public override string Name
        {
            get { return "HANDLER"; }
        }

        // Used to partition the .ini configuration space.

        public override string ConfigName
        {
            get { return "RestHandler"; }
        }

        // We have to rename these because we want
        // to be able to share the values with other
        // classes in our assembly and the base
        // names are protected.

        public string MsgId
        {
            get { return base.MsgID; }
        }

        public string RequestId
        {
            get { return base.RequestID; }
        }

        #endregion overriding properties

        #region overriding methods

        /// <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 override void Initialise(OpenSimBase openSim)
        {
            try
            {
                // This plugin will only be enabled if the broader
                // REST plugin mechanism is enabled.

                Rest.Log.InfoFormat("{0}  Plugin is initializing", MsgId);

                base.Initialise(openSim);

                // IsEnabled is implemented by the base class and
                // reflects an overall RestPlugin status

                if (!IsEnabled)
                {
                    Rest.Log.WarnFormat("{0} Plugins are disabled", MsgId);
                    return;
                }

                Rest.Log.InfoFormat("{0} Rest <{1}> plugin will be enabled", MsgId, Name);
                Rest.Log.InfoFormat("{0} Configuration parameters read from <{1}>", MsgId, ConfigName);

                // These are stored in static variables to make
                // them easy to reach from anywhere in the assembly.

                Rest.main              = openSim;
                if (Rest.main == null)
                    throw new Exception("OpenSim base pointer is null");

                Rest.Plugin            = this;
                Rest.Config            = Config;
                Rest.Prefix            = Prefix;
                Rest.GodKey            = GodKey;
                Rest.Authenticate      = Rest.Config.GetBoolean("authenticate", Rest.Authenticate);
                Rest.Scheme            = Rest.Config.GetString("auth-scheme", Rest.Scheme);
                Rest.Secure            = Rest.Config.GetBoolean("secured", Rest.Secure);
                Rest.ExtendedEscape    = Rest.Config.GetBoolean("extended-escape", Rest.ExtendedEscape);
                Rest.Realm             = Rest.Config.GetString("realm", Rest.Realm);
                Rest.DumpAsset         = Rest.Config.GetBoolean("dump-asset", Rest.DumpAsset);
                Rest.Fill              = Rest.Config.GetBoolean("path-fill", Rest.Fill);
                Rest.DumpLineSize      = Rest.Config.GetInt("dump-line-size", Rest.DumpLineSize);
                Rest.FlushEnabled      = Rest.Config.GetBoolean("flush-on-error", Rest.FlushEnabled);

                // Note: Odd spacing is required in the following strings

                Rest.Log.InfoFormat("{0} Authentication is {1}required", MsgId,
                                    (Rest.Authenticate ? "" : "not "));

                Rest.Log.InfoFormat("{0} Security is {1}enabled", MsgId,
                                    (Rest.Secure ? "" : "not "));

                Rest.Log.InfoFormat("{0} Extended URI escape processing is {1}enabled", MsgId,
                                    (Rest.ExtendedEscape ? "" : "not "));

                Rest.Log.InfoFormat("{0} Dumping of asset data is {1}enabled", MsgId,
                                    (Rest.DumpAsset ? "" : "not "));

                // The supplied prefix MUST be absolute

                if (Rest.Prefix.Substring(0,1) != Rest.UrlPathSeparator)
                {
                    Rest.Log.WarnFormat("{0} Prefix <{1}> is not absolute and must be", MsgId, Rest.Prefix);
                    Rest.Log.InfoFormat("{0} Prefix changed to </{1}>", MsgId, Rest.Prefix);
                    Rest.Prefix = String.Format("{0}{1}", Rest.UrlPathSeparator, Rest.Prefix);
                }

                // If data dumping is requested, report on the chosen line
                // length.

                if (Rest.DumpAsset)
                {
                    Rest.Log.InfoFormat("{0} Dump {1} bytes per line", MsgId, Rest.DumpLineSize);
                }

                // Load all of the handlers present in the
                // assembly

                // In principle, as we're an application plug-in,
                // most of what needs to be done could be done using
                // static resources, however the Open Sim plug-in
                // model makes this an instance, so that's what we
                // need to be.
                // There is only one Communications manager per
                // server, and by inference, only one each of the
                // user, asset, and inventory servers. So we can cache
                // those using a static initializer.
                // We move all of this processing off to another
                // services class to minimize overlap between function
                // and infrastructure.

                LoadHandlers();

                // The intention of a post construction initializer
                // is to allow for setup that is dependent upon other
                // activities outside of the agency.

                foreach (IRest handler in handlers)
                {
                    try
                    {
                        handler.Initialize();
                    }
                    catch (Exception e)
                    {
                        Rest.Log.ErrorFormat("{0} initialization error: {1}", MsgId, e.Message);
                    }
                }

                // Now that everything is setup we can proceed to
                // add THIS agent to the HTTP server's handler list

                if (!AddAgentHandler(Rest.Name,this))
                {
                    Rest.Log.ErrorFormat("{0} Unable to activate handler interface", MsgId);
                    foreach (IRest handler in handlers)
                    {
                        handler.Close();
                    }
                }

            }
            catch (Exception e)
            {
                Rest.Log.ErrorFormat("{0} Plugin initialization has failed: {1}", MsgId, e.Message);
            }
        }

        /// <summary>
        /// In the interests of efficiency, and because we cannot determine whether
        /// or not this instance will actually be harvested, we clobber the only
        /// anchoring reference to the working state for this plug-in. What the
        /// call to close does is irrelevant to this class beyond knowing that it
        /// can nullify the reference when it returns.
        /// To make sure everything is copacetic we make sure the primary interface
        /// is disabled by deleting the handler from the HTTP server tables.
        /// </summary>

        public override void Close()
        {
            Rest.Log.InfoFormat("{0} Plugin is terminating", MsgId);

            try
            {
                RemoveAgentHandler(Rest.Name, this);
            }
            catch (KeyNotFoundException){}

            foreach (IRest handler in handlers)
            {
                handler.Close();
            }
        }

        #endregion overriding methods

        #region interface methods

        /// <summary>
        /// This method is called by the HTTP server to match an incoming
        /// request. It scans all of the strings registered by the
        /// underlying handlers and looks for the best match. It returns
        /// true if a match is found.
        /// The matching process could be made arbitrarily complex.
        /// Note: The match is case-insensitive.
        /// </summary>

        public bool Match(OSHttpRequest request, OSHttpResponse response)
        {

            string path = request.RawUrl.ToLower();

            // Rest.Log.DebugFormat("{0} Match ENTRY", MsgId);

            try
            {
                foreach (string key in pathHandlers.Keys)
                {
                    // Rest.Log.DebugFormat("{0} Match testing {1} against agent prefix <{2}>", MsgId, path, key);

                    // Note that Match will not necessarily find the handler that will
                    // actually be used - it does no test for the "closest" fit. It
                    // simply reflects that at least one possible handler exists.

                    if (path.StartsWith(key))
                    {
                        // Rest.Log.DebugFormat("{0} Matched prefix <{1}>", MsgId, key);

                        // This apparently odd evaluation is needed to prevent a match
                        // on anything other than a URI token boundary. Otherwise we
                        // may match on URL's that were not intended for this handler.

                        return (path.Length == key.Length ||
                                path.Substring(key.Length, 1) == Rest.UrlPathSeparator);
                    }
                }

                path = String.Format("{0}{1}{2}", request.HttpMethod, Rest.UrlMethodSeparator, path);

                foreach (string key in streamHandlers.Keys)
                {
                    // Rest.Log.DebugFormat("{0} Match testing {1} against stream prefix <{2}>", MsgId, path, key);

                    // Note that Match will not necessarily find the handler that will
                    // actually be used - it does no test for the "closest" fit. It
                    // simply reflects that at least one possible handler exists.

                    if (path.StartsWith(key))
                    {
                        // Rest.Log.DebugFormat("{0} Matched prefix <{1}>", MsgId, key);

                        // This apparently odd evaluation is needed to prevent a match
                        // on anything other than a URI token boundary. Otherwise we
                        // may match on URL's that were not intended for this handler.

                        return (path.Length == key.Length ||
                                path.Substring(key.Length, 1) == Rest.UrlPathSeparator);
                    }
                }
            }
            catch (Exception e)
            {
                Rest.Log.ErrorFormat("{0} matching exception for path <{1}> : {2}", MsgId, path, e.Message);
            }

            return false;
        }

        /// <summary>
        /// This is called by the HTTP server once the handler has indicated
        /// that it is able to handle the request.
        /// Preconditions:
        ///  [1] request  != null and is a valid request object
        ///  [2] response != null and is a valid response object
        /// Behavior is undefined if preconditions are not satisfied.
        /// </summary>

        public bool Handle(OSHttpRequest request, OSHttpResponse response)
        {
            bool handled;
            base.MsgID = base.RequestID;

            // Debug only

            if (Rest.DEBUG)
            {
                Rest.Log.DebugFormat("{0} ENTRY", MsgId);
                Rest.Log.DebugFormat("{0}  Agent: {1}", MsgId, request.UserAgent);
                Rest.Log.DebugFormat("{0} Method: {1}", MsgId, request.HttpMethod);

                for (int i = 0; i < request.Headers.Count; i++)
                {
                    Rest.Log.DebugFormat("{0} Header [{1}] : <{2}> = <{3}>",
                                         MsgId, i, request.Headers.GetKey(i), request.Headers.Get(i));
                }
                Rest.Log.DebugFormat("{0}    URI: {1}", MsgId, request.RawUrl);
            }

            // If a path handler worked we're done, otherwise try any
            // available stream handlers too.

            try
            {
                handled = (FindPathHandler(request, response) ||
                    FindStreamHandler(request, response));
            }
            catch (Exception e)
            {
                // A raw exception indicates that something we weren't expecting has
                // happened. This should always reflect a shortcoming in the plugin,
                // or a failure to satisfy the preconditions. It should not reflect
                // an error in the request itself. Under such circumstances the state
                // of the request cannot be determined and we are obliged to mark it
                // as 'handled'.

                Rest.Log.ErrorFormat("{0} Plugin error: {1}", MsgId, e.Message);
                handled = true;
            }

            Rest.Log.DebugFormat("{0} EXIT", MsgId);

            return handled;
        }

        #endregion interface methods

        /// <summary>
        /// If there is a stream handler registered that can handle the
        /// request, then fine. If the request is not matched, do
        /// nothing.
        /// Note: The selection is case-insensitive
        /// </summary>

        private bool FindStreamHandler(OSHttpRequest request, OSHttpResponse response)
        {
            RequestData rdata = new RequestData(request, response, String.Empty);

            string bestMatch = String.Empty;
            string path      = String.Format("{0}:{1}", rdata.method, rdata.path).ToLower();

            Rest.Log.DebugFormat("{0} Checking for stream handler for <{1}>", MsgId, path);

            if (!IsEnabled)
            {
                return false;
            }

            foreach (string pattern in streamHandlers.Keys)
            {
                if (path.StartsWith(pattern))
                {
                    if (pattern.Length > bestMatch.Length)
                    {
                        bestMatch = pattern;
                    }
                }
            }

            // Handle using the best match available

            if (bestMatch.Length > 0)
            {
                Rest.Log.DebugFormat("{0} Stream-based handler matched with <{1}>", MsgId, bestMatch);
                RestStreamHandler handler = streamHandlers[bestMatch];
                rdata.buffer = handler.Handle(rdata.path, rdata.request.InputStream, rdata.request, rdata.response);
                rdata.AddHeader(rdata.response.ContentType,handler.ContentType);
                rdata.Respond("FindStreamHandler Completion");
            }

            return rdata.handled;
        }

        /// <summary>
        /// Add a stream handler for the designated HTTP method and path prefix.
        /// If the handler is not enabled, the request is ignored. If the path
        /// does not start with the REST prefix, it is added. If method-qualified
        /// path has not already been registered, the method is added to the active
        /// handler table.
        /// </summary>

        public void AddStreamHandler(string httpMethod, string path, RestMethod method)
        {
            if (!IsEnabled)
            {
                return;
            }

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

            path = String.Format("{0}{1}{2}", httpMethod, Rest.UrlMethodSeparator, path);

            // Conditionally add to the list

            if (!streamHandlers.ContainsKey(path))
            {
                streamHandlers.Add(path, new RestStreamHandler(httpMethod, path, method));
                Rest.Log.DebugFormat("{0} Added handler for {1}", MsgId, path);
            }
            else
            {
                Rest.Log.WarnFormat("{0} Ignoring duplicate handler for {1}", MsgId, path);
            }
        }

        /// <summary>
        /// Given the supplied request/response, if the handler is enabled, the inbound
        /// information is used to match an entry in the active path handler tables, using
        /// the method-qualified path information. If a match is found, then the handler is
        /// invoked. The result is the boolean result of the handler, or false if no
        /// handler was located. The boolean indicates whether or not the request has been
        /// handled, not whether or not the request was successful - that information is in
        /// the response.
        /// Note: The selection process is case-insensitive
        /// </summary>

        internal bool FindPathHandler(OSHttpRequest request, OSHttpResponse response)
        {
            RequestData rdata = null;
            string bestMatch = null;

            if (!IsEnabled)
            {
                return false;
            }

            // Conditionally add to the list

            Rest.Log.DebugFormat("{0} Checking for path handler for <{1}>", MsgId, request.RawUrl);

            foreach (string pattern in pathHandlers.Keys)
            {
                if (request.RawUrl.ToLower().StartsWith(pattern))
                {
                    if (String.IsNullOrEmpty(bestMatch) || pattern.Length > bestMatch.Length)
                    {
                        bestMatch = pattern;
                    }
                }
            }

            if (!String.IsNullOrEmpty(bestMatch))
            {
                rdata = pathAllocators[bestMatch](request, response, bestMatch);

                Rest.Log.DebugFormat("{0} Path based REST handler matched with <{1}>", MsgId, bestMatch);

                try
                {
                    pathHandlers[bestMatch](rdata);
                }

                // A plugin generated error indicates a request-related error
                // that has been handled by the plugin.

                catch (RestException r)
                {
                    Rest.Log.WarnFormat("{0} Request failed: {1}", MsgId, r.Message);
                }
            }

            return (rdata == null) ? false : rdata.handled;
        }

        /// <summary>
        /// A method handler and a request allocator are stored using the designated
        /// path as a key. If an entry already exists, it is replaced by the new one.
        /// </summary>

        public void AddPathHandler(RestMethodHandler mh, string path, RestMethodAllocator ra)
        {
            if (!IsEnabled)
            {
                return;
            }

            if (pathHandlers.ContainsKey(path))
            {
                Rest.Log.DebugFormat("{0} Replacing handler for <${1}>", MsgId, path);
                pathHandlers.Remove(path);
            }

            if (pathAllocators.ContainsKey(path))
            {
                Rest.Log.DebugFormat("{0} Replacing allocator for <${1}>", MsgId, path);
                pathAllocators.Remove(path);
            }

            Rest.Log.DebugFormat("{0} Adding path handler for {1}", MsgId, path);

            pathHandlers.Add(path, mh);
            pathAllocators.Add(path, ra);
        }
    }
}