context: /*
   * $Id$
   * $URL$
   * $Rev$
   * $Author$
   * $Date$
   *
   * SmartIrc4net - the IRC library for .NET/C# <http://smartirc4net.sf.net>
   *
   * Copyright (c) 2003-2009 Mirco Bauer <meebey@meebey.net> <http://www.meebey.net>
   * Copyright (c) 2008-2009 Thomas Bruderer <apophis@apophis.ch>
   * Copyright (c) 2015 Katy Coe <djkaty@start.no> <http://www.djkaty.com>
   *
   * Full LGPL License: <http://www.gnu.org/licenses/lgpl.txt>
   * 
   * This library is free software; you can redistribute it and/or
   * modify it under the terms of the GNU Lesser General Public
   * License as published by the Free Software Foundation; either
   * version 2.1 of the License, or (at your option) any later version.
   *
   * This library 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
   * Lesser General Public License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public
   * License along with this library; if not, write to the Free Software
   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   */
  
  using System;
  using System.Collections;
  using System.Collections.Concurrent;
  using System.Collections.Generic;
  using System.IO;
  using System.Reflection;
  using System.Threading;
  
  namespace Meebey.SmartIrc4net
  {
      /// <summary>
      /// 
      /// </summary>
      /// <threadsafety static="true" instance="true" />
      public class IrcConnection
      {
          private string           _VersionNumber;
          private string           _VersionString;
          private IIrcTransportManager _Transport;
          private string[]         _AddressList = {"localhost"};
          private int              _CurrentAddress;
          private int              _Port;
          private ConcurrentQueue<string> _ReadQueue;
          private AutoResetEvent   _ReadQueueEvent;
          private WriteManager  _WriteManager;
          private IdleWorkerThread _IdleWorkerThread;
          private int              _SendDelay = 200;
          private bool             _IsDisconnecting;
          private bool             _IsRegistered;
          private int              _AutoRetryAttempt;
          private bool             _AutoRetry;
          private int              _AutoRetryDelay = 30;
          private int              _AutoRetryLimit = 3;
          private bool             _AutoReconnect;
          private int              _IdleWorkerInterval = 5;
          private int              _PingInterval = 60;
          private int              _PingTimeout = 90;
          private DateTime         _LastPingSent;
          private DateTime         _LastPongReceived;
          private TimeSpan         _Lag;
  
          
          /// <event cref="OnReadLine">
          /// Raised when a \r\n terminated line is read from the socket
          /// </event>
          public event ReadLineEventHandler   OnReadLine;
          /// <event cref="OnWriteLine">
          /// Raised when a \r\n terminated line is written to the socket
          /// </event>
          public event WriteLineEventHandler  OnWriteLine;
          /// <event cref="OnConnect">
          /// Raised before the connect attempt
          /// </event>
          public event EventHandler           OnConnecting;
          /// <event cref="OnConnect">
          /// Raised on successful connect
          /// </event>
          public event EventHandler           OnConnected;
          /// <event cref="OnConnect">
          /// Raised on successful re-connect
          /// </event>
          public event EventHandler           OnReconnected;
          /// <event cref="OnConnect">
          /// Raised before the connection is closed
          /// </event>
          public event EventHandler           OnDisconnecting;
          /// <event cref="OnConnect">
          /// Raised when the connection is closed
          /// </event>
          public event EventHandler           OnDisconnected;
          /// <event cref="OnConnectionError">
          /// Raised when the connection got into an error state
          /// </event>
          public event EventHandler           OnConnectionError;
          /// <event cref="AutoConnectErrorEventHandler">
          /// Raised when the connection got into an error state during auto connect loop
          /// </event>
          public event AutoConnectErrorEventHandler   OnAutoConnectError;
  
          /// <summary>
          /// Get the transport being used for the connection
          /// </summary>
          public IIrcTransportManager Transport {
              get {
                  return _Transport;
              }
          }
  
          /// <summary>
          /// Gets the current address of the connection
          /// </summary>
          public string Address {
              get {
                  return _AddressList[_CurrentAddress];
              }
          }
  
          /// <summary>
          /// Gets the used port of the connection
          /// </summary>
          public int Port {
              get {
                  return _Port;
              }
          }
  
          /// <summary>
          /// Gets the address list of the connection
          /// </summary>
          public string[] AddressList {
              get {
                  return _AddressList;
              }
          }
  
          /// <summary>
          /// By default nothing is done when the library looses the connection
          /// to the server.
          /// Default: false
          /// </summary>
          /// <value>
          /// true, if the library should reconnect on lost connections
          /// false, if the library should not take care of it
          /// </value>
          public bool AutoReconnect {
              get {
                  return _AutoReconnect;
              }
              set {
  #if LOG4NET
                  if (value) {
                      Logger.Connection.Info("AutoReconnect enabled");
                  } else {
                      Logger.Connection.Info("AutoReconnect disabled");
                  }
  #endif
                  _AutoReconnect = value;
              }
          }
  
          /// <summary>
          /// If the library should retry to connect when the connection fails.
          /// Default: false
          /// </summary>
          /// <value>
          /// true, if the library should retry to connect
          /// false, if the library should not retry
          /// </value>
          public bool AutoRetry {
              get {
                  return _AutoRetry;
              }
              set {
  #if LOG4NET
                  if (value) {
                      Logger.Connection.Info("AutoRetry enabled");
                  } else {
                      Logger.Connection.Info("AutoRetry disabled");
                  }
  #endif
                  _AutoRetry = value;
              }
          }
  
          /// <summary>
          /// Delay between retry attempts in Connect() in seconds.
          /// Default: 30
          /// </summary>
          public int AutoRetryDelay {
              get {
                  return _AutoRetryDelay;
              }
              set {
                  _AutoRetryDelay = value;
              }
          }
  
          /// <summary>
          /// Maximum number of retries to connect to the server
          /// Default: 3
          /// </summary>
          public int AutoRetryLimit {
              get {
                  return _AutoRetryLimit;
              }
              set {
                  _AutoRetryLimit = value;
              }
          }
  
          /// <summary>
          /// Returns the current amount of reconnect attempts
          /// Default: 3
          /// </summary>
          public int AutoRetryAttempt {
              get {
                  return _AutoRetryAttempt;
              }
          }
  
          /// <summary>
          /// To prevent flooding the IRC server, it's required to delay each
          /// message, given in milliseconds.
          /// Default: 200
          /// </summary>
          public int SendDelay {
              get {
                  return _SendDelay;
              }
              set {
                  _SendDelay = value;
              }
          }
  
          /// <summary>
          /// When a connection error is detected this property will return true
          /// </summary>
          public bool IsConnectionError {
              get {
                  if (_Transport == null)
                      return false;
  
                  return _Transport.IsConnectionError;
              }
          }
  
          /// <summary>
          /// On successful connect to the IRC server, this is set to true.
          /// </summary>
          public bool IsConnected {
              get {
                  if (_Transport == null)
                      return false;
  
                  return _Transport.IsConnected;
              }
          }
  
          /// <summary>
          /// Returns true if the client is currently disconnecting
          /// </summary>
          public bool IsDisconnecting {
              get {
                  lock (this) {
                      return _IsDisconnecting;
                  }
              }
              set {
                  lock (this) {
                      _IsDisconnecting = value;
                  }
              }
          }
  
          /// <summary>
          /// On successful registration on the IRC network, this is set to true.
          /// </summary>
          public bool IsRegistered {
              get {
                  return _IsRegistered;
              }
          }
  
          /// <summary>
          /// Gets the SmartIrc4net version number
          /// </summary>
          public string VersionNumber {
              get {
                  return _VersionNumber;
              }
          }
  
          /// <summary>
          /// Gets the full SmartIrc4net version string
          /// </summary>
          public string VersionString {
              get {
                  return _VersionString;
              }
          }
  
          /// <summary>
          /// Interval in seconds to run the idle worker
          /// Default: 60
          /// </summary>
          public int IdleWorkerInterval {
              get {
                  return _IdleWorkerInterval;
              }
              set {
                  _IdleWorkerInterval = value;
              }
          }
  
          /// <summary>
          /// Interval in seconds to send a PING
          /// Default: 60
          /// </summary>
          public int PingInterval {
              get {
                  return _PingInterval;
              }
              set {
                  _PingInterval = value;
              }
          }
          
          /// <summary>
          /// Timeout in seconds for server response to a PING
          /// Default: 600
          /// </summary>
          public int PingTimeout {
              get {
                  return _PingTimeout;
              }
              set {
                  _PingTimeout = value;
              }
          }
  
          /// <summary>
          /// Latency between client and the server
          /// </summary>
          public TimeSpan Lag {
              get {
                  if (_LastPingSent > _LastPongReceived) {
                      // there is an outstanding ping, thus we don't have a current lag value
                      return DateTime.Now - _LastPingSent;
                  }
                  
                  return _Lag;
              }
          }
  
          /// <summary>
          /// Pause or unpause the write queue
          /// </summary>
          public bool WriteQueuePaused {
              get {
                  return _WriteManager.Paused;
              }
              set {
                  _WriteManager.Paused = value;
              }
          }
  
          /// <summary>
          /// Get the write queue length
          /// </summary>
          public int WriteQueueLength {
              get {
                  return _WriteManager.Length;
              }
          }
  
          /// <summary>
          /// The maximum number of messages that can be sent in a single burst
          /// Set to zero to disable bursting
          /// NOTE: Bursting does not apply to Priority.Critical messages
          /// </summary>
          public int BurstSize { get; set; } = 0;
  
          /// <summary>
          /// The smallest timeframe in which BurstSize messages can be sent (in milliseconds)
          /// If the burst limit is exceeded in the timeframe, sending will be rate limited by SendDelay
          /// until more bursting can be done
          /// If bursting is enabled, the client will NEVER send more than BurstSize messages in BurstInterval ms
          /// regardless of whether a SendDelay between each message would allow it
          /// If BurstSize messages in BurstInterval ms is reached, a SendDelay sleep will occur before checking again
          /// Set to zero to send messages as fast as possible
          /// </summary>
          public int BurstInterval { get; set; } = 0;
  
          /// <summary>
          /// The number of burst messages in a single send after which BurstWait must elapse before attempting to burst again
          /// </summary>
          public int BurstWaitThreshold { get; set; } = 0;
  
          /// <summary>
          /// The minimum amount of time to wait after at least BurstWaitThreshold messages have been burst
          /// before attempting to burst again (in milliseconds)
          /// </summary>
          /// <example>
          /// Standard IRC server: BurstSize = 3; BurstInterval = 0; BurstWaitThreshold = 2; BurstWait = 10000
          /// Up to 3 messages can be burst as fast as possible
          /// If at least 2 messages were burst, wait 10 seconds before bursting again.
          /// </example>
          /// <example>
          /// Twitch IRC server: BurstSize = 20; BurstInterval = 30000; BurstWaitThreshold = 0; BurstWait = 0
          /// Up to 20 messages can be burst within 30 seconds.
          /// Don't wait to re-burst
          /// (this effectively rate limits the connection to a max of 20 messages per 30 seconds)
          /// </example>
          public int BurstWait { get; set; } = 0;
  
          /// <summary>
          /// Returns true if the connection is established and working properly, no errors have been reported and we are not disconnecting
          /// Internal use only.
          /// </summary>
          private bool _ConnectionEstablished {
              get {
                  return IsConnected && !IsDisconnecting && !IsConnectionError;
              }
          }
  
          /// <summary>
          /// Initializes the message queues and write thread
          /// </summary>
          public IrcConnection()
          {
  #if LOG4NET
              Logger.Main.Debug("IrcConnection created");
  #endif
              // setup own callbacks
              OnReadLine        += new ReadLineEventHandler(_SimpleParser);
  
              // use default TCP transport for backwards compatibility if none supplied to Connect()
              _Transport = new IrcTcpTransport();
  
              _ReadQueueEvent = new AutoResetEvent(false);
              _WriteManager = new WriteManager(this);
              _IdleWorkerThread = new IdleWorkerThread(this);
  
              _WriteManager.OnWriteLine += new WriteLineEventHandler(_OnWriteLine);
  
              Assembly assm = Assembly.GetAssembly(this.GetType());
              AssemblyName assm_name = assm.GetName(false);
  
              AssemblyProductAttribute pr = (AssemblyProductAttribute)assm.GetCustomAttributes(typeof(AssemblyProductAttribute), false)[0];
  
              _VersionNumber = assm_name.Version.ToString();
              _VersionString = pr.Product+" "+_VersionNumber;
          }
  
  #if LOG4NET
          ~IrcConnection()
          {
              Logger.Main.Debug("IrcConnection destroyed");
          }
  #endif
  
          /// <summary>
          /// Connects to the specified server and port using the current transport, or default TCP if none pre-specified.
          /// When the connection fails the next server in the list will be used.
          /// </summary>
          /// <param name="addresslist">List of servers to connect to</param>
          /// <param name="port">Portnumber to connect to</param>
          /// <exception cref="CouldNotConnectException">The connection failed</exception>
          /// <exception cref="AlreadyConnectedException">If there is already an active connection</exception>
          public virtual void Connect(string[] addresslist, int port = 0)
          {
              Connect(_Transport, addresslist, port);
          }
  
          /// <summary>
          /// Connects to the specified server and port using the current transport, or default TCP if none pre-specified
          /// </summary>
          /// <param name="address">Server address to connect to</param>
          /// <param name="port">Port number to connect to</param>
          /// <exception cref="CouldNotConnectException">The connection failed</exception>
          /// <exception cref="AlreadyConnectedException">If there is already an active connection</exception>
          public virtual void Connect(string address, int port = 0)
          {
              Connect(_Transport, new string[] { address }, port);
          }
  
          /// <summary>
          /// Connects to the specified transport. The transport must be pre-populated with the host address
          /// </summary>
          /// <param name="transport">Transport protocol to use</param>
          /// <exception cref="CouldNotConnectException">The connection failed</exception>
          /// <exception cref="AlreadyConnectedException">If there is already an active connection</exception>
          public virtual void Connect(IIrcTransportManager transport)
          {
              Connect(transport, new string[] { transport.Address }, transport.Port);
          }
  
          /// <overloads>this method has 3 overloads</overloads>
          /// <summary>
          /// Connects to the specified server and port using the specified transport, when the connection fails
          /// the next server in the list will be used.
          /// </summary>
          /// <param name="transport">Transport protocol to use</param>
          /// <param name="addresslist">List of servers to connect to</param>
          /// <param name="port">Portnumber to connect to</param>
          /// <exception cref="CouldNotConnectException">The connection failed</exception>
          /// <exception cref="AlreadyConnectedException">If there is already an active connection</exception>
          public virtual void Connect(IIrcTransportManager transport, string[] addresslist, int port = 0)
          {
              if (IsConnected) {
                  throw new AlreadyConnectedException("Already connected to: " + Address + ":" + Port);
              }
  
              if (_IdleWorkerInterval >= _PingTimeout || _IdleWorkerInterval >= _PingInterval) {
                  throw new CouldNotConnectException("Idle worker polling interval (IdleWorkerInterval) MUST be less than PingTimeout and PingInterval!");
              }
  #if LOG4NET
              Logger.Connection.Info(String.Format("connecting... (attempt: {0})",
                                                   _AutoRetryAttempt));
  #endif
              _Transport = transport;
              _AddressList = (string[])addresslist.Clone();
              _Port = port;
  
              // always start from the beginning of the address list
              _CurrentAddress = 0;
  
              // no retries yet
              _AutoRetryAttempt = 0;
  
              // repeat up to the maximum number of retries
              do {
                  _AutoRetryAttempt++;
  
                  if (OnConnecting != null) {
                      OnConnecting(this, EventArgs.Empty);
                  }
  
                  try {
                      // Connect with transport
                      _Transport.OnMessageReceived += Transport_OnMessageReceived;
                      _Transport.OnConnectionError += Transport_OnConnectionError;
  
                      // Not all transports support changing the address or port once created
                      // Those that don't should throw NotImplementedException
                      try {
                          _Transport.Address = Address;
                          _Transport.Port = Port;
                      } catch (NotImplementedException) {
                      }
  
                      // throws on error
                      _Transport.Connect();
  
                      // lets power up our queues
                      _ReadQueue = new ConcurrentQueue<string>();
                      _WriteManager.Start();
                      _IdleWorkerThread.Start();
  
  #if LOG4NET
                      Logger.Connection.Info("connected");
  #endif
                      if (OnConnected != null) {
                          OnConnected(this, EventArgs.Empty);
                      }
                  }
                  catch (Exception e) {
  #if LOG4NET
                      Logger.Connection.Info("connection failed: " + e.Message, e);
  #endif
                      _Transport.OnMessageReceived -= Transport_OnMessageReceived;
                      _Transport.OnConnectionError -= Transport_OnConnectionError;
  
                      // fatal error
                      if (e is System.Security.Authentication.AuthenticationException)
                          throw;
  
                      if (_AutoRetry &&
                          (_AutoRetryLimit == -1 ||
                           _AutoRetryLimit == 0 ||
                           _AutoRetryAttempt < _AutoRetryLimit)) {
                          if (OnAutoConnectError != null) {
                              OnAutoConnectError(this, new AutoConnectErrorEventArgs(Address, Port, e));
                          }
  #if LOG4NET
                          Logger.Connection.Debug("delaying new connect attempt for " + _AutoRetryDelay + " sec");
  #endif
                          Thread.Sleep(_AutoRetryDelay * 1000);
  
                          _NextAddress();
                      } else if (!_AutoRetry) {
                          throw new CouldNotConnectException(e.Message, e);
                      }
                  }
              } while (!IsConnected && _AutoRetry &&
                      (_AutoRetryLimit == -1 ||
                      _AutoRetryLimit == 0 ||
                      _AutoRetryAttempt < _AutoRetryLimit));
  
              if (!IsConnected && _AutoRetry && _AutoRetryAttempt == _AutoRetryLimit)
                  throw new CouldNotConnectException("Maximum number of connection retries exceeded");
          }
  
          /// <summary>
          /// Reconnects to the server using the current transport
          /// </summary>
          /// <exception cref="NotConnectedException">
          /// If there was no active connection
          /// </exception>
          /// <exception cref="CouldNotConnectException">
          /// The connection failed
          /// </exception>
          /// <exception cref="AlreadyConnectedException">
          /// If there is already an active connection
          /// </exception>
          public void Reconnect()
          {
  #if LOG4NET
              Logger.Connection.Info("reconnecting...");
  #endif
              Disconnect(true);
              Connect(_Transport, _AddressList, _Port);
  
              if (OnReconnected != null)
                  OnReconnected(this, EventArgs.Empty);
          }
          
          /// <summary>
          /// Disconnects from the server
          /// </summary>
          /// <param name="reconnecting">Set to true if you intend to call Connect() again so that Listen() continues blocking</param>
          /// <exception cref="NotConnectedException">
          /// If there was no active connection
          /// </exception>
          public void Disconnect(bool reconnecting = false)
          {
              if (!IsConnected) {
                  throw new NotConnectedException("The connection could not be disconnected because there is no active connection");
              }
  
              // Disconnect() is not re-entrant.
              // If a call is made to Disconnect() while already disconnecting, it returns immediately
              if (IsDisconnecting) {
                  return;
              }
  #if LOG4NET
              Logger.Connection.Info("disconnecting...");
  #endif
              IsDisconnecting = true;
  
              if (OnDisconnecting != null) {
                  OnDisconnecting(this, EventArgs.Empty);
              }
  
              // NOTE: In the event of a connection error, these threads will have stopped themselves cleanly
              // but there is no harm in making sure. If there is no connection error, they will need to be stopped.
              _IdleWorkerThread.Stop();
              _WriteManager.Stop();
  
              // make sure we do this after the threads are stopped otherwise we could write to a non-existent connection
              _Transport.Disconnect();
              _Transport.OnMessageReceived -= Transport_OnMessageReceived;
              _Transport.OnConnectionError -= Transport_OnConnectionError;
  
              _IsRegistered = false;
  
              IsDisconnecting = false;
              
              if (OnDisconnected != null) {
                  OnDisconnected(this, EventArgs.Empty);
              }
  
              // Release Listen() from blocking - this MUST be called after _Transport.Disconnect() to ensure IsConnected == false
              if (!reconnecting) {
                  _ReadQueueEvent.Set();
              }
  
  #if LOG4NET
              Logger.Connection.Info("disconnected");
  #endif
          }
  
          /// <summary>
          /// 
          /// </summary>
          /// <param name="blocking"></param>
          public void Listen(bool blocking)
          {
              if (blocking) {
                  // We don't use _ConnectionEstablished here because we don't want Listen() to return until
                  // all disconnection steps are finished, so instead we raise _ReadQueueEvent once
                  // IsConnected == false
                  while (IsConnected) {
                      ReadLine(true);
                  }
              } else {
                  while (ReadLine(false).Length > 0) {
                      // loop as long as we receive messages
                  }
              }
          }
  
          /// <summary>
          /// 
          /// </summary>
          public void Listen()
          {
              Listen(true);
          }
          
          /// <summary>
          /// 
          /// </summary>
          /// <param name="blocking"></param>
          public void ListenOnce(bool blocking)
          {
              ReadLine(blocking);
          }
  
          /// <summary>
          /// 
          /// </summary>
          public void ListenOnce()
          {
              ListenOnce(true);
          }
  
          /// <summary>
          /// Read one line of text from the read queue; returns null if the connection has been closed or errors,
          /// or an empty string if there is no new data to read (when non-blocking)
          /// </summary>
          /// <param name="blocking"></param>
          /// <returns></returns>
          public string ReadLine(bool blocking)
          {
              string data = "";
  
              if (blocking) {
                  // block till the queue has data, but bail out on connection error
                  // We don't use _ConnectionEstablished here because we don't want Listen() to return until
                  // all disconnection steps are finished, so instead we raise _ReadQueueEvent once
                  // IsConnected == false
                  while (IsConnected &&
                         _ReadQueue.Count == 0) {
                      _ReadQueueEvent.WaitOne();
                  }
              }
  
              if (_ConnectionEstablished &&
                  _ReadQueue.Count > 0) {
                  _ReadQueue.TryDequeue(out data);
              }
  
              if (data != null && data.Length > 0) {
                  if (OnReadLine != null) {
                      OnReadLine(this, new ReadLineEventArgs(data));
                  }
              }
  
              return data;
          }
  
          /// <summary>
          /// Write a line of text to the connection with the specified priority
          /// </summary>
          /// <param name="data"></param>
          /// <param name="priority"></param>
          public void WriteLine(string data, Priority priority)
          {
              _WriteManager.WriteLine(data, priority);
          }
  
          /// <summary>
          /// Write a line of text to the connection with default (medium) priority
          /// </summary>
          /// <param name="data"></param>
          public void WriteLine(string data)
          {
              WriteLine(data, Priority.Medium);
          }
  
          /// <summary>
          /// Received from the write thread when a line is successfully written to the connection
          /// </summary>
          /// <param name="sender"></param>
          /// <param name="e"></param>
          private void _OnWriteLine(object sender, WriteLineEventArgs e)
          {
              if (OnWriteLine != null) {
                  OnWriteLine(sender, e);
              }
          }
  
          /// <summary>
          /// Received from the transport when a line is successfully received from the connection
          /// </summary>
          /// <param name="sender"></param>
          /// <param name="e"></param>
          private void Transport_OnMessageReceived(object sender, ReadLineEventArgs e)
          {
              // Ignore incoming messages if we are disconnecting or there is a connection error
              // because we don't want to raise _ReadQueueEvent and cause an exit from Listen()
              // if AutoReconnect is enabled and we are in the process of re-connecting
              if (_ConnectionEstablished) {
  
                  // Add it to the read queue
                  _ReadQueue.Enqueue(e.Line);
                  _ReadQueueEvent.Set();
              }
          }
  
          /// <summary>
          /// Received from the transport when a connection error occurs
          /// </summary>
          private void Transport_OnConnectionError()
          {
              // We will now always funnel all connection errors into this method so fire the event here
              // Apart from centralizing it, this also means we can raise OnConnectionError when an error
              // occurs in this class (eg. ping timeout) that will not raise _Transport.IsConnectionError.
              if (OnConnectionError != null) {
                  OnConnectionError(this, EventArgs.Empty);
              }
  
              try {
                  if (AutoReconnect) {
                      // prevent connect -> exception -> connect flood loop
                      // TODO: But not on first disconnect
                      // TODO: We really need to clean up all the threads and stuff before sleeping
                      Thread.Sleep(AutoRetryDelay * 1000);
                      // lets try to recover the connection
                      Reconnect();
                  } else {
                      // make sure we clean up
                      Disconnect();
                  }
              } catch (ConnectionException) {
              }
          }
  
          private void _NextAddress()
          {
              _CurrentAddress++;
              if (_CurrentAddress >= _AddressList.Length) {
                  _CurrentAddress = 0;
              }
  #if LOG4NET
              Logger.Connection.Info("set server to: "+Address);
  #endif
          }
  
          private void _SimpleParser(object sender, ReadLineEventArgs args)
          {
              string   rawline = args.Line;
              string[] rawlineex = rawline.Split(new char[] {' '});
              string   line = null;
              string   prefix = null;
              string   command = null;
  
              if (rawline[0] == ':') {
                  prefix = rawlineex[0].Substring(1);
                  line = rawline.Substring(prefix.Length + 2);
              } else {
                  line = rawline;
              }
              string[] lineex = line.Split(new char[] {' '});
  
              command = lineex[0];
              ReplyCode replycode = ReplyCode.Null;
              int intReplycode;
              if (Int32.TryParse(command, out intReplycode)) {
                  replycode = (ReplyCode) intReplycode;
              }
              if (replycode != ReplyCode.Null) {
                  switch (replycode) {
                      case ReplyCode.Welcome:
                          _IsRegistered = true;
  #if LOG4NET
                          Logger.Connection.Info("logged in");
  #endif
                          break;
                  }
              } else {
                  switch (command) {
                      case "ERROR":
                          // FIXME: handle server errors differently than connection errors!
                          //IsConnectionError = true;
                          break;
                      case "PONG":
                          DateTime now = DateTime.Now;
                          _LastPongReceived = now;
                          _Lag = now - _LastPingSent;
  
  #if LOG4NET
                          Logger.Connection.Debug("PONG received, took: " + _Lag.TotalMilliseconds + " ms");
  #endif
                          break;
                  }
              }
          }
  
          /// <summary>
          /// A write thread and helper functions to write data to the connection
          /// </summary>
          private class WriteManager
          {
              private IrcConnection  _Connection;
              private Thread         _Thread;
              private Hashtable _SendBuffer = Hashtable.Synchronized(new Hashtable());
              private int            _HighCount;
              private int            _AboveMediumCount;
              private int            _MediumCount;
              private int            _BelowMediumCount;
              private int            _LowCount;
              private int            _AboveMediumSentCount;
              private int            _MediumSentCount;
              private int            _BelowMediumSentCount;
              private int            _AboveMediumThresholdCount = 4;
              private int            _MediumThresholdCount      = 2;
              private int            _BelowMediumThresholdCount = 1;
  
              private Queue<DateTime> _BurstTimes = new Queue<DateTime>();
              private DateTime _BurstNextAllowed;
  
              private AutoResetEvent _QueuedEvent;
              private ManualResetEvent _UnpausedEvent = new ManualResetEvent(true);
  
              /// <summary>
              /// Fires when a line has been successfully sent to the network stream
              /// </summary>
              public event WriteLineEventHandler OnWriteLine;
  
              /// <summary>
              /// Pause or unpause the write queue
              /// </summary>
              public bool Paused {
                  get {
                      return !_UnpausedEvent.WaitOne(0);
                  }
                  set {
                      if (value)
                          _UnpausedEvent.Reset();
                      else
                          _UnpausedEvent.Set();
                  }
              }
  
              /// <summary>
              /// Get the queue length
              /// </summary>
              public int Length {
                  get {
                      return ((Queue) _SendBuffer[Priority.High]).Count
                             + ((Queue) _SendBuffer[Priority.AboveMedium]).Count
                             + ((Queue) _SendBuffer[Priority.Medium]).Count
                             + ((Queue) _SendBuffer[Priority.BelowMedium]).Count
                             + ((Queue) _SendBuffer[Priority.Low]).Count;
                  }
              }
  
              /// <summary>
              /// Set up using the specified connection
              /// </summary>
              /// <param name="connection"></param>
              public WriteManager(IrcConnection connection)
              {
                  _Connection = connection;
                  _QueuedEvent = new AutoResetEvent(false);
  
                  _SendBuffer[Priority.High] = Queue.Synchronized(new Queue());
                  _SendBuffer[Priority.AboveMedium] = Queue.Synchronized(new Queue());
                  _SendBuffer[Priority.Medium] = Queue.Synchronized(new Queue());
                  _SendBuffer[Priority.BelowMedium] = Queue.Synchronized(new Queue());
                  _SendBuffer[Priority.Low] = Queue.Synchronized(new Queue());
              }
  
              /// <summary>
              /// 
              /// </summary>
              public void Start()
              {
                  _QueuedEvent.Reset();
                  _UnpausedEvent.Set();
                  _BurstTimes.Clear();
                  _BurstNextAllowed = DateTime.Now;
  
                  ((Queue)_SendBuffer[Priority.High]).Clear();
                  ((Queue)_SendBuffer[Priority.AboveMedium]).Clear();
                  ((Queue)_SendBuffer[Priority.Medium]).Clear();
                  ((Queue)_SendBuffer[Priority.BelowMedium]).Clear();
                  ((Queue)_SendBuffer[Priority.Low]).Clear();
  
                  _Thread = new Thread(new ThreadStart(_Worker));
                  _Thread.Name = "WriteThread (" + _Connection.Address + ") [" + DateTime.Now + "]";
                  _Thread.IsBackground = true;
                  _Thread.Start();
              }
  
              /// <summary>
              /// 
              /// </summary>
              public void Stop()
              {
  #if LOG4NET
                  Logger.Connection.Debug("Stopping WriteThread...");
  #endif
                  _QueuedEvent.Set();
  
                  // We have to check _Thread here in case the connection is disconnected before the thread starts
                  if (_Thread != null) {
                      _Thread.Join();
                  }
              }
  
              /// <summary>
              /// Write a line to the stream if priority is Critical, otherwise queue to one of the write buffers
              /// </summary>
              /// <param name="data">String to send</param>
              /// <param name="priority">Priority level</param>
              public bool WriteLine(string data, Priority priority)
              {
                  if (priority == Priority.Critical) {
                      // We don't use _ConnectionEstablished here because we want it to fall through without exceptions
                      // if there's a connection error
                      if (!_Connection.IsConnected) {
                          return true; // the data was successfully sent... into a void (so that WriteManager doesn't re-queue the messages); clients should check IsConnected
                      }
  
                      if (_Connection.Transport.WriteLine(data)) {
                          if (OnWriteLine != null)
                              OnWriteLine(this, new WriteLineEventArgs(data));
  
                          return true;
                      } else {
                          // Transport.ConnectionError will have been set automatically if we reach this point: CONFIRMED by Unit test 'ClientDirtyDisconnect'
                          return false;
                      }
                  } else {
                      ((Queue) _SendBuffer[priority]).Enqueue(data);
                      _QueuedEvent.Set();
                      return true;
                  }
              }
  
              private bool _WriteLineCritical(string data)
              {
  
gt:                 return WriteLine(data, Priority.Critical);