/*
 * 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.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Web;
using System.Text;
using DotNetOpenId;
using DotNetOpenId.Provider;
using log4net;
using OpenSim.Framework;
using OpenSim.Framework.Servers;

namespace OpenSim.Grid.UserServer
{
    /// <summary>
    /// Temporary, in-memory store for OpenID associations
    /// </summary>
    public class ProviderMemoryStore : IAssociationStore<AssociationRelyingPartyType>
    {
        private class AssociationItem
        {
            public AssociationRelyingPartyType DistinguishingFactor;
            public string Handle;
            public DateTime Expires;
            public byte[] PrivateData;
        }

        Dictionary<string, AssociationItem> m_store = new Dictionary<string, AssociationItem>();
        SortedList<DateTime, AssociationItem> m_sortedStore = new SortedList<DateTime, AssociationItem>();
        object m_syncRoot = new object();

        #region IAssociationStore<AssociationRelyingPartyType> Members

        public void StoreAssociation(AssociationRelyingPartyType distinguishingFactor, Association assoc)
        {
            AssociationItem item = new AssociationItem();
            item.DistinguishingFactor = distinguishingFactor;
            item.Handle = assoc.Handle;
            item.Expires = assoc.Expires.ToLocalTime();
            item.PrivateData = assoc.SerializePrivateData();

            lock (m_syncRoot)
            {
                m_store[item.Handle] = item;
                m_sortedStore[item.Expires] = item;
            }
        }

        public Association GetAssociation(AssociationRelyingPartyType distinguishingFactor)
        {
            lock (m_syncRoot)
            {
                if (m_sortedStore.Count > 0)
                {
                    AssociationItem item = m_sortedStore.Values[m_sortedStore.Count - 1];
                    return Association.Deserialize(item.Handle, item.Expires.ToUniversalTime(), item.PrivateData);
                }
                else
                {
                    return null;
                }
            }
        }

        public Association GetAssociation(AssociationRelyingPartyType distinguishingFactor, string handle)
        {
            AssociationItem item;
            bool success = false;
            lock (m_syncRoot)
                success = m_store.TryGetValue(handle, out item);

            if (success)
                return Association.Deserialize(item.Handle, item.Expires.ToUniversalTime(), item.PrivateData);
            else
                return null;
        }

        public bool RemoveAssociation(AssociationRelyingPartyType distinguishingFactor, string handle)
        {
            lock (m_syncRoot)
            {
                for (int i = 0; i < m_sortedStore.Values.Count; i++)
                {
                    AssociationItem item = m_sortedStore.Values[i];
                    if (item.Handle == handle)
                    {
                        m_sortedStore.RemoveAt(i);
                        break;
                    }
                }

                return m_store.Remove(handle);
            }
        }

        public void ClearExpiredAssociations()
        {
            lock (m_syncRoot)
            {
                List<AssociationItem> itemsCopy = new List<AssociationItem>(m_sortedStore.Values);
                DateTime now = DateTime.Now;

                for (int i = 0; i < itemsCopy.Count; i++)
                {
                    AssociationItem item = itemsCopy[i];

                    if (item.Expires <= now)
                    {
                        m_sortedStore.RemoveAt(i);
                        m_store.Remove(item.Handle);
                    }
                }
            }
        }

        #endregion
    }

    public class OpenIdStreamHandler : IStreamHandler
    {
        #region HTML

        /// <summary>Login form used to authenticate OpenID requests</summary>
        const string LOGIN_PAGE =
@"<html>
<head><title>OpenSim OpenID Login</title></head>
<body>
<h3>OpenSim Login</h3>
<form method=""post"">
<label for=""first"">First Name:</label> <input readonly type=""text"" name=""first"" id=""first"" value=""{0}""/>
<label for=""last"">Last Name:</label> <input readonly type=""text"" name=""last"" id=""last"" value=""{1}""/>
<label for=""pass"">Password:</label> <input type=""password"" name=""pass"" id=""pass""/>
<input type=""submit"" value=""Login"">
</form>
</body>
</html>";

        /// <summary>Page shown for a valid OpenID identity</summary>
        const string OPENID_PAGE =
@"<html>
<head>
<title>{2} {3}</title>
<link rel=""openid2.provider openid.server"" href=""{0}://{1}/openid/server/""/>
</head>
<body>OpenID identifier for {2} {3}</body>
</html>
";

        /// <summary>Page shown for an invalid OpenID identity</summary>
        const string INVALID_OPENID_PAGE = 
@"<html><head><title>Identity not found</title></head>
<body>Invalid OpenID identity</body></html>";

        /// <summary>Page shown if the OpenID endpoint is requested directly</summary>
        const string ENDPOINT_PAGE =
@"<html><head><title>OpenID Endpoint</title></head><body>
This is an OpenID server endpoint, not a human-readable resource. 
For more information, see <a href='http://openid.net/'>http://openid.net/</a>.
</body></html>";

        #endregion HTML

        public string ContentType { get { return m_contentType; } }
        public string HttpMethod { get { return m_httpMethod; } }
        public string Path { get { return m_path; } }

        string m_contentType;
        string m_httpMethod;
        string m_path;
        UserLoginService m_loginService;
        ProviderMemoryStore m_openidStore = new ProviderMemoryStore();

        /// <summary>
        /// Constructor
        /// </summary>
        public OpenIdStreamHandler(string httpMethod, string path, UserLoginService loginService)
        {
            m_loginService = loginService;
            m_httpMethod = httpMethod;
            m_path = path;

            m_contentType = "text/html";
        }

        /// <summary>
        /// Handles all GET and POST requests for OpenID identifier pages and endpoint
        /// server communication
        /// </summary>
        public void Handle(string path, Stream request, Stream response, OSHttpRequest httpRequest, OSHttpResponse httpResponse)
        {
            Uri providerEndpoint = new Uri(String.Format("{0}://{1}{2}", httpRequest.Url.Scheme, httpRequest.Url.Authority, httpRequest.Url.AbsolutePath));

            // Defult to returning HTML content
            m_contentType = "text/html";

            try
            {
                NameValueCollection postQuery = HttpUtility.ParseQueryString(new StreamReader(httpRequest.InputStream).ReadToEnd());
                NameValueCollection getQuery = HttpUtility.ParseQueryString(httpRequest.Url.Query);
                NameValueCollection openIdQuery = (postQuery.GetValues("openid.mode") != null ? postQuery : getQuery);

                OpenIdProvider provider = new OpenIdProvider(m_openidStore, providerEndpoint, httpRequest.Url, openIdQuery);

                if (provider.Request != null)
                {
                    if (!provider.Request.IsResponseReady && provider.Request is IAuthenticationRequest)
                    {
                        IAuthenticationRequest authRequest = (IAuthenticationRequest)provider.Request;
                        string[] passwordValues = postQuery.GetValues("pass");

                        UserProfileData profile;
                        if (TryGetProfile(new Uri(authRequest.ClaimedIdentifier.ToString()), out profile))
                        {
                            // Check for form POST data
                            if (passwordValues != null && passwordValues.Length == 1)
                            {
                                if (profile != null && m_loginService.AuthenticateUser(profile, passwordValues[0]))
                                    authRequest.IsAuthenticated = true;
                                else
                                    authRequest.IsAuthenticated = false;
                            }
                            else
                            {
                                // Authentication was requested, send the client a login form
                                using (StreamWriter writer = new StreamWriter(response))
                                    writer.Write(String.Format(LOGIN_PAGE, profile.FirstName, profile.SurName));
                                return;
                            }
                        }
                        else
                        {
                            // Cannot find an avatar matching the claimed identifier
                            authRequest.IsAuthenticated = false;
                        }
                    }

                    // Add OpenID headers to the response
                    foreach (string key in provider.Request.Response.Headers.Keys)
                        httpResponse.AddHeader(key, provider.Request.Response.Headers[key]);

                    string[] contentTypeValues = provider.Request.Response.Headers.GetValues("Content-Type");
                    if (contentTypeValues != null && contentTypeValues.Length == 1)
                        m_contentType = contentTypeValues[0];

                    // Set the response code and document body based on the OpenID result
                    httpResponse.StatusCode = (int)provider.Request.Response.Code;
                    response.Write(provider.Request.Response.Body, 0, provider.Request.Response.Body.Length);
                    response.Close();
                }
                else if (httpRequest.Url.AbsolutePath.Contains("/openid/server"))
                {
                    // Standard HTTP GET was made on the OpenID endpoint, send the client the default error page
                    using (StreamWriter writer = new StreamWriter(response))
                        writer.Write(ENDPOINT_PAGE);
                }
                else
                {
                    // Try and lookup this avatar
                    UserProfileData profile;
                    if (TryGetProfile(httpRequest.Url, out profile))
                    {
                        using (StreamWriter writer = new StreamWriter(response))
                        {
                            // TODO: Print out a full profile page for this avatar
                            writer.Write(String.Format(OPENID_PAGE, httpRequest.Url.Scheme,
                                httpRequest.Url.Authority, profile.FirstName, profile.SurName));
                        }
                    }
                    else
                    {
                        // Couldn't parse an avatar name, or couldn't find the avatar in the user server
                        using (StreamWriter writer = new StreamWriter(response))
                            writer.Write(INVALID_OPENID_PAGE);
                    }
                }
            }
            catch (Exception ex)
            {
                httpResponse.StatusCode = (int)HttpStatusCode.InternalServerError;
                using (StreamWriter writer = new StreamWriter(response))
                    writer.Write(ex.Message);
            }
        }

        /// <summary>
        /// Parse a URL with a relative path of the form /users/First_Last and try to
        /// retrieve the profile matching that avatar name
        /// </summary>
        /// <param name="requestUrl">URL to parse for an avatar name</param>
        /// <param name="profile">Profile data for the avatar</param>
        /// <returns>True if the parse and lookup were successful, otherwise false</returns>
        bool TryGetProfile(Uri requestUrl, out UserProfileData profile)
        {
            if (requestUrl.Segments.Length == 3 && requestUrl.Segments[1] == "users/")
            {
                // Parse the avatar name from the path
                string username = requestUrl.Segments[requestUrl.Segments.Length - 1];
                string[] name = username.Split('_');

                if (name.Length == 2)
                {
                    profile = m_loginService.GetTheUser(name[0], name[1]);
                    return (profile != null);
                }
            }

            profile = null;
            return false;
        }
    }
}