context: /*
   * Smuxi - Smart MUltipleXed Irc
   *
   * Copyright (c) 2005-2014 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.Text;
  using System.Text.RegularExpressions;
  using System.Security.Cryptography.X509Certificates;
  using System.Globalization;
  using System.Threading;
  using System.Collections;
  using System.Collections.Generic;
  using System.Linq;
  using Meebey.SmartIrc4net;
  using Smuxi.Common;
  using IrcProxyType = Meebey.SmartIrc4net.ProxyType;
  
  namespace Smuxi.Engine
  {
      [ProtocolManagerInfo(Name = "IRC", Description = "Internet Relay Chat", Alias = "irc")]
      public class IrcProtocolManager : ProtocolManagerBase
      {
  #if LOG4NET
          private static readonly log4net.ILog _Logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
  #endif
          private static readonly string       _LibraryTextDomain = "smuxi-engine-irc";
          private IrcFeatures     _IrcClient;
          private ServerModel     _ServerModel;
          private string          _Host;
          private int             _Port;
          private string          _Network;
          private string[]        _Nicknames;
          string _Realname;
          private int             _CurrentNickname;
          private string          _Username;
          private string          _Password;
          private FrontendManager _FrontendManager;
          private bool            _Listening;
          private ChatModel       _NetworkChat;
          private TimeSpan        _LastLag;
          private Thread          _RunThread;
          private Thread          _LagWatcherThread;
          private TaskQueue       _ChannelJoinQueue = new TaskQueue("JoinChannelQueue");
          private List<string>    _QueuedChannelJoinList = new List<string>();
          private List<string>    _ActiveChannelJoinList = new List<string>();
          private AutoResetEvent  _ActiveChannelJoinHandle = new AutoResetEvent(false);
  
          bool HasListMaskSearchSupport { get; set; }
          bool HasSafeListSupport { get; set; }
          IList<ChannelInfo> NetworkChannels { get; set; }
          DateTime NetworkChannelsAge { get; set; }
          TimeSpan NetworkChannelsMaxAge { get; set; }
          List<string> ChannelTypes { get; set; }
  
          public override bool IsConnected {
              get {
                  if ((_IrcClient != null) &&
                      (_IrcClient.IsConnected)) {
                      return true;
                  }
                  return false;
              }
          }
          
          public override string Host {
              get {
                  if (_IrcClient == null) {
                      return null;
                  }
                  return _IrcClient.Address;
              }
          }
          
          public override int Port {
              get {
                  if (_IrcClient == null) {
                      return -1;
                  }
                  return _IrcClient.Port;
              }
          }
          
          public override string NetworkID {
              get {
                  if (String.IsNullOrEmpty(_Network)) {
                      return _IrcClient.Address;
                  }
                  return _Network;
              }
          }
          
          public override string Protocol {
              get {
                  return "IRC";
              }
          }
          
          public override ChatModel Chat {
              get {
                  return _NetworkChat;
              }
          }
  
          private string Prefix {
              get {
                  if (_IrcClient == null) {
                      return String.Empty;
                  }
  
                  if (IrcMe == null) {
                      return _IrcClient.Nickname;
                  }
  
                  return String.Format("{0}!{1}@{2}", _IrcClient.Nickname,
                                       IrcMe.Ident, IrcMe.Host);
              }
          }
  
          private IrcPersonModel IrcMe {
              get {
                  return (IrcPersonModel) Me;
              }
              set {
                  Me = value;
              }
          }
  
          public IrcProtocolManager(Session session) : base(session)
          {
              Trace.Call(session);
  
              NetworkChannelsMaxAge = TimeSpan.FromMinutes(5);
              ChannelTypes = new List<string>(new string[] {"#", "&", "!", "+"});
  
              _IrcClient = new IrcFeatures();
              _IrcClient.AutoRetry = true;
              // keep retrying to connect forever
              _IrcClient.AutoRetryLimit = 0;
              _IrcClient.AutoRetryDelay = 120;
              _IrcClient.AutoReconnect = true;
              _IrcClient.AutoRelogin = true;
              _IrcClient.AutoRejoin = true;
              // HACK: SmartIrc4net <= 0.4.5.1 is not resetting the nickname list
              // after disconnect. This causes random nicks to be used when there
              // are many reconnects like when the network connection goes flaky,
              // see: http://projects.qnetp.net/issues/show/163
              _IrcClient.AutoNickHandling = false;
              _IrcClient.CtcpVersion      = Engine.VersionString;
              _IrcClient.SendDelay        = 250;
              _IrcClient.SupportNonRfc    = true;
              _IrcClient.OnRawMessage     += new IrcEventHandler(_OnRawMessage);
              _IrcClient.OnChannelMessage += new IrcEventHandler(_OnChannelMessage);
              _IrcClient.OnChannelAction  += new ActionEventHandler(_OnChannelAction);
              _IrcClient.OnChannelNotice  += new IrcEventHandler(_OnChannelNotice);
              _IrcClient.OnQueryMessage   += new IrcEventHandler(_OnQueryMessage);
              _IrcClient.OnQueryAction    += new ActionEventHandler(_OnQueryAction);
              _IrcClient.OnQueryNotice    += new IrcEventHandler(_OnQueryNotice);
              _IrcClient.OnJoin           += new JoinEventHandler(_OnJoin);
              _IrcClient.OnNames          += new NamesEventHandler(_OnNames);
              _IrcClient.OnPart           += new PartEventHandler(_OnPart);
              _IrcClient.OnKick           += new KickEventHandler(_OnKick);
              _IrcClient.OnNickChange     += new NickChangeEventHandler(_OnNickChange);
              _IrcClient.OnOwner          += new OwnerEventHandler(OnOwner);
              _IrcClient.OnDeowner        += new DeownerEventHandler(OnDeowner);
              _IrcClient.OnChannelAdmin   += new ChannelAdminEventHandler(OnChannelAdmin);
              _IrcClient.OnDeChannelAdmin += new DeChannelAdminEventHandler(OnDeChannelAdmin);
              _IrcClient.OnOp             += new OpEventHandler(_OnOp);
              _IrcClient.OnDeop           += new DeopEventHandler(_OnDeop);
              _IrcClient.OnHalfop         += new HalfopEventHandler(OnHalfop);
              _IrcClient.OnDehalfop       += new DehalfopEventHandler(OnDehalfop);
              _IrcClient.OnVoice          += new VoiceEventHandler(_OnVoice);
              _IrcClient.OnDevoice        += new DevoiceEventHandler(_OnDevoice);
              _IrcClient.OnModeChange     += new IrcEventHandler(_OnModeChange);
              _IrcClient.OnTopic          += new TopicEventHandler(_OnTopic);
              _IrcClient.OnTopicChange    += new TopicChangeEventHandler(_OnTopicChange);
              _IrcClient.OnQuit           += new QuitEventHandler(_OnQuit);
              _IrcClient.OnRegistered     += new EventHandler(_OnRegistered);
              _IrcClient.OnDisconnected   += new EventHandler(_OnDisconnected);
              _IrcClient.OnAutoConnectError += OnAutoConnectError;
              _IrcClient.OnAway           += new AwayEventHandler(_OnAway);
              _IrcClient.OnUnAway         += new IrcEventHandler(_OnUnAway);
              _IrcClient.OnNowAway        += new IrcEventHandler(_OnNowAway);
              _IrcClient.OnCtcpRequest    += new CtcpEventHandler(_OnCtcpRequest);
              _IrcClient.OnCtcpReply      += new CtcpEventHandler(_OnCtcpReply);
              _IrcClient.OnWho            += OnWho;
              _IrcClient.OnInvite         += OnInvite;
              _IrcClient.OnReadLine       += OnReadLine;
              _IrcClient.OnWriteLine      += OnWriteLine;
  
              _IrcClient.CtcpUserInfo = (string) Session.UserConfig["Connection/Realname"];
              // disabled as we don't use / support DCC yet
              _IrcClient.CtcpDelegates.Remove("dcc");
              // finger we handle ourself, no little helga here!
              _IrcClient.CtcpDelegates["finger"] = delegate(CtcpEventArgs e) {
                  _IrcClient.SendMessage(
                      SendType.CtcpReply, e.Data.Nick,
                      String.Format("{0} {1}",
                          e.CtcpCommand,
                          _IrcClient.CtcpUserInfo
                      )
                  );
              };
              // time we handle ourself
              _IrcClient.CtcpDelegates["time"] = delegate(CtcpEventArgs e) {
                  _IrcClient.SendMessage(
                      SendType.CtcpReply, e.Data.Nick,
                      String.Format("{0} {1}",
                          e.CtcpCommand,
                          DateTime.Now.ToString(
                              "ddd MMM dd HH:mm:ss yyyy",
                              DateTimeFormatInfo.InvariantInfo
                          )
                      )
                  );
              };
          }
  
          private void OnWho(object sender, WhoEventArgs e)
          {
              if (e.WhoInfo.Nick == _IrcClient.Nickname) {
                  // that's me!
                  IrcMe.Ident = e.WhoInfo.Ident;
                  IrcMe.Host = e.WhoInfo.Host;
                  IrcMe.RealName = e.WhoInfo.Realname;
              }
          }
  
          private void OnInvite(object sender, InviteEventArgs e)
          {
              var builder = CreateMessageBuilder();
              builder.AppendEventPrefix();
              builder.MessageType = MessageType.Normal;
              var msg = builder.CreateFormat(_("{0} invites you to {1}"),
                                             CreatePerson(e.Who), e.Channel);
              foreach (var part in msg) {
                  part.IsHighlight = true;
              }
              builder.Append(msg);
              Session.AddMessageToChat(_NetworkChat, builder.ToMessage());
  
              builder = CreateMessageBuilder();
              string host;
              if (String.IsNullOrEmpty(NetworkID)) {
                  host = _IrcClient.Address;
              } else {
                  host = NetworkID;
                  var serverSettings = new ServerListController(Session.UserConfig);
                  var server = serverSettings.GetServerByNetwork(host);
                  if (server == null) {
                      // if the network is not stored in config, we need to
                      // fallback to the bare server address. Otherwise the
                      // frontend will have no idea how to connect to it.
                      host = _IrcClient.Address;
                  }
              }
              string url = String.Format("irc://{0}/{1}", host, e.Channel);
              builder.AppendUrl(url, _("Accept invite (join room)"));
              Session.AddMessageToChat(_NetworkChat, builder.ToMessage());
          }
  
          void OnReadLine(object sender, ReadLineEventArgs e)
          {
              DebugRead(e.Line);
          }
  
          void OnWriteLine(object sender, WriteLineEventArgs e)
          {
              DebugWrite(e.Line);
          }
  
          public override string ToString()
          {
              string result = null;
              if (_IrcClient != null) {
                  if (String.IsNullOrEmpty(_Network)) {
                      result += _IrcClient.Address;
                  } else {
                      result += _Network;
                  }
              }
              result += " (IRC)";
              
              if (IsConnected) {
                  if (_IrcClient.IsAway) {
                      result += " (" + _("away") + ")";
                  }
                  if (_IrcClient.Lag > TimeSpan.FromSeconds(5)) {
                      result += String.Format(" ({0})",
                                      String.Format(
                                          // TRANSLATOR: {0} is the amount of seconds
                                          _("lag: {0} seconds"),
                                          (int) _IrcClient.Lag.TotalSeconds
                                      )
                                );
                  }
              } else {
                  result += " (" + _("not connected") + ")";
              }
              
              return result;
          }
  
          public override void Connect(FrontendManager fm, ServerModel server)
          {
              Trace.Call(fm, server);
  
              if (fm == null) {
                  throw new ArgumentNullException("fm");
              }
              if (server == null) {
                  throw new ArgumentNullException("server");
              }
  
              _FrontendManager = fm;
              _ServerModel = server;
  
              ApplyConfig(Session.UserConfig, server);
  
              // TODO: use config for single network chat or once per network manager
              _NetworkChat = Session.CreateChat<ProtocolChatModel>(
                  _Network, "IRC " + _Network, this
              );
  
              // BUG: race condition when we use Session.AddChat() as it pushes this already
              // to the connected frontend and the frontend will sync and get the page 2 times!
              //Session.Chats.Add(_NetworkChat);
              // NOTABUG: the frontend manager needs to take care for that
              Session.AddChat(_NetworkChat);
              Session.SyncChat(_NetworkChat);
  
              _RunThread = new Thread(new ThreadStart(_Run));
              _RunThread.IsBackground = true;
              _RunThread.Name = String.Format(
                  "IrcProtocolManager ({0}:{1}) listener",
                  server.Hostname, server.Port
               );
              _RunThread.Start();
              
              _LagWatcherThread = new Thread(new ThreadStart(_LagWatcher));
              _LagWatcherThread.IsBackground = true;
              _LagWatcherThread.Name = String.Format(
                  "IrcProtocolManager ({0}:{1}) lag watcher",
                  server.Hostname, server.Port
               );
              _LagWatcherThread.Start();
          }
  
          public void Connect(FrontendManager fm)
          {
              Trace.Call(fm);
              
              MessageBuilder builder;
              try {
                  if (!String.IsNullOrEmpty(_IrcClient.ProxyHost)) {
                      builder = CreateMessageBuilder();
                      builder.AppendEventPrefix();
                      builder.AppendText(_("Using proxy: {0}:{1}"),
                                         _IrcClient.ProxyHost,
                                         _IrcClient.ProxyPort);
                      Session.AddMessageToChat(Chat, builder.ToMessage());
                  }
  
                  string msg;
                  msg = String.Format(_("Connecting to {0} port {1}..."), _Host, _Port);
                  if (fm != null) {
                      fm.SetStatus(msg);
                  }
                  builder = CreateMessageBuilder();
                  builder.AppendEventPrefix().AppendText(msg);
                  Session.AddMessageToChat(Chat, builder.ToMessage());
  
                  // TODO: add SSL support
                  _IrcClient.Connect(_Host, _Port);
                  if (fm != null) {
                      fm.UpdateNetworkStatus();
                  }
  
                  msg = String.Format(_("Connection to {0} established"), _Host);
                  if (fm != null) {
                      fm.SetStatus(msg);
                  }
                  builder = CreateMessageBuilder();
                  builder.AppendEventPrefix().AppendText(msg);
                  Session.AddMessageToChat(Chat, builder.ToMessage());
  
                  builder = CreateMessageBuilder();
                  builder.AppendEventPrefix().AppendText(_("Logging in..."));
                  Session.AddMessageToChat(Chat, builder.ToMessage());
  
                  string realname = _Realname;
                  if (realname == null || realname.Trim().Length == 0) {
                      realname = "unset";
                  }
                  if (!Regex.IsMatch(_Username, "^[a-z0-9]+$", RegexOptions.IgnoreCase)) {
                      builder = CreateMessageBuilder();
                      builder.AppendEventPrefix();
                      builder.AppendWarningText(
                          "Warning: Your username (ident) contains special " +
                          "characters which the IRC server might refuse. " +
                          "If this happens please change your username in the " +
                          "server settings."
                      );
                      Session.AddMessageToChat(_NetworkChat, builder.ToMessage());
                  }
                  _IrcClient.Login(_Nicknames, realname, 0, _Username, _Password);
                  // set Me property very early as we might need to know who we
                  // are before the registration was confirmed in _OnRegistered()
                  Me = CreatePerson(_IrcClient.Nickname);
  
                  foreach (string command in (string[]) Session.UserConfig["Connection/OnConnectCommands"]) {
                      if (command.Length == 0) {
                          continue;
                      } 
                      CommandModel cd = new CommandModel(_FrontendManager, _NetworkChat,
                          (string) Session.UserConfig["Interface/Entry/CommandCharacter"],
                          command);
                      
                      bool handled;
                      handled = Session.Command(cd);
                      if (!handled) {
                          Command(cd);
                      }
                  }
                  _Listening = true;
              } catch (CouldNotConnectException ex) {
                  if (fm != null) {
                      fm.SetStatus(_("Connection failed!"));
                  }
                  builder = CreateMessageBuilder();
                  builder.AppendEventPrefix();
                  builder.AppendText(_("Connection failed! Reason: "));
                  builder.AppendText(ex.Message);
                  Session.AddMessageToChat(Chat, builder.ToMessage());
                  throw;
              }
          }
          
          public override void Disconnect(FrontendManager fm)
          {
              Trace.Call(fm);
  
              MessageBuilder builder;
              if (fm != null) {
                  fm.SetStatus(_("Disconnecting..."));
              }
              if (IsConnected) {
                  builder = CreateMessageBuilder();
                  builder.AppendEventPrefix();
                  builder.AppendText(_("Disconnecting from {0}..."),
                                     _IrcClient.Address);
                  Session.AddMessageToChat(Chat, builder.ToMessage());
                  // else the Listen() thread would try to connect again
                  _Listening = false;
                  _IrcClient.Disconnect();
                  if (fm != null) {
                      fm.SetStatus(String.Format(_("Disconnected from {0}"),
                                                 _IrcClient.Address));
                  }
  
  
gt:                 builder = CreateMessageBuilder();