## rfc1459.py
# Basic RFC1459 stuff.
import asyncio
import copy
import datetime
import ipaddress
import itertools

from pydle.client import BasicClient, NotInChannel, AlreadyInChannel
from . import parsing, protocol


class RFC1459Support(BasicClient):
    """Basic RFC1459 client."""

    DEFAULT_QUIT_MESSAGE = "Quitting"

    ## Internals.

    def _reset_attributes(self):
        super()._reset_attributes()
        # Casemapping.
        self._case_mapping = protocol.DEFAULT_CASE_MAPPING

        # Limitations.
        self._away_message_length_limit = None
        self._channel_length_limit = protocol.CHANNEL_LENGTH_LIMIT
        self._channel_limit_groups = copy.deepcopy(protocol.CHANNEL_LIMITS_GROUPS)
        self._channel_limits = copy.deepcopy(protocol.CHANNEL_LIMITS)
        self._command_parameter_limit = protocol.PARAMETER_LIMIT
        self._list_limit_groups = copy.deepcopy(protocol.LIST_LIMITS_GROUPS)
        self._list_limits = copy.deepcopy(protocol.LIST_LIMITS)
        self._mode_limit = None
        self._nickname_length_limit = protocol.NICKNAME_LENGTH_LIMIT
        self._target_limits = {}
        self._topic_length_limit = protocol.TOPIC_LENGTH_LIMIT

        # Modes, prefixes.
        self._mode = {}
        self._channel_modes = set(protocol.CHANNEL_MODES)
        self._channel_modes_behaviour = copy.deepcopy(protocol.CHANNEL_MODES_BEHAVIOUR)
        self._channel_prefixes = set(protocol.CHANNEL_PREFIXES)
        self._nickname_prefixes = protocol.NICKNAME_PREFIXES.copy()
        self._status_message_prefixes = set()
        self._user_modes = set(protocol.USER_MODES)
        self._user_modes_behaviour = copy.deepcopy(protocol.USER_MODES_BEHAVIOUR)

        # Registration.
        self.registered = False
        self._registration_attempts = 0
        self._attempt_nicknames = self._nicknames[:]

        # Info.
        self._pending["whois"] = parsing.NormalizingDict(
            case_mapping=self._case_mapping
        )
        self._pending["whowas"] = parsing.NormalizingDict(
            case_mapping=self._case_mapping
        )
        self._whois_info = parsing.NormalizingDict(case_mapping=self._case_mapping)
        self._whowas_info = parsing.NormalizingDict(case_mapping=self._case_mapping)

        # Misc.
        self.motd = None
        self.channels = parsing.NormalizingDict(
            self.channels, case_mapping=self._case_mapping
        )
        self.users = parsing.NormalizingDict(
            self.users, case_mapping=self._case_mapping
        )

    def _reset_connection_attributes(self):
        super()._reset_connection_attributes()
        self.password = None

    def _create_channel(self, channel):
        super()._create_channel(channel)
        self.channels[channel].update(
            {
                "modes": {},
                "topic": None,
                "topic_by": None,
                "topic_set": None,
                "created": None,
                "password": None,
                "banlist": None,
                "public": True,
            }
        )

    def _create_user(self, nickname):
        super()._create_user(nickname)
        if nickname in self.users:
            self.users[nickname].update(
                {
                    "away": False,
                    "away_message": None,
                }
            )

    async def _rename_user(self, user, new):
        await super()._rename_user(user, new)

        # Rename in mode lists, too.
        for ch in self.channels.values():
            for status in self._nickname_prefixes.values():
                if status in ch["modes"] and user in ch["modes"][status]:
                    ch["modes"][status].remove(user)
                    ch["modes"][status].append(new)

    def _destroy_user(self, user, channel=None):
        if channel:
            channels = [self.channels[channel]]
        else:
            channels = self.channels.values()

        # Remove user from status list too.
        for ch in channels:
            for status in self._nickname_prefixes.values():
                if status in ch["modes"] and user in ch["modes"][status]:
                    ch["modes"][status].remove(user)

    def _parse_user(self, data):
        if data:
            nickname, username, host = parsing.parse_user(data)

            metadata = {"nickname": nickname}
            if username:
                metadata["username"] = username
            if host:
                metadata["hostname"] = host
        else:
            return None, {}
        return nickname, metadata

    def _parse_user_modes(self, user, modes, current=None):
        if current is None:
            current = self.users[user]["modes"]
        return parsing.parse_modes(modes, current, behaviour=self._user_modes_behaviour)

    def _parse_channel_modes(self, channel, modes, current=None):
        if current is None:
            current = self.channels[channel]["modes"]
        return parsing.parse_modes(
            modes, current, behaviour=self._channel_modes_behaviour
        )

    def _format_host_range(self, host, range, allow_everything=False):
        # IPv4?
        try:
            addr = ipaddress.IPv4Network(host, strict=False)
            max = 4 if allow_everything else 3

            # Round up subnet to nearest octet.
            subnet = addr.prefixlen + (8 - addr.prefixlen % 8)
            # Remove range mask.
            subnet -= min(range, max) * 8

            rangeaddr = addr.supernet(new_prefix=subnet).exploded.split("/", 1)[0]
            return rangeaddr.replace("0", "*")
        except ValueError:
            pass

        # IPv6?
        try:
            addr = ipaddress.IPv6Network(host, strict=False)
            max = 4 if allow_everything else 3

            # Round up subnet to nearest 32-et.
            subnet = addr.prefixlen + (32 - addr.prefixlen % 32)
            # Remove range mask.
            subnet -= min(range, max) * 32

            rangeaddr = addr.supernet(new_prefix=subnet).exploded.split("/", 1)[0]
            return rangeaddr.replace(":0000", ":*")
        except ValueError:
            pass

        # Host?
        if "." in host:
            # Split pieces.
            pieces = host.split(".")
            max = len(pieces)
            if not allow_everything:
                max -= 1

            # Figure out how many to mask.
            to_mask = min(range, max)
            # Mask pieces.
            pieces[:to_mask] = "*" * to_mask
            return ".".join(pieces)

        # Wat.
        if allow_everything and range >= 4:
            return "*"
        return host

    ## Connection.

    async def connect(self, hostname=None, port=None, password=None, **kwargs):
        port = port or protocol.DEFAULT_PORT

        # Connect...
        await super().connect(hostname, port, **kwargs)

        # Check if a password was provided and we don't already have one
        if password is not None and not self.password:
            # if so, set the password.
            self.password = password
        # And initiate the IRC connection.
        await self._register()

    async def _register(self):
        """Perform IRC connection registration."""
        if self.registered:
            return
        self._registration_attempts += 1

        # Don't throttle during registration, most ircds don't care for flooding during registration,
        # and it might speed it up significantly.
        self.connection.throttle = False

        # Password first.
        if self.password:
            await self.rawmsg("PASS", self.password)

        # Then nickname...
        await self.set_nickname(self._attempt_nicknames.pop(0))
        # And now for the rest of the user information.
        await self.rawmsg("USER", self.username, "0", "*", self.realname)

    async def _registration_completed(self, message):
        """We're connected and registered. Receive proper nickname and emit fake NICK message."""
        if not self.registered:
            # Re-enable throttling.
            self.registered = True
            self.connection.throttle = True

            target = message.params[0]
            fakemsg = self._create_message("NICK", target, source=self.nickname)
            await self.on_raw_nick(fakemsg)

    ## Message handling.

    def _has_message(self):
        """Whether or not we have messages available for processing."""
        sep = protocol.MINIMAL_LINE_SEPARATOR.encode(self.encoding)
        return sep in self._receive_buffer

    def _create_message(self, command, *params, **kwargs):
        return parsing.RFC1459Message(command, params, **kwargs)

    def _parse_message(self):
        sep = protocol.MINIMAL_LINE_SEPARATOR.encode(self.encoding)
        message, _, data = self._receive_buffer.partition(sep)
        self._receive_buffer = data
        return parsing.RFC1459Message.parse(message + sep, encoding=self.encoding)

    ## IRC API.

    async def set_nickname(self, nickname):
        """
        Set nickname to given nickname.
        Users should only rely on the nickname actually being changed when receiving an on_nick_change callback.
        """
        await self.rawmsg("NICK", nickname)

    async def join(self, channel, password=None):
        """Join channel, optionally with password."""
        if self.in_channel(channel):
            raise AlreadyInChannel(channel)

        if password:
            await self.rawmsg("JOIN", channel, password)
        else:
            await self.rawmsg("JOIN", channel)

    async def part(self, channel, message=None):
        """Leave channel, optionally with message."""
        if not self.in_channel(channel):
            raise NotInChannel(channel)

        # Message seems to be an extension to the spec.
        if message:
            await self.rawmsg("PART", channel, message)
        else:
            await self.rawmsg("PART", channel)

    async def kick(self, channel, target, reason=None):
        """Kick user from channel."""
        if not self.in_channel(channel):
            raise NotInChannel(channel)

        if reason:
            await self.rawmsg("KICK", channel, target, reason)
        else:
            await self.rawmsg("KICK", channel, target)

    async def ban(self, channel, target, range=0):
        """
        Ban user from channel. Target can be either a user or a host.
        This command will not kick: use kickban() for that.
        range indicates the IP/host range to ban: 0 means ban only the IP/host,
        1+ means ban that many 'degrees' (up to 3 for IP addresses) of the host for range bans.
        """
        if target in self.users:
            host = self.users[target]["hostname"]
        else:
            host = target

        host = self._format_host_range(host, range)
        mask = self._format_host_mask("*", "*", host)
        await self.rawmsg("MODE", channel, "+b", mask)

    async def unban(self, channel, target, range=0):
        """
        Unban user from channel. Target can be either a user or a host.
        See ban documentation for the range parameter.
        """
        if target in self.users:
            host = self.users[target]["hostname"]
        else:
            host = target

        host = self._format_host_range(host, range)
        mask = self._format_host_mask("*", "*", host)
        await self.rawmsg("MODE", channel, "-b", mask)

    async def kickban(self, channel, target, reason=None, range=0):
        """
        Kick and ban user from channel.
        """
        await self.ban(channel, target, range)
        await self.kick(channel, target, reason)

    async def quit(self, message=None):
        """Quit network."""
        if message is None:
            message = self.DEFAULT_QUIT_MESSAGE

        await self.rawmsg("QUIT", message)
        await self.disconnect(expected=True)

    async def cycle(self, channel):
        """Rejoin channel."""
        if not self.in_channel(channel):
            raise NotInChannel(channel)

        password = self.channels[channel]["password"]
        await self.part(channel)
        await self.join(channel, password)

    async def message(self, target, message):
        """Message channel or user."""
        hostmask = self._format_user_mask(self.nickname)
        # Leeway.
        chunklen = (
            protocol.MESSAGE_LENGTH_LIMIT
            - len(
                "{hostmask} PRIVMSG {target} :".format(hostmask=hostmask, target=target)
            )
            - 25
        )

        for line in message.replace("\r", "").split("\n"):
            for chunk in chunkify(line, chunklen):
                # Some IRC servers respond with "412 Bot :No text to send" on empty messages.
                await self.rawmsg("PRIVMSG", target, chunk or " ")

    async def notice(self, target, message):
        """Notice channel or user."""
        hostmask = self._format_user_mask(self.nickname)
        # Leeway.
        chunklen = (
            protocol.MESSAGE_LENGTH_LIMIT
            - len(
                "{hostmask} NOTICE {target} :".format(hostmask=hostmask, target=target)
            )
            - 25
        )

        for line in message.replace("\r", "").split("\n"):
            for chunk in chunkify(line, chunklen):
                await self.rawmsg("NOTICE", target, chunk)

    async def set_mode(self, target, *modes):
        """
        Set mode on target.
        Users should only rely on the mode actually being changed when receiving an on_{channel,user}_mode_change callback.
        """
        if self.is_channel(target) and not self.in_channel(target):
            raise NotInChannel(target)

        await self.rawmsg("MODE", target, *modes)

    async def set_topic(self, channel, topic):
        """
        Set topic on channel.
        Users should only rely on the topic actually being changed when receiving an on_topic_change callback.
        """
        if not self.is_channel(channel):
            raise ValueError("Not a channel: {}".format(channel))
        if not self.in_channel(channel):
            raise NotInChannel(channel)

        await self.rawmsg("TOPIC", channel, topic)

    async def away(self, message):
        """Mark self as away."""
        await self.rawmsg("AWAY", message)

    async def back(self):
        """Mark self as not away."""
        await self.rawmsg("AWAY")

    async def whois(self, nickname):
        """
        Return information about user.
        This is an blocking asynchronous method: it has to be called from a coroutine, as follows:

            info = await self.whois('Nick')
        """
        # Some IRCDs are wonky and send strange responses for spaces in nicknames.
        # We just check if there's a space in the nickname -- if there is,
        # then we immediately set the future's result to None and don't bother checking.
        if protocol.ARGUMENT_SEPARATOR.search(nickname) is not None:
            result = asyncio.get_event_loop().create_future()
            result.set_result(None)
            return result

        if nickname not in self._pending["whois"]:
            await self.rawmsg("WHOIS", nickname)
            self._whois_info[nickname] = {
                "oper": False,
                "idle": 0,
                "away": False,
                "away_message": None,
            }

            # Create a future for when the WHOIS requests succeeds.
            self._pending["whois"][nickname] = asyncio.get_event_loop().create_future()

        return await self._pending["whois"][nickname]

    async def whowas(self, nickname):
        """
        Return information about offline user.
        This is an blocking asynchronous method: it has to be called from a coroutine, as follows:

            info = await self.whowas('Nick')
        """
        # Same treatment as nicknames in whois.
        if protocol.ARGUMENT_SEPARATOR.search(nickname) is not None:
            result = asyncio.get_event_loop().create_future()
            result.set_result(None)
            return result

        if nickname not in self._pending["whowas"]:
            await self.rawmsg("WHOWAS", nickname)
            self._whowas_info[nickname] = {}

            # Create a future for when the WHOWAS requests succeeds.
            self._pending["whowas"][nickname] = asyncio.get_event_loop().create_future()

        return await self._pending["whowas"][nickname]

    ## IRC helpers.

    def normalize(self, input):
        return parsing.normalize(input, case_mapping=self._case_mapping)

    def is_channel(self, chan):
        return any(chan.startswith(prefix) for prefix in self._channel_prefixes)

    def is_same_nick(self, left, right):
        """Check if given nicknames are equal in the server's case mapping."""
        return self.normalize(left) == self.normalize(right)

    def is_same_channel(self, left, right):
        """Check if given nicknames are equal in the server's case mapping."""
        return self.normalize(left) == self.normalize(right)

    ## Overloadable callbacks.

    async def on_connect(self):
        # Auto-join channels.
        for channel in self._autojoin_channels:
            await self.join(channel)

        # super call
        await super().on_connect()

    async def on_invite(self, channel, by):
        """Callback called when the client was invited into a channel by someone."""
        ...

    async def on_user_invite(self, target, channel, by):
        """Callback called when another user was invited into a channel by someone."""
        ...

    async def on_join(self, channel, user):
        """Callback called when a user, possibly the client, has joined the channel."""
        ...

    async def on_kill(self, target, by, reason):
        """Callback called when a user, possibly the client, was killed from the server."""
        ...

    async def on_kick(self, channel, target, by, reason=None):
        """Callback called when a user, possibly the client, was kicked from a channel."""
        ...

    async def on_mode_change(self, channel, modes, by):
        """Callback called when the mode on a channel was changed."""
        ...

    async def on_user_mode_change(self, modes):
        """Callback called when a user mode change occurred for the client."""
        ...

    async def on_message(self, target, by, message):
        """Callback called when the client received a message."""
        ...

    async def on_channel_message(self, target, by, message):
        """Callback received when the client received a message in a channel."""
        ...

    async def on_private_message(self, target, by, message):
        """Callback called when the client received a message in private."""
        ...

    async def on_nick_change(self, old, new):
        """Callback called when a user, possibly the client, changed their nickname."""
        ...

    async def on_notice(self, target, by, message):
        """Callback called when the client received a notice."""
        ...

    async def on_channel_notice(self, target, by, message):
        """Callback called when the client received a notice in a channel."""
        ...

    async def on_private_notice(self, target, by, message):
        """Callback called when the client received a notice in private."""
        ...

    async def on_part(self, channel, user, message=None):
        """Callback called when a user, possibly the client, left a channel."""
        ...

    async def on_topic_change(self, channel, message, by):
        """Callback called when the topic for a channel was changed."""
        ...

    async def on_quit(self, user, message=None):
        """Callback called when a user, possibly the client, left the network."""
        ...

    ## Callback handlers.

    async def on_raw_error(self, message):
        """Server encountered an error and will now close the connection."""
        error = protocol.ServerError(" ".join(message.params))
        await self.on_data_error(error)

    async def on_raw_pong(self, message):
        self.logger.debug(">> PONG received")

    async def on_raw_invite(self, message):
        """INVITE command."""
        nick, metadata = self._parse_user(message.source)
        await self._sync_user(nick, metadata)

        target, channel = message.params
        target, metadata = self._parse_user(target)

        if self.is_same_nick(self.nickname, target):
            await self.on_invite(channel, nick)
        else:
            await self.on_user_invite(target, channel, nick)

    async def on_raw_join(self, message):
        """JOIN command."""
        nick, metadata = self._parse_user(message.source)
        await self._sync_user(nick, metadata)

        channels = message.params[0].split(",")
        if self.is_same_nick(self.nickname, nick):
            # Add to our channel list, we joined here.
            for channel in channels:
                if not self.in_channel(channel):
                    self._create_channel(channel)

                # Request channel mode from IRCd.
                await self.rawmsg("MODE", channel)
        else:
            # Add user to channel user list.
            for channel in channels:
                if self.in_channel(channel):
                    self.channels[channel]["users"].add(nick)

        for channel in channels:
            await self.on_join(channel, nick)

    async def on_raw_kick(self, message):
        """KICK command."""
        kicker, kickermeta = self._parse_user(message.source)
        await self._sync_user(kicker, kickermeta)

        if len(message.params) > 2:
            channels, targets, reason = message.params
        else:
            channels, targets = message.params
            reason = None

        channels = channels.split(",")
        targets = targets.split(",")

        for channel, target in itertools.product(channels, targets):
            target, targetmeta = self._parse_user(target)
            await self._sync_user(target, targetmeta)

            if self.is_same_nick(target, self.nickname):
                self._destroy_channel(channel)
            else:
                # Update nick list on channel.
                if self.in_channel(channel):
                    self._destroy_user(target, channel)

            await self.on_kick(channel, target, kicker, reason)

    async def on_raw_kill(self, message):
        """KILL command."""
        by, bymeta = self._parse_user(message.source)
        target, targetmeta = self._parse_user(message.params[0])
        reason = message.params[1]

        await self._sync_user(target, targetmeta)
        if by in self.users:
            await self._sync_user(by, bymeta)

        await self.on_kill(target, by, reason)
        if self.is_same_nick(self.nickname, target):
            await self.disconnect(expected=False)
        else:
            self._destroy_user(target)

    async def on_raw_mode(self, message):
        """MODE command."""
        nick, metadata = self._parse_user(message.source)
        target, modes = message.params[0], message.params[1:]

        await self._sync_user(nick, metadata)
        if self.is_channel(target):
            if self.in_channel(target):
                # Parse modes.
                self.channels[target]["modes"] = self._parse_channel_modes(
                    target, modes
                )

                await self.on_mode_change(target, modes, nick)
        else:
            target, targetmeta = self._parse_user(target)
            await self._sync_user(target, targetmeta)

            # Update own modes.
            if self.is_same_nick(self.nickname, nick):
                self._mode = self._parse_user_modes(nick, modes, current=self._mode)

            await self.on_user_mode_change(modes)

    async def on_raw_nick(self, message):
        """NICK command."""
        nick, metadata = self._parse_user(message.source)
        new = message.params[0]

        await self._sync_user(nick, metadata)
        # Acknowledgement of nickname change: set it internally, too.
        # Alternatively, we were force nick-changed. Nothing much we can do about it.
        if self.is_same_nick(self.nickname, nick):
            self.nickname = new

        # Go through all user lists and replace.
        await self._rename_user(nick, new)

        # Call handler.
        await self.on_nick_change(nick, new)

    async def on_raw_notice(self, message):
        """NOTICE command."""
        nick, metadata = self._parse_user(message.source)
        target, message = message.params

        await self._sync_user(nick, metadata)

        await self.on_notice(target, nick, message)
        if self.is_channel(target):
            await self.on_channel_notice(target, nick, message)
        else:
            await self.on_private_notice(target, nick, message)

    async def on_raw_part(self, message):
        """PART command."""
        nick, metadata = self._parse_user(message.source)
        channels = message.params[0].split(",")
        if len(message.params) > 1:
            reason = message.params[1]
        else:
            reason = None

        await self._sync_user(nick, metadata)
        if self.is_same_nick(self.nickname, nick):
            # We left the channel. Remove from channel list. :(
            for channel in channels:
                if self.in_channel(channel):
                    await self.on_part(channel, nick, reason)
                    self._destroy_channel(channel)
        else:
            # Someone else left. Remove them.
            for channel in channels:
                await self.on_part(channel, nick, reason)
                self._destroy_user(nick, channel)

    async def on_raw_ping(self, message):
        """PING command."""
        # Respond with a pong.
        await self.rawmsg("PONG", *message.params)

    async def on_raw_privmsg(self, message):
        """PRIVMSG command."""
        nick, metadata = self._parse_user(message.source)
        target, message = message.params

        await self._sync_user(nick, metadata)

        await self.on_message(target, nick, message)
        if self.is_channel(target):
            await self.on_channel_message(target, nick, message)
        else:
            await self.on_private_message(target, nick, message)

    async def on_raw_quit(self, message):
        """QUIT command."""
        nick, metadata = self._parse_user(message.source)

        await self._sync_user(nick, metadata)
        if message.params:
            reason = message.params[0]
        else:
            reason = None

        await self.on_quit(nick, reason)
        # Remove user from database.
        if not self.is_same_nick(self.nickname, nick):
            self._destroy_user(nick)
        # Else, we quit.
        elif self.connected:
            await self.disconnect()

    async def on_raw_topic(self, message):
        """TOPIC command."""
        setter, settermeta = self._parse_user(message.source)
        target, topic = message.params

        await self._sync_user(setter, settermeta)

        # Update topic in our own channel list.
        if self.in_channel(target):
            self.channels[target]["topic"] = topic
            self.channels[target]["topic_by"] = setter
            self.channels[target]["topic_set"] = datetime.datetime.now()

        await self.on_topic_change(target, topic, setter)

    ## Numeric responses.

    # Since RFC1459 specifies no specific banner message upon completion of registration,
    # take any of the below commands as an indication that registration succeeded.

    on_raw_001 = _registration_completed  # Welcome message.
    on_raw_002 = _registration_completed  # Server host.
    on_raw_003 = _registration_completed  # Server creation time.

    async def on_raw_004(self, message):
        """Basic server information."""
        target, hostname, ircd, user_modes, channel_modes = message.params[:5]

        # Set valid channel and user modes.
        self._channel_modes = set(channel_modes)
        self._user_modes = set(user_modes)

    on_raw_008 = _registration_completed  # Server notice mask.
    on_raw_042 = _registration_completed  # Unique client ID.
    on_raw_250 = _registration_completed  # Connection statistics.
    on_raw_251 = _registration_completed  # Amount of users online.
    on_raw_252 = _registration_completed  # Amount of operators online.
    on_raw_253 = _registration_completed  # Amount of unknown connections.
    on_raw_254 = _registration_completed  # Amount of channels.
    on_raw_255 = _registration_completed  # Amount of local users and servers.
    on_raw_265 = _registration_completed  # Amount of local users.
    on_raw_266 = _registration_completed  # Amount of global users.

    async def on_raw_301(self, message):
        """User is away."""
        target, nickname, message = message.params
        info = {"away": True, "away_message": message}

        if nickname in self.users:
            await self._sync_user(nickname, info)
        if nickname in self._pending["whois"]:
            self._whois_info[nickname].update(info)

    async def on_raw_311(self, message):
        """WHOIS user info."""
        target, nickname, username, hostname, _, realname = message.params
        info = {"username": username, "hostname": hostname, "realname": realname}

        await self._sync_user(nickname, info)
        if nickname in self._pending["whois"]:
            self._whois_info[nickname].update(info)

    async def on_raw_312(self, message):
        """WHOIS server info."""
        target, nickname, server, serverinfo = message.params
        info = {"server": server, "server_info": serverinfo}

        if nickname in self._pending["whois"]:
            self._whois_info[nickname].update(info)
        if nickname in self._pending["whowas"]:
            self._whowas_info[nickname].update(info)

    async def on_raw_313(self, message):
        """WHOIS operator info."""
        target, nickname = message.params[:2]
        info = {"oper": True}

        if nickname in self._pending["whois"]:
            self._whois_info[nickname].update(info)

    async def on_raw_314(self, message):
        """WHOWAS user info."""
        target, nickname, username, hostname, _, realname = message.params
        info = {"username": username, "hostname": hostname, "realname": realname}

        if nickname in self._pending["whowas"]:
            self._whowas_info[nickname].update(info)

    on_raw_315 = BasicClient._ignored  # End of /WHO list.

    async def on_raw_317(self, message):
        """WHOIS idle time."""
        target, nickname, idle_time = message.params[:3]
        info = {
            "idle": int(idle_time),
        }

        if nickname in self._pending["whois"]:
            self._whois_info[nickname].update(info)

    async def on_raw_318(self, message):
        """End of /WHOIS list."""
        target, nickname = message.params[:2]

        # Mark future as done.
        if nickname in self._pending["whois"]:
            future = self._pending["whois"].pop(nickname)
            future.set_result(self._whois_info[nickname])

    async def on_raw_319(self, message):
        """WHOIS active channels."""
        target, nickname, channels = message.params[:3]
        channels = {channel.lstrip() for channel in channels.strip().split(" ")}
        info = {"channels": channels}

        if nickname in self._pending["whois"]:
            self._whois_info[nickname].update(info)

    async def on_raw_324(self, message):
        """Channel mode."""
        target, channel = message.params[:2]
        modes = message.params[2:]
        if not self.in_channel(channel):
            return

        self.channels[channel]["modes"] = self._parse_channel_modes(channel, modes)

    async def on_raw_329(self, message):
        """Channel creation time."""
        target, channel, timestamp = message.params
        if not self.in_channel(channel):
            return

        self.channels[channel]["created"] = datetime.datetime.fromtimestamp(
            int(timestamp)
        )

    async def on_raw_332(self, message):
        """Current topic on channel join."""
        target, channel, topic = message.params
        if not self.in_channel(channel):
            return

        self.channels[channel]["topic"] = topic

    async def on_raw_333(self, message):
        """Topic setter and time on channel join."""
        target, channel, setter, timestamp = message.params
        if not self.in_channel(channel):
            return

        # No need to sync user since this is most likely outdated info.
        self.channels[channel]["topic_by"] = self._parse_user(setter)[0]
        self.channels[channel]["topic_set"] = datetime.datetime.fromtimestamp(
            int(timestamp)
        )

    async def on_raw_353(self, message):
        """Response to /NAMES."""
        target, visibility, channel, names = message.params
        if not self.in_channel(channel):
            return

        # Set channel visibility.
        if visibility == protocol.PUBLIC_CHANNEL_SIGIL:
            self.channels[channel]["public"] = True
        elif visibility in (
            protocol.PRIVATE_CHANNEL_SIGIL,
            protocol.SECRET_CHANNEL_SIGIL,
        ):
            self.channels[channel]["public"] = False

        # Update channel user list.
        for entry in names.split(" "):
            statuses = []
            # Make entry safe for _parse_user().
            safe_entry = entry.lstrip("".join(self._nickname_prefixes.keys()))
            # Parse entry and update database.
            nick, metadata = self._parse_user(safe_entry)
            if not nick:
                # nonsense nickname
                continue
            await self._sync_user(nick, metadata)

            # Get prefixes.
            prefixes = set(entry.replace(safe_entry, ""))

            # Check, record and strip status prefixes.
            for prefix, status in self._nickname_prefixes.items():
                # Add to list of statuses by user.
                if prefix in prefixes:
                    statuses.append(status)

            # Add user to user list.
            self.channels[channel]["users"].add(nick)
            # And to channel modes..
            for status in statuses:
                if status not in self.channels[channel]["modes"]:
                    self.channels[channel]["modes"][status] = []
                self.channels[channel]["modes"][status].append(nick)

    on_raw_366 = BasicClient._ignored  # End of /NAMES list.

    async def on_raw_375(self, message):
        """Start message of the day."""
        await self._registration_completed(message)
        self.motd = message.params[1] + "\n"

    async def on_raw_372(self, message):
        """Append message of the day."""
        self.motd += message.params[1] + "\n"

    async def on_raw_376(self, message):
        """End of message of the day."""
        self.motd += message.params[1] + "\n"

        # MOTD is done, let's tell our bot the connection is ready.
        await self.on_connect()

    async def on_raw_401(self, message):
        """No such nick/channel."""
        nickname = message.params[1]

        # Remove nickname from whois requests if it involves one of ours.
        if nickname in self._pending["whois"]:
            future = self._pending["whois"].pop(nickname)
            future.set_result(None)
            del self._whois_info[nickname]

    async def on_raw_402(self, message):
        """No such server."""
        return await self.on_raw_401(message)

    async def on_raw_422(self, message):
        """MOTD is missing."""
        await self._registration_completed(message)
        self.motd = None
        await self.on_connect()

    async def on_raw_421(self, message):
        """Server responded with 'unknown command'."""
        self.logger.warning(
            'Server responded with "Unknown command: %s"', message.params[0]
        )

    async def on_raw_432(self, message):
        """Erroneous nickname."""
        if not self.registered:
            # Nothing else we can do than try our next nickname.
            await self.on_raw_433(message)

    async def on_raw_433(self, message):
        """Nickname in use."""
        if not self.registered:
            self._registration_attempts += 1
            # Attempt to set new nickname.
            if self._attempt_nicknames:
                await self.set_nickname(self._attempt_nicknames.pop(0))
            else:
                await self.set_nickname(
                    self._nicknames[0]
                    + "_" * (self._registration_attempts - len(self._nicknames))
                )

    on_raw_436 = (
        BasicClient._ignored
    )  # Nickname collision, issued right before the server kills us.

    async def on_raw_451(self, message):
        """We have to register first before doing X."""
        self.logger.warning(
            "Attempted to send non-registration command before being registered."
        )

    on_raw_462 = BasicClient._ignored  # You may not re-register.


## Helpers.


def chunkify(message, chunksize):
    if not message:
        yield message
    else:
        while message:
            chunk = message[:chunksize]
            message = message[chunksize:]
            yield chunk
