/*
 * 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.Collections.Specialized;
using System.Reflection;
using log4net;
using Mono.Addins;
using Nini.Config;
using OpenMetaverse;
using OpenMetaverse.StructuredData;
using OpenSim.Framework;
using OpenSim.Framework.Client;
using OpenSim.Region.Framework.Interfaces;
using OpenSim.Region.Framework.Scenes;
using OpenSim.Services.Interfaces;

namespace OpenSim.Services.Connectors.SimianGrid
{
    /// <summary>
    /// Avatar profile flags
    /// </summary>
    [Flags]
    public enum ProfileFlags : uint
    {
        AllowPublish = 1,
        MaturePublish = 2,
        Identified = 4,
        Transacted = 8,
        Online = 16
    }

    /// <summary>
    /// Connects avatar profile and classified queries to the SimianGrid
    /// backend
    /// </summary>
    [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule")]
    public class SimianProfiles : INonSharedRegionModule
    {
        private static readonly ILog m_log =
                LogManager.GetLogger(
                MethodBase.GetCurrentMethod().DeclaringType);

        private string m_serverUrl = String.Empty;
        private bool m_Enabled = false;

        #region INonSharedRegionModule
        
        public Type ReplaceableInterface { get { return null; } }
        public void RegionLoaded(Scene scene) { }
        public void Close() { }

        public SimianProfiles() { }
        public string Name { get { return "SimianProfiles"; } }

        public void AddRegion(Scene scene)
        {
            if (m_Enabled)
            {
                CheckEstateManager(scene);
                scene.EventManager.OnClientConnect += ClientConnectHandler;
            }
        }

        public void RemoveRegion(Scene scene)
        {
            if (m_Enabled)
            {
                scene.EventManager.OnClientConnect -= ClientConnectHandler;
            }
        }

        #endregion INonSharedRegionModule

        public SimianProfiles(IConfigSource source)
        {
            Initialise(source);
        }

        public void Initialise(IConfigSource source)
        {
            IConfig profileConfig = source.Configs["Profiles"];
            if (profileConfig == null)
                return;

            if (profileConfig.GetString("Module", String.Empty) != Name)
                return;

            m_log.DebugFormat("[SIMIAN PROFILES] module enabled");
            m_Enabled = true;

            IConfig gridConfig = source.Configs["UserAccountService"];
            if (gridConfig != null)
            {
                string serviceUrl = gridConfig.GetString("UserAccountServerURI");
                if (!String.IsNullOrEmpty(serviceUrl))
                {
                    if (!serviceUrl.EndsWith("/") && !serviceUrl.EndsWith("="))
                        serviceUrl = serviceUrl + '/';
                    m_serverUrl = serviceUrl;
                }
            }

            if (String.IsNullOrEmpty(m_serverUrl))
                m_log.Info("[SIMIAN PROFILES]: No UserAccountServerURI specified, disabling connector");
        }

        private void ClientConnectHandler(IClientCore clientCore)
        {
            if (clientCore is IClientAPI)
            {
                IClientAPI client = (IClientAPI)clientCore;

                // Classifieds
                client.AddGenericPacketHandler("avatarclassifiedsrequest", AvatarClassifiedsRequestHandler);
                client.OnClassifiedInfoRequest += ClassifiedInfoRequestHandler;
                client.OnClassifiedInfoUpdate += ClassifiedInfoUpdateHandler;
                client.OnClassifiedDelete += ClassifiedDeleteHandler;

                // Picks
                client.AddGenericPacketHandler("avatarpicksrequest", HandleAvatarPicksRequest);
                client.AddGenericPacketHandler("pickinforequest", HandlePickInfoRequest);
                client.OnPickInfoUpdate += PickInfoUpdateHandler;
                client.OnPickDelete += PickDeleteHandler;

                // Notes
                client.AddGenericPacketHandler("avatarnotesrequest", HandleAvatarNotesRequest);
                client.OnAvatarNotesUpdate += AvatarNotesUpdateHandler;

                // Profiles
                client.OnRequestAvatarProperties += RequestAvatarPropertiesHandler;

                client.OnUpdateAvatarProperties += UpdateAvatarPropertiesHandler;
                client.OnAvatarInterestUpdate += AvatarInterestUpdateHandler;
                client.OnUserInfoRequest += UserInfoRequestHandler;
                client.OnUpdateUserInfo += UpdateUserInfoHandler;
            }
        }

        #region Classifieds

        private void AvatarClassifiedsRequestHandler(Object sender, string method, List<String> args)
        {
            if (!(sender is IClientAPI))
                return;
            IClientAPI client = (IClientAPI)sender;

            UUID targetAvatarID;
            if (args.Count < 1 || !UUID.TryParse(args[0], out targetAvatarID))
            {
                m_log.Error("[SIMIAN PROFILES]: Unrecognized arguments for " + method);
                return;
            }

            // FIXME: Query the generic key/value store for classifieds
            client.SendAvatarClassifiedReply(targetAvatarID, new Dictionary<UUID, string>(0));
        }

        private void ClassifiedInfoRequestHandler(UUID classifiedID, IClientAPI client)
        {
            // FIXME: Fetch this info
            client.SendClassifiedInfoReply(classifiedID, UUID.Zero, 0, Utils.DateTimeToUnixTime(DateTime.UtcNow + TimeSpan.FromDays(1)),
                0, String.Empty, String.Empty, UUID.Zero, 0, UUID.Zero, String.Empty, Vector3.Zero, String.Empty, 0, 0);
        }

        private void ClassifiedInfoUpdateHandler(UUID classifiedID, uint category, string name, string description,
            UUID parcelID, uint parentEstate, UUID snapshotID, Vector3 globalPos, byte classifiedFlags, int price,
            IClientAPI client)
        {
            // FIXME: Save this info
        }

        private void ClassifiedDeleteHandler(UUID classifiedID, IClientAPI client)
        {
            // FIXME: Delete the specified classified ad
        }

        #endregion Classifieds

        #region Picks

        private void HandleAvatarPicksRequest(Object sender, string method, List<String> args)
        {
            if (!(sender is IClientAPI))
                return;
            IClientAPI client = (IClientAPI)sender;

            UUID targetAvatarID;
            if (args.Count < 1 || !UUID.TryParse(args[0], out targetAvatarID))
            {
                m_log.Error("[SIMIAN PROFILES]: Unrecognized arguments for " + method);
                return;
            }

            // FIXME: Fetch these
            client.SendAvatarPicksReply(targetAvatarID, new Dictionary<UUID, string>(0));
        }

        private void HandlePickInfoRequest(Object sender, string method, List<String> args)
        {
            if (!(sender is IClientAPI))
                return;
            IClientAPI client = (IClientAPI)sender;

            UUID avatarID;
            UUID pickID;
            if (args.Count < 2 || !UUID.TryParse(args[0], out avatarID) || !UUID.TryParse(args[1], out pickID))
            {
                m_log.Error("[SIMIAN PROFILES]: Unrecognized arguments for " + method);
                return;
            }

            // FIXME: Fetch this
            client.SendPickInfoReply(pickID, avatarID, false, UUID.Zero, String.Empty, String.Empty, UUID.Zero, String.Empty,
                String.Empty, String.Empty, Vector3.Zero, 0, false);
        }

        private void PickInfoUpdateHandler(IClientAPI client, UUID pickID, UUID creatorID, bool topPick, string name,
            string desc, UUID snapshotID, int sortOrder, bool enabled)
        {
            // FIXME: Save this
        }

        private void PickDeleteHandler(IClientAPI client, UUID pickID)
        {
            // FIXME: Delete
        }

        #endregion Picks

        #region Notes

        private void HandleAvatarNotesRequest(Object sender, string method, List<String> args)
        {
            if (!(sender is IClientAPI))
                return;
            IClientAPI client = (IClientAPI)sender;

            UUID targetAvatarID;
            if (args.Count < 1 || !UUID.TryParse(args[0], out targetAvatarID))
            {
                m_log.Error("[SIMIAN PROFILES]: Unrecognized arguments for " + method);
                return;
            }

            // FIXME: Fetch this
            client.SendAvatarNotesReply(targetAvatarID, String.Empty);
        }

        private void AvatarNotesUpdateHandler(IClientAPI client, UUID targetID, string notes)
        {
            // FIXME: Save this
        }

        #endregion Notes

        #region Profiles

        private void RequestAvatarPropertiesHandler(IClientAPI client, UUID avatarID)
        {
            m_log.DebugFormat("[SIMIAN PROFILES]: Request avatar properties for {0}",avatarID);
            
            OSDMap user = FetchUserData(avatarID);

            ProfileFlags flags = ProfileFlags.AllowPublish | ProfileFlags.MaturePublish;

            if (user != null)
            {
                OSDMap about = null;
                if (user.ContainsKey("LLAbout"))
                {
                    try
                    {
                        about = OSDParser.DeserializeJson(user["LLAbout"].AsString()) as OSDMap;
                    }
                    catch
                    {
                        m_log.WarnFormat("[SIMIAN PROFILES]: Unable to decode LLAbout");
                    }
                }

                if (about == null)
                    about = new OSDMap(0);

                // Check if this user is a grid operator
                byte[] charterMember;
                if (user["AccessLevel"].AsInteger() >= 200)
                    charterMember = Utils.StringToBytes("Operator");
                else
                    charterMember = Utils.EmptyBytes;

                // Check if the user is online
                if (client.Scene is Scene)
                {
                    OpenSim.Services.Interfaces.PresenceInfo[] presences = ((Scene)client.Scene).PresenceService.GetAgents(new string[] { avatarID.ToString() });
                    if (presences != null && presences.Length > 0)
                        flags |= ProfileFlags.Online;
                }

                // Check if the user is identified
                if (user["Identified"].AsBoolean())
                    flags |= ProfileFlags.Identified;

                client.SendAvatarProperties(avatarID, about["About"].AsString(), user["CreationDate"].AsDate().ToString("M/d/yyyy",
                    System.Globalization.CultureInfo.InvariantCulture), charterMember, about["FLAbout"].AsString(), (uint)flags,
                    about["FLImage"].AsUUID(), about["Image"].AsUUID(), about["URL"].AsString(), user["Partner"].AsUUID());

                OSDMap interests = null;
                if (user.ContainsKey("LLInterests"))
                {
                    try
                    {
                        interests = OSDParser.DeserializeJson(user["LLInterests"].AsString()) as OSDMap;
                        client.SendAvatarInterestsReply(avatarID, interests["WantMask"].AsUInteger(), interests["WantText"].AsString(), interests["SkillsMask"].AsUInteger(), interests["SkillsText"].AsString(), interests["Languages"].AsString());
                    }
                    catch { }
                }

                if (about == null)
                    about = new OSDMap(0);
            }
            else
            {
                m_log.Warn("[SIMIAN PROFILES]: Failed to fetch profile information for " + client.Name + ", returning default values");
                client.SendAvatarProperties(avatarID, String.Empty, "1/1/1970", Utils.EmptyBytes,
                        String.Empty, (uint)flags, UUID.Zero, UUID.Zero, String.Empty, UUID.Zero);
            }
        }

        private void UpdateAvatarPropertiesHandler(IClientAPI client, UserProfileData profileData)
        {
            OSDMap map = new OSDMap
            {
                { "About", OSD.FromString(profileData.AboutText) },
                { "Image", OSD.FromUUID(profileData.Image) },
                { "FLAbout", OSD.FromString(profileData.FirstLifeAboutText) },
                { "FLImage", OSD.FromUUID(profileData.FirstLifeImage) },
                { "URL", OSD.FromString(profileData.ProfileUrl) }
            };

            AddUserData(client.AgentId, "LLAbout", map);
        }

        private void AvatarInterestUpdateHandler(IClientAPI client, uint wantmask, string wanttext, uint skillsmask,
            string skillstext, string languages)
        {
            OSDMap map = new OSDMap
            {
                { "WantMask", OSD.FromInteger(wantmask) },
                { "WantText", OSD.FromString(wanttext) },
                { "SkillsMask", OSD.FromInteger(skillsmask) },
                { "SkillsText", OSD.FromString(skillstext) },
                { "Languages", OSD.FromString(languages) }
            };

            AddUserData(client.AgentId, "LLInterests", map);
        }

        private void UserInfoRequestHandler(IClientAPI client)
        {
            m_log.Error("[SIMIAN PROFILES]: UserInfoRequestHandler");

            // Fetch this user's e-mail address
            NameValueCollection requestArgs = new NameValueCollection
            {
                { "RequestMethod", "GetUser" },
                { "UserID", client.AgentId.ToString() }
            };

            OSDMap response = WebUtil.PostToService(m_serverUrl, requestArgs);
            string email = response["Email"].AsString();

            if (!response["Success"].AsBoolean())
                m_log.Warn("[SIMIAN PROFILES]: GetUser failed during a user info request for " + client.Name);

            client.SendUserInfoReply(false, true, email);
        }

        private void UpdateUserInfoHandler(bool imViaEmail, bool visible, IClientAPI client)
        {
            m_log.Info("[SIMIAN PROFILES]: Ignoring user info update from " + client.Name);
        }

        #endregion Profiles

        /// <summary>
        /// Sanity checks regions for a valid estate owner at startup
        /// </summary>
        private void CheckEstateManager(Scene scene)
        {
            EstateSettings estate = scene.RegionInfo.EstateSettings;

            if (estate.EstateOwner == UUID.Zero)
            {
                // Attempt to lookup the grid admin
                UserAccount admin = scene.UserAccountService.GetUserAccount(scene.RegionInfo.ScopeID, UUID.Zero);
                if (admin != null)
                {
                    m_log.InfoFormat("[SIMIAN PROFILES]: Setting estate {0} (ID: {1}) owner to {2}", estate.EstateName,
                        estate.EstateID, admin.Name);

                    estate.EstateOwner = admin.PrincipalID;
                    estate.Save();
                }
                else
                {
                    m_log.WarnFormat("[SIMIAN PROFILES]: Estate {0} (ID: {1}) does not have an owner", estate.EstateName, estate.EstateID);
                }
            }
        }

        private bool AddUserData(UUID userID, string key, OSDMap value)
        {
            NameValueCollection requestArgs = new NameValueCollection
            {
                { "RequestMethod", "AddUserData" },
                { "UserID", userID.ToString() },
                { key, OSDParser.SerializeJsonString(value) }
            };

            OSDMap response = WebUtil.PostToService(m_serverUrl, requestArgs);
            bool success = response["Success"].AsBoolean();

            if (!success)
                m_log.WarnFormat("[SIMIAN PROFILES]: Failed to add user data with key {0} for {1}: {2}", key, userID, response["Message"].AsString());

            return success;
        }

        private OSDMap FetchUserData(UUID userID)
        {
            m_log.DebugFormat("[SIMIAN PROFILES]: Fetch information about {0}",userID);
            
            NameValueCollection requestArgs = new NameValueCollection
            {
                { "RequestMethod", "GetUser" },
                { "UserID", userID.ToString() }
            };

            OSDMap response = WebUtil.PostToService(m_serverUrl, requestArgs);
            if (response["Success"].AsBoolean() && response["User"] is OSDMap)
            {
                return (OSDMap)response["User"];
            }
            else
            {
                m_log.Error("[SIMIAN PROFILES]: Failed to fetch user data for " + userID + ": " + response["Message"].AsString());
            }

            return null;
        }
    }
}
