## parsing.py
# RFC1459 parsing and construction.
import collections.abc
import pydle.protocol
from . import protocol


class RFC1459Message(pydle.protocol.Message):
    def __init__(self, command, params, source=None, _raw=None, _valid=True, **kw):
        self._kw = kw
        self._kw['command'] = command
        self._kw['params'] = params
        self._kw['source'] = source
        self._valid = _valid
        self._raw = _raw
        self.__dict__.update(self._kw)

    @classmethod
    def parse(cls, line, encoding=pydle.protocol.DEFAULT_ENCODING):
        """
        Parse given line into IRC message structure.
        Returns a Message.
        """
        valid = True

        # Decode message.
        try:
            message = line.decode(encoding)
        except UnicodeDecodeError:
            # Try our fallback encoding.
            message = line.decode(pydle.protocol.FALLBACK_ENCODING)

        # Sanity check for message length.
        if len(message) > protocol.MESSAGE_LENGTH_LIMIT:
            valid = False

        # Strip message separator.
        if message.endswith(protocol.LINE_SEPARATOR):
            message = message[:-len(protocol.LINE_SEPARATOR)]
        elif message.endswith(protocol.MINIMAL_LINE_SEPARATOR):
            message = message[:-len(protocol.MINIMAL_LINE_SEPARATOR)]

        # Sanity check for forbidden characters.
        if any(ch in message for ch in protocol.FORBIDDEN_CHARACTERS):
            valid = False

        # Extract message sections.
        # Format: (:source)? command parameter*
        if message.startswith(':'):
            parts = protocol.ARGUMENT_SEPARATOR.split(message[1:], 2)
        else:
            parts = [None] + protocol.ARGUMENT_SEPARATOR.split(message, 1)

        if len(parts) == 3:
            source, command, raw_params = parts
        elif len(parts) == 2:
            source, command = parts
            raw_params = ''
        else:
            raise pydle.protocol.ProtocolViolation('Improper IRC message format: not enough elements.', message=message)

        # Sanity check for command.
        if not protocol.COMMAND_PATTERN.match(command):
            valid = False

        # Extract parameters properly.
        # Format: (word|:sentence)*

        # Only parameter is a 'trailing' sentence.
        if raw_params.startswith(protocol.TRAILING_PREFIX):
            params = [raw_params[len(protocol.TRAILING_PREFIX):]]
        # We have a sentence in our parameters.
        elif ' ' + protocol.TRAILING_PREFIX in raw_params:
            index = raw_params.find(' ' + protocol.TRAILING_PREFIX)

            # Get all single-word parameters.
            params = protocol.ARGUMENT_SEPARATOR.split(raw_params[:index].rstrip(' '))
            # Extract last parameter as sentence
            params.append(raw_params[index + len(protocol.TRAILING_PREFIX) + 1:])
        # We have some parameters, but no sentences.
        elif raw_params:
            params = protocol.ARGUMENT_SEPARATOR.split(raw_params)
        # No parameters.
        else:
            params = []

        # Commands can be either [a-zA-Z]+ or [0-9]+.
        # In the former case, force it to uppercase.
        # In the latter case (a numeric command), try to represent it as such.
        try:
            command = int(command)
        except ValueError:
            command = command.upper()

        # Return parsed message.
        return RFC1459Message(command, params, source=source, _valid=valid, _raw=message)

    def construct(self, force=False):
        """ Construct a raw IRC message. """
        # Sanity check for command.
        command = str(self.command)
        if not protocol.COMMAND_PATTERN.match(command) and not force:
            raise pydle.protocol.ProtocolViolation('The constructed command does not follow the command pattern ({pat})'.format(pat=protocol.COMMAND_PATTERN.pattern), message=command)
        message = command.upper()

        # Add parameters.
        if not self.params:
            message += ' '
        for idx, param in enumerate(self.params):
            # Trailing parameter?
            if not param or ' ' in param or param[0] == ':':
                if idx + 1 < len(self.params) and not force:
                    raise pydle.protocol.ProtocolViolation('Only the final parameter of an IRC message can be trailing and thus contain spaces, or start with a colon.', message=param)
                message += ' ' + protocol.TRAILING_PREFIX + param
            # Regular parameter.
            else:
                message += ' ' + param

        # Prepend source.
        if self.source:
            message = ':' + self.source + ' ' + message

        # Sanity check for characters.
        if any(ch in message for ch in protocol.FORBIDDEN_CHARACTERS) and not force:
            raise pydle.protocol.ProtocolViolation('The constructed message contains forbidden characters ({chs}).'.format(chs=', '.join(protocol.FORBIDDEN_CHARACTERS)), message=message)

        # Sanity check for length.
        message += protocol.LINE_SEPARATOR
        if len(message) > protocol.MESSAGE_LENGTH_LIMIT and not force:
            raise pydle.protocol.ProtocolViolation('The constructed message is too long. ({len} > {maxlen})'.format(len=len(message), maxlen=protocol.MESSAGE_LENGTH_LIMIT), message=message)

        return message


def normalize(input, case_mapping=protocol.DEFAULT_CASE_MAPPING):
    """ Normalize input according to case mapping. """
    if case_mapping not in protocol.CASE_MAPPINGS:
        raise pydle.protocol.ProtocolViolation('Unknown case mapping ({})'.format(case_mapping))

    input = input.lower()

    if case_mapping in ('rfc1459', 'rfc1459-strict'):
        input = input.replace('{', '[').replace('}', ']').replace('|', '\\')
    if case_mapping == 'rfc1459':
        input = input.replace('~', '^')

    return input


class NormalizingDict(collections.abc.MutableMapping):
    """ A dict that normalizes entries according to the given case mapping. """
    def __init__(self, *args, case_mapping):
        self.storage = {}
        self.case_mapping = case_mapping
        self.update(dict(*args))

    def __getitem__(self, key):
        if not isinstance(key, str):
            raise KeyError(key)
        return self.storage[normalize(key, case_mapping=self.case_mapping)]

    def __setitem__(self, key, value):
        if not isinstance(key, str):
            raise KeyError(key)
        self.storage[normalize(key, case_mapping=self.case_mapping)] = value

    def __delitem__(self, key):
        if not isinstance(key, str):
            raise KeyError(key)
        del self.storage[normalize(key, case_mapping=self.case_mapping)]

    def __iter__(self):
        return iter(self.storage)

    def __len__(self):
        return len(self.storage)

    def __repr__(self):
        return '{mod}.{cls}({dict}, case_mapping={cm})'.format(
            mod=__name__, cls=self.__class__.__name__,
            dict=self.storage, cm=self.case_mapping)


# Parsing.

def parse_user(raw):
    """ Parse nick(!user(@host)?)? structure. """
    nick = raw
    user = None
    host = None

    # Attempt to extract host.
    if protocol.HOST_SEPARATOR in raw:
        raw, host = raw.split(protocol.HOST_SEPARATOR)
    # Attempt to extract user.
    if protocol.USER_SEPARATOR in raw:
        nick, user = raw.split(protocol.USER_SEPARATOR)

    return nick, user, host


def parse_modes(modes, current, behaviour):
    """ Parse mode change string(s) and return updated dictionary. """
    current = current.copy()
    modes = modes[:]

    # Iterate in a somewhat odd way over the list because we want to modify it during iteration.
    i = 0
    while i < len(modes):
        piece = modes[i]
        add = True
        sigiled = False

        for mode in piece:
            # Set mode to addition or deletion of modes.
            if mode == '+':
                add = True
                sigiled = True
                continue
            if mode == '-':
                add = False
                sigiled = True
                continue

            # Find mode behaviour.
            for type, affected in behaviour.items():
                if mode in affected:
                    break
            else:
                # If we don't have a behaviour for this mode, assume it has no parameters...
                type = protocol.BEHAVIOUR_NO_PARAMETER

            # Don't parse modes that are meant for list retrieval.
            if type == protocol.BEHAVIOUR_LIST and not sigiled:
                continue

            # Do we require a parameter?
            if type in (protocol.BEHAVIOUR_PARAMETER, protocol.BEHAVIOUR_LIST) or (type == protocol.BEHAVIOUR_PARAMETER_ON_SET and add):
                # Do we _have_ a parameter?
                if i + 1 == len(modes):
                    raise pydle.protocol.ProtocolViolation('Attempted to parse mode with parameter ({s}{mode}) but no parameters left in mode list.'.format(
                        mode=mode, s='+' if add else '-'), ' '.join(modes))
                param = modes.pop(i + 1)

            # Now update the actual mode dict with our new values.
            if type in (protocol.BEHAVIOUR_PARAMETER, protocol.BEHAVIOUR_LIST):
                # Add/remove parameter from list.
                if add:
                    if mode not in current:
                        current[mode] = []
                    current[mode].append(param)
                else:
                    if mode in current and param in current[mode]:
                        current[mode].remove(param)
            elif type == protocol.BEHAVIOUR_PARAMETER_ON_SET and add:
                # Simply set parameter.
                current[mode] = param
            else:
                # Simply add/remove option.
                if add:
                    current[mode] = True
                else:
                    if mode in current:
                        del current[mode]
        i += 1

    return current
