context: /*
   * Smuxi - Smart MUltipleXed Irc
   *
   * Copyright (c) 2005-2015 Mirco Bauer <meebey@meebey.net>
   *
   * 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;
  using System.Linq;
  using System.Text;
  using System.Text.RegularExpressions;
  using System.Collections;
  using System.Collections.Generic;
  using System.Runtime.Remoting;
  using System.Threading;
  using Smuxi.Common;
  
  namespace Smuxi.Engine
  {
      public class Session : PermanentRemoteObject, IFrontendUI 
      {
  #if LOG4NET
          private static readonly log4net.ILog f_Logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
  #endif
          private static readonly string                _LibraryTextDomain = "smuxi-engine";
          public static CertificateValidator CertificateValidator { get; private set; }
          private int                                   _Version = 0;
          private IDictionary<string, FrontendManager>  _FrontendManagers;
          private IList<IProtocolManager>               _ProtocolManagers;
          private List<ChatModel>                       _Chats;
          private SessionChatModel                      _SessionChat;
          private Config                                _Config;
          private string                                _Username;
          private ProtocolManagerFactory                _ProtocolManagerFactory;
          private UserConfig                            _UserConfig;
          private FilterListController                  _FilterListController;
          private ICollection<FilterModel>              _Filters;
          Timer NewsFeedTimer { get; set; }
          List<string> SeenNewsFeedIds { get; set; }
          DateTime NewsFeedLastModified { get; set; }
          TimeSpan NewsFeedUpdateInterval { get; set; }
          TimeSpan NewsFeedRetryInterval { get; set; }
          bool IsFirstNewsFeedEntry { get; set; }
          internal MessageBuilderSettings MessageBuilderSettings { get; private set; }
  
          public event EventHandler<GroupChatPersonAddedEventArgs> GroupChatPersonAdded;
          public event EventHandler<GroupChatPersonRemovedEventArgs> GroupChatPersonRemoved;
          public event EventHandler<GroupChatPersonUpdatedEventArgs> GroupChatPersonUpdated;
          public event EventHandler<EventMessageEventArgs> EventMessage;
  
          public IList<IProtocolManager> ProtocolManagers {
              get {
                  return _ProtocolManagers;
              }
          }
  
          public IProtocolManager FirstProtocolManager {
              get {
                  lock (_ProtocolManagers) {
                      if (_ProtocolManagers.Count == 0) {
                          return null;
                      } else {
                          return _ProtocolManagers[0];
                      }
                  }
              }
          }
          
          public IList<ChatModel> Chats {
              get {
                  return _Chats;
              }
          }
          
          public SessionChatModel SessionChat {
              get {
                  return _SessionChat;
              }
          }
          
          public int Version {
              get {
                  return _Version;
              }
          }
  
          public Config Config {
              get {
                  return _Config;
              }
          }
          
          public UserConfig UserConfig {
              get {
                  return _UserConfig;
              }
          }
          
          public string Username {
              get {
                  return _Username;
              }
          }
  
          public bool IsLocal {
              get {
                  return _Username == "local";
              }
          }
  
          internal ICollection<FilterModel> Filters {
              get {
                  return _Filters;
              }
              set {
                  _Filters = value;
              }
          }
  
          static Session()
          {
              CertificateValidator = new CertificateValidator();
          }
  
          public Session(Config config, ProtocolManagerFactory protocolManagerFactory,
                         string username)
          {
              Trace.Call(config, protocolManagerFactory, username);
              
              if (config == null) {
                  throw new ArgumentNullException("config");
              }
              if (protocolManagerFactory == null) {
                  throw new ArgumentNullException("protocolManagerFactory");
              }
              if (username == null) {
                  throw new ArgumentNullException("username");
              }
              
              _Config = config;
              _ProtocolManagerFactory = protocolManagerFactory;
              _Username = username;
              
              _FrontendManagers = new Dictionary<string, FrontendManager>();
              _ProtocolManagers = new List<IProtocolManager>();
              _UserConfig = new UserConfig(config, username);
              _UserConfig.Changed += OnUserConfigChanged;
              _FilterListController = new FilterListController(_UserConfig);
              _Filters = _FilterListController.GetFilterList().Values;
              MessageBuilderSettings = new MessageBuilderSettings();
              MessageBuilderSettings.ApplyConfig(_UserConfig);
              _Chats = new List<ChatModel>();
  
              InitSessionChat();
  
              SeenNewsFeedIds = new List<string>();
              NewsFeedUpdateInterval = TimeSpan.FromHours(12);
              NewsFeedRetryInterval = TimeSpan.FromMinutes(5);
              IsFirstNewsFeedEntry = true;
              NewsFeedTimer = new Timer(delegate { UpdateNewsFeed(); }, null,
                                        TimeSpan.Zero, NewsFeedUpdateInterval);
          }
  
          public void ExecuteOnStartupCommands()
          {
              foreach (string command in (string[]) _UserConfig["OnStartupCommands"]) {
                  if (command.Length == 0) {
                      continue;
                  }
                  var cd = new CommandModel(null, _SessionChat,
                      (string)_UserConfig["Interface/Entry/CommandCharacter"],
                      command);
                  var handled = Command(cd);
                  if (!handled) {
  #if LOG4NET
                      f_Logger.Error("ExecuteOnStartupCommands(): command unknown: " + command);
  #endif
                      var builder = CreateMessageBuilder().
                          AppendEventPrefix().
                          AppendErrorText("unknown command: {0}", command);
                      AddMessageToChat(_SessionChat, builder.ToMessage(), true);
                  }
              }
          }
  
          public void ProcessAutoConnect()
          {
              var serverCon = new ServerListController(_UserConfig);
              IList<ServerModel> servers = serverCon.GetServerList();
              foreach (ServerModel server in servers) {
                  if (!server.OnStartupConnect) {
                      continue;
                  }
  
                  var msg = String.Format(
                      _("Automatically connecting to {0}..."),
                      String.Format("{0}/{1} ({2}:{3})", server.Protocol,
                          server.Network, server.Hostname, server.Port)
                  );
  #if LOG4NET
                  f_Logger.Info(msg);
  #endif
                  var builder = CreateMessageBuilder().
                      AppendEventPrefix().
                      AppendText(msg);
                  AddMessageToChat(_SessionChat, builder.ToMessage(), true);
  
                  var srv = server;
                  // run connects in background threads as they block
                  ThreadPool.QueueUserWorkItem(delegate {
                      bool isError = false;
                      try {
                          IProtocolManager protocolManager = Connect(srv, null);
  
                          // if the connect command was correct, we should be
                          // able to get the chat model
                          if (protocolManager.Chat == null) {
                              isError = true;
                          }
                      } catch (Exception ex) {
  #if LOG4NET
                          f_Logger.Error("ProcessAutoConnect(): Exception during "+
                              "automatic connect: ", ex);
  #endif
                          isError = true;
                      }
                      if (isError) {
                          builder = CreateMessageBuilder();
                          builder.AppendEventPrefix();
                          builder.AppendErrorText(
                              _("Automatic connect to {0} failed!"),
                              srv.Hostname + ":" + srv.Port
                          );
                          AddMessageToChat(_SessionChat, builder.ToMessage(), true);
                      }
                  });
              }
          }
  
          public IProtocolManager NextProtocolManager(IProtocolManager currentProtocolManager)
          {
              lock (_ProtocolManagers) {
                  if (_ProtocolManagers.Count == 0) {
                      return null;
                  }
                  int pos = 0;
                  if (currentProtocolManager != null) {
                      pos = _ProtocolManagers.IndexOf(currentProtocolManager);
                  }
                  if (pos < _ProtocolManagers.Count - 1) {
                      pos++;
                  } else {
                      pos = 0;
                  }
                  return _ProtocolManagers[pos];
              }
          }
  
          protected MessageBuilder CreateMessageBuilder()
          {
              var builder = new MessageBuilder();
              // copy settings so the caller can override settings without
              // changing the settings of the complete session
              builder.Settings = new MessageBuilderSettings(MessageBuilderSettings);
              return builder;
          }
  
          public void RegisterFrontendUI(IFrontendUI ui)
          {
              Trace.Call(ui);
              
              if (ui == null) {
                  throw new ArgumentNullException("ui");
              }
              
              string uri = GetUri(ui);
  #if LOG4NET
              f_Logger.Debug("Registering UI with URI: " + uri);
  #endif
              // add the FrontendManager to the hashtable with an unique .NET remoting identifier
              FrontendManager fm = new FrontendManager(this, ui);
              lock (_FrontendManagers) {
                  _FrontendManagers[uri] = fm;
              }
          }
          
          internal void DeregisterFrontendManager(FrontendManager fm)
          {
              Trace.Call(fm);
  
              if (fm == null) {
                  throw new ArgumentNullException("fm");
              }
              
              string key = null;
              lock (_FrontendManagers) {
                  foreach (KeyValuePair<string, FrontendManager> kv in _FrontendManagers) {
                      if (kv.Value == fm) {
                          key = kv.Key;
                          break;
                      }
                  }
              }
              if (key == null) {
  #if LOG4NET
                  f_Logger.Debug("DeregisterFrontendManager(fm): could not find " +
                                 "frontend manager (probably already cleanly " +
                                 " deregistered), ignoring...");
  #endif
                  return;
              }
              
              lock (_FrontendManagers) {
                  _FrontendManagers.Remove(key);
              }
  
  #if LOG4NET
              f_Logger.Debug("DeregisterFrontendUI(fm): disposing FrontendManager");
  #endif
              fm.Dispose();
  
              CheckPresenceStatus();
          }
          
          public void DeregisterFrontendUI(IFrontendUI ui)
          {
              Trace.Call(ui);
              
              if (ui == null) {
                  throw new ArgumentNullException("ui");
              }
              
              string uri = GetUri(ui);
  #if LOG4NET
              f_Logger.Debug("DeregisterFrontendUI(ui): deregistering UI with URI: "+uri);
  #endif
              FrontendManager manager;
              lock (_FrontendManagers) {
                  _FrontendManagers.TryGetValue(uri, out manager);
                  _FrontendManagers.Remove(uri);
              }
              if (manager == null) {
  #if LOG4NET
                  f_Logger.Error("DeregisterFrontendUI(ui): can't dispose as FrontendManager not found with URI: " + uri);
  #endif
              } else {
  #if LOG4NET
                  f_Logger.Debug("DeregisterFrontendUI(ui): disposing FrontendManager with URI: " + uri);
  #endif
                  manager.Dispose();
              }
  
              CheckPresenceStatus();
          }
          
          public FrontendManager GetFrontendManager(IFrontendUI ui)
          {
              Trace.Call(ui);
              
              if (ui == null) {
                  throw new ArgumentNullException("ui");
              }
              
              lock (_FrontendManagers) {
                  return _FrontendManagers[GetUri(ui)];
              }
          }
          
          private string GetUri(IFrontendUI ui)
          {
              if (ui == null) {
                  throw new ArgumentNullException("ui");
              }
              
              if (IsLocal) {
                  return "local";
              }
              
              return RemotingServices.GetObjectUri((MarshalByRefObject)ui);
          }
          
          public static bool IsLocalFrontend(IFrontendUI ui)
          {
              if (ui == null) {
                  throw new ArgumentNullException("ui");
              }
              
              return RemotingServices.GetObjectUri((MarshalByRefObject)ui) == null;
          }
          
          public ChatModel GetChat(string id, ChatType chatType, IProtocolManager networkManager)
          {
              if (id == null) {
                  throw new ArgumentNullException("id");
              }
              
              lock (_Chats) {
                  foreach (ChatModel chat in _Chats) {
                      if ((chat.ID.ToLower() == id.ToLower()) &&
                          (chat.ChatType == chatType) &&
                          (chat.ProtocolManager == networkManager)) {
                          return chat;
                      }
                  }
              }
              
              return null;
          }
          
          public bool Command(CommandModel cd)
          {
              Trace.Call(cd);
              
              if (cd == null) {
                  throw new ArgumentNullException("cd");
              }
              
              bool handled = false;
              if (cd.IsCommand) {
                  switch (cd.Command) {
                      case "help":
                          CommandHelp(cd);
                          if (cd.Chat.ChatType == ChatType.Session) {
                              handled = true;
                          }
                          break;
                      case "server":
                      case "connect":
                          CommandConnect(cd);
                          handled = true;
                          break;
                      case "disconnect":
                          CommandDisconnect(cd);
                          handled = true;
                          break;
                      case "reconnect":
                          CommandReconnect(cd);
                          handled = true;
                          break;
                      case "network":
                          CommandNetwork(cd);
                          handled = true;
                          break;
                      case "config":
                          CommandConfig(cd);
                          handled = true;
                          break;
                      case "shutdown":
                          CommandShutdown(cd);
                          handled = true;
                          break;
                      case "echo":
                          CommandEcho(cd);
                          handled = true;
                          break;
                      default:
                          var filteredCmd = IOSecurity.GetFilteredPath(cd.Command);
                          var hooks = new HookRunner("engine", "session",
                                                     "command-" + filteredCmd);
                          var pm = cd.Chat.ProtocolManager;
                          hooks.Environments.Add(new CommandHookEnvironment(cd));
                          hooks.Environments.Add(new ChatHookEnvironment(cd.Chat));
                          if (pm != null) {
                              hooks.Environments.Add(new ProtocolManagerHookEnvironment(pm));
                          }
  
                          var cmdChar = (string) UserConfig["Interface/Entry/CommandCharacter"];
                          hooks.Commands.Add(new SessionHookCommand(this, cd.Chat, cmdChar));
                          if (pm != null) {
                              hooks.Commands.Add(new ProtocolManagerHookCommand(pm, cd.Chat, cmdChar));
                          }
  
                          // show time
                          hooks.Init();
                          if (hooks.HasHooks) {
                              hooks.Run();
                              handled = true;
                          }
                          break;
                  }
              } else {
                  // normal text
                  if (cd.Chat.ChatType == ChatType.Session &&
                      cd.FrontendManager != null &&
                      cd.FrontendManager.CurrentProtocolManager == null) {
                      _NotConnected(cd);
                      handled = true;
                  }
              }
              
              return handled;
          }
          
          public void CommandHelp(CommandModel cd)
          {
              Trace.Call(cd);
              
              if (cd == null) {
                  throw new ArgumentNullException("cd");
              }
              var builder = CreateMessageBuilder();
              // TRANSLATOR: this line is used as a label / category for a
              // list of commands below
              builder.AppendHeader(_("Engine Commands"));
              AddMessageToFrontend(cd, builder.ToMessage());
  
              string[] help = {
                  "connect/server protocol [protocol-parameters]",
                  "connect/server network",
                  "disconnect [server]",
                  "network list",
                  "network close [network]",
                  "network switch [network]",
                  "config (save|load|list)",
                  "config get key",
                  "config set key=value",
                  "config remove key",
                  "shutdown"
              };
  
              foreach (string line in help) {
                  builder = CreateMessageBuilder();
                  builder.AppendEventPrefix();
                  builder.AppendText(line);
                  AddMessageToFrontend(cd, builder.ToMessage());
              }
          }
          
          public void CommandConnect(CommandModel cd)
          {
              Trace.Call(cd);
              
              if (cd == null) {
                  throw new ArgumentNullException("cd");
              }
              
              FrontendManager fm = cd.FrontendManager;
              MessageBuilder builder = null;
              string protocol = null;
              ServerModel server = null;
              // first lookup by network name
              if (cd.DataArray.Length == 2) {
                  var network = cd.Parameter;
                  var serverSettings = new ServerListController(UserConfig);
                  server = serverSettings.GetServerByNetwork(network);
                  if (server == null) {
                      // HACK: simply assume the user meant irc if not specified
                      // as Smuxi is still primarly an IRC client
                      protocol = "irc";
                      string cmd = String.Format("{0}connect irc {1}",
                                                 cd.CommandCharacter, cd.Parameter);
                      cd = new CommandModel(fm, cd.Chat, cd.CommandCharacter, cmd);
                  }
              } else if (cd.DataArray.Length >= 3) {
                  protocol = cd.DataArray[1];
              } else {
                  _NotEnoughParameters(cd);
                  return;
              }
              
              IProtocolManager protocolManager = null;
              // TODO: detect matching protocol managers, how to parse host and port
              // though in a protocol neutral ?
              /*
              foreach (IProtocolManager nm in _ProtocolManagers) {
                  if (nm.Host == server &&
                      nm.Port == port) {
                      // reuse network manager
                      if (nm.IsConnected) {
                          fm.AddTextToCurrentChat("-!- " + String.Format(
                              _("Already connected to: {0}:{1}"), server, port));
                          return;
                      }
                      networkManager = nm;
                      break;
                  }
              }
              */
  
              if (protocolManager == null && server == null) {
                  try {
                      protocolManager = CreateProtocolManager(protocol);
                      lock (_ProtocolManagers) {
                          _ProtocolManagers.Add(protocolManager);
                      }
                  } catch (ArgumentException ex) {
                      if (ex.ParamName != "protocol") {
                          throw;
                      }
                      // this is an unknown protocol error
                      builder = CreateMessageBuilder();
                      builder.AppendEventPrefix();
                      builder.AppendErrorText(ex.Message);
                      AddMessageToFrontend(cd, builder.ToMessage());
                      return;
                  }
              }
              // HACK: this is hacky as the Command parser of the protocol manager
              // will pass this command to it's connect method only if cd was
              // constructed correctly beginning with /connect
              // So make sure it's like it needs to be!
              if (cd.Command != "connect") {
                  string cmd = String.Format("{0}connect {1}",
                                  cd.CommandCharacter,
                                  String.Join(" ", cd.DataArray, 1,
                                              cd.DataArray.Length - 1));
                  cd = new CommandModel(fm, cd.Chat, cd.CommandCharacter, cmd);
              }
  
              // run in background so it can't block the command queue
              ThreadPool.QueueUserWorkItem(delegate {
                  try {
                      if (protocolManager == null && server != null) {
                          protocolManager = Connect(server, fm);
                      } else {
                          protocolManager.Command(cd);
                      }
  
                      // set this as current protocol manager
                      // but only if there was none set (we might be on a chat for example)
                      // or if this is the neutral "smuxi" tab
                      if (fm.CurrentProtocolManager == null ||
                          (fm.CurrentChat != null && fm.CurrentChat.ChatType == ChatType.Session)) {
                          fm.CurrentProtocolManager = protocolManager;
                          fm.UpdateNetworkStatus();
                      }
                  } catch (Exception ex) {
  #if LOG4NET
                      f_Logger.Error("CommandConnect(): ", ex);
  #endif
                      builder = CreateMessageBuilder();
                      builder.AppendEventPrefix();
                      builder.AppendErrorText(_("Connect failed!"));
                      fm.AddMessageToChat(cd.Chat, builder.ToMessage());
                  }
              });
          }
          
          public void CommandDisconnect(CommandModel cd)
          {
              Trace.Call(cd);
              
              if (cd == null) {
                  throw new ArgumentNullException("cd");
              }
              
              FrontendManager fm = cd.FrontendManager;
              IProtocolManager victim = null;
              if (cd.DataArray.Length >= 2) {
                  string server = cd.DataArray[1];
                  victim = GetProtocolManagerByHost(server);
                  if (victim == null) {
                      var builder = CreateMessageBuilder();
                      builder.AppendEventPrefix();
                      builder.AppendErrorText(
                          _("Disconnect failed - could not find server: {0}"),
                          server
                      );
                      AddMessageToFrontend(cd, builder.ToMessage());
                      return;
                  }
              } else {
                  victim = cd.Chat.ProtocolManager;
              }
  
              if (victim == null) {
                  return;
              }
              victim.Disconnect(fm);
              victim.Dispose();
              lock (_ProtocolManagers) {
                  _ProtocolManagers.Remove(victim);
              }
          }
          
          public void CommandReconnect(CommandModel cd)
          {
              Trace.Call(cd);
              
              if (cd == null) {
                  throw new ArgumentNullException("cd");
              }
              
              var pm = cd.Chat.ProtocolManager;
              if (pm == null) {
                  return;
              }
  
              ThreadPool.QueueUserWorkItem(delegate {
                  try {
                      pm.Reconnect(cd.FrontendManager);
                  } catch (Exception ex) {
  #if LOG4NET
                      f_Logger.Error("CommandReconnect(): ", ex);
  #endif
                      var builder = CreateMessageBuilder();
                      builder.AppendEventPrefix();
                      builder.AppendErrorText(_("Reconnect failed!"));
                      AddMessageToFrontend(cd, builder.ToMessage());
                  }
              });
          }
          
          public void CommandConfig(CommandModel cd)
          {
              Trace.Call(cd);
              
              if (cd == null) {
                  throw new ArgumentNullException("cd");
              }
              
              if (cd.DataArray.Length < 2) {
                  _NotEnoughParameters(cd);
                  return;
              }
              var builder = CreateMessageBuilder();
              builder.AppendEventPrefix();
              var action = cd.DataArray[1].ToLower();
              switch (action) {
                  case "load":
                      _Config.Load();
                      builder.AppendText(_("Configuration reloaded"));
                      break;
                  case "save":
                      _Config.Save();
                      builder.AppendText(_("Configuration saved"));
                      break;
                  case "get":
                  case "list":
                      string key = null;
                      if (action == "get" && cd.DataArray.Length >= 3) {
                          key = cd.DataArray[2];
                      }
                      foreach (var entry in _UserConfig.OrderBy(kvp => kvp.Key)) {
                          if (key != null &&
                              entry.Key.IndexOf(key, StringComparison.InvariantCultureIgnoreCase) == -1) {
                              continue;
                          }
                          builder = CreateMessageBuilder();
                          builder.AppendEventPrefix();
                          builder.AppendText("{0} = {1}", entry.Key, entry.Value);
                          AddMessageToFrontend(cd, builder.ToMessage());
                      }
                      return;
                  case "set":
                      if (cd.DataArray.Length < 3) {
                          _NotEnoughParameters(cd);
                          return;
                      }
                      var setParam = String.Join(" ", cd.DataArray.Skip(2).ToArray());
                      if (!setParam.Contains("=")) {
                          builder.AppendErrorText(
                              _("Invalid key/value format.")
                          );
                          AddMessageToFrontend(cd, builder.ToMessage());
                          return;
                      }
                      var setKey = setParam.Split('=')[0].Trim();
                      var setValue = String.Join(
                          "=", setParam.Split('=').Skip(1).ToArray()
                      ).Trim();
                      object oldValue = _UserConfig[setKey];
                      if (oldValue == null && setKey.StartsWith("MessagePatterns/")) {
                          var id = setKey.Split('/')[1];
                          var parsedId = Int32.Parse(id);
                          var msgPatternSettings = new MessagePatternListController(_UserConfig);
                          var pattern = msgPatternSettings.Get(parsedId);
                          if (pattern == null) {
                              // pattern does not exist, create it with default values
                              pattern = new MessagePatternModel(parsedId);
                              msgPatternSettings.Add(pattern, parsedId);
                              oldValue = _UserConfig[setKey];
                          }
                      }
                      if (oldValue == null) {
                          builder.AppendErrorText(
                              _("Invalid config key: '{0}'"),
                              setKey
                          );
                          AddMessageToFrontend(cd, builder.ToMessage());
                          return;
                      }
  
                      try {
                          object newValue = Convert.ChangeType(setValue, oldValue.GetType());
                          _UserConfig[setKey] = newValue;
                          builder.AppendText("{0} = {1}", setKey, newValue.ToString());
                          if (setKey.StartsWith("MessagePatterns/")) {
                              MessageBuilderSettings.ApplyConfig(UserConfig);
                          }
                      } catch (InvalidCastException) {
                          builder.AppendErrorText(
                              _("Could not convert config value: '{0}' to type: {1}"),
                              setValue,
                              oldValue.GetType().Name
                          );
                      } catch (FormatException) {
                          builder.AppendErrorText(
                              _("Could not convert config value: '{0}' to type: {1}"),
                              setValue,
                              oldValue.GetType().Name
                          );
                      }
                      break;
                  case "remove": {
                      if (cd.DataArray.Length < 3) {
                          _NotEnoughParameters(cd);
                          return;
                      }
                      var removeParam = cd.DataArray[2];
                      if (!removeParam.StartsWith("MessagePatterns/")) {
                          builder.AppendErrorText(
                              _("Invalid config remove key: '{0}'. Valid remove " +
                                "keys: MessagePatterns/{{ID}}."),
                              removeParam
                          );
                          AddMessageToFrontend(cd, builder.ToMessage());
                          return;
                      }
                      var id = removeParam.Split('/')[1];
                      var parsedId = Int32.Parse(id);
                      var patternController = new MessagePatternListController(_UserConfig);
                      var pattern = patternController.Get(parsedId);
                      if (pattern == null) {
                          builder.AppendErrorText(
                              _("Message pattern with ID: '{0}' does not exist."),
                              id
                          );
                      } else {
                          patternController.Remove(parsedId);
                          MessageBuilderSettings.ApplyConfig(UserConfig);
                          builder.AppendText(
                              _("Message pattern with ID: '{0}' removed."),
                              id
                          );
                      }
                      break;
                  }
                  default:
                      builder.AppendErrorText(
                          _("Invalid parameter for config; use load, save, get or set.")
                      );
                      break;
              }
              AddMessageToFrontend(cd, builder.ToMessage());
          }
          
          public void CommandShutdown(CommandModel cmd)
          {
              Trace.Call(cmd);
  
  #if LOG4NET
              f_Logger.Info("Shutting down...");
  #endif
  
              var frontendMgr = cmd != null ? cmd.FrontendManager : null;
              Shutdown(true, frontendMgr);
  
              if (IsLocal) {
                  // allow the frontend to cleanly terminate
                  return;
              }
  
  #if LOG4NET
              f_Logger.Debug("Terminating process...");
  #endif
              Environment.Exit(0);
          }
  
          public void CommandEcho(CommandModel cmd)
          {
              Trace.Call(cmd);
  
              if (cmd == null) {
                  throw new ArgumentNullException("cmd");
              }
  
              var msg = CreateMessageBuilder().
                  AppendEventPrefix().
                      AppendText(cmd.Parameter).
                      ToMessage();
              if (cmd.FrontendManager == null) {
                  AddMessageToChat(cmd.Chat, msg, true);
              } else {
                  cmd.FrontendManager.AddMessageToChat(cmd.Chat, msg);
              }
          }
  
          public void CommandNetwork(CommandModel cd)
          {
              Trace.Call(cd);
              
              if (cd == null) {
                  throw new ArgumentNullException("cd");
              }
  
              if (cd.DataArray.Length == 1) {
                  // no parameter given, fallback to list
                  _CommandNetworkList(cd);
                  return;
              }
  
              if (cd.DataArray.Length >= 2) {
                  switch (cd.DataArray[1].ToLower()) {
                      case "list":
                          _CommandNetworkList(cd);
                          break;
                      case "switch":
                          _CommandNetworkSwitch(cd);
                          break;
                      case "close":
                          _CommandNetworkClose(cd);
                          break;
                      default:
                          var builder = CreateMessageBuilder();
                          builder.AppendEventPrefix();
                          builder.AppendText(
                              _("Invalid parameter for network; use list, " +
                                "switch, or close")
                          );
                          AddMessageToFrontend(cd, builder.ToMessage());
                          break;
                  }
              } else {
                  _NotEnoughParameters(cd);
              }
          }
          
          private void _CommandNetworkList(CommandModel cmd)
          {
              var servers = new ServerListController(UserConfig);
              var availableNetworks = servers.GetNetworks();
              var connectedNetworks = new List<IProtocolManager>();
              lock (_ProtocolManagers) {
                  foreach (IProtocolManager pm in _ProtocolManagers) {
                      if (pm.IsConnected) {
                          connectedNetworks.Add(pm);
                          if (!String.IsNullOrEmpty(pm.NetworkID)) {
                              availableNetworks.Remove(pm.NetworkID);
                          }
                      }
                  }
              }
  
              var msg = CreateMessageBuilder();
              // TRANSLATOR: this line is used as a label / category for a
              // list of networks below
              msg.AppendHeader(_("Connected Networks"));
              AddMessageToFrontend(cmd, msg.ToMessage());
              foreach (var network in connectedNetworks) {
                  msg = CreateMessageBuilder();
                  msg.AppendEventPrefix();
                  msg.AppendText("{0}: {1} ", _("Network"),  network.NetworkID);
                  msg.AppendText("{0}: {1} ", _("Protocol"), network.Protocol);
                  msg.AppendText("{0}: {1} ", _("Host"),     network.Host);
                  msg.AppendText("{0}: {1}",  _("Port"),     network.Port);
                  AddMessageToFrontend(cmd, msg.ToMessage());
              }
              if (connectedNetworks.Count == 0) {
                  msg = CreateMessageBuilder();
                  // TRANSLATOR: no connected networks
                  msg.AppendEventPrefix().AppendText("<{0}>", _("None"));
                  AddMessageToFrontend(cmd, msg.ToMessage());
              }
  
              msg = CreateMessageBuilder();
              // TRANSLATOR: this line is used as a label / category for a
              // list of networks below
              msg.AppendHeader(_("Available Networks"));
              AddMessageToFrontend(cmd, msg.ToMessage());
              foreach (var network in availableNetworks) {
                  if (network == null || network.Trim().Length == 0) {
                      continue;
                  }
                  msg = CreateMessageBuilder();
                  msg.AppendEventPrefix();
                  msg.AppendText("{0}: {1}", _("Network"), network);
                  AddMessageToFrontend(cmd, msg.ToMessage());
              }
              if (availableNetworks.Count == 0) {
                  msg = CreateMessageBuilder();
                  // TRANSLATOR: no available networks
                  msg.AppendEventPrefix().AppendText("<{0}>", _("None"));
                  AddMessageToFrontend(cmd, msg.ToMessage());
              }
          }
          
          private void _CommandNetworkClose(CommandModel cd)
          {
              IProtocolManager pm = null;
              if (cd.DataArray.Length >= 3) {
                  // named protocol manager
                  string network = cd.DataArray[2];
                  pm = GetProtocolManagerByNetwork(network);
                  if (pm == null) {
                      var builder = CreateMessageBuilder();
                      builder.AppendText(_("Network close failed - could not " +
                                           "find network: {0}"), network);
                      AddMessageToFrontend(cd, builder.ToMessage());
                      return;
                  }
              } else if (cd.DataArray.Length >= 2) {
                  // network manager of chat
                  pm = cd.Chat.ProtocolManager;
              }
  
              if (pm == null) {
                  return;
              }
  
              // disconnect in background as could be blocking
              ThreadPool.QueueUserWorkItem(delegate {
                  try {
                      pm.Disconnect(cd.FrontendManager);
                      pm.Dispose();
                      // Dispose() takes care of removing the chat from session (frontends)
                      lock (_ProtocolManagers) {
                          _ProtocolManagers.Remove(pm);
                      }
                  } catch (Exception ex) {
  #if LOG4NET
                      f_Logger.Error("_CommandNetworkClose(): Exception", ex);
  #endif
                  }
              });
          }
          
          private void _CommandNetworkSwitch(CommandModel cd)
          {
              FrontendManager fm = cd.FrontendManager;
              if (fm == null) {
                  _NotEnoughParameters(cd);
                  return;
              }
              if (cd.DataArray.Length >= 3) {
                  // named network manager
                  string network = cd.DataArray[2];
                  var pm = GetProtocolManagerByNetwork(network);
                  if (pm == null) {
                      var builder = CreateMessageBuilder();
                      builder.AppendText(_("Network switch failed - could not " +
                                           "find network: {0}"), network);
                      fm.AddMessageToChat(cd.Chat, builder.ToMessage());
                      return;
                  }
                  fm.CurrentProtocolManager = pm;
                  fm.UpdateNetworkStatus();
              } else if (cd.DataArray.Length >= 2) {
                  // next network manager
                  fm.NextProtocolManager();
              } else {
                  _NotEnoughParameters(cd);
              }
          }
          
          private void _NotConnected(CommandModel cd)
          {
              var builder = CreateMessageBuilder();
              builder.AppendText(_("Not connected to any network"));
              AddMessageToFrontend(cd, builder.ToMessage());
          }
          
          private void _NotEnoughParameters(CommandModel cd)
          {
              var builder = CreateMessageBuilder();
              builder.AppendText(_("Not enough parameters for {0} command"),
                                 cd.Command);
              AddMessageToFrontend(cd, builder.ToMessage());
          }
          
          public void UpdateNetworkStatus()
          {
              Trace.Call();
              
              lock (_FrontendManagers) {
                  foreach (FrontendManager fm in _FrontendManagers.Values) {
                      fm.UpdateNetworkStatus();
                  }
              }
          }
  
          public T CreateChat<T>(string id,
                                 string name,
                                 IProtocolManager protocolManager)
                                where T : ChatModel
          {
              Trace.Call(id, name, protocolManager);
  
              T chat;
              Type chatType = typeof(T);
              if (chatType == typeof(SessionChatModel)) {
                  chat = (T) Activator.CreateInstance(chatType, id, name);
              } else if (chatType == typeof(PersonChatModel)) {
                  throw new NotSupportedException(
                      "PersonModel is not supported, use " +
                      "Session.CreatePersionChat() instead"
                  );
              } else {
                  chat = (T) Activator.CreateInstance(chatType,
                                                      id, name, protocolManager);
              }
              chat.ApplyConfig(UserConfig);
              return chat;
          }
  
          public PersonChatModel CreatePersonChat(PersonModel person,
                                                  IProtocolManager protocolManager)
          {
              Trace.Call(person, protocolManager);
  
              if (person == null) {
                  throw new ArgumentNullException("person");
              }
              return CreatePersonChat(person, person.ID, person.IdentityName,
                                      protocolManager);
          }
  
          public PersonChatModel CreatePersonChat(PersonModel person,
                                                  string id, string name,
                                                  IProtocolManager protocolManager)
          {
              Trace.Call(person, id, name, protocolManager);
  
              var chat = new PersonChatModel(person, id, name, protocolManager);
              chat.ApplyConfig(UserConfig);
              return chat;
          }
  
          public void AddChat(ChatModel chat)
          {
              Trace.Call(chat);
              
              if (chat == null) {
                  throw new ArgumentNullException("chat");
              }
  
              chat.Position = GetSortedChatPosition(chat);
              lock (_Chats) {
                  _Chats.Add(chat);
                  if (chat.Position == -1) {
                      chat.Position = _Chats.IndexOf(chat);
                  } else {
                      MoveChat(chat, chat.Position);
                  }
              }
  
              lock (_FrontendManagers) {
                  foreach (FrontendManager fm in _FrontendManagers.Values) {
                      fm.AddChat(chat);
                  }
              }
          }
          
          public void RemoveChat(ChatModel chat)
          {
              Trace.Call(chat);
              
              if (chat == null) {
                  throw new ArgumentNullException("chat");
              }
              
              lock (_Chats) {
                  if (!_Chats.Remove(chat)) {
  #if LOG4NET
                      f_Logger.Warn("RemoveChat(): _Chats.Remove(" + chat + ") failed, ignoring...");
  #endif
                      chat.Close();
                      return;
                  }
                  chat.Close();
  
                  // refresh chat positions
                  foreach (var schat in _Chats) {
                      schat.Position = _Chats.IndexOf(schat);
                  }
              }
              
              lock (_FrontendManagers) {
                  foreach (FrontendManager fm in _FrontendManagers.Values) {
                      fm.RemoveChat(chat);
                  }
              }
          }
          
          public void EnableChat(ChatModel chat)
          {
              Trace.Call(chat);
              
              if (chat == null) {
                  throw new ArgumentNullException("chat");
              }
              
              chat.IsEnabled = true;
              lock (_FrontendManagers) {
                  foreach (FrontendManager fm in _FrontendManagers.Values) {
                      fm.EnableChat(chat);
                  }
              }
          }
          
          public void DisableChat(ChatModel chat)
          {
              Trace.Call(chat);
              
              if (chat == null) {
                  throw new ArgumentNullException("chat");
              }
              
              chat.IsEnabled = false;
              lock (_FrontendManagers) {
                  foreach (FrontendManager fm in _FrontendManagers.Values) {
                      fm.DisableChat(chat);
                  }
              }
          }
          
          public void SyncChat(ChatModel chat)
          {
              Trace.Call(chat);
              
              if (chat == null) {
                  throw new ArgumentNullException("chat");
              }
              
              lock (_FrontendManagers) {
                  foreach (FrontendManager fm in _FrontendManagers.Values) {
                      fm.SyncChat(chat);
                  }
              }
          }
          
          public void MoveChat(ChatModel chat, int newPosition)
          {
              Trace.Call(chat, newPosition);
  
              if (chat == null) {
                  throw new ArgumentNullException("chat");
              }
  
              lock (_Chats) {
                  _Chats.Remove(chat);
                  _Chats.Insert(newPosition, chat);
                  foreach (var schat in _Chats) {
                      schat.Position = _Chats.IndexOf(schat);
                  }
              }
          }
  
          [Obsolete("This method is deprecated, use AddMessageToChat(ChatModel, MessageModel) instead!")]
          public void AddTextToChat(ChatModel chat, string text)
          {
              AddTextToChat(chat, text, false);
          }
  
          [Obsolete("This method is deprecated, use AddMessageToChat(ChatModel, MessageModel, bool) instead!")]
          public void AddTextToChat(ChatModel chat, string text, bool ignoreFilters)
          {
              if (chat == null) {
                  throw new ArgumentNullException("chat");
              }
              if (text == null) {
                  throw new ArgumentNullException("text");
              }
              
              AddMessageToChat(chat, new MessageModel(text), ignoreFilters);
          }
          
          public void AddMessageToChat(ChatModel chat, MessageModel msg)
          {
              AddMessageToChat(chat, msg, false);
          }
  
          public void AddMessageToChat(ChatModel chat, MessageModel msg,
                                       bool ignoreFilters)
          {
              if (chat == null) {
                  throw new ArgumentNullException("chat");
              }
              if (msg == null) {
                  throw new ArgumentNullException("msg");
              }
              
              bool isFiltered = !ignoreFilters && IsFilteredMessage(chat, msg);
              LogMessage(chat, msg, isFiltered);
              if (isFiltered) {
                  return;
              }
  
              lock (chat.MessageBuffer) {
                  try {
                      chat.MessageBuffer.Add(msg);
                  } catch (Exception ex) {
  #if LOG4NET
                      Trace.Call(chat, msg, ignoreFilters);
                      f_Logger.ErrorFormat(
                          "AddMessageToChat({0}, {1}, {2}): " +
                          "chat.MessageBuffer.Add() threw exception:",
                          chat, msg, ignoreFilters
                      );
                      f_Logger.Error("AddMessageToChat(): ", ex);
  #endif
                      if (chat.MessageBuffer is Db4oMessageBuffer ||
                          chat.MessageBuffer is SqliteMessageBuffer) {
  #if LOG4NET
                          f_Logger.Error(
                              "AddMessageToChat(): " +
                              "Falling back to volatile message buffer..."
                          );
  #endif
                          chat.ResetMessageBuffer();
                          chat.InitMessageBuffer(MessageBufferPersistencyType.Volatile);
  
                          var builder = CreateMessageBuilder();
                          builder.AppendEventPrefix();
                          builder.AppendErrorText(
                              _("Failed to write to chat history. " +
                                "Your chat history will not be preserved. " +
                                "Reason: {0}"),
                              ex.Message
                          );
                          chat.MessageBuffer.Add(builder.ToMessage());
  
                          chat.MessageBuffer.Add(msg);
                      }
                  }
              }
              lock (_FrontendManagers) {
                  foreach (FrontendManager fm in _FrontendManagers.Values) {
                      fm.AddMessageToChat(chat, msg);
                  }
              }
  
              if (msg.MessageType == MessageType.Event) {
                  // on-event-message
                  OnEventMessage(
                      // at this point we no longer know who sent this nor to whom
                      new EventMessageEventArgs(chat, msg, String.Empty, String.Empty)
                  );
              }
          }
          
          public void AddPersonToGroupChat(GroupChatModel groupChat, PersonModel person)
          {
              if (groupChat == null) {
                  throw new ArgumentNullException("groupChat");
              }
              if (person == null) {
                  throw new ArgumentNullException("person");
              }
              
  #if LOG4NET
              f_Logger.Debug("AddPersonToGroupChat() groupChat.Name: "+groupChat.Name+" person.IdentityName: "+person.IdentityName);
  #endif
              groupChat.UnsafePersons.Add(person.ID, person);
              lock (_FrontendManagers) {
                  foreach (FrontendManager fm in _FrontendManagers.Values) {
                      fm.AddPersonToGroupChat(groupChat, person);
                  }
              }
  
              OnGroupChatPersonAdded(
                  new GroupChatPersonAddedEventArgs(groupChat, person)
              );
          }
          
          public void UpdatePersonInGroupChat(GroupChatModel groupChat, PersonModel oldPerson, PersonModel newPerson)
          {
              if (groupChat == null) {
                  throw new ArgumentNullException("groupChat");
              }
              if (oldPerson == null) {
                  throw new ArgumentNullException("oldPerson");
              }
              if (newPerson == null) {
                  throw new ArgumentNullException("newPerson");
              }
              
  #if LOG4NET
              f_Logger.Debug("UpdatePersonInGroupChat()" +
                            " groupChat.Name: " + groupChat.Name +
                            " oldPerson.IdentityName: " + oldPerson.IdentityName +
                            " newPerson.IdentityName: " + newPerson.IdentityName);
  #endif
              // FIXME: do we have to lock groupChat.UnsafePersons here?
              // probably not, as long as the ProtocolManager who owns this chat
              // is only running one thread
              groupChat.UnsafePersons.Remove(oldPerson.ID);
              groupChat.UnsafePersons.Add(newPerson.ID, newPerson);
              lock (_FrontendManagers) {
                  foreach (FrontendManager fm in _FrontendManagers.Values) {
                      fm.UpdatePersonInGroupChat(groupChat, oldPerson, newPerson);
                  }
              }
  
              OnGroupChatPersonUpdated(
                  new GroupChatPersonUpdatedEventArgs(groupChat, oldPerson, newPerson)
              );
          }
      
          public void UpdateTopicInGroupChat(GroupChatModel groupChat, MessageModel topic)
          {
              if (groupChat == null) {
                  throw new ArgumentNullException("groupChat");
              }
              if (topic == null) {
                  throw new ArgumentNullException("topic");
              }
  
  #if LOG4NET
              f_Logger.Debug("UpdateTopicInGroupChat() groupChat.Name: " + groupChat.Name + " topic: " + topic.ToString());
  #endif
              groupChat.Topic = topic;
              lock (_FrontendManagers) {
                  foreach (FrontendManager fm in _FrontendManagers.Values) {
                      fm.UpdateTopicInGroupChat(groupChat, topic);
                  }
              }
          }
      
          public void RemovePersonFromGroupChat(GroupChatModel groupChat, PersonModel person)
          {
              if (groupChat == null) {
                  throw new ArgumentNullException("groupChat");
              }
              if (person == null) {
                  throw new ArgumentNullException("person");
              }
              
  #if LOG4NET
              f_Logger.Debug("RemovePersonFromGroupChat() groupChat.Name: " + groupChat.Name + " person.ID: "+person.ID);
  #endif
              groupChat.UnsafePersons.Remove(person.ID);
              lock (_FrontendManagers) {
                  foreach (FrontendManager fm in _FrontendManagers.Values) {
                      fm.RemovePersonFromGroupChat(groupChat, person);
                  }
              }
  
              OnGroupChatPersonRemoved(
                  new GroupChatPersonRemovedEventArgs(groupChat, person)
              );
          }
          
          public void SetNetworkStatus(string status)
          {
              if (status == null) {
                  throw new ArgumentNullException("status");
              }
              
  #if LOG4NET
              f_Logger.Debug("SetNetworkStatus() status: "+status);
  #endif
              lock (_FrontendManagers) {
                  foreach (FrontendManager fm in _FrontendManagers.Values) {
                      fm.SetNetworkStatus(status);
                  }
              }
          }
          
          public void SetStatus(string status)
          {
              if (status == null) {
                  throw new ArgumentNullException("status");
              }
              
  #if LOG4NET
              f_Logger.Debug("SetStatus() status: "+status);
  #endif
              lock (_FrontendManagers) {
                  foreach (FrontendManager fm in _FrontendManagers.Values) {
                      fm.SetStatus(status);
                  }
              }
          }
          
          public IList<string> GetSupportedProtocols()
          {
              return _ProtocolManagerFactory.GetProtocols();
          }
          
          public IProtocolManager Connect(ServerModel server, FrontendManager frontendManager)
          {
              Trace.Call(server, frontendManager);
              
              if (server == null) {
                  throw new ArgumentNullException("server");
              }
              if (String.IsNullOrEmpty(server.Protocol)) {
                  throw new ArgumentNullException("server.Protocol");
              }
  
              IProtocolManager protocolManager = CreateProtocolManager(
                  server.Protocol
              );
              lock (_ProtocolManagers) {
                  _ProtocolManagers.Add(protocolManager);
              }
  
              string password = null;
              // only pass non-empty passwords to Connect()
              if (!String.IsNullOrEmpty(server.Password)) {
                  password = server.Password;
              }
  
              if (server.OnConnectCommands != null && server.OnConnectCommands.Count > 0) {
                  protocolManager.Connected += delegate {
                      ThreadPool.QueueUserWorkItem(delegate {
                          try {
                              foreach (string command in server.OnConnectCommands) {
                                  if (command.Length == 0) {
                                      continue;
                                  }
  
                                  try {
                                      var cd = new CommandModel(
                                          frontendManager,
                                          protocolManager.Chat,
                                          (string) _UserConfig["Interface/Entry/CommandCharacter"],
                                          command
                                      );
                                      protocolManager.Command(cd);
                                  } catch (Exception ex) {
  #if LOG4NET
                                      f_Logger.Error("Connect(): Command in Connected event: Exception", ex);
  #endif
                                      var msg = CreateMessageBuilder().
                                          AppendErrorText("Command '{0}' failed. Reason: {1} ({2})",
                                                          command, ex.Message, ex.GetType()).
                                          ToMessage();
                                      AddMessageToFrontend (frontendManager, protocolManager.Chat, msg);
                                  }
                              }
                          } catch (Exception ex) {
  #if LOG4NET
                              f_Logger.Error("Connect(): Connected event: Exception", ex);
  #endif
                          }
                      });
                  };
              }
  
              protocolManager.Connect(frontendManager, server);
              if (protocolManager.Chat == null) {
                  // just in case the ProtocolManager is not setting the
                  // protocol chat
                  throw new ApplicationException(_("Connect failed."));
              }
  
              return protocolManager;
          }
          
          private IProtocolManager CreateProtocolManager(string protocol)
          {
              ProtocolManagerInfoModel info =
                  _ProtocolManagerFactory.GetProtocolManagerInfoByAlias(protocol);
              if (info == null) {
                  if (_ProtocolManagerFactory.ProtocolManagerInfos.Count != 1) {
                      throw new ArgumentException(
                              String.Format(
                                  _("No protocol manager found for the protocol: {0}"),
                                  protocol
                              ),
                              "protocol"
                      );
                  }
  
                  // ok, we forgive the user not passing a valid protocol by
                  // falling back to the only available protocol
                  info = _ProtocolManagerFactory.ProtocolManagerInfos[0];
              }
  
              return _ProtocolManagerFactory.CreateProtocolManager(info, this);
          }
          
          public void LogMessage(ChatModel chat, MessageModel msg, bool isFiltered)
          {
              if (chat == null) {
                  throw new ArgumentNullException("chat");
              }
              if (msg == null) {
                  throw new ArgumentNullException("msg");
              }
  
              if (!(bool) UserConfig["Logging/Enabled"]) {
                  return;
              }
              if (isFiltered && !(bool) UserConfig["Logging/LogFilteredMessages"]) {
                  return;
              }
  
              if (chat.ChatType == ChatType.Session ||
                  chat.ChatType == ChatType.Protocol) {
                  return;
              }
  
              try {
                  // HACK: twitter retrieves older messages and we don't want to
                  // re-log those when the twitter connection is re-opened
                  var protocol = chat.ProtocolManager.Protocol.ToLower();
                  if (protocol == "twitter") {
                      return;
                  }
  
                  // don't log chatstates
  
gt:                 switch (msg.MessageType) {