/*
 * Smuxi - Smart MUltipleXed Irc
 *
 * Copyright (c) 2005-2016 Mirco Bauer <meebey@meebey.net>
 * Copyright (c) 2011 Tuukka Hastrup <Tuukka.Hastrup@iki.fi>
 * Copyright (c) 2013-2014 Oliver Schneider <smuxi@oli-obk.de>
 *
 * Full GPL License: <http://www.gnu.org/licenses/gpl.txt>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
 */

using System;
using System.IO;
using System.Net.Security;
using System.Xml;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;

using agsXMPP;
using agsXMPP.protocol;
using agsXMPP.protocol.client;
using agsXMPP.protocol.x.muc;
using agsXMPP.protocol.iq;
using agsXMPP.protocol.iq.roster;
using agsXMPP.protocol.iq.disco;
using agsXMPP.protocol.extensions.caps;
using agsXMPP.protocol.extensions.chatstates;
using XmppMessageType = agsXMPP.protocol.client.MessageType;
using agsXMPP.Factory;
using agsXMPP.Net;

using Starksoft.Net.Proxy;

using Smuxi.Common;
using System.Runtime.CompilerServices;
using agsXMPP.protocol.extensions.nickname;

namespace Smuxi.Engine
{
    [ProtocolManagerInfo(Name = "XMPP", Description = "Extensible Messaging and Presence Protocol", Alias = "xmpp")]
    public class XmppProtocolManager : ProtocolManagerBase
    {
#if LOG4NET
        static readonly log4net.ILog _Logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#endif
        static readonly string LibraryTextDomain = "smuxi-engine-xmpp";
        XmppClientConnection JabberClient { get; set; }
        MucManager MucManager { get; set; }
        DiscoManager Disco { get; set; }
        string[] Nicknames { get; set; }
        Dictionary<Jid, XmppPersonModel> Contacts { get; set; }
        Dictionary<string, DiscoInfo> DiscoCache { get; set; }
        ChatModel NetworkChat { get; set; }
        GroupChatModel ContactChat { get; set; }
        XmppServerModel Server { get; set; }
        bool AutoReconnect { get; set; }
        TimeSpan AutoReconnectDelay { get; set; }
        bool IsDisposed { get; set; }
        bool ShowChatStates { get; set; }
        // pidgin's psychic mode
        bool OpenNewChatOnChatState { get; set; }

        public override string NetworkID {
            get {
                return Host;
            }
        }

        public override string Protocol {
            get {
                return "XMPP";
            }
        }

        public override ChatModel Chat {
            get {
                return NetworkChat;
            }
        }

        public override bool IsConnected {
            get {
                return JabberClient.Authenticated;
            }
        }

        public XmppProtocolManager(Session session) : base(session)
        {
            Trace.Call(session);
            Contacts = new Dictionary<Jid, XmppPersonModel>();
            DiscoCache = new Dictionary<string, DiscoInfo>();

            ShowChatStates = true;
            OpenNewChatOnChatState = true;

            JabberClient = new XmppClientConnection();
            JabberClient.AutoRoster = true;
            JabberClient.AutoPresence = true;
            JabberClient.OnMessage += OnMessage;
            JabberClient.OnClose += OnClose;
            JabberClient.OnLogin += OnLogin;
            JabberClient.OnError += OnError;
            JabberClient.OnStreamError += OnStreamError;
            JabberClient.OnPresence += OnPresence;
            JabberClient.OnRosterItem += OnRosterItem;
            JabberClient.OnReadXml += OnReadXml;
            JabberClient.OnWriteXml += OnWriteXml;
            JabberClient.OnAuthError += OnAuthError;
            JabberClient.SendingServiceUnavailable += OnSendingServiceUnavailable;
            JabberClient.AutoAgents = false; // outdated feature
            JabberClient.EnableCapabilities = true;
            JabberClient.Capabilities.Node = "https://smuxi.im";
            JabberClient.ClientVersion = Engine.VersionString;

            // identify smuxi
            var ident = JabberClient.DiscoInfo.AddIdentity();
            ident.Category = "client";
            ident.Type = "pc";
            ident.Name = Engine.VersionString;

            // add features here (this is just for notification of other clients)
            JabberClient.DiscoInfo.AddFeature().Var = "http://jabber.org/protocol/caps";
            JabberClient.DiscoInfo.AddFeature().Var = "jabber:iq:last";
            JabberClient.DiscoInfo.AddFeature().Var = "http://jabber.org/protocol/muc";
            JabberClient.DiscoInfo.AddFeature().Var = "http://jabber.org/protocol/disco#info";
            JabberClient.DiscoInfo.AddFeature().Var = "http://jabber.org/protocol/xhtml-im";

            Disco = new DiscoManager(JabberClient);
            Disco.AutoAnswerDiscoInfoRequests = true;

            MucManager = new MucManager(JabberClient);
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void OnSendingServiceUnavailable(object sender, SendingServiceUnavailableEventArgs e)
        {
            if (e.Stanza.To == null) {
                // can only be received by the server
                return;
            }
            if (e.Stanza.To == JabberClient.MyJID.Server) {
                // explicitly targeting the server
                return;
            }
            XmppPersonModel person;
            if (!Contacts.TryGetValue(e.Stanza.To.Bare, out person)) {
                e.Cancel = true;
                return;
            }
            if (person.Subscription != SubscriptionType.both &&
                person.Subscription != SubscriptionType.from) {
                e.Cancel = true;
                return;
            }
            // the person already knows we are online, this does not give away our privacy
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void OnStreamError(object sender, agsXMPP.Xml.Dom.Element e)
        {
            Trace.Call(sender, e);
            var error = e as agsXMPP.protocol.Error;
            var builder = CreateMessageBuilder();
            builder.AppendEventPrefix();
            // TODO: create user readable error messages from the error.Condition
            //builder.AppendErrorText(error.Condition.ToString());
            switch (error.Condition) {
                case StreamErrorCondition.SystemShutdown:
                    builder.AppendErrorText(_("The server has shut down"));
                    break;
                case StreamErrorCondition.Conflict:
                    builder.AppendErrorText(_("Another client logged in with the same resource, you have been disconnected"));
                    break;
                case StreamErrorCondition.SeeOtherHost:
                    Server.Hostname = e.GetTag("see-other-host");
                    Reconnect(null);
                    break;
                default:
                    builder.AppendErrorText(error.Text ?? error.Condition.ToString());
                    break;
            }
            Session.AddMessageToChat(NetworkChat, builder.ToMessage());
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void OnAuthError(object sender, agsXMPP.Xml.Dom.Element e)
        {
            var builder = CreateMessageBuilder();
            builder.AppendEventPrefix();
            builder.AppendErrorText(_("Authentication failed, either username does not exist or invalid password"));
            Session.AddMessageToChat(NetworkChat, builder.ToMessage());
            builder = CreateMessageBuilder();
            builder.AppendEventPrefix();
            builder.AppendMessage(_("If you want to create an account with the specified user and password, type /register now"));
            Session.AddMessageToChat(NetworkChat, builder.ToMessage());
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public override void Connect(FrontendManager fm, ServerModel server)
        {
            Trace.Call(fm, server);

            if (server == null) {
                throw new ArgumentNullException("server");
            }
            
            if (server is XmppServerModel) {
                Server = (XmppServerModel) server;
            } else {
                Server = new XmppServerModel();
                if (server.ServerID != null) {
                    Server.Load(Session.UserConfig, Protocol, server.ServerID);
                }
                // HACK: previous line overwrites any passed values with the values from config
                // thus we have to copy the original values:
                Server.Hostname = server.Hostname;
                Server.Network = server.Network;
                Server.OnConnectCommands = server.OnConnectCommands;
                Server.OnStartupConnect = server.OnStartupConnect;
                Server.Password = server.Password;
                Server.Port = server.Port;
                Server.Protocol = server.Protocol;
                Server.ServerID = server.ServerID;
                Server.UseEncryption = server.UseEncryption;
                Server.Username = server.Username;
                Server.ValidateServerCertificate = server.ValidateServerCertificate;
            }
            
            Host = Server.Hostname;
            Port = Server.Port;

            // TODO: use config for single network chat or once per network manager
            NetworkChat = Session.CreateChat<ProtocolChatModel>(
                NetworkID, String.Format("{0} {1}", Protocol, Host), this
            );
            Session.AddChat(NetworkChat);
            Session.SyncChat(NetworkChat);

            Connect();
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void Connect()
        {
            Trace.Call();
            Contacts.Clear();

            AutoReconnect = true;
            AutoReconnectDelay = TimeSpan.FromMinutes(1);

            ApplyConfig(Session.UserConfig, Server);

            OpenContactChat();
            
#if LOG4NET
            _Logger.Debug("calling JabberClient.Open()");
#endif
            JabberClient.Open();
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public override void Reconnect(FrontendManager fm)
        {
            Trace.Call(fm);
            // IsConnected checks for a working xmpp connection
            // we need to know the socket's state here
            if (JabberClient.XmppConnectionState != XmppConnectionState.Disconnected) {
                AutoReconnect = true;
                AutoReconnectDelay = TimeSpan.Zero;
                JabberClient.Close();
            } else {
                JabberClient.ClientSocket.OnValidateCertificate -= ValidateCertificate;
                JabberClient.SocketConnectionType = SocketConnectionType.Direct;
                Reconnect();
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public override void Disconnect(FrontendManager fm)
        {
            Trace.Call(fm);
            AutoReconnect = false;
            JabberClient.Close();
        }

        public override void Dispose()
        {
            Trace.Call();
            IsDisposed = true;

            base.Dispose();
            AutoReconnect = false;
            JabberClient.SocketDisconnect();
        }

        // this method is used as status / title
        public override string ToString()
        {
            var status = String.Format("{0} ({1})", JabberClient.Server, Protocol);
            if (!IsConnected) {
                status += " (" + _("not connected") + ")";
            }
            return status;
        }

        DiscoItems ServerDiscoItems { get; set; }
        List<Jid> CachedMucJids { get; set; }
        Dictionary<Jid, DiscoInfo> CachedMucInfo { get; set; }
        DateTime CachedMucJidsTimeStamp { get; set; }

        // no need to synchronize this method as it only checks for null
        public override IList<GroupChatModel> FindGroupChats(GroupChatModel filter)
        {
            Trace.Call(filter);
            
            var list = new List<GroupChatModel>();
            if (ContactChat == null) {
                list.Add(new GroupChatModel("Contacts", "Contacts", this));
            }

            // find all transport/conference groups/whatnot
            DiscoItem[] discoItems;
            if (ServerDiscoItems == null) {
                var reset = new AutoResetEvent(false);
                lock (this) {
                    Disco.DiscoverItems(JabberClient.Server, (sender, e) => FindGroupChatsDiscoItems(e, reset));
                }
                reset.WaitOne();
            }
            lock (this) {
                if (ServerDiscoItems == null) {
                    return list;
                } else {
                    discoItems = ServerDiscoItems.GetDiscoItems();
                }
            }

            var resetList = new List<AutoResetEvent>();

            if ((CachedMucJids == null) ||
                ((DateTime.Now - CachedMucJidsTimeStamp) > TimeSpan.FromMinutes(5))) {
                // find all conference groups
                var mucList = new List<Jid>();
                foreach (var discoItem in discoItems) {
                    var reset = new AutoResetEvent(false);
                    var jid = discoItem.Jid;
                    lock (this) {
                        Disco.DiscoverInformation(discoItem.Jid, (sender, e) => FindGroupChatsItemDiscoInfo(e, reset, mucList, jid));
                    }
                    resetList.Add(reset);
                }
                foreach (var reset in resetList) {
                    reset.WaitOne();
                }
                resetList.Clear();

                // find all chats in all conference groups
                var jidList = new List<Jid>();
                foreach (var mucGroup in mucList) {
                    var reset = new AutoResetEvent(false);
                    lock (this) {
                        Disco.DiscoverItems(mucGroup, (sender, e) => FindGroupChatsDiscoMucs(e, reset, jidList));
                    }
                    resetList.Add(reset);
                }
                foreach (var reset in resetList) {
                    reset.WaitOne();
                }
                CachedMucJids = jidList;
                CachedMucJidsTimeStamp = DateTime.Now;
                CachedMucInfo = new Dictionary<Jid, DiscoInfo>();
            }

            // filter found items
            var filteredList = new List<Jid>();
            if (filter == null || String.IsNullOrEmpty(filter.Name)) {
                filteredList = CachedMucJids;
            } else {
                string searchPattern = null;
                if (!filter.Name.StartsWith("*") && !filter.Name.EndsWith("*")) {
                    searchPattern = String.Format("*{0}*", filter.Name);
                } else {
                    searchPattern = filter.Name;
                }
                foreach (var jid in CachedMucJids) {
                    if (!Pattern.IsMatch(jid, searchPattern)) {
                        continue;
                    }
                    filteredList.Add(jid);
                }
            }

            // get info on all chats matching the pattern
            resetList.Clear();
            foreach (var jid in CachedMucJids) {
                bool isCached = false;
                DiscoInfo info;
                lock (this) {
                    isCached = CachedMucInfo.TryGetValue(jid, out info);
                }
                if (isCached) {
                    FindGroupChatsChatInfoParse(jid, info, list);
                    continue;
                }
                var reset = new AutoResetEvent(false);
                lock (this) {
                    Disco.DiscoverInformation(jid, (sender, e) => FindGroupChatsChatInfo(e, reset, list));
                }
                resetList.Add(reset);
            }
            foreach (var reset in resetList) {
                reset.WaitOne();
            }
            return list;
        }

        void FindGroupChatsChatInfoParse(Jid jid, DiscoInfo items, List<GroupChatModel> list)
        {
            var ident = items.SelectSingleElement<DiscoIdentity>();
            string name;
            if (ident != null && !String.IsNullOrEmpty(ident.Name)) {
                name = ident.Name + " [" + jid + "]";
            } else {
                name = jid;
            }
            var chat = new GroupChatModel(jid, name, null);
            chat.PersonCount = -1;
            var x = items.SelectSingleElement<agsXMPP.protocol.x.data.Data>();
            if (x != null) {
                var users_field = x.GetField("muc#roominfo_occupants");
                var topic_field = x.GetField("muc#roominfo_subject");
                var desc_field = x.GetField("muc#roominfo_description");
                if (users_field != null) {
                    chat.PersonCount = int.Parse(users_field.GetValue());
                }
                if (topic_field != null) {
                    chat.Topic = new MessageModel(topic_field.GetValue());
                } else if (desc_field != null) {
                    chat.Topic = new MessageModel(desc_field.GetValue());
                }
            }
            lock (list) {
                list.Add(chat);
            }
        }

        void FindGroupChatsChatInfo(IQEventArgs e, AutoResetEvent reset, List<GroupChatModel> list)
        {
            if (e.IQ.Error == null) {
                var items = (DiscoInfo)e.IQ.Query;
                lock (this) {
                    CachedMucInfo[e.IQ.From] = items;
                }
                FindGroupChatsChatInfoParse(e.IQ.From, items, list);
            }
            e.Handled = true;
            reset.Set();
        }

        void FindGroupChatsDiscoMucs(IQEventArgs e, AutoResetEvent reset, List<Jid> list)
        {
            if (e.IQ.Error == null) {
                var items = (DiscoItems)e.IQ.Query;
                foreach (var item in items.GetDiscoItems()) {
                    // no locking required, these callbacks are sequential
                    list.Add(item.Jid);
                }
            }
            e.Handled = true;
            reset.Set();
        }

        void FindGroupChatsItemDiscoInfo(IQEventArgs e, AutoResetEvent reset, List<Jid> mucList, Jid jid)
        {
            if (e.IQ.Error == null) {
                var discoInfo = (DiscoInfo)e.IQ.Query;
                if (discoInfo.HasFeature(agsXMPP.Uri.MUC)) {
                    // no locking required, these callbacks are sequential
                    mucList.Add(jid);
                }
            }
            e.Handled = true;
            reset.Set();
        }

        void FindGroupChatsDiscoItems(IQEventArgs e, AutoResetEvent reset)
        {
            if (e.IQ.Error == null) {
                lock (this) {
                    ServerDiscoItems = (DiscoItems)e.IQ.Query;
                }
            }
            e.Handled = true;
            reset.Set();
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public void OpenContactChat()
        {
            if (ContactChat == null) {
                ContactChat = Session.CreateChat<GroupChatModel>(
                    "Contacts", "Contacts", this
                );
                Session.AddChat(ContactChat);
            } else if (!ContactChat.IsEnabled) {
                Session.EnableChat(ContactChat);
            } else {
                // already open
                return;
            }

            foreach (var pair in Contacts) {
                if (pair.Value.Resources.Count != 0) {
                    ContactChat.UnsafePersons.Add(pair.Key, pair.Value.ToPersonModel());
                }
            }

            // HACK: lower probability of sync race condition during connect
            ThreadPool.QueueUserWorkItem(delegate {
                Thread.Sleep(5000);
                lock (this) {
                    if (IsDisposed) {
                        return;
                    }
                    if (ContactChat != null) {
                        Session.SyncChat(ContactChat);
                    }
                }
            });
        }

        // no need to synchronize as no members are accessed
        public override void OpenChat(FrontendManager fm, ChatModel chat)
        {
            Trace.Call(fm, chat);
            if (chat.ID == "Contacts") {
                OpenContactChat();
                return;
            }
            CommandModel cmd = new CommandModel(fm, NetworkChat, chat.ID);
            switch (chat.ChatType) {
                case ChatType.Person:
                    CommandMessageQuery(cmd);
                    break;
                case ChatType.Group:
                    CommandJoin(cmd);
                    break;
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public override void CloseChat(FrontendManager fm, ChatModel chat)
        {
            Trace.Call(fm, chat);

            if (chat == ContactChat) {
                Session.RemoveChat(chat);
                ContactChat = null;
            } else if (chat.ChatType == ChatType.Group) {
                if (IsConnected) {
                    var groupchat = (XmppGroupChatModel)chat;
                    if (!groupchat.IsSynced) {
                        Session.RemoveChat(chat);
                    } else {
                        MucManager.LeaveRoom(chat.ID, ((XmppGroupChatModel)chat).OwnNickname);
                    }
                } else {
                    Session.RemoveChat(chat);
                }
            } else if (chat.ChatType == ChatType.Person) {
                Session.RemoveChat(chat);
            } else {
#if LOG4NET
                _Logger.Error("CloseChat(): Invalid chat type");
#endif
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public override void SetPresenceStatus(PresenceStatus status,
                                               string message)
        {
            Trace.Call(status, message);

            if (!IsConnected) {
                return;
            }

            switch (status) {
                case PresenceStatus.Online:
                    JabberClient.Show = ShowType.NONE;
                    JabberClient.Priority = Server.Priorities[status];
                    JabberClient.Status = message;
                    break;
                case PresenceStatus.Away:
                    JabberClient.Priority = Server.Priorities[status];
                    JabberClient.Show = ShowType.away;
                    JabberClient.Status = message;
                    break;
            }

            JabberClient.SendMyPresence();

            // send presence update to all MUCs, see XEP-0045:
            // http://xmpp.org/extensions/xep-0045.html#changepres
            foreach (var chat in Chats) {
                if (!(chat is XmppGroupChatModel)) {
                    continue;
                }
                var muc = (XmppGroupChatModel) chat;

                var to = new Jid(muc.ID) {
                    Resource = muc.OwnNickname
                };

                var presence = new Presence() {
                    Show = JabberClient.Show,
                    Status = JabberClient.Status,
                    From = JabberClient.MyJID,
                    To = to
                };

                if (JabberClient.EnableCapabilities) {
                    presence.AddChild(JabberClient.Capabilities);
                }
                JabberClient.Send(presence);
            }

            base.SetPresenceStatus(status, message);
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public void CommandRegister(CommandModel command)
        {
            Trace.Call(command);
            Connect();
            JabberClient.RegisterAccount = true;
            // TODO: add callbacks to process in case of error or success
        }

        public override bool Command(CommandModel command)
        {
            bool handled = false;
            if (IsConnected) {
                if (command.IsCommand) {
                    switch (command.Command) {
                        case "help":
                            CommandHelp(command);
                            handled = true;
                            break;
                        case "msg":
                        case "query":
                            CommandMessageQuery(command);
                            handled = true;
                            break;
                        case "me":
                            CommandMe(command);
                            handled = true;
                            break;
                        case "say":
                            CommandSay(command);
                            handled = true;
                            break;
                        case "joinas":
                            CommandJoinAs(command);
                            handled = true;
                            break;
                        case "join":
                            CommandJoin(command);
                            handled = true;
                            break;
                        case "invite":
                            CommandInvite(command);
                            handled = true;
                            break;
                        case "part":
                        case "leave":
                            CommandPart(command);
                            handled = true;
                            break;
                        case "away":
                            CommandAway(command);
                            handled = true;
                            break;
                        case "roster":
                            CommandRoster(command);
                            handled = true;
                            break;
                        case "contact":
                            CommandContact(command);
                            handled = true;
                            break;
                        case "priority":
                            CommandPriority(command);
                            handled = true;
                            break;
                        case "whois":
                            CommandWhoIs(command);
                            handled = true;
                            break;
                        case "register":
                            CommandRegister(command);
                            handled = true;
                            break;
                    }
                } else {
                    _Say(command.Chat, command.Data);
                    handled = true;
                }
            } else {
                if (command.IsCommand) {
                    // commands which work even without being connected
                    switch (command.Command) {
                        case "help":
                            CommandHelp(command);
                            handled = true;
                            break;
                        case "connect":
                            CommandConnect(command);
                            handled = true;
                            break;
                    }
                } else {
                    // normal text, without connection
                    NotConnected(command);
                    handled = true;
                }
            }

            return handled;
        }

        public void CommandMe(CommandModel command)
        {
            if (command.Data.Length <= 4) {
                return;
            }

            string actionstring = command.Data.Substring(3);
            // http://xmpp.org/extensions/xep-0245.html
            // says we should append "/me " no matter what our command char is
            _Say(command.Chat, "/me" + actionstring, true, false);

            // groupchat echos messages anyway
            if (command.Chat.ChatType == ChatType.Person) {
                var builder = CreateMessageBuilder();
                builder.AppendActionPrefix();
                builder.AppendIdendityName(Me);
                builder.AppendText(actionstring);
                Session.AddMessageToChat(command.Chat, builder.ToMessage());
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void printResource(MessageBuilder builder, XmppResourceModel res)
        {
            builder.AppendText("\n\tName: {0}", res.Name);
            var pres = res.Presence;
            builder.AppendText("\n\tPresence:");
            builder.AppendText("\n\t\tShow:\t{0}", pres.Show);
            builder.AppendText("\n\t\tStatus:\t{0}", pres.Status);
            builder.AppendText("\n\t\tLast:\t{0}", (pres.Last!=null)?pres.Last.Seconds.ToString():"");
            builder.AppendText("\n\t\tPriority:\t{0}", pres.Priority);
            builder.AppendText("\n\t\tType:\t{0}", pres.Type);
            builder.AppendText("\n\t\tXDelay:\t{0}", (pres.XDelay!=null)?pres.XDelay.Stamp.ToString():"");
            if (res.Disco != null) {
                builder.AppendText("\n\tFeatures:");
                foreach(var feat in res.Disco.GetFeatures()) {
                    builder.AppendText("\n\t\t{0}", feat.Var);
                }
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public void CommandWhoIs(CommandModel cmd)
        {
            Jid jid;
            if (cmd.DataArray.Length < 2) {
                if ((cmd.DataArray.Length == 1)
                    && (cmd.Chat is PersonChatModel)) {
                    jid = (cmd.Chat as PersonChatModel).Person.ID;
                } else {
                    NotEnoughParameters(cmd);
                    return;
                }
            } else {
                jid = GetJidFromNickname(cmd.DataArray[1]);
            }
            XmppPersonModel person;
            var builder = CreateMessageBuilder();
            if (!Contacts.TryGetValue(jid.Bare, out person)) {
                builder.AppendErrorText(_("Could not find contact {0}"), jid);
                Session.AddMessageToFrontend(cmd, builder.ToMessage());
                return;
            }
            if (!String.IsNullOrEmpty(jid.Resource)) {
                if (person.Resources.Count > 1) {
                    builder.AppendText(_("Contact {0} has {1} known resources"), jid.Bare, person.Resources.Count);
                }
                XmppResourceModel res;
                if (!person.Resources.TryGetValue(jid.Resource??"", out res)) {
                    builder.AppendErrorText(_("{0} is not a known resource"), jid.Resource);
                    Session.AddMessageToFrontend(cmd, builder.ToMessage());
                    return;
                }
                printResource(builder, res);
                Session.AddMessageToFrontend(cmd, builder.ToMessage());
                return;
            }
            builder.AppendText(_("Contact's JID: {0}"), person.Jid);
            builder.AppendText("\n");
            switch (person.Subscription) {
                case SubscriptionType.both:
                    builder.AppendText(_("You have a mutual subscription with this contact"));
                    break;
                case SubscriptionType.none:
                    builder.AppendText(_("You have no subscription with this contact and this contact is not subscribed to you"));
                    break;
                case SubscriptionType.to:
                    builder.AppendText(_("You are subscribed to this contact, but the contact is not subscribed to you"));
                    break;
                case SubscriptionType.from:
                    builder.AppendText(_("You are not subscribed to this contact, but the contact is subscribed to you"));
                    break;
                case SubscriptionType.remove:
#if LOG4NET
                    _Logger.Debug("a contact with SubscriptionType remove has been found");
#endif
                    break;
            }
            int i = 0;
            foreach(var res in person.Resources) {
                builder.AppendText("\nResource({0}):", i);
                printResource(builder, res.Value);
                i++;
            }
            Session.AddMessageToFrontend(cmd, builder.ToMessage());
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public void CommandContact(CommandModel cd)
        {
            FrontendManager fm = cd.FrontendManager;
            // todo: allow length of 2 in private chat windows
            if (cd.DataArray.Length < 3) {
                NotEnoughParameters(cd);
                return;
            }
            Jid jid = GetJidFromNickname(cd.DataArray[2]);
            string cmd = cd.DataArray[1];
            // the logic here is taken from
            // http://xmpp.org/rfcs/rfc3921.html#int
            switch (cmd) {
                case "addgroup":
                    if (cd.DataArray.Length < 4) {
                        NotEnoughParameters(cd);
                        return;
                    }
                    JabberClient.RosterManager.AddRosterItem(jid, null, cd.DataArray[3]);
                    break;
                case "addonly":
                    JabberClient.RosterManager.AddRosterItem(jid);
                    break;
                case "add":
                    XmppPersonModel person;
                    if (Contacts.TryGetValue(jid.Bare, out person)) {
                        if (person.Subscription == SubscriptionType.both) break;
                        if (person.Subscription != SubscriptionType.to) {
                            JabberClient.PresenceManager.Subscribe(jid);
                        }
                        if (person.Subscription != SubscriptionType.from) {
                            // in case we already know this contact… but he can't see us
                            JabberClient.PresenceManager.ApproveSubscriptionRequest(jid);
                        }
                    } else {
                        JabberClient.RosterManager.AddRosterItem(jid);
                        JabberClient.PresenceManager.Subscribe(jid);
                        JabberClient.PresenceManager.ApproveSubscriptionRequest(jid);
                    }
                    break;
                case "subscribe":
                    JabberClient.PresenceManager.Subscribe(jid);
                    break;
                case "unsubscribe":
                    // stop receiving status updates from this contact
                    // that contact will still receive your updates
                    JabberClient.PresenceManager.Unsubscribe(jid);
                    break;
                case "remove":
                case "rm":
                case "del":
                case "delete":
                    JabberClient.RosterManager.RemoveRosterItem(jid);
                    // unsubscribing is unnecessary, the server is required to do this
                    break;
                case "accept":
                case "allow":
                case "approve":
                case "auth":
                case "authorize":
                    JabberClient.PresenceManager.ApproveSubscriptionRequest(jid);
                    break;
                case "deny":
                case "refuse":
                    // stop the contact from receiving your updates
                    // you will still receive the contact's status updates
                    JabberClient.PresenceManager.RefuseSubscriptionRequest(jid);
                    break;
                case "rename":
                    if (cd.DataArray.Length < 4) {
                        JabberClient.RosterManager.UpdateRosterItem(jid, "");
                    } else {
                        var newNick = String.Join(" ", cd.DataArray.Skip(3).ToArray());
                        JabberClient.RosterManager.UpdateRosterItem(jid, newNick);
                    }
                    break;
                default:
                    var builder = CreateMessageBuilder();
                    builder.AppendText(_("Invalid contact command: {0}"), cmd);
                    Session.AddMessageToFrontend(cd, builder.ToMessage());
                    return;
            }
        }

        public void CommandHelp(CommandModel cmd)
        {
            var builder = CreateMessageBuilder();
            // TRANSLATOR: this line is used as a label / category for a
            // list of commands below
            builder.AppendHeader(_("{0} Commands"), Protocol);
            Session.AddMessageToFrontend(cmd, builder.ToMessage());

            string[] help = {
            "connect xmpp/jabber server port username password [resource]",
            "msg/query jid/nick message",
            "say message",
            "join muc-jid [password]",
            "part/leave [muc-jid]",
            "away [away-message]",
            "roster [full]",
            "contact add/remove jid/nick",
            "contact rename jid/nick [newnick]"
            };

            foreach (string line in help) {
                builder = CreateMessageBuilder();
                builder.AppendEventPrefix();
                builder.AppendText(line);
                Session.AddMessageToFrontend(cmd, builder.ToMessage());
            }

            // TRANSLATOR: this line is used as a label / category for a
            // list of commands below
            builder = CreateMessageBuilder();
            builder.AppendHeader(_("Advanced {0} Commands"), Protocol);
            Session.AddMessageToFrontend(cmd, builder.ToMessage());

            string[] help2 = {
            "contact addonly/subscribe/unsubscribe/approve/deny",
            "whois jid",
            "joinas muc-jid nickname [password]",
            "priority away/online/temp priority-value"
            };

            foreach (string line in help2) {
                builder = CreateMessageBuilder();
                builder.AppendEventPrefix();
                builder.AppendText(line);
                Session.AddMessageToFrontend(cmd, builder.ToMessage());
            }
        }

        public void CommandConnect(CommandModel cd)
        {
            FrontendManager fm = cd.FrontendManager;

            var server = new XmppServerModel();
            if (cd.DataArray.Length >= 3) {
                server.Hostname = cd.DataArray[2];
            } else {
                NotEnoughParameters(cd);
                return;
            }

            if (cd.DataArray.Length >= 4) {
                try {
                    server.Port = Int32.Parse(cd.DataArray[3]);
                } catch (FormatException) {
                    var builder = CreateMessageBuilder();
                    builder.AppendText(_("Invalid port: {0}"), cd.DataArray[3]);
                    Session.AddMessageToFrontend(cd, builder.ToMessage());
                    return;
                }
            } else {
                NotEnoughParameters(cd);
                return;
            }

            if (cd.DataArray.Length >= 5) {
                server.Username = cd.DataArray[4];
            } else {
                NotEnoughParameters(cd);
                return;
            }

            if (cd.DataArray.Length >= 6) {
                server.Password = cd.DataArray[5];
            } else {
                NotEnoughParameters(cd);
                return;
            }

            if (cd.DataArray.Length >= 7) {
                server.Resource = cd.DataArray[6];
            }

            Connect(fm, server);
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public void CommandPriority(CommandModel command)
        {
            if (command.DataArray.Length < 3) {
                var builder = CreateMessageBuilder();
                builder.AppendText(_("Priority for Available is: {0}"), Server.Priorities[PresenceStatus.Online]);
                Session.AddMessageToFrontend(command, builder.ToMessage());
                builder = CreateMessageBuilder();
                builder.AppendText(_("Priority for Away is: {0}"), Server.Priorities[PresenceStatus.Away]);
                Session.AddMessageToFrontend(command, builder.ToMessage());
                return;
            }
            string subcmd = command.DataArray[1];
            int prio;
            if (!int.TryParse(command.DataArray[2], out prio) || prio < -128 || prio > 127) {
                var builder = CreateMessageBuilder();
                builder.AppendText(_("Invalid priority: {0} (valid priorities are between -128 and 127 inclusive)"), command.DataArray[2]);
                Session.AddMessageToFrontend(command, builder.ToMessage());
                return;
            }
            JabberClient.Priority = prio;
            bool change_current_prio = false;
            switch (subcmd) {
                case "temp":
                case "temporary":
                    change_current_prio = true;
                    // only set priority
                    break;
                case "away":
                    Server.Priorities[PresenceStatus.Away] = prio;
                    change_current_prio = (JabberClient.Show == ShowType.away);
                    JabberClient.Priority = prio;
                    break;
                case "online":
                case "available":
                    Server.Priorities[PresenceStatus.Online] = prio;
                    change_current_prio = (JabberClient.Show == ShowType.NONE);
                    JabberClient.Priority = prio;
                    break;
                default:
                    return;
            }
            if (change_current_prio) {
                // set priority and keep all other presence info
                JabberClient.SendMyPresence();
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        Jid GetJidFromNickname(string nickname)
        {
            XmppPersonModel it;
            Jid jid = nickname;
            if (Contacts.TryGetValue(jid, out it)) {
                // nickname is a jid we know
                return jid;
            }
            if (Contacts.TryGetValue(jid.Bare, out it)) {
                // is a jid with resource
                return jid;
            }

            // arg is not a jid in our rostermanager
            // find a jid to which the nickname belongs
            foreach (var pair in Contacts) {
                if (pair.Value.IdentityName != null &&
                    pair.Value.IdentityName.Replace(" ", "_") == nickname) {
                    return pair.Key;
                }
            }
            // not found in roster, message directly to jid
            // TODO: check jid for validity
            return jid;
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void MessageQuery(Jid jid, string message)
        {
            var chat = GetOrCreatePersonChat(jid);
            if (message != null && message.Trim().Length > 0) {
                _Say(chat, message);
            }
        }

        public void CommandMessageQuery(CommandModel cd)
        {
            if (cd.DataArray.Length < 2) {
                NotEnoughParameters(cd);
                return;
            }
            Jid jid = GetJidFromNickname(cd.DataArray[1]);
            if (cd.DataArray.Length >= 3) {
                // we have a message
                string message = String.Join(" ", cd.DataArray, 2, cd.DataArray.Length-2);
                MessageQuery(jid, message);
            } else {
                MessageQuery(jid, null);
            }
        }

        public void CommandJoin(CommandModel cd)
        {
            if (cd.DataArray.Length < 2) {
                NotEnoughParameters(cd);
                return;
            }
            string password = null;
            if (cd.DataArray.Length > 2) {
                password = cd.DataArray[2];
            }
            JoinRoom(cd.DataArray[1], null, password);
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void JoinRoom(Jid jid, string nickname, string password)
        {
            XmppGroupChatModel chat = (XmppGroupChatModel)GetChat(jid, ChatType.Group);
            if (nickname == null) {
                nickname = Nicknames[0];
            }
            if (chat == null) {
                chat = Session.CreateChat<XmppGroupChatModel>(jid, jid, this);
                Session.AddChat(chat);
            }
            if (chat.IsJoining) {
                // double call to JoinRoom
                return;
            }
            chat.IsJoining = true;
            MucManager.JoinRoom(jid, nickname, password);
            if (password != null) {
                chat.Password = password;
            }
            chat.IsSynced = false;
            chat.OwnNickname = nickname;
        }

        public void CommandJoinAs(CommandModel cd)
        {
            if (cd.DataArray.Length < 3) {
                NotEnoughParameters(cd);
                return;
            }
            string password = null;
            if (cd.DataArray.Length > 3) {
                password = cd.DataArray[3];
            }
            JoinRoom(cd.DataArray[1], cd.DataArray[2], password);
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public void CommandPart(CommandModel cd)
        {
            string jid;
            if (cd.DataArray.Length >= 2)
                jid = cd.DataArray[1];
            else
                jid = cd.Chat.ID;
            XmppGroupChatModel chat = (XmppGroupChatModel)GetChat(jid, ChatType.Group);
            if (chat != null) {
                MucManager.LeaveRoom(jid, chat.OwnNickname);
            }
        }

        public void CommandInvite(CommandModel cd)
        {
            if (cd.DataArray.Length < 3) {
                NotEnoughParameters(cd);
                return;
            }
            string password = null;
            if (cd.DataArray.Length > 3) {
                password = cd.DataArray[3];
            }
            Invite(cd.DataArray[2], cd.DataArray[1], null, password);
        }

        void Invite(Jid jid, Jid room, string reason, string password)
        {
            Invite(new Jid[]{jid}, room, reason, password);
        }

        void Invite(string[] jids_string, string room, string reason, string password)
        {
            var jids = new Jid[jids_string.Length];
            for (int i = 0; i < jids.Length; i++) {
                jids[i] = jids_string[i];
            }
            Invite(jids, room, reason, password);
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void Invite(Jid[] jid, Jid room, string reason, string password)
        {
            JoinRoom(room, null, password);
            XmppGroupChatModel chat = (XmppGroupChatModel)GetChat(room, ChatType.Group);
            // if no password is passed, but we are already in the chatroom and know
            // about a password, use that password
            if (password == null && chat != null) {
                password = chat.Password;
            }
            MucManager.Invite(jid, room, reason, password);
        }

        public void CommandAway(CommandModel cd)
        {
            if (cd.DataArray.Length >= 2) {
                SetPresenceStatus(PresenceStatus.Away, cd.Parameter);
            } else {
                SetPresenceStatus(PresenceStatus.Online, null);
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        public void CommandRoster(CommandModel cd)
        {
            bool full = false;
            if (cd.Parameter == "full") {
                full = true;
            }

            MessageBuilder builder = CreateMessageBuilder();
            builder.AppendHeader("Roster");
            Session.AddMessageToFrontend(cd, builder.ToMessage());

            foreach (var pair in Contacts) {
                string status = "+";
                var contact = pair.Value;
                if (contact.Resources.Count == 0) {
                    if (!full) {
                        continue;
                    }
                    status = "-";
                }
                builder = CreateMessageBuilder();
                builder.AppendText("{0} {1}\t({2}): {3},{4}",
                                   status,
                                   contact.IdentityName,
                                   pair.Key,
                                   contact.Subscription,
                                   contact.Ask
                );
                foreach (var p in contact.Resources) {
                    builder.AppendText("\t|\t{0}:{1}:{2}",
                                       p.Key,
                                       p.Value.Presence.Type.ToString(),
                                       p.Value.Presence.Priority
                    );
                    if (!String.IsNullOrEmpty(p.Value.Presence.Status)) {
                        builder.AppendText(":\"{0}\"", p.Value.Presence.Status);
                    }
                }
                Session.AddMessageToFrontend(cd, builder.ToMessage());
            }
        }

        public void CommandSay(CommandModel cd)
        {
            _Say(cd.Chat, cd.Parameter);
        }  

        void _Say(ChatModel chat, string text)
        {
            _Say(chat, text, true);
        }

        void _Say(ChatModel chat, string text, bool send)
        {
            _Say(chat, text, send, true);
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void _Say(ChatModel chat, string text, bool send, bool display)
        {
            if (!chat.IsEnabled) {
                return;
            }
            if (chat == ContactChat) {
                return;
            }

            if (send) {
                if (chat.ChatType == ChatType.Person) {
                    var _person = (chat as PersonChatModel).Person as PersonModel;
                    XmppPersonModel person = GetOrCreateContact(_person.ID, _person.IdentityName);
                    SendPrivateMessage(person, _person.ID, text);
                } else if (chat.ChatType == ChatType.Group) {
                    JabberClient.Send(new Message(chat.ID, XmppMessageType.groupchat, text));
                    return; // don't show now. the message will be echoed back if it's sent successfully
                }
            }

            var builder = CreateMessageBuilder();
            builder.AppendSenderPrefix(Me);
            builder.AppendMessage(text);
            var msg = builder.ToMessage();
            if (display) {
                Session.AddMessageToChat(chat, msg);
            }
            OnMessageSent(
                new MessageEventArgs(chat, msg, null, chat.ID)
            );
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void _SendPrivateMessage(XmppPersonModel person, Jid jid, string text)
        {
            var mesg = new Message(jid, XmppMessageType.chat, text);
            XmppResourceModel res;
            if (person.Resources.TryGetValue(jid.Resource ?? "", out res)) {
                if (res.NicknameContactKnowsFromMe != Nicknames[0]) {
                    res.NicknameContactKnowsFromMe = Nicknames[0];
                    mesg.Nickname = new Nickname(Nicknames[0]);
                }
            }
            JabberClient.Send(mesg);
        }

        void SendPrivateMessage(XmppPersonModel person, string text)
        {
            SendPrivateMessage(person, null, text);
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void SendPrivateMessage(XmppPersonModel person, Jid jid, string text)
        {
            if (jid == null) {
                jid = person.Jid;
            }
            if ((jid.Server == "gmail.com") ||
                (jid.Server == "googlemail.com")) {
                // don't send to all high prio resources or to specific resources
                // because gtalk clones any message to all resources anyway
                _SendPrivateMessage(person, jid.Bare, text);
            } else if (!String.IsNullOrEmpty(jid.Resource)) {
                _SendPrivateMessage(person, jid, text);
            } else {
                var resources = person.GetResourcesWithHighestPriority();
                if (resources.Count == 0) {
                    // no connected resource, send to bare jid
                    _SendPrivateMessage(person, jid.Bare, text);
                } else {
                    foreach (var res in resources) {
                        if (String.IsNullOrEmpty(res.Name)) {
                            // don't send messages to empty resources
                            continue;
                        }
                        Jid j = new Jid(jid);
                        j.Resource = res.Name;
                        _SendPrivateMessage(person, j, text);
                    }
                }
            }
        }

        void OnReadXml(object sender, string text)
        {
            if (!DebugProtocol) {
                return;
            }

            try {
                var strWriter = new StringWriter();
                var xmlWriter = new XmlTextWriter(strWriter);
                xmlWriter.Formatting = Formatting.Indented;
                xmlWriter.Indentation = 2;
                xmlWriter.IndentChar =  ' ';
                
                var document = new XmlDocument();
                document.LoadXml(text);
                document.WriteContentTo(xmlWriter);
                
                DebugRead("\n" + strWriter.ToString());
            } catch (XmlException) {
                DebugRead("\n" + text);
            } catch (Exception ex) {
#if LOG4NET
                _Logger.Error("OnProtocol(): Exception", ex);
#endif
            }
        }

        void OnWriteXml(object sender, string text)
        {
            if (!DebugProtocol) {
                return;
            }

            try {
                if (text == null || text.Trim().Length == 0) {
                    // suppress logging keep-alive messages
                    return;
                }

                var strWriter = new StringWriter();
                var xmlWriter = new XmlTextWriter(strWriter);
                xmlWriter.Formatting = Formatting.Indented;
                xmlWriter.Indentation = 2;
                xmlWriter.IndentChar =  ' ';

                var document = new XmlDocument();
                document.LoadXml(text);
                document.WriteContentTo(xmlWriter);

                DebugWrite("\n" + strWriter.ToString());
            } catch (XmlException) {
                // HACK: in case of an invalid doucment fallback to
                // plain string logging
                DebugWrite("\n" + text);
            } catch (Exception ex) {
#if LOG4NET
                _Logger.Error("OnWriteText(): Exception", ex);
#endif
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        XmppPersonModel GetOrCreateContact(Jid jid, string name)
        {
            XmppPersonModel p;
            if (!Contacts.TryGetValue(jid.Bare, out p)) {
                p = new XmppPersonModel(jid, name, this);
                Contacts[jid.Bare] = p;
            }
            return p;
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void OnRosterItem(object sender, RosterItem rosterItem)
        {
            // setting to none also removes the person from chat, as we'd never get an offline message anymore
            if (rosterItem.Subscription == SubscriptionType.none
                || rosterItem.Subscription == SubscriptionType.remove) {
                if (rosterItem.Subscription == SubscriptionType.remove) {
                    Contacts.Remove(rosterItem.Jid);
                }
                if (ContactChat == null) {
                    return;
                }
                PersonModel oldp = ContactChat.GetPerson(rosterItem.Jid);
                if (oldp == null) {
                    // doesn't exist, don't need to do anything
                    return;
                }
                Session.RemovePersonFromGroupChat(ContactChat, oldp);
                return;
            }
            // create or update a roster item
            var contact = GetOrCreateContact(rosterItem.Jid.Bare, rosterItem.Name ?? rosterItem.Jid);
            contact.Temporary = false;
            contact.Subscription = rosterItem.Subscription;
            contact.Ask = rosterItem.Ask;
            string oldIdentityName = contact.IdentityName;
            var oldIdentityNameColored = contact.IdentityNameColored;

            contact.IdentityName = rosterItem.Name ?? rosterItem.Jid;

            if (oldIdentityName == contact.IdentityName) {
                // identity name didn't change
                // the rest of this function only handles changed identity names
                return;
            }

            contact.IdentityNameColored = null; // uncache
            ProcessIdentityNameChanged(contact, oldIdentityNameColored, oldIdentityName);
        }

        void ProcessIdentityNameChanged(XmppPersonModel contact, TextMessagePartModel oldIdentityNameColored, string oldIdentityName)
        {
            var builder = CreateMessageBuilder();
            builder.AppendEventPrefix();
            string idstring = (oldIdentityName == contact.Jid.Bare)?"":GenerateIdString(contact);
            oldIdentityNameColored.BackgroundColor = TextColor.None;
            builder.AppendFormat("{2}{1} is now known as {0}", contact, idstring, oldIdentityNameColored);

            if (ContactChat != null) {
                PersonModel oldp = ContactChat.GetPerson(contact.ID);
                if (oldp != null) {
                    Session.UpdatePersonInGroupChat(ContactChat, oldp, contact.ToPersonModel());
                    Session.AddMessageToChat(ContactChat, new MessageModel(builder.ToMessage()));
                }
            }
            
            var chat = Session.GetChat(contact.ID, ChatType.Person, this) as PersonChatModel;
            if (chat != null) {
                chat.Name = contact.IdentityName;
                builder.MessageType = MessageType.ChatNameChanged;
                var msg = builder.ToMessage();
                Session.AddMessageToChat(chat, msg);

                chat.Person = contact.ToPersonModel();
                var msg2 = new MessageModel(msg);
                msg2.MessageType = MessageType.PersonChatPersonChanged;
                Session.AddMessageToChat(chat, msg2);
            }
        }

        protected virtual string GenerateIdString(PersonModel contact)
        {
            if (contact.ID == contact.IdentityName) {
                return "";
            }
            var jid = new Jid(contact.ID);
            return String.Format(" [{0}]", jid.Bare);
        }

        void RequestCapabilities(Jid jid, Capabilities caps)
        {
            string hash = caps.Node + "#" + caps.Version;
            RequestCapabilities(jid, hash);
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void RequestCapabilities(Jid jid, string hash)
        {
            // already in cache?
            DiscoInfo info;
            if (DiscoCache.TryGetValue(hash, out info)) {
                AddCapabilityToResource(jid, info);
                return;
            }
            // prevent duplicate requests
            DiscoCache[hash] = null;
            // request it
            Disco.DiscoverInformation(jid,
                (object sender, IQEventArgs e) =>
                    OnDiscoInfo(e, hash)
            );
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void AddCapabilityToResource(Jid jid, DiscoInfo info)
        {
            XmppPersonModel contact;
            if (!Contacts.TryGetValue(jid.Bare, out contact)) {
                return;
            }
            XmppResourceModel res;
            if (!contact.Resources.TryGetValue(jid.Resource??"", out res)) {
                return;
            }
            res.Disco = info;
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void OnDiscoInfo(IQEventArgs e, string hash)
        {
            if (e.IQ.Error != null) {
#if LOG4NET
                _Logger.DebugFormat("An error happened during service discovery: {0}", e.IQ);
#endif
                // clear item from cache so the request is done again some time
                DiscoCache.Remove(hash);
                e.Handled = true;
                return;
            }
            if (e.IQ.Type != IqType.result) {
#if LOG4NET
                _Logger.Debug("OnDiscoInfo(): iq is not a result");
#endif
                return;
            }
            if (!(e.IQ.Query is DiscoInfo)) {
#if LOG4NET
                _Logger.Debug("OnDiscoInfo(): query is not a DiscoInfo");
#endif
                return;
            }
            var info = (DiscoInfo)e.IQ.Query;
            DiscoCache[hash] = info;
            e.Handled = true;
            if (String.IsNullOrEmpty(e.IQ.From.User)) {
                // server capabilities
                var builder = CreateMessageBuilder();
                builder.AppendText("The Server supports the following features: ");
                Session.AddMessageToChat(NetworkChat, builder.ToMessage());
                foreach (var feature in info.GetFeatures()) {
                    builder = CreateMessageBuilder();
                    builder.AppendText(feature.Var);
                    Session.AddMessageToChat(NetworkChat, builder.ToMessage());
                }
            } else {
                AddCapabilityToResource(e.IQ.From, info);
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        MessageModel CreatePresenceUpdateMessage(Jid jid, PersonModel person, Presence pres)
        {
            var builder = CreateMessageBuilder();
            builder.AppendEventPrefix();
            string idstring = GenerateIdString(person);
            // print the type (and in case of available detailed type)
            switch (pres.Type) {
                case PresenceType.available:
                    switch(pres.Show) {
                        case ShowType.NONE:
                            builder.AppendFormat(_("{0}{1} is available"), person, idstring);
                            builder.AppendPresenceState(person, MessageType.PresenceStateOnline);
                            break;
                        case ShowType.away:
                            builder.AppendFormat(_("{0}{1} is away"), person, idstring);
                            builder.AppendPresenceState(person, MessageType.PresenceStateAway);
                            break;
                        case ShowType.xa:
                            builder.AppendFormat(_("{0}{1} is extended away"), person, idstring);
                            builder.AppendPresenceState(person, MessageType.PresenceStateAway);
                            break;
                        case ShowType.dnd:
                            builder.AppendFormat(_("{0}{1} wishes not to be disturbed"), person, idstring);
                            builder.AppendPresenceState(person, MessageType.PresenceStateAway);
                            break;
                        case ShowType.chat:
                            builder.AppendFormat(_("{0}{1} wants to chat"), person, idstring);
                            builder.AppendPresenceState(person, MessageType.PresenceStateOnline);
                            break;
                    }
                    break;
                case PresenceType.unavailable:
                    builder.AppendPresenceState(person, MessageType.PresenceStateOffline);
                    builder.AppendFormat(_("{0}{1} is offline"), person, idstring);
                    break;
                case PresenceType.subscribe:
                    if ((person as XmppPersonModel).Ask == AskType.subscribe) {
                        builder = CreateMessageBuilder();
                        builder.AppendActionPrefix();
                        builder.AppendFormat(_("Automatically allowed {0} to subscribe to you, since you are already asking to subscribe"),
                                           person
                        );
                    } else {
                        builder.AppendFormat(_("{0}{1} wishes to subscribe to you"),
                                             person, idstring);
                        // you have to respond
                        builder.MarkAsHighlight();
                    }
                    break;
                case PresenceType.subscribed:
                    // you can now see their presences
                    builder.AppendFormat(_("{0}{1} allowed you to subscribe"), person, idstring);
                    break;
                case PresenceType.unsubscribed:
                    if ((person as XmppPersonModel).Subscription == SubscriptionType.from) {
                        builder = CreateMessageBuilder();
                        builder.AppendActionPrefix();
                        builder.AppendFormat(
                            _("Automatically removed {0}'s subscription to " +
                              "your presences after losing the subscription " +
                              "to theirs"),
                            person
                        );
                    } else {
                        // you cannot (anymore?) see their presences
                        builder.AppendFormat(_("{0}{1} denied/removed your subscription"), person, idstring);
                    }
                    break;
                case PresenceType.unsubscribe:
                    // you might still be able to see their presences
                    builder.AppendFormat(_("{0}{1} unsubscribed from you"), person, idstring);
                    break;
                case PresenceType.error:
                    if (pres.Error == null) {
                        builder.AppendErrorText(_("received a malformed error message: {0}"), pres);
                        break;
                    }
                    switch (pres.Error.Type) {
                        case ErrorType.cancel:
                            switch (pres.Error.Condition) {
                                case ErrorCondition.RemoteServerNotFound:
                                    builder.AppendErrorText(_("{0}{1}'s server could not be found"), person.IdentityName, idstring);
                                    break;
                                case ErrorCondition.Conflict:
                                    builder.AppendErrorText(_("{0}{1} is already using your requested resource"), person.IdentityName, idstring);
                                    break;
                                default:
                                    if (!String.IsNullOrEmpty(pres.Error.ErrorText)) {
                                        builder.AppendErrorText(pres.Error.ErrorText);
                                    } else {
                                        builder.AppendErrorText(
                                            _("There is currently no useful error message for {0}, {1}, {2}{3}"),
                                            pres.Error.Type,
                                            pres.Error.Condition,
                                            person.IdentityName,
                                            idstring);
                                    }
                                    break;
                            }
                            break;
                        case ErrorType.auth:
                            switch (pres.Error.Condition) {
                                case ErrorCondition.Forbidden:
                                    builder.AppendErrorText(
                                        _("You do not have permission to access {0}{1}")
                                        , person.IdentityName,
                                        idstring);
                                    break;
                                default:
                                    if (!String.IsNullOrEmpty(pres.Error.ErrorText)) {
                                        builder.AppendErrorText(pres.Error.ErrorText);
                                    } else {
                                        builder.AppendErrorText(
                                            _("There is currently no useful error message for {0}, {1}, {2}{3}"),
                                            pres.Error.Type,
                                            pres.Error.Condition,
                                            person.IdentityName,
                                            idstring);
                                    }
                                    break;
                            }
                            break;
                        default:
                            if (!String.IsNullOrEmpty(pres.Error.ErrorText)) {
                                builder.AppendErrorText(pres.Error.ErrorText);
                            } else {
                                builder.AppendErrorText(
                                    _("There is currently no useful error message for {0}, {1}, {2}{3}"),
                                    pres.Error.Type,
                                    pres.Error.Condition,
                                    person.IdentityName,
                                    idstring);
                            }
                            break;
                    }
                    break;
            }
            // print timestamp of presence
            if (pres.XDelay != null || pres.Last != null) {
                DateTime stamp = DateTime.MinValue;
                TimeSpan span = TimeSpan.MinValue;
                if (pres.XDelay != null) {
                    stamp = pres.XDelay.Stamp;
                    span = DateTime.Now.Subtract(stamp);
                } else if (pres.Last != null) {
                    span = TimeSpan.FromSeconds(pres.Last.Seconds);
                    stamp = DateTime.Now.Subtract(span);
                }
                string spanstr;
                if (span > TimeSpan.FromDays(1)) {
                    spanstr = String.Format(
                        "{0:00}:{1:00}:{2:00}:{3:00}",
                        span.TotalDays, span.Hours, span.Minutes, span.Seconds
                    );
                    spanstr = String.Format(_("{0} days"), spanstr);
                } else if (span > TimeSpan.FromHours(1)) {
                    spanstr = String.Format(
                        "{0:00}:{1:00}:{2:00}",
                        span.Hours, span.Minutes, span.Seconds
                    );
                    spanstr = String.Format(_("{0} hours"), spanstr);
                } else if (span > TimeSpan.FromMinutes(1)) {
                    spanstr = String.Format("{0:00}:{1:00}",
                                            span.Minutes, span.Seconds);
                    spanstr = String.Format(_("{0} minutes"), spanstr);
                } else {
                    spanstr = String.Format("{0:00}", span.Seconds);
                    spanstr = String.Format(_("{0} seconds"), spanstr);
                }

                string timestamp = null;
                try {
                    string format = Session.UserConfig["Interface/Notebook/TimestampFormat"] as string;
                    if (!String.IsNullOrEmpty(format)) {
                        timestamp = stamp.ToString(format);
                    }
                } catch (FormatException e) {
                    timestamp = "Timestamp Format ERROR: " + e.Message;
                }
                builder.AppendText(_(" since {0} ({1})"), timestamp, spanstr);
            }
            // print user defined message
            if (pres.Status != null && pres.Status.Trim().Length > 0) {
                builder.AppendText(": {0}", pres.Status);
            }
            return builder.ToMessage();
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void PrintGroupChatPresence(XmppGroupChatModel chat, PersonModel person, Presence pres)
        {
            Jid jid = pres.From;

            var msg = CreatePresenceUpdateMessage(person.ID, person, pres);
            Session.AddMessageToChat(chat, msg);
            // clone directly to muc person chat
            // don't care about real jid, that has its own presence packets
            var personChat = Session.GetChat(jid, ChatType.Person, this);
            if (personChat != null) {
                Session.AddMessageToChat(personChat, msg);
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void OnGroupChatPresence(XmppGroupChatModel chat, Presence pres)
        {
            Jid jid = pres.From;
            var person = new PersonModel(jid, pres.From.Resource ?? pres.From, NetworkID, Protocol, this);
            PrintGroupChatPresence(chat, person, pres);
            switch (pres.Type) {
                case PresenceType.available:
                    // don't do anything if the contact already exists
                    if (chat.UnsafePersons.ContainsKey(person.ID)) {
                        return;
                    }
                    // is the chat synced? add the new contact the regular way
                    if (chat.IsSynced) {
                        Session.AddPersonToGroupChat(chat, person);
                        return;
                    }

                    chat.UnsafePersons.Add(person.ID, person);

                    // did I join? then the chat roster is fully received
                    if (pres.From.Resource == chat.OwnNickname ||
                        pres.MucUser.StatusCodes.Exists(x => x.Code == StatusCode.SelfPresence)) {
                        if (pres.MucUser.StatusCodes.Exists(x => x.Code == StatusCode.ModifiedNick)) {
                            // as per XEP-0045 7.2.3 Example 24 the server is
                            // allowed to give us a different nick than we requested
                            chat.OwnNickname = pres.From.Resource;
                        }
                        chat.IsJoining = false;
                        // HACK: lower probability of sync race condition swallowing messages
                        ThreadPool.QueueUserWorkItem(delegate {
                            Thread.Sleep(1000);
                            lock (this) {
                                if (IsDisposed) {
                                    return;
                                }
                                chat.IsSynced = true;
                                Session.SyncChat(chat);
                                Session.EnableChat(chat);
                            }
                        });
                    }
                    break;
                case PresenceType.unavailable:
                    Session.RemovePersonFromGroupChat(chat, person);
                    // did I leave? then I "probably" left the room
                    if (pres.From.Resource == chat.OwnNickname) {
                        Session.RemoveChat(chat);
                    }
                    break;
                case PresenceType.error:
                    OnGroupChatPresenceError(chat, pres);
                    break;
            }
        }

        MessageModel CreateGroupChatPresenceErrorMessage(Presence pres)
        {
            var builder = CreateMessageBuilder();
            builder.AppendEventPrefix();
            if (pres.Error == null) {
                builder.AppendErrorText(_("An unknown groupchat error occurred: {0}"), pres);
                return builder.ToMessage();
            }
            switch (pres.Error.Type) {
                case ErrorType.cancel:
                    switch (pres.Error.Condition) {
                        case ErrorCondition.RemoteServerNotFound:
                            builder.AppendErrorText(_("Server of groupchat \"{0}\" not found."), pres.From.Bare);
                            break;
                        case ErrorCondition.ServiceUnavailable:
                            builder.AppendErrorText(_("MUC service is not available for \"{0}\""), pres.From.Bare);
                            break;
                    }
                    break;
                case ErrorType.auth:
                    switch (pres.Error.Condition) {
                        case ErrorCondition.NotAuthorized:
                            builder.AppendErrorText(_("You do not have permission to join \"{0}\""), pres.From.Bare);
                            break;
                    }
                    break;
            }
            if (String.IsNullOrEmpty(pres.Error.ErrorText) && builder.IsEmpty) {
                builder.AppendErrorText(_("An unhandled groupchat error occurred: {0}"), pres.From.Bare);
            } else {
                builder.AppendErrorText(": {0}", pres.Error.ErrorText);
            }
            return builder.ToMessage();
        }

        void OnGroupChatPresenceError(XmppGroupChatModel chat, Presence pres)
        {
            if (pres.Error != null &&
                pres.Error.Type == ErrorType.cancel &&
                pres.Error.Condition == ErrorCondition.Conflict) {
                // nickname already in use; autorejoin with _ appended
                JoinRoom(chat.ID, chat.OwnNickname + "_", chat.Password);
                return;
            }

            var msg = CreateGroupChatPresenceErrorMessage(pres);
            Session.AddMessageToChat(NetworkChat, msg);
            Session.RemoveChat(chat);
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void PrintPrivateChatPresence(XmppPersonModel person, Presence pres)
        {
            Jid jid = pres.From;
            XmppResourceModel resource;
            if (person.Resources.TryGetValue(jid.Resource??"", out resource)) {
                if (resource.Presence.Show == pres.Show
                    && resource.Presence.Status == pres.Status
                    && resource.Presence.Last == pres.Last
                    && resource.Presence.XDelay == pres.XDelay
                    && resource.Presence.Priority == pres.Priority
                    && resource.Presence.Type == pres.Type
                    ) {
                    // presence didn't change enough to warrent a display message -> abort
                    return;
                }
            }
            MessageModel msg = CreatePresenceUpdateMessage(jid, person, pres);
            if (!String.IsNullOrEmpty(jid.Resource)) {
                var directchat = Session.GetChat(jid, ChatType.Person, this);
                if (directchat != null) {
                    // in case of direct chat we still send this message
                    Session.AddMessageToChat(directchat, msg);
                }
            }
            // a nonexisting resource going offline?
            if (pres.Type == PresenceType.unavailable) {
                if (!person.Resources.ContainsKey(jid.Resource??"")) {
                    return;
                }
            }
            var res = person.GetOrCreateResource(jid);
            var oldpres = res.Presence;
            res.Presence = pres;
            // highest pres
            Jid hjid = jid;
            Jid nextjid = jid;
            // 2nd highest pres
            Presence hpres = pres;
            Presence nextpres = null;
            bool amHighest = true;
            bool wasHighest = true;
            foreach (var pair in person.Resources) {
                if (pair.Value == res) continue;
                if (nextpres == null || pair.Value.Presence.Priority > nextpres.Priority) {
                    nextjid.Resource = pair.Key;
                    nextpres = pair.Value.Presence;
                }
                if (pair.Value.Presence.Priority > hpres.Priority) {
                    // someone has a higher priority than I do
                    // print the status of that resource
                    hjid.Resource = pair.Key;
                    hpres = pair.Value.Presence;
                    amHighest = false;
                }
                if (oldpres != null && pair.Value.Presence.Priority > oldpres.Priority) {
                    wasHighest = false;
                }
            }
            if (pres.Type == PresenceType.available) {
                // wasn't and isn't highiest prio -> ignore
                if (!wasHighest && !amHighest) return;
                // just another below zero prio -> ignore
                if (amHighest && pres.Priority < 0) return;
                // was highest, isn't anymore -> show presence of new highest
                if (wasHighest && !amHighest) {
                    msg = CreatePresenceUpdateMessage(hjid, person, hpres);
                }
            } else if (pres.Type == PresenceType.unavailable) {
                // still a resource left with positive priority
                if (nextpres != null && nextpres.Priority >= 0) {
                    msg = CreatePresenceUpdateMessage(nextjid, person, nextpres);
                }
            }
            var chat = Session.GetChat(jid.Bare, ChatType.Person, this);
            if (chat != null) {
                Session.AddMessageToChat(chat, msg);
            }
            if (ContactChat != null) {
                Session.AddMessageToChat(ContactChat, msg);
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void OnPrivateChatPresence(Presence pres)
        {
            Jid jid = pres.From;
            if (jid.Bare == JabberClient.MyJID.Bare) {
                // don't process any of my own resources
                return;
            }
            var person = GetOrCreateContact(jid.Bare, jid);
            PrintPrivateChatPresence(person, pres);
            switch (pres.Type) {
                case PresenceType.available:
                    if (pres.Priority < 0) break;
                    if (ContactChat == null) break;
                    if (ContactChat.UnsafePersons.ContainsKey(jid.Bare)) break;
                    Session.AddPersonToGroupChat(ContactChat, person.ToPersonModel());
                    break;
                case PresenceType.unavailable:
                    person.RemoveResource(jid);
                    if (pres.Priority < 0) break;
                    if (ContactChat == null) break;
                    if (!ContactChat.UnsafePersons.ContainsKey(jid.Bare)) break;
                    var pers = ContactChat.GetPerson(jid.Bare);
                    Session.RemovePersonFromGroupChat(ContactChat, pers);
                    break;
                case PresenceType.subscribe:
                    if (person.Ask == AskType.subscribe) {
                        // we are currently asking the contact OR are subscribed to him
                        // so we allow the contact to subscribe
                        // TODO: make the following dependent on some user setable boolean
                        JabberClient.PresenceManager.ApproveSubscriptionRequest(jid);
                    }
                    break;
                case PresenceType.subscribed:
                    // we are now able to see that contact's presences
                    break;
                case PresenceType.unsubscribed:
                    // the contact does not wish us to see his presences anymore
                    if (person.Subscription == SubscriptionType.from) {
                        // but the contact can still see us
                        // TODO: make the following dependent on some user setable boolean
                        JabberClient.PresenceManager.RefuseSubscriptionRequest(jid);
                    } else {
                        // TODO: this contact was just created in OnPresence… prevent it from doing that?
                        // TODO: this can happen when a subscription=none contact sends a deny…
                        Contacts.Remove(jid.Bare);
                    }
                    break;
                case PresenceType.unsubscribe:
                    // the contact does not wish to see our presence anymore?
                    // we could care less
                    break;
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void OnPresence(object sender, Presence pres)
        {
            Trace.Call(sender, pres);

            Jid jid = pres.From;
            if (jid == JabberClient.MyJID) return; // we don't care about ourself
            if (pres.Capabilities != null && pres.Type == PresenceType.available) {
                // only test capabilities of users going online or changing something in their online state
                RequestCapabilities(jid, pres.Capabilities);
            }

            if (pres.MucUser != null || pres.Muc != null) {
                var groupChat = (XmppGroupChatModel) Session.GetChat(jid.Bare, ChatType.Group, this);
                if (groupChat == null) {
                    var builder = CreateMessageBuilder();
                    builder.AppendEventPrefix();
                    builder.AppendErrorText(_("Received a presence update from {0}, but there's no corresponding chat window"), pres.From.Bare);
                    Session.AddMessageToChat(NetworkChat, builder.ToMessage());
                    if (pres.Type == PresenceType.error) {
                        var msg = CreateGroupChatPresenceErrorMessage(pres);
                        Session.AddMessageToChat(NetworkChat, msg);
                    } else {
                        MucManager.LeaveRoom(jid.Bare, jid.Resource);
                    }
                } else {
                    OnGroupChatPresence(groupChat, pres);
                }
            } else {
                OnPrivateChatPresence(pres);
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void OnGroupChatMessage(Message msg)
        {
            string group_jid = msg.From.Bare;
            XmppGroupChatModel groupChat = (XmppGroupChatModel) Session.GetChat(group_jid, ChatType.Group, this);
            if (groupChat == null) {
                var builder = CreateMessageBuilder();
                builder.AppendEventPrefix();
                builder.AppendErrorText(_("Received a groupchat message from {0} but there's no corresponding chat window: {1}"), msg.From, msg.Body);
                Session.AddMessageToChat(NetworkChat, builder.ToMessage());
                return;
            }
            // resource can be empty for room messages
            var sender_id = msg.From.Resource ?? msg.From.Bare;
            var person = groupChat.GetPerson(sender_id);
            if (person == null) {
                // happens in case of a delayed message if the participant has left meanwhile
                // TODO: or in case of a room message?
                person = new PersonModel(sender_id,
                                         sender_id,
                                         NetworkID, Protocol, this);
            }
            
            // XXX maybe only a Google Talk bug requires this:
            if (msg.XDelay != null) {
                var stamp = msg.XDelay.Stamp;
                if (stamp > groupChat.LatestSeenStamp) {
                    groupChat.LatestSeenStamp = stamp;
                } else {
                    return; // already seen newer delayed message
                }
                if (groupChat.SeenNewMessages) {
                    return; // already seen newer messages
                }
            } else {
                groupChat.SeenNewMessages = true;
            }

            // mark highlights only for received messages
            MessageModel message;
            if (person.ID == groupChat.OwnNickname) {
                message = CreateEchoGroupChatMessage(groupChat, msg);
            } else {
                message = CreateGroupChatMessage(groupChat, person, msg);
            }
            Session.AddMessageToChat(groupChat, message);
            OnMessageReceived(
                new MessageEventArgs(groupChat, message, msg.From, groupChat.ID)
            );
        }

        void AddMessageToChatIfNotFiltered(MessageModel msg, ChatModel chat, bool isNew)
        {
            if (Session.IsFilteredMessage(chat, msg)) {
                Session.LogMessage(chat, msg, true);
                return;
            }
            if (isNew) {
                Session.AddChat(chat);
            }
            Session.AddMessageToChat(chat, msg, true);
            if (isNew) {
                Session.SyncChat(chat);
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void ProcessNickname(XmppPersonModel person, Nickname nick)
        {
            if (String.IsNullOrEmpty(nick.Value)) {
                return;
            }
            // only rename person if it doesn't have a preset name
            if (person.IdentityName == person.ID) {
                var oldIdentityNameColored = person.IdentityNameColored;
                var oldIdentityName = person.IdentityName;
                person.IdentityName = nick.Value;
                ProcessIdentityNameChanged(person, oldIdentityNameColored, oldIdentityName);
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void OnPrivateChatMessage(Message msg)
        {
            var chat = Session.GetChat(msg.From, ChatType.Person, this) as PersonChatModel;
            bool isNew = false;
            if (chat == null) {
                // in case full jid doesn't have a chat window, use bare jid
                chat = GetOrCreatePersonChat(msg.From.Bare, out isNew);
            }
            if (msg.Nickname != null) {
                ProcessNickname(GetOrCreateContact(msg.From, msg.Nickname.Value), msg.Nickname);
            }
            var message = CreatePrivateChatMessage(chat.Person, msg);
            AddMessageToChatIfNotFiltered(message, chat, isNew);
            OnMessageReceived(
                new MessageEventArgs(chat, message, msg.From, null)
            );
        }

        MessageModel CreateEchoGroupChatMessage(XmppGroupChatModel groupChat, Message msg)
        {
            var builder = CreateMessageBuilder();
            string msgstring;
            if (msg.Html != null) {
                msgstring = msg.Html.ToString();
            } else {
                msgstring = msg.Body.Trim();
            }
            var self = new PersonModel(
                groupChat.OwnNickname,
                groupChat.OwnNickname,
                NetworkID, Protocol, this);
            if (msgstring.StartsWith("/me ")) {
                // leave the " " intact
                msgstring = msgstring.Substring(3);
                builder.AppendActionPrefix();
                builder.AppendIdendityName(self);
            } else {
                builder.AppendSenderPrefix(self);
            }

            if (msg.Html != null) {
                builder.AppendHtmlMessage(msgstring);
            } else {
                builder.AppendMessage(msgstring);
            }

            if (msg.XDelay != null) {
                builder.TimeStamp = msg.XDelay.Stamp;
            }
            return builder.ToMessage();
        }

        MessageModel CreateGroupChatMessage(XmppGroupChatModel groupChat, PersonModel person, Message msg)
        {
            var builder = CreateMessageBuilder();
            string msgstring;
            if (msg.Html != null) {
                msgstring = msg.Html.ToString();
            } else {
                msgstring = msg.Body.Trim();
            }

            if (msgstring.StartsWith("/me ")) {
                // leave the " " intact
                msgstring = msgstring.Substring(3);
                builder.AppendActionPrefix();
                builder.AppendIdendityName(person);
            } else {
                builder.AppendSenderPrefix(person);
            }

            if (msg.Html != null) {
                builder.AppendHtmlMessage(msgstring);
            } else {
                builder.AppendMessage(msgstring);
            }
            // mark hilights only for OwnNickname, too
            builder.Me = new PersonModel(
                groupChat.OwnNickname,
                groupChat.OwnNickname,
                NetworkID, Protocol, this);
            builder.MarkHighlights();

            if (msg.XDelay != null) {
                builder.TimeStamp = msg.XDelay.Stamp;
            }
            return builder.ToMessage();
        }

        MessageModel CreatePrivateChatMessage(PersonModel person, Message msg)
        {
            var builder = CreateMessageBuilder();
            string msgstring;
            if (msg.Html != null) {
                msgstring = msg.Html.ToString();
            } else {
                msgstring = msg.Body.Trim();
            }

            if (msgstring.StartsWith("/me ")) {
                // leave the " " intact
                msgstring = msgstring.Substring(3);
                builder.AppendActionPrefix();
                builder.AppendIdendityName(person, true);
            } else {
                builder.AppendSenderPrefix(person, true);
            }

            if (msg.Html != null) {
                builder.AppendHtmlMessage(msgstring);
            } else {
                builder.AppendMessage(msgstring);
            }

            if (msg.XDelay != null) {
                builder.TimeStamp = msg.XDelay.Stamp;
            }
            return builder.ToMessage();
        }

        void OnGroupChatMessageError(Message msg, XmppGroupChatModel chat)
        {
            var builder = CreateMessageBuilder();
            // TODO: nicer formatting
            if (msg.Error.ErrorText != null) {
                builder.AppendErrorText(msg.Error.ErrorText);
            } else {
                builder.AppendErrorText(msg.Error.ToString());
            }
            Session.AddMessageToChat(chat, builder.ToMessage());
        }

        void OnPrivateChatMessageError(Message msg, PersonChatModel chat)
        {
            var builder = CreateMessageBuilder();
            // TODO: nicer formatting
            if (msg.Error.ErrorText != null) {
                builder.AppendErrorText(msg.Error.ErrorText);
            } else {
                builder.AppendErrorText(msg.Error.ToString());
            }
            Session.AddMessageToChat(chat, builder.ToMessage());
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void OnMessage(object sender, Message msg)
        {
            // process chatstates
            if (msg.Chatstate != agsXMPP.protocol.extensions.chatstates.Chatstate.None) {
                OnChatState(msg);
            }
            if (String.IsNullOrEmpty(msg.Body)) {
                // TODO: capture events and stuff
                return;
            }
            switch (msg.Type) {
                case XmppMessageType.groupchat:
                    OnGroupChatMessage(msg);
                    break;
                case XmppMessageType.chat:
                case XmppMessageType.headline:
                case XmppMessageType.normal:
                    if (String.IsNullOrEmpty(msg.From.User)) {
                        OnServerMessage(msg);
                    } else if (msg.MucUser != null) {
                        OnMucMessage(msg);
                    } else {
                        OnPrivateChatMessage(msg);
                    }
                    break;
                case XmppMessageType.error:
                {
                    var chat = Session.GetChat(msg.From, ChatType.Group, this);
                    if (chat != null) {
                        OnGroupChatMessageError(msg, chat as XmppGroupChatModel);
                        break;
                    }
                    chat = Session.GetChat(msg.From, ChatType.Person, this);
                    if (chat != null) {
                        OnPrivateChatMessageError(msg, chat as PersonChatModel);
                        break;
                    }
                    // no person and no groupchat open? -> dump in networkchat
                    var builder = CreateMessageBuilder();
                    // TODO: nicer formatting
                    if (msg.Error.ErrorText != null) {
                        builder.AppendErrorText(msg.Error.ErrorText);
                    } else {
                        builder.AppendErrorText(msg.Error.ToString());
                    }
                    Session.AddMessageToChat(NetworkChat, builder.ToMessage());
                }
                    break;
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void OnMucMessage (Message msg)
        {
            User user = msg.MucUser;
            string text;
            if (user.Invite != null) {
                if (user.Invite.Reason != null && user.Invite.Reason.Trim().Length > 0) {
                    text = String.Format(_("You have been invited to {2} by {0} because {1}"),
                                         user.Invite.From,
                                         user.Invite.Reason,
                                         msg.From
                                         );
                } else {
                    text = String.Format(_("You have been invited to {1} by {0}"),
                                         user.Invite.From,
                                         msg.From
                                         );
                }
            } else {
                text = msg.ToString();
            }
            var builder = CreateMessageBuilder();
            builder.AppendEventPrefix();
            var txt = builder.CreateText(text);
            txt.IsHighlight = true;
            builder.AppendText(txt);
            Session.AddMessageToChat(NetworkChat, builder.ToMessage());
            builder = CreateMessageBuilder();
            string url;
            if (!String.IsNullOrEmpty(user.Password)) {
                url = String.Format("xmpp:{0}?join;password={1}", msg.From, user.Password);
            } else {
                url = String.Format("xmpp:{0}?join", msg.From);
            }
            builder.AppendUrl(url, _("Accept invite (join room)"));
            Session.AddMessageToChat(NetworkChat, builder.ToMessage());
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void OnChatState(Message msg)
        {
            if (!ShowChatStates) {
                return;
            }
            if (msg.Body != null) {
                return;
            }
            switch (msg.Type) {
                case XmppMessageType.chat:
                case XmppMessageType.headline:
                case XmppMessageType.normal:
                {
                    var chat = GetChat(msg.From, ChatType.Person) as PersonChatModel;
                    bool isNew = false;
                    // no full jid chat
                    if (chat == null) {
                        // create chat
                        chat = GetOrCreatePersonChat(msg.From.Bare, out isNew);
                        if (isNew) {
                            if (!OpenNewChatOnChatState) {
                                return;
                            }
                            if (msg.Chatstate != Chatstate.composing) {
                                // there is NO reason to open a new chat window for
                                // a chatstate other than composing
                                return;
                            }
                            Session.AddChat(chat);
                        }
                    }
                    var builder = CreateMessageBuilder();
                    switch (msg.Chatstate) {
                        case Chatstate.composing:
                            builder.AppendChatState(chat.Person, MessageType.ChatStateComposing);
                            break;
                        case Chatstate.paused:
                            builder.AppendChatState(chat.Person, MessageType.ChatStatePaused);
                            break;
                        default:
                            builder.AppendChatState(chat.Person, MessageType.ChatStateReset);
                            break;
                    }
                    Session.AddMessageToChat(chat, builder.ToMessage());
                    if (isNew) {
                        Session.SyncChat(chat);
                    }
                }
                    break;
                default:
                    break;
            }
        }

        void OnServerMessage(Message msg)
        {
            var builder = CreateMessageBuilder();
            builder.AppendText("<{0}> {1}", msg.From, msg.Body);
            builder.MarkHighlights();
            // todo: can server messages have an xdelay?
            if (msg.XDelay != null) {
                builder.TimeStamp = msg.XDelay.Stamp;
            }
            Session.AddMessageToChat(NetworkChat, builder.ToMessage());
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        PersonChatModel GetOrCreatePersonChat(Jid jid)
        {
            bool isNew;
            var chat = GetOrCreatePersonChat(jid, out isNew);
            if (isNew) {
                Session.AddChat(chat);
                Session.SyncChat(chat);
            }
            return chat;
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        PersonChatModel GetOrCreatePersonChat(Jid jid, out bool isNew)
        {
            var chat = (PersonChatModel) Session.GetChat(jid, ChatType.Person, this);
            isNew = false;
            if (chat != null) return chat;
            var person = GetOrCreateContact(jid.Bare, jid);
            PersonModel pers;
            if (!String.IsNullOrEmpty(jid.Resource)) {
                pers = new PersonModel(jid, person.IdentityName, NetworkID, Protocol, this);
            } else {
                pers = person.ToPersonModel();
            }
            isNew = true;
            chat = Session.CreatePersonChat(pers, this);
            if (jid == JabberClient.MyJID || jid == JabberClient.MyJID.Bare) {
                var builder = CreateMessageBuilder();
                builder.AppendEventPrefix();
                builder.AppendText("Note: you are now talking to yourself");
                Session.AddMessageToChat(chat, builder.ToMessage());
            }
            return chat;
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void OnClose(object sender)
        {
            Trace.Call(sender);

            foreach (var chat in Chats) {
                // don't disable the protocol chat, else the user loses all
                // control for the protocol manager! e.g. after a manual
                // reconnect or server-side disconnect
                if (chat.ChatType == ChatType.Protocol) {
                    continue;
                }

                Session.DisableChat(chat);
            }

            OnDisconnected(EventArgs.Empty);

            // reset socket
            JabberClient.ClientSocket.OnValidateCertificate -= ValidateCertificate;
            JabberClient.SocketConnectionType = SocketConnectionType.Direct;

            if (AutoReconnect) {
                Reconnect(AutoReconnectDelay);
            }
        }

        void Reconnect()
        {
            var builder = CreateMessageBuilder();
            builder.AppendEventPrefix();
            builder.AppendText(_("Reconnecting to {0}"),
                               JabberClient.Server);
            Session.AddMessageToChat(Chat, builder.ToMessage());
            Connect();
        }

        void Reconnect(TimeSpan span)
        {
            int delay = (int)span.TotalMilliseconds;
            if (delay <= 0) {
                Reconnect();
            }
            var builder = CreateMessageBuilder();
            builder.AppendEventPrefix();
            builder.AppendText(_("Reconnecting to {0} in {1} seconds"),
                               JabberClient.Server, span.TotalSeconds);
            Session.AddMessageToChat(Chat, builder.ToMessage());
            ThreadPool.QueueUserWorkItem(delegate {
                Thread.Sleep(delay);
                lock (this) {
                    // prevent this timer from calling connect after it has been closed
                    if (IsDisposed) {
                        return;
                    }
                    // prevent this timer from calling connect if during the timout
                    // some other event already began a connect
                    if (JabberClient.XmppConnectionState != XmppConnectionState.Disconnected) {
                        return;
                    }
                    Connect();
                }
            });
        }

        void OnError(object sender, Exception ex)
        {
            Trace.Call(sender);

#if LOG4NET
            _Logger.Error("OnError(): Exception", ex);
#endif

            var builder = CreateMessageBuilder();
            builder.AppendEventPrefix();
            builder.AppendErrorText(_("Error: {0}"), String.Empty);
            builder.AppendMessage(ex.Message);
            Session.AddMessageToChat(NetworkChat, builder.ToMessage());
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void OnLogin(object sender)
        {
            Trace.Call(sender);

            var builder = CreateMessageBuilder();
            builder.AppendEventPrefix();
            builder.AppendText(_("Authenticated"));
            Session.AddMessageToChat(Chat, builder.ToMessage());
            RequestCapabilities(JabberClient.Server, JabberClient.Server);

            OnConnected(EventArgs.Empty);
            foreach (var chat in Chats) {
                if (chat is PersonChatModel) {
                    Session.EnableChat(chat);
                    Session.SyncChat(chat);
                } else if (chat is XmppGroupChatModel) {
                    var muc = (XmppGroupChatModel)chat;
                    JoinRoom(muc.ID, muc.OwnNickname, muc.Password);
                }
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        void ApplyConfig(UserConfig config, XmppServerModel server)
        {
            if (String.IsNullOrEmpty(server.Nickname)) {
                Nicknames = (string[]) config["Connection/Nicknames"];
            } else {
                Nicknames = new string[] { server.Nickname };
            }

            if (server.Username.Contains("@")) {
                var jid_user = server.Username.Split('@')[0];
                var jid_host = server.Username.Split('@')[1];
                JabberClient.ConnectServer = server.Hostname;
                JabberClient.AutoResolveConnectServer = false;
                JabberClient.Username = jid_user;
                JabberClient.Server = jid_host;
            } else {
                JabberClient.Server = server.Hostname;
                JabberClient.Username = server.Username;
            }
            JabberClient.Port = server.Port;
            JabberClient.Password = server.Password;

            var proxySettings = new ProxySettings();
            proxySettings.ApplyConfig(config);
            var protocol = Server.UseEncryption ? "xmpps" : "xmpp";
            var serverUri = String.Format("{0}://{1}:{2}", protocol,
                                          Server.Hostname, Server.Port);
            var proxy = proxySettings.GetWebProxy(serverUri);
            var socket = JabberClient.ClientSocket as ClientSocket;
            if (proxy == null) {
                socket.Proxy = null;
            } else {
                var builder = CreateMessageBuilder();
                builder.AppendEventPrefix();
                builder.AppendText(_("Using proxy: {0}:{1}"),
                                   proxy.Address.Host,
                                   proxy.Address.Port);
                Session.AddMessageToChat(Chat, builder.ToMessage());

                var proxyScheme = proxy.Address.Scheme;
                var proxyType = Starksoft.Net.Proxy.ProxyType.None;
                try {
                    proxyType = (Starksoft.Net.Proxy.ProxyType) Enum.Parse(
                        typeof(Starksoft.Net.Proxy.ProxyType),
                        proxy.Address.Scheme,
                        true
                    );
                } catch (ArgumentException ex) {
#if LOG4NET
                    _Logger.Error("ApplyConfig(): Couldn't parse proxy type: " +
                                  proxyScheme, ex);
#endif
                }

                var proxyFactory = new ProxyClientFactory();
                if (String.IsNullOrEmpty(proxySettings.ProxyUsername) &&
                    String.IsNullOrEmpty(proxySettings.ProxyPassword)) {
                    socket.Proxy = proxyFactory.CreateProxyClient(
                        proxyType,
                        proxy.Address.Host,
                        proxy.Address.Port
                    );
                } else {
                    socket.Proxy = proxyFactory.CreateProxyClient(
                        proxyType,
                        proxy.Address.Host,
                        proxy.Address.Port,
                        proxySettings.ProxyUsername,
                        proxySettings.ProxyPassword
                    );
                }
            }

            Me = new PersonModel(
                JabberClient.MyJID.Bare,
                Nicknames[0],
                NetworkID, Protocol, this
            );
            Me.IdentityNameColored.ForegroundColor = new TextColor(0, 0, 255);
            Me.IdentityNameColored.BackgroundColor = TextColor.None;
            Me.IdentityNameColored.Bold = true;

            // XMPP specific settings
            JabberClient.Resource = server.Resource;

            if (server.UseEncryption) {
                // HACK: Google Talk doesn't support StartTLS :(
                if (server.Hostname == "talk.google.com" &&
                    server.Port == 5223) {
                    JabberClient.ForceStartTls = false;
                    JabberClient.UseSSL = true;
                } else {
                    JabberClient.ForceStartTls = true;
                }
            } else {
                JabberClient.ForceStartTls = false;
                JabberClient.UseStartTLS = true;
            }
            if (!server.ValidateServerCertificate) {
                JabberClient.ClientSocket.OnValidateCertificate += ValidateCertificate;
            }
        }

        static bool ValidateCertificate(object sender,
                                         X509Certificate certificate,
                                         X509Chain chain,
                                         SslPolicyErrors sslPolicyErrors)
        {
            return true;
        }

        static string _(string msg)
        {
            return LibraryCatalog.GetString(msg, LibraryTextDomain);
        }
    }
}
