/*
 * 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.Reflection;
using OpenMetaverse;
using log4net;
using Nini.Config;
using Nwc.XmlRpc;
using OpenSim.Framework;
using OpenSim.Framework.Communications.Cache;
using OpenSim.Framework.Servers;
using OpenSim.Region.Interfaces;
using OpenSim.Region.Environment.Interfaces;
using OpenSim.Region.Environment.Scenes;

namespace OpenSim.Region.Environment.Modules.Avatar.Friends
{
    /*
        This module handles adding/removing friends, and the the presence
        notification process for login/logoff of friends.

        The presence notification works as follows:
        - After the user initially connects to a region (so we now have a UDP
          connection to work with), this module fetches the friends of user
          (those are cached), their on-/offline status, and info about the
          region they are in from the MessageServer.
        - (*) It then informs the user about the on-/offline status of her friends.
        - It then informs all online friends currently on this region-server about
          user's new online status (this will save some network traffic, as local
          messages don't have to be transferred inter-region, and it will be all
          that has to be done in Standalone Mode).
        - For the rest of the online friends (those not on this region-server),
          this module uses the provided region-information to map users to
          regions, and sends one notification to every region containing the
          friends to inform on that server.
        - The region-server will handle that in the following way:
          - If it finds the friend, it informs her about the user being online.
          - If it doesn't find the friend (maybe she TPed away in the meantime),
            it stores that information.
          - After it processed all friends, it returns the list of friends it
            couldn't find.
        - If this list isn't empty, the FriendsModule re-requests information
          about those online friends that have been missed and starts at (*)
          again until all friends have been found, or until it tried 3 times
          (to prevent endless loops due to some uncaught error).

        NOTE: Online/Offline notifications don't need to be sent on region change.

        We implement two XMLRpc handlers here, handling all the inter-region things
        we have to handle:
        - On-/Offline-Notifications (bulk)
        - Terminate Friendship messages (single)
     */

    public class FriendsModule : IRegionModule, IFriendsModule
    {
        private class Transaction
        {
            public UUID agentID;
            public string agentName;
            public uint count;

            public Transaction(UUID agentID, string agentName)
            {
                this.agentID = agentID;
                this.agentName = agentName;
                this.count = 1;
            }
        }

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

        private Cache m_friendLists = new Cache(CacheFlags.AllowUpdate);

        private Dictionary<UUID, ulong> m_rootAgents = new Dictionary<UUID, ulong>();

        private Dictionary<UUID, UUID> m_pendingCallingcardRequests = new Dictionary<UUID,UUID>();

        private Scene m_initialScene; // saves a lookup if we don't have a specific scene
        private Dictionary<ulong, Scene> m_scenes = new Dictionary<ulong,Scene>();
        private IMessageTransferModule m_TransferModule = null;

        #region IRegionModule Members

        public void Initialise(Scene scene, IConfigSource config)
        {
            lock (m_scenes)
            {
                if (m_scenes.Count == 0)
                {
                    scene.CommsManager.HttpServer.AddXmlRPCHandler("presence_update_bulk", processPresenceUpdateBulk);
                    scene.CommsManager.HttpServer.AddXmlRPCHandler("terminate_friend", processTerminateFriend);
                    m_friendLists.DefaultTTL = new TimeSpan(1, 0, 0);  // store entries for one hour max
                    m_initialScene = scene;
                }

                if (!m_scenes.ContainsKey(scene.RegionInfo.RegionHandle))
                    m_scenes[scene.RegionInfo.RegionHandle] = scene;
            }
            
            scene.RegisterModuleInterface<IFriendsModule>(this);
            
            scene.EventManager.OnNewClient += OnNewClient;
            scene.EventManager.OnIncomingInstantMessage += OnGridInstantMessage;
            scene.EventManager.OnAvatarEnteringNewParcel += AvatarEnteringParcel;
            scene.EventManager.OnMakeChildAgent += MakeChildAgent;
            scene.EventManager.OnClientClosed += ClientClosed;
        }

        public void PostInitialise()
        {
            if (m_scenes.Count > 0)
            {
                m_TransferModule = m_initialScene.RequestModuleInterface<IMessageTransferModule>();
            }
            if (m_TransferModule == null)
                m_log.Error("[FRIENDS]: Unable to find a message transfer module, friendship offers will not work");
        }

        public void Close()
        {
        }

        public string Name
        {
            get { return "FriendsModule"; }
        }

        public bool IsSharedModule
        {
            get { return true; }
        }

        #endregion

        /// <summary>
        /// Receive presence information changes about clients in other regions.
        /// </summary>
        /// <param name="req"></param>
        /// <returns></returns>
        public XmlRpcResponse processPresenceUpdateBulk(XmlRpcRequest req)
        {
            Hashtable requestData = (Hashtable)req.Params[0];

            List<UUID> friendsNotHere = new List<UUID>();

            // this is called with the expectation that all the friends in the request are on this region-server.
            // But as some time passed since we checked (on the other region-server, via the MessagingServer),
            // some of the friends might have teleported away.
            // Actually, even now, between this line and the sending below, some people could TP away. So,
            // we'll have to lock the m_rootAgents list for the duration to prevent/delay that.
            lock (m_rootAgents)
            {
                List<ScenePresence> friendsHere = new List<ScenePresence>();
                
                try
                {
                    UUID agentID = new UUID((string)requestData["agentID"]);
                    bool agentOnline = (bool)requestData["agentOnline"];
                    int count = (int)requestData["friendCount"];
                    for (int i = 0; i < count; ++i)
                    {
                        UUID uuid;
                        if (UUID.TryParse((string)requestData["friendID_" + i], out uuid))
                        {
                            if (m_rootAgents.ContainsKey(uuid)) friendsHere.Add(GetRootPresenceFromAgentID(uuid));
                            else friendsNotHere.Add(uuid);
                        }
                    }

                    // now send, as long as they are still here...
                    UUID[] agentUUID = new UUID[] { agentID };
                    if (agentOnline)
                    {
                        foreach (ScenePresence agent in friendsHere)
                        {
                            agent.ControllingClient.SendAgentOnline(agentUUID);
                        }
                    }
                    else
                    {
                        foreach (ScenePresence agent in friendsHere)
                        {
                            agent.ControllingClient.SendAgentOffline(agentUUID);
                        }
                    }
                }
                catch(Exception e)
                {
                    m_log.Warn("[FRIENDS]: Got exception while parsing presence_update_bulk request:", e);
                }
            }

            // no need to lock anymore; if TPs happen now, worst case is that we have an additional agent in this region,
            // which should be caught on the next iteration...
            Hashtable result = new Hashtable();
            int idx = 0;
            foreach (UUID uuid in friendsNotHere)
            {
                result["friendID_" + idx++] = uuid.ToString();
            }
            result["friendCount"] = idx;

            XmlRpcResponse response = new XmlRpcResponse();
            response.Value = result;

            return response;
        }

        public XmlRpcResponse processTerminateFriend(XmlRpcRequest req)
        {
            Hashtable requestData = (Hashtable)req.Params[0];

            bool success = false;

            UUID agentID;
            UUID friendID;
            if (requestData.ContainsKey("agentID") && UUID.TryParse((string)requestData["agentID"], out agentID) &&
                requestData.ContainsKey("friendID") && UUID.TryParse((string)requestData["friendID"], out friendID))
            {
                // try to find it and if it is there, prevent it to vanish before we sent the message
                lock (m_rootAgents)
                {
                    if (m_rootAgents.ContainsKey(agentID))
                    {
                        m_log.DebugFormat("[FRIEND]: Sending terminate friend {0} to agent {1}", friendID, agentID);
                        GetRootPresenceFromAgentID(agentID).ControllingClient.SendTerminateFriend(friendID);
                        success = true;
                    }
                }
            }

            // return whether we were successful
            Hashtable result = new Hashtable();
            result["success"] = success;

            XmlRpcResponse response = new XmlRpcResponse();
            response.Value = result;
            return response;
        }

        private void OnNewClient(IClientAPI client)
        {
            // All friends establishment protocol goes over instant message
            // There's no way to send a message from the sim
            // to a user to 'add a friend' without causing dialog box spam

            // Subscribe to instant messages
            client.OnInstantMessage += OnInstantMessage;

            // Friend list management
            client.OnApproveFriendRequest += OnApproveFriendRequest;
            client.OnDenyFriendRequest += OnDenyFriendRequest;
            client.OnTerminateFriendship += OnTerminateFriendship;

            // ... calling card handling...
            client.OnOfferCallingCard += OnOfferCallingCard;
            client.OnAcceptCallingCard += OnAcceptCallingCard;
            client.OnDeclineCallingCard += OnDeclineCallingCard;

            // we need this one exactly once per agent session (see comments in the handler below)
            client.OnEconomyDataRequest += OnEconomyDataRequest;

            // if it leaves, we want to know, too
            client.OnLogout += OnLogout;
        }

        private void ClientClosed(UUID AgentId)
        {
            // agent's client was closed. As we handle logout in OnLogout, this here has only to handle
            // TPing away (root agent is closed) or TPing/crossing in a region far enough away (client
            // agent is closed).
            // NOTE: In general, this doesn't mean that the agent logged out, just that it isn't around
            // in one of the regions here anymore.
            lock (m_rootAgents)
            {
                if (m_rootAgents.ContainsKey(AgentId))
                {
                    m_rootAgents.Remove(AgentId);
                }
            }
        }

        private void AvatarEnteringParcel(ScenePresence avatar, int localLandID, UUID regionID)
        {
            lock (m_rootAgents)
            {
                m_rootAgents[avatar.UUID] = avatar.RegionHandle;
                // Claim User! my user!  Mine mine mine!
            }
        }

        private void MakeChildAgent(ScenePresence avatar)
        {
            lock (m_rootAgents)
            {
                if (m_rootAgents.ContainsKey(avatar.UUID))
                {
                    // only delete if the region matches. As this is a shared module, the avatar could be
                    // root agent in another region on this server.
                    if (m_rootAgents[avatar.UUID] == avatar.RegionHandle)
                    {
                        m_rootAgents.Remove(avatar.UUID);
//                        m_log.Debug("[FRIEND]: Removing " + avatar.Firstname + " " + avatar.Lastname + " as a root agent");
                    }
                }
            }
        }

        private ScenePresence GetRootPresenceFromAgentID(UUID AgentID)
        {
            ScenePresence returnAgent = null;
            lock (m_scenes)
            {
                ScenePresence queryagent = null;
                foreach (Scene scene in m_scenes.Values)
                {
                    queryagent = scene.GetScenePresence(AgentID);
                    if (queryagent != null)
                    {
                        if (!queryagent.IsChildAgent)
                        {
                            returnAgent = queryagent;
                            break;
                        }
                    }
                }
            }
            return returnAgent;
        }

        private ScenePresence GetAnyPresenceFromAgentID(UUID AgentID)
        {
            ScenePresence returnAgent = null;
            lock (m_scenes)
            {
                ScenePresence queryagent = null;
                foreach (Scene scene in m_scenes.Values)
                {
                    queryagent = scene.GetScenePresence(AgentID);
                    if (queryagent != null)
                    {
                        returnAgent = queryagent;
                        break;
                    }
                }
            }
            return returnAgent;
        }
        
        public void OfferFriendship(UUID fromUserId, IClientAPI toUserClient, string offerMessage)
        {
            CachedUserInfo userInfo = m_initialScene.CommsManager.UserProfileCacheService.GetUserDetails(fromUserId);
                
            if (userInfo != null)
            {
                GridInstantMessage msg = new GridInstantMessage(
                    toUserClient.Scene, fromUserId, userInfo.UserProfile.Name, toUserClient.AgentId,
                    (byte)InstantMessageDialog.FriendshipOffered, offerMessage, false, Vector3.Zero); 
            
                FriendshipOffered(msg);
            }
            else
            {
                m_log.ErrorFormat("[FRIENDS]: No user found for id {0} in OfferFriendship()", fromUserId);
            }
        }        

        #region FriendRequestHandling

        private void OnInstantMessage(IClientAPI client, GridInstantMessage im)
        {
            // Friend Requests go by Instant Message..    using the dialog param
            // https://wiki.secondlife.com/wiki/ImprovedInstantMessage

            if (im.dialog == (byte)InstantMessageDialog.FriendshipOffered) // 38
            {
                // fromAgentName is the *destination* name (the friend we offer friendship to)                
                ScenePresence initiator = GetAnyPresenceFromAgentID(new UUID(im.fromAgentID));
                im.fromAgentName = initiator != null ? initiator.Name : "(hippo)";
                
                FriendshipOffered(im);
            }
            else if (im.dialog == (byte)InstantMessageDialog.FriendshipAccepted) // 39
            {
                FriendshipAccepted(client, im);
            }
            else if (im.dialog == (byte)InstantMessageDialog.FriendshipDeclined) // 40
            {
                FriendshipDeclined(client, im);
            }
        }
        
        /// <summary>
        /// Invoked when a user offers a friendship.
        /// </summary>
        /// 
        /// <param name="im"></param>    
        /// <param name="client"></param>
        private void FriendshipOffered(GridInstantMessage im)
        {
            // this is triggered by the initiating agent:
            // A local agent offers friendship to some possibly remote friend.
            // A IM is triggered, processed here and sent to the friend (possibly in a remote region).           

            m_log.DebugFormat("[FRIEND]: Offer(38) - From: {0}, FromName: {1} To: {2}, Session: {3}, Message: {4}, Offline {5}",
                       im.fromAgentID, im.fromAgentName, im.toAgentID, im.imSessionID, im.message, im.offline);

            // 1.20 protocol sends an UUID in the message field, instead of the friendship offer text.
            // For interoperability, we have to clear that
            if (Util.isUUID(im.message)) im.message = "";

            // be sneeky and use the initiator-UUID as transactionID. This means we can be stateless.
            // we have to look up the agent name on friendship-approval, though.
            im.imSessionID = im.fromAgentID;

            if (m_TransferModule != null)
            {
                // Send it to whoever is the destination.
                // If new friend is local, it will send an IM to the viewer.
                // If new friend is remote, it will cause a OnGridInstantMessage on the remote server
                m_TransferModule.SendInstantMessage(
                    im,
                    delegate(bool success) 
                    {
                        m_log.DebugFormat("[FRIEND]: sending IM success = {0}", success);
                    }
                );
            }            
        }
        
        /// <summary>
        /// Invoked when a user accepts a friendship offer.
        /// </summary>
        /// <param name="im"></param>
        /// <param name="client"></param>
        private void FriendshipAccepted(IClientAPI client, GridInstantMessage im)
        {
            m_log.DebugFormat("[FRIEND]: 39 - from client {0}, agent {2} {3}, imsession {4} to {5}: {6} (dialog {7})",
              client.AgentId, im.fromAgentID, im.fromAgentName, im.imSessionID, im.toAgentID, im.message, im.dialog);            
        }
        
        /// <summary>
        /// Invoked when a user declines a friendship offer.
        /// </summary>
        /// May not currently be used - see OnDenyFriendRequest() instead
        /// <param name="im"></param>
        /// <param name="client"></param>
        private void FriendshipDeclined(IClientAPI client, GridInstantMessage im)
        {
            UUID fromAgentID = new UUID(im.fromAgentID);
            UUID toAgentID = new UUID(im.toAgentID);
            
            // declining the friendship offer causes a type 40 IM being sent to the (possibly remote) initiator
            // toAgentID is initiator, fromAgentID declined friendship
            m_log.DebugFormat("[FRIEND]: 40 - from client {0}, agent {1} {2}, imsession {3} to {4}: {5} (dialog {6})",
              client != null ? client.AgentId.ToString() : "<null>",
              fromAgentID, im.fromAgentName, im.imSessionID, im.toAgentID, im.message, im.dialog);

            // Send the decline to whoever is the destination.
            GridInstantMessage msg 
                = new GridInstantMessage(
                    client.Scene, fromAgentID, client.Name, toAgentID,
                    im.dialog, im.message, im.offline != 0, im.Position);
            
            // If new friend is local, it will send an IM to the viewer.
            // If new friend is remote, it will cause a OnGridInstantMessage on the remote server
            m_TransferModule.SendInstantMessage(msg,
                delegate(bool success) {
                    m_log.DebugFormat("[FRIEND]: sending IM success = {0}", success);
                }
            );            
        }

        private void OnGridInstantMessage(GridInstantMessage msg)
        {
            // This event won't be raised unless we have that agent,
            // so we can depend on the above not trying to send
            // via grid again
            m_log.DebugFormat("[FRIEND]: Got GridIM from {0}, to {1}, imSession {2}, message {3}, dialog {4}",
                              msg.fromAgentID, msg.toAgentID, msg.imSessionID, msg.message, msg.dialog);
            
            if (msg.dialog == (byte)InstantMessageDialog.FriendshipOffered ||
                msg.dialog == (byte)InstantMessageDialog.FriendshipAccepted ||
                msg.dialog == (byte)InstantMessageDialog.FriendshipDeclined)
            {
                // this should succeed as we *know* the root agent is here.
                m_TransferModule.SendInstantMessage(msg,
                    delegate(bool success) {
                        m_log.DebugFormat("[FRIEND]: sending IM success = {0}", success);
                    }
                );
            }

            if (msg.dialog == (byte)InstantMessageDialog.FriendshipAccepted)
            {
                // for accept friendship, we have to do a bit more
                ApproveFriendship(new UUID(msg.fromAgentID), new UUID(msg.toAgentID), msg.fromAgentName);
            }
        }

        private void ApproveFriendship(UUID fromAgentID, UUID toAgentID, string fromName)
        {
            m_log.DebugFormat("[FRIEND]: Approve friendship from {0} (ID: {1}) to {2}",
                              fromAgentID, fromName, toAgentID);

            // a new friend was added in the initiator's and friend's data, so the cache entries are wrong now.
            lock (m_friendLists)
            {
                m_friendLists.Invalidate(fromAgentID);
                m_friendLists.Invalidate(toAgentID);
            }

            // now send presence update and add a calling card for the new friend

            ScenePresence initiator = GetAnyPresenceFromAgentID(toAgentID);
            if (initiator == null)
            {
                // quite wrong. Shouldn't happen.
                m_log.WarnFormat("[FRIEND]: Coudn't find initiator of friend request {0}", toAgentID);
                return;
            }

            m_log.DebugFormat("[FRIEND]: Tell {0} that {1} is online",
                              initiator.Name, fromName);
            // tell initiator that friend is online
            initiator.ControllingClient.SendAgentOnline(new UUID[] { fromAgentID });

            // find the folder for the friend...
            InventoryFolderImpl folder =
                initiator.Scene.CommsManager.UserProfileCacheService.GetUserDetails(toAgentID).FindFolderForType((int)InventoryType.CallingCard);
            if (folder != null)
            {
                // ... and add the calling card
                CreateCallingCard(initiator.ControllingClient, fromAgentID, folder.ID, fromName);
            }
        }

        private void OnApproveFriendRequest(IClientAPI client, UUID agentID, UUID friendID, List<UUID> callingCardFolders)
        {
            m_log.DebugFormat("[FRIEND]: Got approve friendship from {0} {1}, agentID {2}, tid {3}",
                              client.Name, client.AgentId, agentID, friendID);

            // store the new friend persistently for both avatars
            m_initialScene.StoreAddFriendship(friendID, agentID, (uint) FriendRights.CanSeeOnline);

            // The cache entries aren't valid anymore either, as we just added a friend to both sides.
            lock (m_friendLists)
            {
                m_friendLists.Invalidate(agentID);
                m_friendLists.Invalidate(friendID);
            }

            // if it's a local friend, we don't have to do the lookup
            ScenePresence friendPresence = GetAnyPresenceFromAgentID(friendID);

            if (friendPresence != null)
            {
                m_log.Debug("[FRIEND]: Local agent detected.");

                // create calling card
                CreateCallingCard(client, friendID, callingCardFolders[0], friendPresence.Name);

                // local message means OnGridInstantMessage won't be triggered, so do the work here.
                friendPresence.ControllingClient.SendInstantMessage(agentID, agentID.ToString(), friendID, client.Name,
                                                                    (byte)InstantMessageDialog.FriendshipAccepted,
                                                                    (uint)Util.UnixTimeSinceEpoch());
                ApproveFriendship(agentID, friendID, client.Name);
            }
            else
            {
                m_log.Debug("[FRIEND]: Remote agent detected.");

                // fetch the friend's name for the calling card.
                CachedUserInfo info = m_initialScene.CommsManager.UserProfileCacheService.GetUserDetails(friendID);

                // create calling card
                CreateCallingCard(client, friendID, callingCardFolders[0],
                                  info.UserProfile.FirstName + " " + info.UserProfile.SurName);

                // Compose (remote) response to friend.
                GridInstantMessage msg = new GridInstantMessage(client.Scene, agentID, client.Name, friendID,
                                                                (byte)InstantMessageDialog.FriendshipAccepted,
                                                                agentID.ToString(), false, Vector3.Zero);
                if (m_TransferModule != null)
                {
                    m_TransferModule.SendInstantMessage(msg,
                        delegate(bool success) {
                            m_log.DebugFormat("[FRIEND]: sending IM success = {0}", success);
                        }
                    );
                }
            }

            // tell client that new friend is online
            client.SendAgentOnline(new UUID[] { friendID });
        }

        private void OnDenyFriendRequest(IClientAPI client, UUID agentID, UUID friendID, List<UUID> callingCardFolders)
        {
            m_log.DebugFormat("[FRIEND]: Got deny friendship from {0} {1}, agentID {2}, tid {3}",
                              client.Name, client.AgentId, agentID, friendID);

            // Compose response to other agent.
            GridInstantMessage msg = new GridInstantMessage(client.Scene, agentID, client.Name, friendID,
                                                            (byte)InstantMessageDialog.FriendshipDeclined,
                                                            agentID.ToString(), false, Vector3.Zero);
            // send decline to initiator
            if (m_TransferModule != null)
            {
                m_TransferModule.SendInstantMessage(msg,
                    delegate(bool success) {
                        m_log.DebugFormat("[FRIEND]: sending IM success = {0}", success);
                    }
                );
            }
        }

        private void OnTerminateFriendship(IClientAPI client, UUID agentID, UUID exfriendID)
        {
            // client.AgentId == agentID!

            // this removes the friends from the stored friendlists. After the next login, they will be gone...
            m_initialScene.StoreRemoveFriendship(agentID, exfriendID);

            // ... now tell the two involved clients that they aren't friends anymore.

            // I don't know why we have to tell <agent>, as this was caused by her, but that's how it works in SL...
            client.SendTerminateFriend(exfriendID);

            // now send the friend, if online
            ScenePresence presence = GetAnyPresenceFromAgentID(exfriendID);
            if (presence != null)
            {
                m_log.DebugFormat("[FRIEND]: Sending terminate friend {0} to agent {1}", agentID, exfriendID);
                presence.ControllingClient.SendTerminateFriend(agentID);
            }
            else
            {
                // retry 3 times, in case the agent TPed from the last known region...
                for (int retry = 0; retry < 3; ++retry)
                {
                    // wasn't sent, so ex-friend wasn't around on this region-server. Fetch info and try to send
                    UserAgentData data = m_initialScene.CommsManager.UserService.GetAgentByUUID(exfriendID);
                    
                    if (null == data)
                        break;
                    
                    if (!data.AgentOnline)
                    {
                        m_log.DebugFormat("[FRIEND]: {0} is offline, so not sending TerminateFriend", exfriendID);
                        break; // if ex-friend isn't online, we don't need to send
                    }

                    m_log.DebugFormat("[FRIEND]: Sending remote terminate friend {0} to agent {1}@{2}",
                                      agentID, exfriendID, data.Handle);

                    // try to send to foreign region, retry if it fails (friend TPed away, for example)
                    if (m_initialScene.TriggerTerminateFriend(data.Handle, exfriendID, agentID)) break;
                }
            }

            // clean up cache: FriendList is wrong now...
            lock (m_friendLists)
            {
                m_friendLists.Invalidate(agentID);
                m_friendLists.Invalidate(exfriendID);
            }
        }

        #endregion

        #region CallingCards

        private void OnOfferCallingCard(IClientAPI client, UUID destID, UUID transactionID)
        {
            m_log.DebugFormat("[CALLING CARD]: got offer from {0} for {1}, transaction {2}",
                              client.AgentId, destID, transactionID);
            // This might be slightly wrong. On a multi-region server, we might get the child-agent instead of the root-agent
            // (or the root instead of the child)
            ScenePresence destAgent = GetAnyPresenceFromAgentID(destID);
            if (destAgent == null)
            {
                client.SendAlertMessage("The person you have offered a card to can't be found anymore.");
                return;
            }

            lock (m_pendingCallingcardRequests)
            {
                m_pendingCallingcardRequests[transactionID] = client.AgentId;
            }
            // inform the destination agent about the offer
            destAgent.ControllingClient.SendOfferCallingCard(client.AgentId, transactionID);
        }

        private void CreateCallingCard(IClientAPI client, UUID creator, UUID folder, string name)
        {
            InventoryItemBase item = new InventoryItemBase();
            item.AssetID = UUID.Zero;
            item.AssetType = (int)AssetType.CallingCard;
            item.BasePermissions = (uint)PermissionMask.Copy;
            item.CreationDate = Util.UnixTimeSinceEpoch();
            item.Creator = creator;
            item.CurrentPermissions = item.BasePermissions;
            item.Description = "";
            item.EveryOnePermissions = (uint)PermissionMask.None;
            item.Flags = 0;
            item.Folder = folder;
            item.GroupID = UUID.Zero;
            item.GroupOwned = false;
            item.ID = UUID.Random();
            item.InvType = (int)InventoryType.CallingCard;
            item.Name = name;
            item.NextPermissions = item.EveryOnePermissions;
            item.Owner = client.AgentId;
            item.SalePrice = 10;
            item.SaleType = (byte)SaleType.Not;
            ((Scene)client.Scene).AddInventoryItem(client, item);
        }

        private void OnAcceptCallingCard(IClientAPI client, UUID transactionID, UUID folderID)
        {
            m_log.DebugFormat("[CALLING CARD]: User {0} ({1} {2}) accepted tid {3}, folder {4}",
                              client.AgentId,
                              client.FirstName, client.LastName,
                              transactionID, folderID);
            UUID destID;
            lock (m_pendingCallingcardRequests)
            {
                if (!m_pendingCallingcardRequests.TryGetValue(transactionID, out destID))
                {
                    m_log.WarnFormat("[CALLING CARD]: Got a AcceptCallingCard from {0} without an offer before.",
                                     client.Name);
                    return;
                }
                // else found pending calling card request with that transaction.
                m_pendingCallingcardRequests.Remove(transactionID);
            }


            ScenePresence destAgent = GetAnyPresenceFromAgentID(destID);
            // inform sender of the card that destination declined the offer
            if (destAgent != null) destAgent.ControllingClient.SendAcceptCallingCard(transactionID);

            // put a calling card into the inventory of receiver
            CreateCallingCard(client, destID, folderID, destAgent.Name);
        }

        private void OnDeclineCallingCard(IClientAPI client, UUID transactionID)
        {
            m_log.DebugFormat("[CALLING CARD]: User {0} (ID:{1}) declined card, tid {2}",
                              client.Name, client.AgentId, transactionID);
            UUID destID;
            lock (m_pendingCallingcardRequests)
            {
                if (!m_pendingCallingcardRequests.TryGetValue(transactionID, out destID))
                {
                    m_log.WarnFormat("[CALLING CARD]: Got a AcceptCallingCard from {0} without an offer before.",
                                     client.Name);
                    return;
                }
                // else found pending calling card request with that transaction.
                m_pendingCallingcardRequests.Remove(transactionID);
            }

            ScenePresence destAgent = GetAnyPresenceFromAgentID(destID);
            // inform sender of the card that destination declined the offer
            if (destAgent != null) destAgent.ControllingClient.SendDeclineCallingCard(transactionID);
        }

        /// <summary>
        /// Send presence information about a client to other clients in both this region and others.
        /// </summary>
        /// <param name="client"></param>
        /// <param name="friendList"></param>
        /// <param name="iAmOnline"></param>
        private void SendPresenceState(IClientAPI client, List<FriendListItem> friendList, bool iAmOnline)
        {
            //m_log.DebugFormat("[FRIEND]: {0} logged {1}; sending presence updates", client.Name, iAmOnline ? "in" : "out");

            if (friendList == null || friendList.Count == 0)
            {
                //m_log.DebugFormat("[FRIEND]: {0} doesn't have friends.", client.Name);
                return; // nothing we can do if she doesn't have friends...
            }

            // collect sets of friendIDs; to send to (online and offline), and to receive from
            // TODO: If we ever switch to .NET >= 3, replace those Lists with HashSets.
            // I can't believe that we have Dictionaries, but no Sets, considering Java introduced them years ago...
            List<UUID> friendIDsToSendTo = new List<UUID>();
            List<UUID> candidateFriendIDsToReceive = new List<UUID>();
            
            foreach (FriendListItem item in friendList)
            {
                if (((item.FriendListOwnerPerms | item.FriendPerms) & (uint)FriendRights.CanSeeOnline) != 0)
                {
                    // friend is allowed to see my presence => add
                    if ((item.FriendListOwnerPerms & (uint)FriendRights.CanSeeOnline) != 0) 
                        friendIDsToSendTo.Add(item.Friend);

                    if ((item.FriendPerms & (uint)FriendRights.CanSeeOnline) != 0) 
                        candidateFriendIDsToReceive.Add(item.Friend);
                }
            }

            // we now have a list of "interesting" friends (which we have to find out on-/offline state for),
            // friends we want to send our online state to (if *they* are online, too), and
            // friends we want to receive online state for (currently unknown whether online or not)

            // as this processing might take some time and friends might TP away, we try up to three times to
            // reach them. Most of the time, we *will* reach them, and this loop won't loop
            int retry = 0;
            do
            {
                // build a list of friends to look up region-information and on-/offline-state for
                List<UUID> friendIDsToLookup = new List<UUID>(friendIDsToSendTo);
                foreach (UUID uuid in candidateFriendIDsToReceive)
                {
                    if (!friendIDsToLookup.Contains(uuid)) friendIDsToLookup.Add(uuid);
                }

                m_log.DebugFormat(
                    "[FRIEND]: {0} to lookup, {1} to send to, {2} candidates to receive from for agent {3}",
                    friendIDsToLookup.Count, friendIDsToSendTo.Count, candidateFriendIDsToReceive.Count, client.Name);

                // we have to fetch FriendRegionInfos, as the (cached) FriendListItems don't
                // necessarily contain the correct online state...
                Dictionary<UUID, FriendRegionInfo> friendRegions = m_initialScene.GetFriendRegionInfos(friendIDsToLookup);
                m_log.DebugFormat(
                    "[FRIEND]: Found {0} regionInfos for {1} friends of {2}",
                    friendRegions.Count, friendIDsToLookup.Count, client.Name);

                // argument for SendAgentOn/Offline; we shouldn't generate that repeatedly within loops.
                UUID[] agentArr = new UUID[] { client.AgentId };

                // first, send to friend presence state to me, if I'm online...
                if (iAmOnline)
                {
                    List<UUID> friendIDsToReceive = new List<UUID>();
                    
                    for (int i = candidateFriendIDsToReceive.Count - 1; i >= 0; --i)
                    {
                        UUID uuid = candidateFriendIDsToReceive[i];
                        FriendRegionInfo info;
                        if (friendRegions.TryGetValue(uuid, out info) && info != null && info.isOnline)
                        {
                            friendIDsToReceive.Add(uuid);
                        }
                    }
                    
                    m_log.DebugFormat(
                        "[FRIEND]: Sending {0} online friends to {1}", friendIDsToReceive.Count, client.Name);
                    
                    if (friendIDsToReceive.Count > 0) 
                        client.SendAgentOnline(friendIDsToReceive.ToArray());

                    // clear them for a possible second iteration; we don't have to repeat this
                    candidateFriendIDsToReceive.Clear();
                }

                // now, send my presence state to my friends
                for (int i = friendIDsToSendTo.Count - 1; i >= 0; --i)
                {
                    UUID uuid = friendIDsToSendTo[i];
                    FriendRegionInfo info;
                    if (friendRegions.TryGetValue(uuid, out info) && info != null && info.isOnline)
                    {
                        // any client is good enough, root or child...
                        ScenePresence agent = GetAnyPresenceFromAgentID(uuid);
                        if (agent != null)
                        {
                            m_log.DebugFormat("[FRIEND]: Found local agent {0}", agent.Name);

                            // friend is online and on this server...
                            if (iAmOnline) agent.ControllingClient.SendAgentOnline(agentArr);
                            else agent.ControllingClient.SendAgentOffline(agentArr);

                            // done, remove it
                            friendIDsToSendTo.RemoveAt(i);
                        }
                    }
                    else
                    {
                        m_log.DebugFormat("[FRIEND]: Friend {0} ({1}) is offline; not sending.", uuid, i);

                        // friend is offline => no need to try sending
                        friendIDsToSendTo.RemoveAt(i);
                    }
                }

                m_log.DebugFormat("[FRIEND]: Have {0} friends to contact via inter-region comms.", friendIDsToSendTo.Count);

                // we now have all the friends left that are online (we think), but not on this region-server
                if (friendIDsToSendTo.Count > 0)
                {
                    // sort them into regions
                    Dictionary<ulong, List<UUID>> friendsInRegion = new Dictionary<ulong,List<UUID>>();
                    foreach (UUID uuid in friendIDsToSendTo)
                    {
                        ulong handle = friendRegions[uuid].regionHandle; // this can't fail as we filtered above already
                        List<UUID> friends;
                        if (!friendsInRegion.TryGetValue(handle, out friends))
                        {
                            friends = new List<UUID>();
                            friendsInRegion[handle] = friends;
                        }
                        friends.Add(uuid);
                    }
                    m_log.DebugFormat("[FRIEND]: Found {0} regions to send to.", friendRegions.Count);

                    // clear uuids list and collect missed friends in it for the next retry
                    friendIDsToSendTo.Clear();

                    // send bulk updates to the region
                    foreach (KeyValuePair<ulong, List<UUID>> pair in friendsInRegion)
                    {
                        m_log.DebugFormat("[FRIEND]: Inform {0} friends in region {1} that user {2} is {3}line",
                                          pair.Value.Count, pair.Key, client.Name, iAmOnline ? "on" : "off");

                        friendIDsToSendTo.AddRange(m_initialScene.InformFriendsInOtherRegion(client.AgentId, pair.Key, pair.Value, iAmOnline));
                    }
                }
                // now we have in friendIDsToSendTo only the agents left that TPed away while we tried to contact them.
                // In most cases, it will be empty, and it won't loop here. But sometimes, we have to work harder and try again...
            }
            while (++retry < 3 && friendIDsToSendTo.Count > 0);
        }

        private void OnEconomyDataRequest(UUID agentID)
        {
            // KLUDGE: This is the only way I found to get a message (only) after login was completed and the
            // client is connected enough to receive UDP packets).
            // This packet seems to be sent only once, just after connection was established to the first
            // region after login.
            // We use it here to trigger a presence update; the old update-on-login was never be heard by
            // the freshly logged in viewer, as it wasn't connected to the region at that time.
            // TODO: Feel free to replace this by a better solution if you find one.

            // get the agent. This should work every time, as we just got a packet from it
            //ScenePresence agent = GetRootPresenceFromAgentID(agentID);
            // KLUDGE 2: As this is sent quite early, the avatar isn't here as root agent yet. So, we have to cheat a bit
            ScenePresence agent = GetAnyPresenceFromAgentID(agentID);

            // just to be paranoid...
            if (agent == null)
            {
                m_log.ErrorFormat("[FRIEND]: Got a packet from agent {0} who can't be found anymore!?", agentID);
                return;
            }

            List<FriendListItem> fl;
            lock (m_friendLists)
            {
                fl = (List<FriendListItem>)m_friendLists.Get(agent.ControllingClient.AgentId,
                                                             m_initialScene.GetFriendList);
            }

            // tell everyone that we are online
            SendPresenceState(agent.ControllingClient, fl, true);
        }

        private void OnLogout(IClientAPI remoteClient)
        {
            List<FriendListItem> fl;
            lock (m_friendLists)
            {
                fl = (List<FriendListItem>)m_friendLists.Get(remoteClient.AgentId,
                                                             m_initialScene.GetFriendList);
            }

            // tell everyone that we are offline
            SendPresenceState(remoteClient, fl, false);
        }
    }

    #endregion
}