<?php
/**
 * Net_SmartIRC
 * This is a PHP class for communication with IRC networks,
 * which conforms to the RFC 2812 (IRC protocol).
 * It's an API that handles all IRC protocol messages.
 * This class is designed for creating IRC bots, chats and showing IRC related
 * info on webpages.
 *
 * Documentation, a HOWTO, and examples are included in SmartIRC.
 *
 * Here you will find a service bot which I am also developing
 * <http://cvs.meebey.net/atbs> and <http://cvs.meebey.net/phpbitch>
 * Latest versions of Net_SmartIRC you will find on the project homepage
 * or get it through PEAR since SmartIRC is an official PEAR package.
 *
 * Official Project Homepage: <http://pear.php.net/package/Net_SmartIRC/>
 *
 * Net_SmartIRC conforms to RFC 2812 (Internet Relay Chat: Client Protocol)
 *
 * Copyright (c) 2002-2005 Mirco Bauer <meebey@meebey.net> <http://www.meebey.net>
 *
 * PHP version 5
 *
 * 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
 *
 */
// ------- PHP code ----------
require_once 'Net/SmartIRC/defines.php';
require_once 'Net/SmartIRC/irccommands.php';
require_once 'Net/SmartIRC/messagehandler.php';
define('SMARTIRC_VERSION', '1.1.14');
define('SMARTIRC_VERSIONSTRING', 'Net_SmartIRC '.SMARTIRC_VERSION);

/**
 * main SmartIRC class
 *
 * @category Net
 * @package Net_SmartIRC
 * @version 1.1.14
 * @author clockwerx
 * @author Mirco 'meebey' Bauer <meebey@meebey.net>
 * @author garrettw
 * @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
 * @link http://pear.php.net/package/Net_SmartIRC
 */
class Net_SmartIRC extends Net_SmartIRC_messagehandler
{
    /**
     * @var integer
     */
    const DEF_AUTORETRY_MAX = 5;

    /**
     * @var integer
     */
    const DEF_DISCONNECT_TIME = 1000;

    /**
     * @var integer
     */
    const DEF_LOGFILE = 'Net_SmartIRC.log';

    /**
     * @var integer
     */
    const DEF_MAX_TIMER = 300000;

    /**
     * @var integer
     */
    const DEF_RECEIVE_DELAY = 100;

    /**
     * @var integer
     */
    const DEF_RECONNECT_DELAY = 10000;

    /**
     * @var integer
     */
    const DEF_SEND_DELAY = 250;

    /**
     * @var integer
     */
    const DEF_TX_RX_TIMEOUT = 300;

    /**
     * @var string
     */
    const IP_PATTERN = '/(?:(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:\[[0-9A-Fa-f:]+\])|(?:[a-zA-Z0-9-_.]+)):[0-9]{1,5}/';

    /**
     * @var resource
     */
    protected $_socket;

    /**
     * @var string
     */
    protected $_address;

    /**
     * @var integer
     */
    protected $_port;

    /**
     * @var string
     */
    protected $_bindto = '0:0';

    /**
     * @var string
     */
    protected $_nick;

    /**
     * @var string
     */
    protected $_username;

    /**
     * @var string
     */
    protected $_realname;

    /**
     * @var string
     */
    protected $_usermode;

    /**
     * @var string
     */
    protected $_password;

    /**
     * @var array
     */
    protected $_performs = array();

    /**
     * @var boolean
     */
    protected $_state = SMARTIRC_STATE_DISCONNECTED;

    /**
     * @var array
     */
    protected $_actionhandler = array();

    /**
     * @var array
     */
    protected $_timehandler = array();

    /**
     * @var integer
     */
    protected $_debuglevel = SMARTIRC_DEBUG_NOTICE;

    /**
     * @var array
     */
    protected $_messagebuffer = array(
		SMARTIRC_HIGH     => array(),
		SMARTIRC_MEDIUM   => array(),
		SMARTIRC_LOW 	  => array(),
	);

    /**
     * @var integer
     */
    protected $_messagebuffersize;

    /**
     * @var integer
     */
    protected $_receivedelay = self::DEF_RECEIVE_DELAY;

    /**
     * @var integer
     */
    protected $_senddelay = self::DEF_SEND_DELAY;

    /**
     * @var integer
     */
    protected $_logdestination = SMARTIRC_STDOUT;

    /**
     * @var resource
     */
    protected $_logfilefp = 0;

    /**
     * @var string
     */
    protected $_logfile = self::DEF_LOGFILE;

    /**
     * @var integer
     */
    protected $_disconnecttime = self::DEF_DISCONNECT_TIME;

    /**
     * @var boolean
     */
    protected $_loggedin = false;

    /**
     * @var boolean
     */
    protected $_benchmark = false;

    /**
     * @var integer
     */
    protected $_benchmark_starttime;

    /**
     * @var integer
     */
    protected $_benchmark_stoptime;

    /**
     * @var integer
     */
    protected $_actionhandlerid = 0;

    /**
     * @var integer
     */
    protected $_timehandlerid = 0;

    /**
     * @var array
     */
    protected $_motd = array();

    /**
     * Stores all channels in this array where we are joined, works only if channelsyncing is activated.
     * Eg. for accessing a user, use it like this: (in this example the SmartIRC object is stored in $irc)
     * $irc->getUser('#test', 'meebey')->nick;
     *
     * @see setChannelSyncing()
     * @see Net_SmartIRC_channel
     * @see Net_SmartIRC_channeluser
     * @var array
     */
    protected $_channels = array();

    /**
     * @var boolean
     */
    protected $_channelsyncing = false;

    /**
     * Stores all users that had/have contact with us (channel/query/notice etc.), works only if usersyncing is activated.
     * Eg. for accessing a user, use it like this: (in this example the SmartIRC object is stored in $irc)
     * $irc->user['meebey']->host;
     *
     * @see setUserSyncing()
     * @see Net_SmartIRC_ircuser
     * @var array
     */
    protected $_users = array();

    /**
     * @var boolean
     */
    protected $_usersyncing = false;

    /**
     * Stores the path to the modules that can be loaded.
     *
     * @var string
     */
    protected $_modulepath = '.';

    /**
     * Stores all objects of the modules.
     *
     * @var string
     */
    protected $_modules = array();

    /**
     * @var string
     */
    protected $_ctcpversion = SMARTIRC_VERSIONSTRING;

    /**
     * @var mixed
     */
    protected $_mintimer = false;

    /**
     * @var integer
     */
    protected $_maxtimer = self::DEF_MAX_TIMER;

    /**
     * @var integer
     */
    protected $_txtimeout = self::DEF_TX_RX_TIMEOUT;

    /**
     * @var integer
     */
    protected $_rxtimeout = self::DEF_TX_RX_TIMEOUT;

    /**
     * @var integer
     */
    protected $_lastrx;

    /**
     * @var integer
     */
    protected $_lasttx;

    /**
     *
     */
    protected $_lastsentmsgtime = 0;

    /**
     * @var integer
     */
    protected $_reconnectdelay = self::DEF_RECONNECT_DELAY;

    /**
     * @var boolean
     */
    protected $_autoretry = false;

    /**
     * @var integer
     */
    protected $_autoretrymax = self::DEF_AUTORETRY_MAX;

    /**
     * @var integer
     */
    protected $_autoretrycount = 0;

    /**
     * @var boolean
     */
    protected $_connectionerror = false;

    /**
     * @var boolean
     */
    protected $_runasdaemon = false;

    /**
     * @var boolean
     */
    protected $_interrupt = false;


    /**
     * All numeric IRC replycodes, the index is the numeric replycode.
     *
     * @see $SMARTIRC_nreplycodes
     * @var array
     */
    public $nreplycodes;

    /**
     * Constructor. Initiates the messagebuffer and "links" the replycodes from
     * global into properties. Also some PHP runtime settings are configured.
     *
     * @api
     * @param array $params Properties to set during instantiation
     * @return object
     */
    public function __construct($params = array())
    {
        // can't stop using the global without potentially breaking BC
        $this->nreplycodes = &$GLOBALS['SMARTIRC_nreplycodes'];

        if (isset($_SERVER['REQUEST_METHOD'])) {
            // the script is called from a browser, lets set default log destination
            // to SMARTIRC_BROWSEROUT (makes browser friendly output)
            $this->setLogDestination(SMARTIRC_BROWSEROUT);
        }

        // you'll want to pass an array that includes keys like:
        // ModulePath, Debug, ChannelSyncing, AutoRetry, RunAsDaemon
        // so we can call their setters here
        foreach ($params as $varname => $val) {
            $funcname = 'set' . $varname;
            $this->$funcname($val);
        }
    }

    /**
     * Keeps BC since private properties were once publicly accessible.
     *
     * @param string $name The property name asked for
     * @return mixed the property's value
     */
    public function __get($name)
    {
        // PHP allows $this->getChannel($param)->memberofobject,
        // but we need to not break BC.
        if ($name == 'channel' || $name == 'user'):
            $name = '_' . $name . 's';
        endif;
        return $this->$name;
    }

    /**
     * Handle calls to renamed or deprecated functions
     *
     * @param string $method
     * @param array $args
     * @return mixed|void
     */
    public function __call($method, $args)
    {
        $map = array(
            'setChannelSynching'      => 'setChannelSyncing',
            'setDebug'                => 'setDebugLevel',
            'channel'                 => 'getChannel',
            '_nicknameinuse'          => '_event_err_nicknameinuse',
            'setAutoReconnect'        => '',
            'setUseSockets'           => '',
        );

        if (array_key_exists($method, $map)) {
            if (empty($map[$method])) {
                $this->log(SMARTIRC_DEBUG_NOTICE,
                    "WARNING: you are using $method() which is deprecated "
                    ."and no longer functional.",
                    __FILE__, __LINE__);
            } else {
                $this->log(SMARTIRC_DEBUG_NOTICE,
                    "WARNING: you are using $method() which is a deprecated "
                    ."method, using {$map[$method]}() instead!", __FILE__, __LINE__);
                return call_user_func_array(array($this, $map[$method]), $args);
            }
        } else {
            $this->log(SMARTIRC_DEBUG_NOTICE,
                "WARNING: $method() does not exist!", __FILE__, __LINE__);
        }
    }

    /**
     * Enables/disables autoretry for connecting to a server.
     *
     * @api
     * @param boolean $boolean
     * @return boolean
     */
    public function setAutoRetry($boolean)
    {
        return ($this->_autoretry = (bool) $boolean);
    }

    /**
     * Sets the maximum number of attempts to connect to a server
     * before giving up.
     *
     * @api
     * @param integer|null $autoretrymax
     * @return integer
     */
    public function setAutoRetryMax($autoretrymax = null)
    {
        if (is_integer($autoretrymax)) {
            if ($autoretrymax == 0) {
                $this->setAutoRetry(false);
            } else {
                $this->_autoretrymax = $autoretrymax;
            }
        } else {
            $this->_autoretrymax = self::DEF_AUTORETRY_MAX;
        }
        return $this->_autoretrymax;
    }

    /**
     * Enables/disables the benchmark engine.
     *
     * @api
     * @param boolean $boolean
     * @return boolean
     */
    public function setBenchmark($boolean)
    {
        return ($this->_benchmark = (bool) $boolean);
    }

    /**
     * Sets an IP address (and optionally, a port) to bind the socket to.
     *
     * Limits the bot to claiming only one of the machine's IPs as its home.
     * Call with no parameters to unbind.
     *
     * @api
     * @param string $addr
     * @param int $port
     * @return string The bound address with port
     */
    public function setBindAddress($addr = '0', $port = 0)
    {
        if (preg_match(self::IP_PATTERN, $addr) === 0) {
            $addr .= ':' . $port;
        }
        return ($this->_bindto = $addr);
    }

    /**
     * Enables/disables channel syncing.
     *
     * Channel syncing means, all users on all channel we are joined are tracked in the
     * channel array. This makes it very handy for botcoding.
     *
     * @api
     * @param boolean $boolean
     * @return boolean
     */
    public function setChannelSyncing($boolean)
    {
        if ($boolean) {
            $this->_channelsyncing = true;
            $this->log(SMARTIRC_DEBUG_CHANNELSYNCING,
                'DEBUG_CHANNELSYNCING: Channel syncing enabled',
                __FILE__, __LINE__
            );
        } else {
            $this->_channelsyncing = false;
            $this->log(SMARTIRC_DEBUG_CHANNELSYNCING,
                'DEBUG_CHANNELSYNCING: Channel syncing disabled',
                __FILE__, __LINE__
            );
        }
        return $this->_channelsyncing;
    }

    /**
     * Sets the CTCP version reply string.
     *
     * @api
     * @param string $versionstring
     * @return string
     */
    public function setCtcpVersion($versionstring)
    {
        return ($this->_ctcpversion = $versionstring);
    }

    /**
     * Sets the level of debug messages.
     *
     * Sets the debug level (bitwise), useful for testing/developing your code.
     * Here the list of all possible debug levels:
     * SMARTIRC_DEBUG_NONE
     * SMARTIRC_DEBUG_NOTICE
     * SMARTIRC_DEBUG_CONNECTION
     * SMARTIRC_DEBUG_SOCKET
     * SMARTIRC_DEBUG_IRCMESSAGES
     * SMARTIRC_DEBUG_MESSAGETYPES
     * SMARTIRC_DEBUG_ACTIONHANDLER
     * SMARTIRC_DEBUG_TIMEHANDLER
     * SMARTIRC_DEBUG_MESSAGEHANDLER
     * SMARTIRC_DEBUG_CHANNELSYNCING
     * SMARTIRC_DEBUG_MODULES
     * SMARTIRC_DEBUG_USERSYNCING
     * SMARTIRC_DEBUG_ALL
     *
     * Default: SMARTIRC_DEBUG_NOTICE
     *
     * @api
     * @see DOCUMENTATION
     * @see SMARTIRC_DEBUG_NOTICE
     * @param integer $level
     * @return integer
     */
    public function setDebugLevel($level)
    {
        return ($this->_debuglevel = $level);
    }

    /**
     * Sets the delaytime before closing the socket when disconnect.
     *
     * @api
     * @param integer|null $milliseconds
     * @return integer
     */
    public function setDisconnectTime($milliseconds = null)
    {
        if (is_integer($milliseconds)
             && $milliseconds >= self::DEF_DISCONNECT_TIME
        ) {
            $this->_disconnecttime = $milliseconds;
        } else {
            $this->_disconnecttime = self::DEF_DISCONNECT_TIME;
        }
        return $this->_disconnecttime;
    }

    /**
     * Sets the destination of all log messages.
     *
     * Sets the destination of log messages.
     * $type can be:
     * SMARTIRC_FILE for saving the log into a file
     * SMARTIRC_STDOUT for echoing the log to stdout
     * SMARTIRC_SYSLOG for sending the log to the syslog
     * Default: SMARTIRC_STDOUT
     *
     * @api
     * @see SMARTIRC_STDOUT
     * @param integer $type must be on of the constants
     * @return integer
     */
    public function setLogDestination($type)
    {
        switch ($type) {
            case SMARTIRC_FILE:
            case SMARTIRC_STDOUT:
            case SMARTIRC_SYSLOG:
            case SMARTIRC_BROWSEROUT:
            case SMARTIRC_NONE:
                $this->_logdestination = $type;
                break;

            default:
                $this->log(SMARTIRC_DEBUG_NOTICE,
                    'WARNING: unknown logdestination type ('.$type
                    .'), will use STDOUT instead', __FILE__, __LINE__);
                $this->_logdestination = SMARTIRC_STDOUT;
        }
        return $this->_logdestination;
    }

    /**
     * Sets the file for the log if the destination is set to file.
     *
     * Sets the logfile, if {@link setLogDestination logdestination} is set to SMARTIRC_FILE.
     * This should be only used with full path!
     *
     * @api
     * @param string $file
     * @return string
     */
    public function setLogFile($file)
    {
        return ($this->_logfile = $file);
    }

    /**
     * Sets the paths for the modules.
     *
     * @api
     * @param integer $path
     * @return string
     */
    public function setModulePath($path)
    {
        return ($this->_modulepath = $path);
    }

    /**
     * Sets the delay for receiving data from the IRC server.
     *
     * Sets the delaytime between messages that are received, this reduces your CPU load.
     * Don't set this too low (min 100ms).
     * Default: 100
     *
     * @api
     * @param integer|null $milliseconds
     * @return integer
     */
    public function setReceiveDelay($milliseconds = null)
    {
        if (is_integer($milliseconds)
            && $milliseconds >= self::DEF_RECEIVE_DELAY
        ) {
            $this->_receivedelay = $milliseconds;
        } else {
            $this->_receivedelay = self::DEF_RECEIVE_DELAY;
        }
        return $this->_receivedelay;
    }

    /**
     * Sets the delaytime before attempting reconnect.
     * Value of 0 disables the delay entirely.
     *
     * @api
     * @param integer|null $milliseconds
     * @return integer
     */
    public function setReconnectDelay($milliseconds = null)
    {
        if (is_integer($milliseconds)) {
            $this->_reconnectdelay = $milliseconds;
        } else {
            $this->_reconnectdelay = self::DEF_RECONNECT_DELAY;
        }
        return $this->_reconnectdelay;
    }

    /**
     * Sets whether the script should be run as a daemon or not
     * ( actually disables/enables ignore_user_abort() )
     *
     * @api
     * @param boolean $boolean
     * @return boolean
     */
    public function setRunAsDaemon($boolean)
    {
        $this->_runasdaemon = (bool) $boolean;
        ignore_user_abort($this->_runasdaemon);
        return $this->_runasdaemon;
    }

    /**
     * Sets the delay for sending data to the IRC server.
     *
     * Sets the delay time between sending messages, to avoid flooding
     * IRC servers. If your bot has special flooding permissions on the
     * network you're connected to, you can set this quite low to send
     * messages faster.
     * Default: 250
     *
     * @api
     * @param integer|null $milliseconds
     * @return integer
     */
    public function setSendDelay($milliseconds = null)
    {
        if (is_integer($milliseconds)) {
            $this->_senddelay = $milliseconds;
        } else {
            $this->_senddelay = self::DEF_SEND_DELAY;
        }
        return $this->_senddelay;
    }

    /**
     * Sets the receive timeout.
     *
     * If the timeout occurs, the connection will be reinitialized
     * Default: 300 seconds
     *
     * @api
     * @param integer|null $seconds
     * @return integer
     */
    public function setReceiveTimeout($seconds = null)
    {
        if (is_integer($seconds)) {
            $this->_rxtimeout = $seconds;
        } else {
            $this->_rxtimeout = self::DEF_TX_RX_TIMEOUT;
        }
        return $this->_rxtimeout;
    }

    /**
     * Sets the transmit timeout.
     *
     * If the timeout occurs, the connection will be reinitialized
     * Default: 300 seconds
     *
     * @api
     * @param integer|null $seconds
     * @return integer
     */
    public function setTransmitTimeout($seconds = null)
    {
        if (is_integer($seconds)) {
            $this->_txtimeout = $seconds;
        } else {
            $this->_txtimeout = self::DEF_TX_RX_TIMEOUT;
        }
        return $this->_txtimeout;
    }

    /**
     * Enables/disables user syncing.
     *
     * User syncing means, all users we have or had contact with through channel, query or
     * notice are tracked in the $irc->user array. This is very handy for botcoding.
     *
     * @api
     * @param boolean $boolean
     * @return boolean
     */
    public function setUserSyncing($boolean)
    {
        if ($boolean) {
            $this->_usersyncing = true;
			$this->log(SMARTIRC_DEBUG_USERSYNCING,
                'DEBUG_USERSYNCING: User syncing enabled', __FILE__, __LINE__);
        } else {
            $this->_usersyncing = false;
			$this->log(SMARTIRC_DEBUG_USERSYNCING,
                'DEBUG_USERSYNCING: User syncing disabled', __FILE__, __LINE__);
        }
        return $this->_usersyncing;
    }

    /**
     * Starts the benchmark (sets the counters).
     *
     * @api
     * @return Net_SmartIRC
     */
    public function startBenchmark()
    {
        $this->_benchmark_starttime = microtime(true);
        $this->log(SMARTIRC_DEBUG_NOTICE, 'benchmark started', __FILE__, __LINE__);
        return $this;
    }

    /**
     * Stops the benchmark and displays the result.
     *
     * @api
     * @return Net_SmartIRC
     */
    public function stopBenchmark()
    {
        $this->_benchmark_stoptime = microtime(true);
        $this->log(SMARTIRC_DEBUG_NOTICE, 'benchmark stopped', __FILE__, __LINE__);

        if ($this->_benchmark) {
            $this->showBenchmark();
        }
        return $this;
    }

    /**
     * Shows the benchmark result.
     *
     * @api
     * @return Net_SmartIRC
     */
    public function showBenchmark()
    {
        $this->log(SMARTIRC_DEBUG_NOTICE, 'benchmark time: '
            .((float)$this->_benchmark_stoptime-(float)$this->_benchmark_starttime),
            __FILE__, __LINE__
        );
        return $this;
    }

    /**
     * Adds an entry to the log.
     *
     * Adds an entry to the log with Linux style log format.
     * Possible $level constants (can also be combined with "|"s)
     * SMARTIRC_DEBUG_NONE
     * SMARTIRC_DEBUG_NOTICE
     * SMARTIRC_DEBUG_CONNECTION
     * SMARTIRC_DEBUG_SOCKET
     * SMARTIRC_DEBUG_IRCMESSAGES
     * SMARTIRC_DEBUG_MESSAGETYPES
     * SMARTIRC_DEBUG_ACTIONHANDLER
     * SMARTIRC_DEBUG_TIMEHANDLER
     * SMARTIRC_DEBUG_MESSAGEHANDLER
     * SMARTIRC_DEBUG_CHANNELSYNCING
     * SMARTIRC_DEBUG_MODULES
     * SMARTIRC_DEBUG_USERSYNCING
     * SMARTIRC_DEBUG_ALL
     *
     * @api
     * @see SMARTIRC_DEBUG_NOTICE
     * @param integer $level bit constants (SMARTIRC_DEBUG_*)
     * @param string $entry the new log entry
     * @param string|null $file The source file originating the log() call
     * @param int|null $line The line of code that called log()
     * @return boolean
     */
    public function log($level, $entry, $file = null, $line = null)
    {
        // prechecks
        if (!(
            is_integer($level)
            && ($level & SMARTIRC_DEBUG_ALL)
        )) {
            $this->log(SMARTIRC_DEBUG_NOTICE,
                'WARNING: invalid log level passed to log() ('.$level.')',
                __FILE__, __LINE__
            );
            return false;
        }

        if (!($level & $this->_debuglevel)
            || $this->_logdestination == SMARTIRC_NONE
        ) {
            return true;
        }

        if (substr($entry, -1) != "\n") {
            $entry .= "\n";
        }

        if ($file !== null && $line !== null) {
            $file = basename($file);
            $entry = $file.'('.$line.') '.$entry;
        } else {
            $entry = 'unknown(0) '.$entry;
        }

        $formattedentry = date('M d H:i:s ').$entry;

        switch ($this->_logdestination) {
            case SMARTIRC_STDOUT:
                echo $formattedentry;
                flush();
                break;

            case SMARTIRC_BROWSEROUT:
                echo '<pre>'.htmlentities($formattedentry).'</pre>';
                break;

            case SMARTIRC_FILE:
                if (!is_resource($this->_logfilefp)) {
                    if ($this->_logfilefp === null) {
                        // we reconncted and don't want to destroy the old log entries
                        $this->_logfilefp = fopen($this->_logfile,'a');
                    } else {
                        $this->_logfilefp = fopen($this->_logfile,'w');
                    }
                }

                fwrite($this->_logfilefp, $formattedentry);
                fflush($this->_logfilefp);
                break;

            case SMARTIRC_SYSLOG:
                if (!is_int($this->_logfilefp)) {
                    $this->_logfilefp = openlog('Net_SmartIRC', LOG_NDELAY,
                        LOG_DAEMON
                    );
                }

                syslog(LOG_INFO, $entry);
        }
        return true;
    }

    /**
     * Returns the full motd.
     *
     * @api
     * @return array
     */
    public function getMotd()
    {
        return $this->_motd;
    }

    /**
     * Returns the usermode.
     *
     * @api
     * @return string
     */
    public function getUsermode()
    {
        return $this->_usermode;
    }

    /**
     * Returns a reference to the channel object of the specified channelname.
     *
     * @api
     * @param string $channelname
     * @return object
     */
    public function &getChannel($channelname)
    {
        $err = null;

        if (!$this->_channelsyncing) {
            $this->log(SMARTIRC_DEBUG_NOTICE,
                'WARNING: getChannel() is called and the required Channel '
                .'Syncing is not activated!', __FILE__, __LINE__
            );
            return $err;
        }

        if (!isset($this->_channels[strtolower($channelname)])) {
            $this->log(SMARTIRC_DEBUG_NOTICE,
                'WARNING: getChannel() is called and the required channel '
                .$channelname.' has not been joined!', __FILE__, __LINE__
            );
            return $err;
        }

        return $this->_channels[strtolower($channelname)];
    }

    /**
     * Returns a reference to the user object for the specified username and channelname.
     *
     * @api
     * @param string $channelname
     * @param string $username
     * @return object
     */
    public function &getUser($channelname, $username)
    {
        if (!$this->_channelsyncing) {
            $this->log(SMARTIRC_DEBUG_NOTICE, 'WARNING: getUser() is called and'
                .' the required Channel Syncing is not activated!',
                __FILE__, __LINE__
            );
            return;
        }

        if ($this->isJoined($channelname, $username)) {
            return $this->getChannel($channelname)->users[strtolower($username)];
        }
    }

    /**
     * Creates the sockets and connects to the IRC server on the given port.
     *
     * Returns this SmartIRC object on success, and false on failure.
     *
     * @api
     * @param string $addr
     * @param integer $port
     * @param bool $reconnecting For internal use only
     * @return boolean|Net_SmartIRC
     */
    public function connect($addr, $port = 6667, $reconnecting = false)
    {
        ob_implicit_flush();
        $this->log(SMARTIRC_DEBUG_CONNECTION, 'DEBUG_CONNECTION: connecting',
            __FILE__, __LINE__
        );

        if ($hasPort = preg_match(self::IP_PATTERN, $addr)) {
            $colon = strrpos($addr, ':');
            $this->_address = substr($addr, 0, $colon);
            $this->_port = (int) substr($addr, $colon + 1);
        } elseif ($hasPort === 0) {
            $this->_address = $addr;
            $this->_port = $port;
            $addr .= ':' . $port;
        }

        $timeout = ini_get("default_socket_timeout");
        $context = stream_context_create(array('socket' => array('bindto' => $this->_bindto)));
        $this->log(SMARTIRC_DEBUG_SOCKET, 'DEBUG_SOCKET: binding to '.$this->_bindto,
            __FILE__, __LINE__);


        if ($this->_socket = stream_socket_client($addr, $errno, $errstr,
                $timeout, STREAM_CLIENT_CONNECT, $context)
        ) {
            if (!stream_set_blocking($this->_socket, 0)) {
                $this->log(SMARTIRC_DEBUG_SOCKET, 'DEBUG_SOCKET: unable to unblock stream',
                    __FILE__, __LINE__
                );
                $this->throwError('unable to unblock stream');
            }

            $this->log(SMARTIRC_DEBUG_CONNECTION, 'DEBUG_CONNECTION: connected',
                __FILE__, __LINE__
            );

            $this->_autoretrycount = 0;
            $this->_connectionerror = false;

            $this->registerTimeHandler($this->_rxtimeout * 125, $this, '_pingcheck');

            $this->_lasttx = $this->_lastrx = time();
            $this->_updatestate();
            return $this;
        }

        $error_msg = "couldn't connect to \"$addr\" reason: \"$errstr ($errno)\"";
        $this->log(SMARTIRC_DEBUG_SOCKET, 'DEBUG_NOTICE: '.$error_msg,
            __FILE__, __LINE__
        );
        $this->throwError($error_msg);

        return ($reconnecting) ? false : $this->reconnect();
    }

    /**
     * Disconnects from the IRC server nicely with a QUIT or just destroys the socket.
     *
     * Disconnects from the IRC server in the given quickness mode.
     * $quick:
     * - true, just close the socket
     * - false, send QUIT and wait {@link $_disconnectime $_disconnectime} before
     *   closing the socket
     *
     * @api
     * @param boolean $quick default: false
     * @return boolean|Net_SmartIRC
     */
    function disconnect($quick = false)
    {
        if ($this->_updatestate() != SMARTIRC_STATE_CONNECTED) {
            return false;
        }

        if (!$quick) {
            $this->send('QUIT', SMARTIRC_CRITICAL);
            usleep($this->_disconnecttime*1000);
        }

        fclose($this->_socket);

        $this->_updatestate();
        $this->log(SMARTIRC_DEBUG_CONNECTION, 'DEBUG_CONNECTION: disconnected',
            __FILE__, __LINE__
        );

        if ($this->_channelsyncing) {
            // let's clean our channel array
            $this->_channels = array();
            $this->log(SMARTIRC_DEBUG_CHANNELSYNCING, 'DEBUG_CHANNELSYNCING: '
                .'cleaned channel array', __FILE__, __LINE__
            );
        }

        if ($this->_usersyncing) {
            // let's clean our user array
            $this->_users = array();
            $this->log(SMARTIRC_DEBUG_USERSYNCING, 'DEBUG_USERSYNCING: cleaned '
                .'user array', __FILE__, __LINE__
            );
        }

        if ($this->_logdestination == SMARTIRC_FILE) {
            fclose($this->_logfilefp);
            $this->_logfilefp = null;
        } else if ($this->_logdestination == SMARTIRC_SYSLOG) {
            closelog();
        }

        return $this;
    }

    /**
     * Reconnects to the IRC server with the same login info,
     * it also rejoins the channels
     *
     * @api
     * @return boolean|Net_SmartIRC
     */
    public function reconnect()
    {
        // remember in which channels we are joined
        $channels = array();
        foreach ($this->_channels as $value) {
            if (empty($value->key)) {
                $channels[] = array('name' => $value->name);
            } else {
                $channels[] = array('name' => $value->name, 'key' => $value->key);
            }
        }

        $this->disconnect(true);

        while ($this->_autoretry === true
            && ($this->_autoretrymax == 0 || $this->_autoretrycount < $this->_autoretrymax)
            && $this->_updatestate() != SMARTIRC_STATE_CONNECTED
        ) {
            $this->_autoretrycount++;

            if ($this->_reconnectdelay > 0) {
                $this->log(SMARTIRC_DEBUG_CONNECTION, 'DEBUG_CONNECTION: delaying '
                    .'reconnect for '.$this->_reconnectdelay.' ms',
                    __FILE__, __LINE__
                );

                for ($i = 0; $i < $this->_reconnectdelay; $i++) {
                    $this->_callTimeHandlers();
                    usleep(1000);
                }
            }

            $this->_callTimeHandlers();
            $this->log(SMARTIRC_DEBUG_CONNECTION, 'DEBUG_CONNECTION: reconnecting...',
                __FILE__, __LINE__
            );

            if ($this->connect($this->_address, $this->_port, true) !== false) {
                break;
            }
        }

        if ($this->_updatestate() != SMARTIRC_STATE_CONNECTED) {
            return false;
        }

        $this->login($this->_nick, $this->_realname, $this->_usermode,
            $this->_username, $this->_password
        );

        // rejoin the channels
        foreach ($channels as $value) {
            if (isset($value['key'])) {
                $this->join($value['name'], $value['key']);
            } else {
                $this->join($value['name']);
            }
        }

        return $this;
    }

    /**
     * login and register nickname on the IRC network
     *
     * Registers the nickname and user information on the IRC network.
     *
     * @api
     * @param string $nick
     * @param string $realname
     * @param integer $usermode
     * @param string $username
     * @param string $password
     * @return Net_SmartIRC
     */
    public function login($nick, $realname, $usermode = 0, $username = null,
        $password = null
    ) {
        $this->log(SMARTIRC_DEBUG_CONNECTION, 'DEBUG_CONNECTION: logging in',
            __FILE__, __LINE__
        );

        $this->_nick = str_replace(' ', '', $nick);
        $this->_realname = $realname;

        if ($username !== null) {
            $this->_username = str_replace(' ', '', $username);
        } else {
            $this->_username = str_replace(' ', '', exec('whoami'));
        }

        if ($password !== null) {
            $this->_password = $password;
            $this->send('PASS '.$this->_password, SMARTIRC_CRITICAL);
        }

        if (!is_numeric($usermode)) {
            $ipos = strpos($usermode, 'i');
            $wpos = strpos($usermode, 'w');
            $val = 0;
            if ($ipos) $val += 8;
            if ($wpos) $val += 4;

            if ($val == 0) {
                $this->log(SMARTIRC_DEBUG_NOTICE, 'DEBUG_NOTICE: login() usermode ('
                    .$usermode.') is not valid, using 0 instead',
                    __FILE__, __LINE__
                );
            }
            $usermode = $val;
        }

        $this->send('NICK '.$this->_nick, SMARTIRC_CRITICAL);
        $this->send('USER '.$this->_username.' '.$usermode.' '.SMARTIRC_UNUSED
            .' :'.$this->_realname, SMARTIRC_CRITICAL
        );

        if (count($this->_performs)) {
            // if we have extra commands to send, do it now
            foreach ($this->_performs as $command) {
                $this->send($command, SMARTIRC_HIGH);
            }
            // if we sent "ns auth" commands, we may need to resend our nick
            $this->send('NICK '.$this->_nick, SMARTIRC_HIGH);
        }

        return $this;
    }

    // </IRC methods>

    /**
     * adds a command to the list of commands to be sent after login() info
     *
     * @api
     * @param string $cmd the command to add to the perform list
     * @return Net_SmartIRC
     */
    public function perform($cmd)
    {
        $this->_performs[] = $cmd;
        return $this;
    }

    /**
     * sends an IRC message
     *
     * Adds a message to the messagequeue, with the optional priority.
     * $priority:
     * SMARTIRC_CRITICAL
     * SMARTIRC_HIGH
     * SMARTIRC_MEDIUM
     * SMARTIRC_LOW
     *
     * @api
     * @param string $data
     * @param integer $priority must be one of the priority constants
     * @return boolean|Net_SmartIRC
     */
    public function send($data, $priority = SMARTIRC_MEDIUM)
    {
        switch ($priority) {
            case SMARTIRC_CRITICAL:
                $this->_rawsend($data);
                break;

            case SMARTIRC_HIGH:
            case SMARTIRC_MEDIUM:
            case SMARTIRC_LOW:
                $this->_messagebuffer[$priority][] = $data;
                break;

            default:
                $this->log(SMARTIRC_DEBUG_NOTICE, "WARNING: message ($data) "
                    ."with an invalid priority passed ($priority), message is "
                    .'ignored!', __FILE__, __LINE__
                );
                return false;
        }

        return $this;
    }

    /**
     * checks if the bot is connected
     *
     * @api
     * @return boolean
     */
    public function isConnected()
    {
        return ($this->_updatestate() === SMARTIRC_STATE_CONNECTED);
    }

    /**
     * checks if the passed nickname is our own nickname
     *
     * @api
     * @param string $nickname
     * @return boolean
     */
    public function isMe($nickname)
    {
        return ($nickname == $this->_nick);
    }

    /**
     * checks if we or the given user is joined to the specified channel and returns the result
     * ChannelSyncing is required for this.
     *
     * @api
     * @see setChannelSyncing
     * @param string $channel
     * @param string $nickname
     * @return boolean
     */
    public function isJoined($channel, $nickname = null)
    {
        if (!$this->_channelsyncing) {
            $this->log(SMARTIRC_DEBUG_NOTICE, 'WARNING: isJoined() is called '
                .'and the required Channel Syncing is not activated!',
                __FILE__, __LINE__
            );
            return false;
        }

        if (!isset($this->_channels[strtolower($channel)])) {
            if ($nickname !== null) {
                $this->log(SMARTIRC_DEBUG_NOTICE, 'WARNING: isJoined() is called'
                    .' on a user in a channel we are not joined to!',
                    __FILE__, __LINE__
                );
            }
            return false;
        }

        if ($nickname === null) {
            return true;
        }

        return isset($this->getChannel($channel)->users[strtolower($nickname)]);
    }

    /**
     * Checks if we or the given user is founder on the specified channel and returns the result.
     * ChannelSyncing is required for this.
     *
     * @api
     * @see setChannelSyncing
     * @param string $channel
     * @param string $nickname
     * @return boolean
     */
    public function isFounder($channel, $nickname = null)
    {
        if (!$this->_channelsyncing) {
            $this->log(SMARTIRC_DEBUG_NOTICE, 'WARNING: isFounder() is called '
                .'and the required Channel Syncing is not activated!',
                __FILE__, __LINE__
            );
            return false;
        }

        if ($nickname === null) {
            $nickname = $this->_nick;
        }

        return ($this->isJoined($channel, $nickname)
            && $this->getUser($channel, $nickname)->founder
        );
    }

    /**
     * Checks if we or the given user is admin on the specified channel and returns the result.
     * ChannelSyncing is required for this.
     *
     * @api
     * @see setChannelSyncing
     * @param string $channel
     * @param string $nickname
     * @return boolean
     */
    public function isAdmin($channel, $nickname = null)
    {
        if (!$this->_channelsyncing) {
            $this->log(SMARTIRC_DEBUG_NOTICE, 'WARNING: isAdmin() is called '
                .'and the required Channel Syncing is not activated!',
                __FILE__, __LINE__
            );
            return false;
        }

        if ($nickname === null) {
            $nickname = $this->_nick;
        }

        return ($this->isJoined($channel, $nickname)
            && $this->getUser($channel, $nickname)->admin
        );
    }

    /**
     * Checks if we or the given user is opped on the specified channel and returns the result.
     * ChannelSyncing is required for this.
     *
     * @api
     * @see setChannelSyncing
     * @param string $channel
     * @param string $nickname
     * @return boolean
     */
    public function isOpped($channel, $nickname = null)
    {
        if (!$this->_channelsyncing) {
            $this->log(SMARTIRC_DEBUG_NOTICE, 'WARNING: isOpped() is called '
                .'and the required Channel Syncing is not activated!',
                __FILE__, __LINE__
            );
            return false;
        }

        if ($nickname === null) {
            $nickname = $this->_nick;
        }

        return ($this->isJoined($channel, $nickname)
            && $this->getUser($channel, $nickname)->op
        );
    }

    /**
     * Checks if we or the given user is hopped on the specified channel and returns the result.
     * ChannelSyncing is required for this.
     *
     * @api
     * @see setChannelSyncing
     * @param string $channel
     * @param string $nickname
     * @return boolean
     */
    public function isHopped($channel, $nickname = null)
    {
        if (!$this->_channelsyncing) {
            $this->log(SMARTIRC_DEBUG_NOTICE, 'WARNING: isHopped() is called '
                .'and the required Channel Syncing is not activated!',
                __FILE__, __LINE__
            );
            return false;
        }

        if ($nickname === null) {
            $nickname = $this->_nick;
        }

        return ($this->isJoined($channel, $nickname)
            && $this->getUser($channel, $nickname)->hop
        );
    }

    /**
     * Checks if we or the given user is voiced on the specified channel and returns the result.
     * ChannelSyncing is required for this.
     *
     * @api
     * @see setChannelSyncing
     * @param string $channel
     * @param string $nickname
     * @return boolean
     */
    public function isVoiced($channel, $nickname = null)
    {
        if (!$this->_channelsyncing) {
            $this->log(SMARTIRC_DEBUG_NOTICE, 'WARNING: isVoiced() is called '
                .'and the required Channel Syncing is not activated!',
                __FILE__, __LINE__
            );
            return false;
        }

        if ($nickname === null) {
            $nickname = $this->_nick;
        }

        return ($this->isJoined($channel, $nickname)
            && $this->getUser($channel, $nickname)->voice
        );
    }

    /**
     * Checks if the hostmask is on the specified channel banned and returns the result.
     * ChannelSyncing is required for this.
     *
     * @api
     * @see setChannelSyncing
     * @param string $channel
     * @param string $hostmask
     * @return boolean
     */
    public function isBanned($channel, $hostmask)
    {
        if (!$this->_channelsyncing) {
            $this->log(SMARTIRC_DEBUG_NOTICE, 'WARNING: isBanned() is called '
                .'and the required Channel Syncing is not activated!',
                __FILE__, __LINE__
            );
            return false;
        }

        return ($this->isJoined($channel)
                && array_search($hostmask, $this->getChannel($channel)->bans
            ) !== false
        );
    }

    /**
     * Provides a mechanism to interrupt a listen() loop by a bot or something
     *
     * @api
     * @param bool $ival Whether to interrupt the next listen iteration
     * @return boolean
     */
    public function interrupt($ival = true)
    {
        return ($this->_interrupt = $ival);
    }

    /**
     * goes into receive mode
     *
     * Goes into receive and idle mode. Only call this if you want to "spawn" the bot.
     * No further lines of PHP code will be processed after this call, only the bot methods!
     *
     * @api
     * @return Net_SmartIRC
     */
    public function listen()
    {
        set_time_limit(0);
        while ($this->listenOnce() && !$this->_interrupt) {}
        return $this;
    }

    /**
     * goes into receive mode _only_ for one pass
     *
     * Goes into receive mode. It will return when one pass is complete.
     * Use this when you want to connect to multiple IRC servers.
     *
     * @api
     * @return boolean|Net_SmartIRC
     */
    public function listenOnce()
    {
        // if we're not connected, we can't listen, so return
        if ($this->_updatestate() != SMARTIRC_STATE_CONNECTED) {
            return false;
        }

        // before we listen...
        if ($this->_loggedin) {
            // see if any timehandler needs to be called
            $this->_callTimeHandlers();

            // also let's send any queued messages
            if ($this->_lastsentmsgtime == 0) {
                $this->_lastsentmsgtime = microtime(true);
            }

            $highcount = count($this->_messagebuffer[SMARTIRC_HIGH]);
            $mediumcount = count($this->_messagebuffer[SMARTIRC_MEDIUM]);
            $lowcount = count($this->_messagebuffer[SMARTIRC_LOW]);
            $this->_messagebuffersize = $highcount+$mediumcount+$lowcount;

            // don't send them too fast
            if ($this->_messagebuffersize
                && microtime(true)
                    >= ($this->_lastsentmsgtime+($this->_senddelay/1000))
            ) {
                $result = null;
                if ($highcount) {
                    $this->_rawsend(array_shift($this->_messagebuffer[SMARTIRC_HIGH]));
                    $this->_lastsentmsgtime = microtime(true);
                } else if ($mediumcount) {
                    $this->_rawsend(array_shift($this->_messagebuffer[SMARTIRC_MEDIUM]));
                    $this->_lastsentmsgtime = microtime(true);
                } else if ($lowcount) {
                    $this->_rawsend(array_shift($this->_messagebuffer[SMARTIRC_LOW]));
                    $this->_lastsentmsgtime = microtime(true);
                }
            }
        }

        // calculate selecttimeout
        $compare = array($this->_maxtimer, $this->_receivedelay * 1000);

        if ($this->_mintimer) {
            $compare[] = $this->_mintimer;
        }

        $selecttimeout = ($this->_messagebuffersize != 0)
            ? $this->_senddelay
            : min($compare)
        ;

        // check the socket to see if data is waiting for us
        // this will trigger a warning when a signal is received
        $r = array($this->_socket);
        $w = null;
        $e = null;
        $result = stream_select($r, $w, $e, 0, $selecttimeout);

        $rawdata = null;

        if ($result) {
            // the socket got data to read
            $rawdata = '';
            do {
                if ($get = fgets($this->_socket)):
                    $rawdata .= $get;
                endif;
                $rawlen = strlen($rawdata);
            } while ($rawlen && $rawdata{$rawlen - 1} != "\n");

        } else if ($result === false) {
            // panic! panic! something went wrong! maybe received a signal.
            $this->log(SMARTIRC_DEBUG_NOTICE, 'WARNING: stream_select()'
                .' returned false, something went wrong!',
                __FILE__, __LINE__
            );
            exit;
        }
        // no data on the socket

        $timestamp = time();
        if (empty($rawdata)) {
            if ($this->_lastrx < ($timestamp - $this->_rxtimeout)) {
                $this->log(SMARTIRC_DEBUG_CONNECTION, 'DEBUG_CONNECTION: '
                    .'receive timeout detected, doing reconnect...',
                    __FILE__, __LINE__
                );
                $this->_connectionerror = true;
            } else if ($this->_lasttx < ($timestamp - $this->_txtimeout)) {
                $this->log(SMARTIRC_DEBUG_CONNECTION, 'DEBUG_CONNECTION: '
                    .'transmit timeout detected, doing reconnect...',
                    __FILE__, __LINE__
                );
                $this->_connectionerror = true;
            }
        } else {
            $this->_lastrx = $timestamp;

            // split up incoming lines, remove any empty ones and
            // trim whitespace off the rest
            $rawdataar = array_map('trim', array_filter(explode("\r\n", $rawdata)));

            // parse and handle them
            foreach ($rawdataar as $rawline) {
                $this->log(SMARTIRC_DEBUG_IRCMESSAGES, 'DEBUG_IRCMESSAGES: '
                    ."received: \"$rawline\"", __FILE__, __LINE__
                );

                // building our data packet
                $ircdata = new Net_SmartIRC_data();
                $ircdata->rawmessage = $rawline;
                $ircdata->rawmessageex = explode(' ', $rawline); // kept for BC

                // parsing the message {
                $prefix = $trailing = '';
                $prefixEnd = -1;

                // parse out the prefix
                if ($rawline{0} == ':') {
                    $prefixEnd = strpos($rawline, ' ');
                    $prefix = substr($rawline, 1, $prefixEnd - 1);
                }

                // parse out the trailing
                if ($trailingStart = strpos($rawline, ' :')) { // this is not ==
                    $trailing = substr($rawline, $trailingStart + 2);
                } else {
                    $trailingStart = strlen($rawline);
                }

                // parse out command and params
                $params = explode(' ', substr($rawline,
                                              $prefixEnd + 1,
                                              $trailingStart - $prefixEnd - 1
                ));
                $command = array_shift($params);
                // }

                $ircdata->from = $prefix;
                $ircdata->params = $params;
                $ircdata->message = $trailing;
                $ircdata->messageex = explode(' ', $trailing);

                // parse ident thingy
                if (preg_match('/^(\S+)!(\S+)@(\S+)$/', $prefix, $matches)) {
                    $ircdata->nick = $matches[1];
                    $ircdata->ident = $matches[2];
                    $ircdata->host = $matches[3];
                } else {
                    $ircdata->nick = '';
                    $ircdata->ident = '';
                    $ircdata->host = $prefix;
                }

                // figure out what SMARTIRC_TYPE this message is
                switch ($command) {
                    case SMARTIRC_RPL_WELCOME:
                    case SMARTIRC_RPL_YOURHOST:
                    case SMARTIRC_RPL_CREATED:
                    case SMARTIRC_RPL_MYINFO:
                    case SMARTIRC_RPL_BOUNCE:
                        $ircdata->type = SMARTIRC_TYPE_LOGIN;
                        break;

                    case SMARTIRC_RPL_LUSERCLIENT:
                    case SMARTIRC_RPL_LUSEROP:
                    case SMARTIRC_RPL_LUSERUNKNOWN:
                    case SMARTIRC_RPL_LUSERME:
                    case SMARTIRC_RPL_LUSERCHANNELS:
                        $ircdata->type = SMARTIRC_TYPE_INFO;
                        break;

                    case SMARTIRC_RPL_MOTDSTART:
                    case SMARTIRC_RPL_MOTD:
                    case SMARTIRC_RPL_ENDOFMOTD:
                        $ircdata->type = SMARTIRC_TYPE_MOTD;
                        break;

                    case SMARTIRC_RPL_NAMREPLY:
                        $ircdata->type = SMARTIRC_TYPE_NAME;
                        if ($params[0] == $this->_nick):
                            $ircdata->channel = $params[2];
                        else:
                            $ircdata->channel = $params[1];
                        endif;
                        break;

                    case SMARTIRC_RPL_ENDOFNAMES:
                        $ircdata->type = SMARTIRC_TYPE_NAME;
                        if ($params[0] == $this->_nick):
                            $ircdata->channel = $params[1];
                        else:
                            $ircdata->channel = $params[0];
                        endif;
                        break;

                    case SMARTIRC_RPL_WHOREPLY:
                    case SMARTIRC_RPL_ENDOFWHO:
                        $ircdata->type = SMARTIRC_TYPE_WHO;
                        if ($params[0] == $this->_nick):
                            $ircdata->channel = $params[1];
                        else:
                            $ircdata->channel = $params[0];
                        endif;
                        break;

                    case SMARTIRC_RPL_LISTSTART:
                        $ircdata->type = SMARTIRC_TYPE_NONRELEVANT;
                        break;

                    case SMARTIRC_RPL_LIST:
                    case SMARTIRC_RPL_LISTEND:
                        $ircdata->type = SMARTIRC_TYPE_LIST;
                        break;

                    case SMARTIRC_RPL_BANLIST:
                    case SMARTIRC_RPL_ENDOFBANLIST:
                        $ircdata->type = SMARTIRC_TYPE_BANLIST;
                        $ircdata->channel = $params[0];
                        break;

                    case SMARTIRC_RPL_TOPIC:
                        $ircdata->type = SMARTIRC_TYPE_TOPIC;
                        $ircdata->channel = $params[0];
                        break;

                    case SMARTIRC_RPL_WHOISUSER:
                    case SMARTIRC_RPL_WHOISSERVER:
                    case SMARTIRC_RPL_WHOISOPERATOR:
                    case SMARTIRC_RPL_WHOISIDLE:
                    case SMARTIRC_RPL_ENDOFWHOIS:
                    case SMARTIRC_RPL_WHOISCHANNELS:
                        $ircdata->type = SMARTIRC_TYPE_WHOIS;
                        break;

                    case SMARTIRC_RPL_WHOWASUSER:
                    case SMARTIRC_RPL_ENDOFWHOWAS:
                        $ircdata->type = SMARTIRC_TYPE_WHOWAS;
                        break;

                    case SMARTIRC_RPL_UMODEIS:
                        $ircdata->type = SMARTIRC_TYPE_USERMODE;
                        break;

                    case SMARTIRC_RPL_CHANNELMODEIS:
                        $ircdata->type = SMARTIRC_TYPE_CHANNELMODE;
                        $ircdata->channel = $params[0];
                        break;

                    case SMARTIRC_ERR_NICKNAMEINUSE:
                    case SMARTIRC_ERR_NOTREGISTERED:
                        $ircdata->type = SMARTIRC_TYPE_ERROR;
                        break;

                    case 'PRIVMSG':
                        if (strspn($ircdata->params[0], '&#+!')) {
                            $ircdata->type = SMARTIRC_TYPE_CHANNEL;
                            $ircdata->channel = $params[0];
                            break;
                        }
                        if ($ircdata->message{0} == chr(1)) {
                            if (preg_match("/^\1ACTION .*\1\$/", $ircdata->message)) {
                                $ircdata->type = SMARTIRC_TYPE_ACTION;
                                $ircdata->channel = $params[0];
                                break;
                            }
                            if (preg_match("/^\1.*\1\$/", $ircdata->message)) {
                                $ircdata->type = (SMARTIRC_TYPE_CTCP_REQUEST | SMARTIRC_TYPE_CTCP);
                                break;
                            }
                        }
                        $ircdata->type = SMARTIRC_TYPE_QUERY;
                        break;

                    case 'NOTICE':
                        if (preg_match("/^\1.*\1\$/", $ircdata->message)) {
                            $ircdata->type = (SMARTIRC_TYPE_CTCP_REPLY | SMARTIRC_TYPE_CTCP);
                            break;
                        }
                        $ircdata->type = SMARTIRC_TYPE_NOTICE;
                        break;

                    case 'INVITE':
                        $ircdata->type = SMARTIRC_TYPE_INVITE;
                        break;

                    case 'JOIN':
                        $ircdata->type = SMARTIRC_TYPE_JOIN;
                        $ircdata->channel = (!empty($params[0])) ? $params[0] : $ircdata->message;
                        break;

                    case 'TOPIC':
                        $ircdata->type = SMARTIRC_TYPE_TOPICCHANGE;
                        $ircdata->channel = $params[0];
                        break;

                    case 'NICK':
                        $ircdata->type = SMARTIRC_TYPE_NICKCHANGE;
                        break;

                    case 'KICK':
                        $ircdata->type = SMARTIRC_TYPE_KICK;
                        $ircdata->channel = $params[0];
                        break;

                    case 'PART':
                        $ircdata->type = SMARTIRC_TYPE_PART;
                        $ircdata->channel = $params[0];
                        break;

                    case 'MODE':
                        $ircdata->type = SMARTIRC_TYPE_MODECHANGE;
                        $ircdata->channel = $params[0];
                        break;

                    case 'QUIT':
                        $ircdata->type = SMARTIRC_TYPE_QUIT;
                        break;

                    default:
                        $this->log(SMARTIRC_DEBUG_IRCMESSAGES, 'DEBUG_IRCMESSAGES: '
                            ."command type UNKNOWN ($command)",
                            __FILE__, __LINE__
                        );
                        $ircdata->type = SMARTIRC_TYPE_UNKNOWN;
                        break;
                }

                $this->log(SMARTIRC_DEBUG_MESSAGEPARSER, 'DEBUG_MESSAGEPARSER: '
                    .'ircdata nick:"'.$ircdata->nick
                    .'" ident:"'.$ircdata->ident
                    .'" host:"'.$ircdata->host
                    .'" type:"'.$ircdata->type
                    .'" from:"'.$ircdata->from
                    .'" channel:"'.$ircdata->channel
                    .'" message:"'.$ircdata->message.'"', __FILE__, __LINE__
                );

                // lets see if we have a messagehandler for it
                if (is_numeric($command)) {
                    if (!array_key_exists($command, $this->nreplycodes)) {
                        $this->log(SMARTIRC_DEBUG_MESSAGEHANDLER,
                            'DEBUG_MESSAGEHANDLER: cannot translate unrecognized'
                            ." messagecode $command into a command type",
                            __FILE__, __LINE__
                        );
                        $methodname = 'event_' . $command;
                    } else {
                        $methodname = 'event_'.strtolower($this->nreplycodes[$command]);
                    }

                    $_methodname = '_'.$methodname;
                    $_codetype = 'by numeric';
                } else {
                    $methodname = 'event_'.strtolower($command);
                    $_methodname = '_'.$methodname;
                    $_codetype = 'by string';
                }

                $found = false;

                // if exists call internal method for the handling
                if (method_exists($this, $_methodname)) {
                    $this->log(SMARTIRC_DEBUG_MESSAGEHANDLER, 'DEBUG_MESSAGEHANDLER: '
                        .'calling internal method "'.get_class($this).'->'
                        .$_methodname.'" ('.$_codetype.')', __FILE__, __LINE__
                    );
                    $this->$_methodname($ircdata);
                    $found = true;
                }

                // if exists call user defined method for the handling
                if (method_exists($this, $methodname)) {
                    $this->log(SMARTIRC_DEBUG_MESSAGEHANDLER, 'DEBUG_MESSAGEHANDLER: '
                        .'calling user defined method "'.get_class($this).'->'
                        .$methodname.'" ('.$_codetype.')', __FILE__, __LINE__
                    );
                    $this->$methodname($ircdata);
                    $found = true;
                }

                if (!$found) {
                    $this->log(SMARTIRC_DEBUG_MESSAGEHANDLER, 'DEBUG_MESSAGEHANDLER: no'
                        .' method found for "'.$command.'" ('.$methodname.')',
                        __FILE__, __LINE__
                    );
                }

                // now the actionhandlers are coming
                foreach ($this->_actionhandler as $i => $handlerinfo) {

                    $hmsg = $handlerinfo['message'];
                    $regex = ($hmsg{0} == $hmsg{strlen($hmsg) - 1})
                        ? $hmsg
                        : '/' . $hmsg . '/';

                    if (($handlerinfo['type'] & $ircdata->type)
                        && preg_match($regex, $ircdata->message)
                    ) {
                        $this->log(SMARTIRC_DEBUG_ACTIONHANDLER, 'DEBUG_ACTIONHANDLER: '
                            ."actionhandler match found for id: $i type: "
                            .$ircdata->type.' message: "'.$ircdata->message
                            ."\" regex: \"$regex\"", __FILE__, __LINE__
                        );

                        $callback = $handlerinfo['callback'];

                        $cbstring = (is_array($callback))
                            ? (is_object($callback[0])
                                ? get_class($callback[0])
                                : $callback[0]
                              ) . '->' . $callback[1]
                            : '(anonymous function)';

                        if (is_callable($callback)) {
                            $this->log(SMARTIRC_DEBUG_ACTIONHANDLER,
                                'DEBUG_ACTIONHANDLER: calling "'.$cbstring.'"',
                                __FILE__, __LINE__
                            );
                            call_user_func($callback, $this, $ircdata);
                        } else {
                            $this->log(SMARTIRC_DEBUG_ACTIONHANDLER,
                                'DEBUG_ACTIONHANDLER: callback is invalid! "'.$cbstring.'"',
                                __FILE__, __LINE__
                            );
                        }
                    }
                }

                unset($ircdata);
            }
        }

        // if we've done anything that didn't work and the connection is broken,
        // log it and fix it
        if ($this->_connectionerror) {
            $this->log(SMARTIRC_DEBUG_CONNECTION, 'DEBUG_CONNECTION: connection'
                .' error detected, attempting reconnect!', __FILE__, __LINE__
            );
            $this->reconnect();
        }
        return $this;
    }

    /**
     * waits for a special message type and returns the answer
     *
     * Creates a special actionhandler for that given TYPE and returns the answer.
     * This will only receive the requested type, immediately quit and disconnect from the IRC server.
     * Made for showing IRC statistics on your homepage, or other IRC related information.
     *
     * @api
     * @param integer $messagetype see in the documentation 'Message Types'
     * @param string  $regex the pattern to match on
     * @return array answer from the IRC server for this $messagetype
     */
    public function listenFor($messagetype, $regex = '.*')
    {
        $listenfor = new Net_SmartIRC_listenfor();
        $this->registerActionHandler($messagetype, $regex, array($listenfor, 'handler'));
        $this->listen();
        return $listenfor->result;
    }

    /**
     * registers a new actionhandler and returns the assigned id
     *
     * Registers an actionhandler in Net_SmartIRC for calling it later.
     * The actionhandler id is needed for unregistering the actionhandler.
     *
     * @api
     * @see example.php
     * @param integer $handlertype bits constants, see in this documentation Message Types
     * @param string $regexhandler the message that has to be in the IRC message in regex syntax
     * @param object|callable $object either an object with the method, or a callable
     * @param string $methodname the methodname that will be called when the handler happens
     * @return integer assigned actionhandler id
     */
    public function registerActionHandler($handlertype, $regexhandler, $object,
        $methodname = ''
    ) {
        // precheck
        if (!($handlertype & SMARTIRC_TYPE_ALL)) {
            $this->log(SMARTIRC_DEBUG_NOTICE, 'WARNING: passed invalid handler'
                .'type to registerActionHandler()', __FILE__, __LINE__
            );
            return false;
        }

        if (!empty($methodname)) {
            $object = array($object, $methodname);
        }

        $id = $this->_actionhandlerid++;
        $this->_actionhandler[] = array(
            'id' => $id,
            'type' => $handlertype,
            'message' => $regexhandler,
            'callback' => $object,
        );
        $this->log(SMARTIRC_DEBUG_ACTIONHANDLER, 'DEBUG_ACTIONHANDLER: '
            .'actionhandler('.$id.') registered', __FILE__, __LINE__
        );
        return $id;
    }

    /**
     * unregisters an existing actionhandler
     *
     * @api
     * @param integer $handlertype
     * @param string $regexhandler
     * @param object $object
     * @param string $methodname
     * @return boolean
     */
    public function unregisterActionHandler($handlertype, $regexhandler,
        $object, $methodname = ''
    ) {
        // precheck
        if (!($handlertype & SMARTIRC_TYPE_ALL)) {
            $this->log(SMARTIRC_DEBUG_NOTICE, 'WARNING: passed invalid handler'
                .'type to unregisterActionHandler()', __FILE__, __LINE__
            );
            return false;
        }

        if (!empty($methodname)) {
            $object = array($object, $methodname);
        }

        foreach ($this->_actionhandler as $i => &$handlerinfo) {
            if ($handlerinfo['type'] == $handlertype
                && $handlerinfo['message'] == $regexhandler
                && $handlerinfo['callback'] == $object
            ) {
                $id = $handlerinfo['id'];
                unset($this->_actionhandler[$i]);

                $this->log(SMARTIRC_DEBUG_ACTIONHANDLER, 'DEBUG_ACTIONHANDLER: '
                    .'actionhandler('.$id.') unregistered', __FILE__, __LINE__
                );
                $this->_actionhandler = array_values($this->_actionhandler);
                return $this;
            }
        }

        $this->log(SMARTIRC_DEBUG_ACTIONHANDLER, 'DEBUG_ACTIONHANDLER: could '
            .'not find actionhandler type: "'.$handlertype.'" message: "'
            .$regexhandler.'" matching callback. Nothing unregistered', __FILE__, __LINE__
        );
        return false;
    }

    /**
     * unregisters an existing actionhandler via the id
     *
     * @api
     * @param integer|array $id
     * @return boolean|void
     */
    public function unregisterActionId($id)
    {
        if (is_array($id)) {
            foreach ($id as $each) {
                $this->unregisterActionId($each);
            }
            return $this;
        }

        foreach ($this->_actionhandler as $i => &$handlerinfo) {
            if ($handlerinfo['id'] == $id) {
                unset($this->_actionhandler[$i]);

                $this->log(SMARTIRC_DEBUG_ACTIONHANDLER, 'DEBUG_ACTIONHANDLER: '
                    .'actionhandler('.$id.') unregistered', __FILE__, __LINE__
                );
                $this->_actionhandler = array_values($this->_actionhandler);
                return $this;
            }
        }

        $this->log(SMARTIRC_DEBUG_ACTIONHANDLER, 'DEBUG_ACTIONHANDLER: could '
            .'not find actionhandler id: '.$id.' _not_ unregistered',
            __FILE__, __LINE__
        );
        return false;
    }

    /**
     * registers a timehandler and returns the assigned id
     *
     * Registers a timehandler in Net_SmartIRC, which will be called in the specified interval.
     * The timehandler id is needed for unregistering the timehandler.
     *
     * @api
     * @see example7.php
     * @param integer $interval interval time in milliseconds
     * @param object|callable $object either an object with the method, or a callable
     * @param string $methodname the methodname that will be called when the handler happens
     * @return integer assigned timehandler id
     */
    public function registerTimeHandler($interval, $object, $methodname = '')
    {
        $id = $this->_timehandlerid++;

        if (!empty($methodname)) {
            $object = array($object, $methodname);
        }

        $this->_timehandler[] = array(
            'id' => $id,
            'interval' => $interval,
            'callback' => $object,
            'lastmicrotimestamp' => microtime(true),
        );
        $this->log(SMARTIRC_DEBUG_TIMEHANDLER, 'DEBUG_TIMEHANDLER: timehandler('
            .$id.') registered', __FILE__, __LINE__
        );

        if (($this->_mintimer == false) || ($interval < $this->_mintimer)) {
            $this->_mintimer = $interval;
        }

        return $id;
    }

    /**
     * unregisters an existing timehandler via the id
     *
     * @api
     * @see example7.php
     * @param integer $id
     * @return boolean
     */
    public function unregisterTimeId($id)
    {
        if (is_array($id)) {
            foreach ($id as $each) {
                $this->unregisterTimeId($each);
            }
            return $this;
        }

        foreach ($this->_timehandler as $i => &$handlerinfo) {
            if ($handlerinfo['id'] == $id) {
                unset($this->_timehandler[$i]);

                $this->log(SMARTIRC_DEBUG_TIMEHANDLER, 'DEBUG_TIMEHANDLER: '
                    .'timehandler('.$id.') unregistered', __FILE__, __LINE__
                );
                $this->_timehandler = array_values($this->_timehandler);

                $timerarray = array();
                foreach ($this->_timehandler as $values) {
                    $timerarray[] = $values->interval;
                }

                $this->_mintimer = (
                    array_multisort($timerarray, SORT_NUMERIC, SORT_ASC)
                    && isset($timerarray[0])
                ) ? $timerarray[0]
                    : false;

                return $this;
            }
        }

        $this->log(SMARTIRC_DEBUG_TIMEHANDLER, 'DEBUG_TIMEHANDLER: could not '
            ."find timehandler id: $id _not_ unregistered",
            __FILE__, __LINE__
        );
        return false;
    }

    /**
     * loads a module using preset path and given name
     *
     * @api
     * @param string $name
     * @return boolean|Net_SmartIRC
     */
    public function loadModule($name)
    {
        // is the module already loaded?
        if (isset($this->_modules[$name])) {
            $this->log(SMARTIRC_DEBUG_NOTICE, 'WARNING! module with the name "'
                .$name.'" already loaded!', __FILE__, __LINE__
            );
            return false;
        }

        $classname = "Net_SmartIRC_module_$name";
        if (class_exists($classname)) {
            $this->log(SMARTIRC_DEBUG_MODULES, "DEBUG_MODULES: \"$name\" module class"
                .' exists, initializing...', __FILE__, __LINE__
            );
        } else {
            $filename = $this->_modulepath."/$name.php";
            if (!file_exists($filename)) {
                $this->log(SMARTIRC_DEBUG_MODULES, "DEBUG_MODULES: couldn't load "
                    ."module; file \"$filename\" doesn't exist", __FILE__, __LINE__
                );
                return false;
            }
            // pray that there is no parse error, it will kill us!
            include_once($filename);

            if (!class_exists($classname)) {
                $this->log(SMARTIRC_DEBUG_MODULES, "DEBUG_MODULES: class $classname"
                    ." not found in $filename, aborting...", __FILE__, __LINE__
                );
                return false;
            }

            $this->log(SMARTIRC_DEBUG_MODULES, 'DEBUG_MODULES: loading module '
                ."\"$name\" from file...", __FILE__, __LINE__
            );
        }

        $methods = array_flip(get_class_methods($classname));

        if (!(isset($methods['__construct']) || isset($methods['module_init']))) {
            $this->log(SMARTIRC_DEBUG_MODULES, 'DEBUG_MODULES: required method '
                .$classname.'::__construct() not found, aborting...',
                __FILE__, __LINE__
            );
            return false;
        }

        if (!(isset($methods['__destruct']) || isset($methods['module_exit']))) {
            $this->log(SMARTIRC_DEBUG_MODULES, 'DEBUG_MODULES: required method '
                .$classname.'::__destruct() not found, aborting...',
                __FILE__, __LINE__
            );
            return false;
        }

        $vars = get_class_vars($classname);
        $required = array('name', 'description', 'author', 'license');

        foreach ($required as $varname) {
            if (!isset($vars[$varname])) {
                $this->log(SMARTIRC_DEBUG_NOTICE, 'NOTICE: required module'
                    ."property {$classname}::\${$varname} not found.",
                    __FILE__, __LINE__
                );
            }
        }

        // looks like the module satisfies us, so instantiate it
        if (isset($methods['module_init'])) {
            // we're using an old module_init style module
            $this->_modules[$name] = new $classname;
            $this->log(SMARTIRC_DEBUG_MODULES, 'DEBUG_MODULES: calling '
                .$classname.'::module_init()', __FILE__, __LINE__
            );
            $this->_modules[$name]->module_init($this);
        } else if (func_num_args() == 1) {
            // we're using a new __construct style module, which maintains its
            // own reference to the $irc client object it's being used on
            $this->_modules[$name] = new $classname($this);
        } else
        // we're using new style AND we have args to pass to the constructor
        if (func_num_args() == 2) {
            // only one arg, so pass it as is
            $this->_modules[$name] = new $classname($this, func_get_arg(1));
        } else {
            // multiple args, so pass them in an array
            $this->_modules[$name] = new $classname($this, array_slice(func_get_args(), 1));
        }

        $this->log(SMARTIRC_DEBUG_MODULES, 'DEBUG_MODULES: successfully loaded'
            ." module: $name", __FILE__, __LINE__
        );
        return $this;
    }

    /**
     * unloads a module by the name originally loaded with
     *
     * @api
     * @param string $name
     * @return boolean|Net_SmartIRC
     */
    public function unloadModule($name)
    {
        $this->log(SMARTIRC_DEBUG_MODULES, 'DEBUG_MODULES: unloading module: '
            ."$name...", __FILE__, __LINE__
        );

        if (isset($this->_modules[$name])) {
            if (in_array('module_exit',
                    get_class_methods(get_class($this->_modules[$name]))
            )) {
                $this->_modules[$name]->module_exit($this);
            }

            unset($this->_modules[$name]);
            $this->log(SMARTIRC_DEBUG_MODULES, 'DEBUG_MODULES: successfully'
                ." unloaded module: $name", __FILE__, __LINE__);
            return $this;
        }

        $this->log(SMARTIRC_DEBUG_MODULES, "DEBUG_MODULES: couldn't unload"
            ." module: $name (it's not loaded!)", __FILE__, __LINE__
        );
        return false;
    }

    /**
     * Returns an array of the module names that are currently loaded
     *
     * @api
     * @return array
     */
    public function loadedModules()
    {
        return array_keys($this->_modules);
    }

    // <protected methods>
    /**
     * adds an user to the channelobject or updates his info
     *
     * @internal
     * @param object $channel
     * @param object $newuser
     * @return Net_SmartIRC
     */
    protected function _adduser(&$channel, &$newuser)
    {
        $lowerednick = strtolower($newuser->nick);
        if ($this->isJoined($channel->name, $newuser->nick)) {
            $this->log(SMARTIRC_DEBUG_CHANNELSYNCING, 'DEBUG_CHANNELSYNCING: '
                .'updating user: '.$newuser->nick.' on channel: '
                .$channel->name, __FILE__, __LINE__
            );

            // lets update the existing user
            $currentuser = &$channel->users[$lowerednick];

            $props = array('ident', 'host', 'realname', 'ircop', 'founder',
                'admin', 'op', 'hop', 'voice', 'away', 'server', 'hopcount'
            );
            foreach ($props as $prop) {
                if ($newuser->$prop !== null) {
                    $currentuser->$prop = $newuser->$prop;
                }
            }
        } else {
            $this->log(SMARTIRC_DEBUG_CHANNELSYNCING, 'DEBUG_CHANNELSYNCING: '
                .'adding user: '.$newuser->nick.' to channel: '.$channel->name,
                __FILE__, __LINE__
            );

            $channel->users[$lowerednick] = $newuser;
        }

        $user = &$channel->users[$lowerednick];
        $modes = array('founder', 'admin', 'op', 'hop', 'voice');

        foreach ($modes as $mode) {
            if ($user->$mode) {
                $this->log(SMARTIRC_DEBUG_CHANNELSYNCING,
                    "DEBUG_CHANNELSYNCING: adding $mode: ".$user->nick
                    .' to channel: '.$channel->name, __FILE__, __LINE__
                );
                $ms = $mode.'s';
                $channel->{$ms}[$user->nick] = true;
            }
        }
        return $this;
    }

    /**
     * looks for any time handlers that have timed out and calls them if valid
     *
     * @internal
     * @return void
     */
    protected function _callTimeHandlers()
    {
        foreach ($this->_timehandler as &$handlerinfo) {
            $microtimestamp = microtime(true);
            if ($microtimestamp >= $handlerinfo['lastmicrotimestamp']
                + ($handlerinfo['interval'] / 1000.0)
            ) {
                $callback = $handlerinfo['callback'];
                $handlerinfo['lastmicrotimestamp'] = $microtimestamp;

                $cbstring = (is_array($callback))
                    ? (is_object($callback[0])
                        ? get_class($callback[0])
                        : $callback[0]
                      ) . '->' . $callback[1]
                    : '(anonymous function)';

                if (is_callable($callback)) {
                    $this->log(SMARTIRC_DEBUG_TIMEHANDLER, 'DEBUG_TIMEHANDLER: calling "'.$cbstring.'"',
                        __FILE__, __LINE__
                    );
                    call_user_func($callback, $this);
                } else {
                    $this->log(SMARTIRC_DEBUG_TIMEHANDLER,
                        'DEBUG_TIMEHANDLER: callback is invalid! "'.$cbstring.'"',
                        __FILE__, __LINE__
                    );
                }
            }
        }
    }

    /**
     * An active-pinging system to keep the bot from dropping the connection
     *
     * @internal
     * @return void
     */
    protected function _pingcheck()
    {
        $time = time();
        if ($time - $this->_lastrx > $this->_rxtimeout) {
            $this->reconnect();
            $this->_lastrx = $time;
        } elseif ($time - $this->_lastrx > $this->_rxtimeout/2) {
            $this->send('PING '.$this->_address, SMARTIRC_CRITICAL);
        }
    }

    /**
     * sends a raw message to the IRC server
     *
     * Don't use this directly! Use message() or send() instead.
     *
     * @internal
     * @param string $data
     * @return boolean
     */
    protected function _rawsend($data)
    {
        if ($this->_updatestate() != SMARTIRC_STATE_CONNECTED) {
            return false;
        }

        $this->log(SMARTIRC_DEBUG_IRCMESSAGES, 'DEBUG_IRCMESSAGES: sent: "'
            .$data.'"', __FILE__, __LINE__
        );

        $result = fwrite($this->_socket, $data.SMARTIRC_CRLF);

        if (!$result) {
            // writing to the socket failed, means the connection is broken
            $this->_connectionerror = true;
        } else {
            $this->_lasttx = time();
        }

        return $result;
    }

    /**
     * removes an user from one channel or all if he quits
     *
     * @internal
     * @param object $ircdata
     * @return Net_SmartIRC
     */
    protected function _removeuser($ircdata)
    {
        if ($ircdata->type & (SMARTIRC_TYPE_PART | SMARTIRC_TYPE_QUIT)) {
            $nick = $ircdata->nick;
        } else if ($ircdata->type & SMARTIRC_TYPE_KICK) {
            $nick = $ircdata->params[1];
        } else {
            $this->log(SMARTIRC_DEBUG_CHANNELSYNCING, 'DEBUG_CHANNELSYNCING: '
                .'unknown TYPE ('.$ircdata->type
                .') in _removeuser(), trying default', __FILE__, __LINE__
            );
            $nick = $ircdata->nick;
        }

        $lowerednick = strtolower($nick);

        if ($this->_nick == $nick) {
            $this->log(SMARTIRC_DEBUG_CHANNELSYNCING,
                'DEBUG_CHANNELSYNCING: we left channel: '.$ircdata->channel
                .' destroying...', __FILE__, __LINE__
            );
            unset($this->_channels[strtolower($ircdata->channel)]);
        } else {
            $lists = array('founders', 'admins', 'ops', 'hops', 'voices');

            if ($ircdata->type & SMARTIRC_TYPE_QUIT) {
                $this->log(SMARTIRC_DEBUG_CHANNELSYNCING,
                    'DEBUG_CHANNELSYNCING: user '.$nick
                    .' quit, removing him from all channels', __FILE__, __LINE__
                );

                // remove the user from all channels
                $channelkeys = array_keys($this->_channels);
                foreach ($channelkeys as $channelkey) {
                    // loop through all channels
                    $channel = &$this->getChannel($channelkey);
                    foreach ($channel->users as $uservalue) {
                        // loop through all user in this channel
                        if ($nick == $uservalue->nick) {
                            // found him, kill him
                            $this->log(SMARTIRC_DEBUG_CHANNELSYNCING,
                                'DEBUG_CHANNELSYNCING: found him on channel: '
                                .$channel->name.' destroying...',
                                __FILE__, __LINE__
                            );
                            unset($channel->users[$lowerednick]);

                            foreach ($lists as $list) {
                                if (isset($channel->{$list}[$nick])) {
                                    // die!
                                    $this->log(SMARTIRC_DEBUG_CHANNELSYNCING,
                                        'DEBUG_CHANNELSYNCING: removing him '
                                        ."from $list list", __FILE__, __LINE__
                                    );
                                    unset($channel->{$list}[$nick]);
                                }
                            }
                        }
                    }
                }
            } else {
                $this->log(SMARTIRC_DEBUG_CHANNELSYNCING,
                    'DEBUG_CHANNELSYNCING: removing user: '.$nick
                    .' from channel: '.$ircdata->channel, __FILE__, __LINE__
                );
                $channel = &$this->getChannel($ircdata->channel);
                unset($channel->users[$lowerednick]);

                foreach ($lists as $list) {
                    if (isset($channel->{$list}[$nick])) {
                        $this->log(SMARTIRC_DEBUG_CHANNELSYNCING,
                            'DEBUG_CHANNELSYNCING: removing him '
                            ."from $list list", __FILE__, __LINE__
                        );
                        unset($channel->{$list}[$nick]);
                    }
                }
            }
        }
        return $this;
    }

    /**
     * updates and returns the current connection state
     *
     * @internal
     * @return boolean
     */
    protected function _updatestate()
    {
        if (is_resource($this->_socket)) {
            $rtype = get_resource_type($this->_socket);
            if ($this->_socket !== false
                && (strtolower($rtype) == 'socket' || $rtype == 'stream')
            ) {
                $this->_state = SMARTIRC_STATE_CONNECTED;
            }
        } else {
            $this->_state = SMARTIRC_STATE_DISCONNECTED;
            $this->_loggedin = false;
        }

        return $this->_state;
    }

    public function isError($object) // is this even needed/used?
    {
        return (is_object($object)
            && strtolower(get_class($object)) == 'net_smartirc_error'
        );
    }

    protected function throwError($message)
    {
        return new Net_SmartIRC_Error($message);
    }
}

/**
 * Struct for parsed incoming messages
 */
class Net_SmartIRC_data
{
    /**
     * @var string
     */
    public $from;

    /**
     * @var string
     */
    public $nick;

    /**
     * @var string
     */
    public $ident;

    /**
     * @var string
     */
    public $host;

    /**
     * @var string
     */
    public $channel;

    /**
     * @var array
     */
    public $params = array();

    /**
     * @var string
     */
    public $message;

    /**
     * @var array
     */
    public $messageex = array();

    /**
     * @var integer
     */
    public $type;

    /**
     * @var string
     */
    public $rawmessage;

    /**
     * @var array
     */
    public $rawmessageex = array();
}

/**
 * Struct for individual channel data
 */
class Net_SmartIRC_channel
{
    /**
     * @var string
     */
    public $name;

    /**
     * @var string
     */
    public $key;

    /**
     * @var array
     */
    public $users = array();

    /**
     * @var array
     */
    public $founders = array();

    /**
     * @var array
     */
    public $admins = array();

    /**
     * @var array
     */
    public $ops = array();

    /**
     * @var array
     */
    public $hops = array();

    /**
     * @var array
     */
    public $voices = array();

    /**
     * @var array
     */
    public $bans = array();

    /**
     * @var string
     */
    public $topic;

    /**
     * @var string
     */
    public $user_limit = false;

    /**
     * @var string
     */
    public $mode;

    /**
     * @var integer
     */
    public $synctime_start = 0;

    /**
     * @var integer
     */
    public $synctime_stop = 0;

    /**
     * @var integer
     */
    public $synctime;
}

/**
 * Struct for individual user data
 */
class Net_SmartIRC_user
{
    /**
     * @var string
     */
    public $nick;

    /**
     * @var string
     */
    public $ident;

    /**
     * @var string
     */
    public $host;

    /**
     * @var string
     */
    public $realname;

    /**
     * @var boolean
     */
    public $ircop;

    /**
     * @var boolean
     */
    public $away;

    /**
     * @var string
     */
    public $server;

    /**
     * @var integer
     */
    public $hopcount;
}

/**
 * Struct for extra data that applies to each user in each channel they're in
 */
class Net_SmartIRC_channeluser extends Net_SmartIRC_user
{
    /**
     * @var boolean
     */
    public $founder;

    /**
     * @var boolean
     */
    public $admin;

    /**
     * @var boolean
     */
    public $op;

    /**
     * @var boolean
     */
    public $hop;

    /**
     * @var boolean
     */
    public $voice;
}

/**
 * Struct for data that applies to each user server-wide
 */
class Net_SmartIRC_ircuser extends Net_SmartIRC_user
{
    /**
     * @var array
     */
    public $joinedchannels = array();
}

/**
 * Built-in bot used by Net_SmartIRC::listenFor()
 */
class Net_SmartIRC_listenfor
{
    /**
     * @var array
     */
    public $result = array();

    /**
     * stores the received answer into the result array
     *
     * @api
     * @param object $irc
     * @param object $ircdata
     * @return void
     */
    public function handler(&$irc, &$ircdata)
    {
        $irc->log(SMARTIRC_DEBUG_ACTIONHANDLER,
            'DEBUG_ACTIONHANDLER: listenfor handler called', __FILE__, __LINE__
        );
        $this->result[] = $ircdata;
        $irc->disconnect();
    }
}

class Net_SmartIRC_Error
{
    private $error_msg;

    public function __construct($message)
    {
        $this->error_msg = $message;
    }

    public function getMessage()
    {
        return $this->error_msg;
    }
}
