# -*- coding: utf-8 -*-
"""Validation classes for various types of data."""

from __future__ import unicode_literals

import re
from operator import attrgetter

from marshmallow.compat import basestring, text_type, zip_longest
from marshmallow.exceptions import ValidationError


class Validator(object):
    """Base abstract class for validators.

    .. note::
        This class does not provide any behavior. It is only used to
        add a useful `__repr__` implementation for validators.
    """

    def __repr__(self):
        args = self._repr_args()
        args = '{0}, '.format(args) if args else ''

        return (
            '<{self.__class__.__name__}({args}error={self.error!r})>'
            .format(self=self, args=args)
        )

    def _repr_args(self):
        """A string representation of the args passed to this validator. Used by
        `__repr__`.
        """
        return ''


class URL(Validator):
    """Validate a URL.

    :param bool relative: Whether to allow relative URLs.
    :param str error: Error message to raise in case of a validation error.
        Can be interpolated with `{input}`.
    :param set schemes: Valid schemes. By default, ``http``, ``https``,
        ``ftp``, and ``ftps`` are allowed.
    :param bool require_tld: Whether to reject non-FQDN hostnames
    """

    class RegexMemoizer(object):

        def __init__(self):
            self._memoized = {}

        def _regex_generator(self, relative, require_tld):
            return re.compile(r''.join((
                r'^',
                r'(' if relative else r'',
                r'(?:[a-z0-9\.\-\+]*)://',  # scheme is validated separately
                r'(?:[^:@]+?(:[^:@]*?)?@|)',  # basic auth
                r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+',
                r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|',  # domain...
                r'localhost|',  # localhost...
                (r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.?)|'
                 if not require_tld else r''),  # allow dotless hostnames
                r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|',  # ...or ipv4
                r'\[?[A-F0-9]*:[A-F0-9:]+\]?)',  # ...or ipv6
                r'(?::\d+)?',  # optional port
                r')?' if relative else r'',  # host is optional, allow for relative URLs
                r'(?:/?|[/?]\S+)$',
            )), re.IGNORECASE)

        def __call__(self, relative, require_tld):
            key = (relative, require_tld)
            if key not in self._memoized:
                self._memoized[key] = self._regex_generator(relative, require_tld)

            return self._memoized[key]

    _regex = RegexMemoizer()

    default_message = 'Not a valid URL.'
    default_schemes = set(['http', 'https', 'ftp', 'ftps'])

    # TODO; Switch position of `error` and `schemes` in 3.0
    def __init__(self, relative=False, error=None, schemes=None, require_tld=True):
        self.relative = relative
        self.error = error or self.default_message
        self.schemes = schemes or self.default_schemes
        self.require_tld = require_tld

    def _repr_args(self):
        return 'relative={0!r}'.format(self.relative)

    def _format_error(self, value):
        return self.error.format(input=value)

    def __call__(self, value):
        message = self._format_error(value)
        if not value:
            raise ValidationError(message)

        # Check first if the scheme is valid
        if '://' in value:
            scheme = value.split('://')[0].lower()
            if scheme not in self.schemes:
                raise ValidationError(message)

        regex = self._regex(self.relative, self.require_tld)

        if not regex.search(value):
            raise ValidationError(message)

        return value


class Email(Validator):
    """Validate an email address.

    :param str error: Error message to raise in case of a validation error. Can be
        interpolated with `{input}`.
    """

    USER_REGEX = re.compile(
        r"(^[-!#$%&'*+/=?^`{}|~\w]+(\.[-!#$%&'*+/=?^`{}|~\w]+)*$"  # dot-atom
        # quoted-string
        r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]'
        r'|\\[\001-\011\013\014\016-\177])*"$)', re.IGNORECASE | re.UNICODE)

    DOMAIN_REGEX = re.compile(
        # domain
        r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+'
        r'(?:[A-Z]{2,6}|[A-Z0-9-]{2,})$'
        # literal form, ipv4 address (SMTP 4.1.3)
        r'|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)'
        r'(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', re.IGNORECASE | re.UNICODE)

    DOMAIN_WHITELIST = ('localhost',)

    default_message = 'Not a valid email address.'

    def __init__(self, error=None):
        self.error = error or self.default_message

    def _format_error(self, value):
        return self.error.format(input=value)

    def __call__(self, value):
        message = self._format_error(value)

        if not value or '@' not in value:
            raise ValidationError(message)

        user_part, domain_part = value.rsplit('@', 1)

        if not self.USER_REGEX.match(user_part):
            raise ValidationError(message)

        if domain_part not in self.DOMAIN_WHITELIST:
            if not self.DOMAIN_REGEX.match(domain_part):
                try:
                    domain_part = domain_part.encode('idna').decode('ascii')
                except UnicodeError:
                    pass
                else:
                    if self.DOMAIN_REGEX.match(domain_part):
                        return value
                raise ValidationError(message)

        return value


class Range(Validator):
    """Validator which succeeds if the value it is passed is greater
    or equal to ``min`` and less than or equal to ``max``. If ``min``
    is not specified, or is specified as `None`, no lower bound
    exists. If ``max`` is not specified, or is specified as `None`,
    no upper bound exists.

    :param min: The minimum value (lower bound). If not provided, minimum
        value will not be checked.
    :param max: The maximum value (upper bound). If not provided, maximum
        value will not be checked.
    :param str error: Error message to raise in case of a validation error.
        Can be interpolated with `{input}`, `{min}` and `{max}`.
    """

    message_min = 'Must be at least {min}.'
    message_max = 'Must be at most {max}.'
    message_all = 'Must be between {min} and {max}.'

    def __init__(self, min=None, max=None, error=None):
        self.min = min
        self.max = max
        self.error = error

    def _repr_args(self):
        return 'min={0!r}, max={1!r}'.format(self.min, self.max)

    def _format_error(self, value, message):
        return (self.error or message).format(input=value, min=self.min, max=self.max)

    def __call__(self, value):
        if self.min is not None and value < self.min:
            message = self.message_min if self.max is None else self.message_all
            raise ValidationError(self._format_error(value, message))

        if self.max is not None and value > self.max:
            message = self.message_max if self.min is None else self.message_all
            raise ValidationError(self._format_error(value, message))

        return value


class Length(Range):
    """Validator which succeeds if the value passed to it has a
    length between a minimum and maximum. Uses len(), so it
    can work for strings, lists, or anything with length.

    :param int min: The minimum length. If not provided, minimum length
        will not be checked.
    :param int max: The maximum length. If not provided, maximum length
        will not be checked.
    :param int equal: The exact length. If provided, maximum and minimum
        length will not be checked.
    :param str error: Error message to raise in case of a validation error.
        Can be interpolated with `{input}`, `{min}` and `{max}`.
    """

    message_min = 'Shorter than minimum length {min}.'
    message_max = 'Longer than maximum length {max}.'
    message_all = 'Length must be between {min} and {max}.'
    message_equal = 'Length must be {equal}.'

    def __init__(self, min=None, max=None, error=None, equal=None):
        if equal is not None and any([min, max]):
            raise ValueError(
                'The `equal` parameter was provided, maximum or '
                'minimum parameter must not be provided.'
            )

        super(Length, self).__init__(min, max, error)
        self.equal = equal

    def _repr_args(self):
        return 'min={0!r}, max={1!r}, equal={2!r}'.format(self.min, self.max, self.equal)

    def _format_error(self, value, message):
        return (self.error or message).format(input=value, min=self.min, max=self.max,
                                              equal=self.equal)

    def __call__(self, value):
        length = len(value)

        if self.equal is not None:
            if length != self.equal:
                raise ValidationError(self._format_error(value, self.message_equal))
            return value

        if self.min is not None and length < self.min:
            message = self.message_min if self.max is None else self.message_all
            raise ValidationError(self._format_error(value, message))

        if self.max is not None and length > self.max:
            message = self.message_max if self.min is None else self.message_all
            raise ValidationError(self._format_error(value, message))

        return value


class Equal(Validator):
    """Validator which succeeds if the ``value`` passed to it is
    equal to ``comparable``.

    :param comparable: The object to compare to.
    :param str error: Error message to raise in case of a validation error.
        Can be interpolated with `{input}` and `{other}`.
    """

    default_message = 'Must be equal to {other}.'

    def __init__(self, comparable, error=None):
        self.comparable = comparable
        self.error = error or self.default_message

    def _repr_args(self):
        return 'comparable={0!r}'.format(self.comparable)

    def _format_error(self, value):
        return self.error.format(input=value, other=self.comparable)

    def __call__(self, value):
        if value != self.comparable:
            raise ValidationError(self._format_error(value))
        return value


class Regexp(Validator):
    """Validate ``value`` against the provided regex.

    :param regex: The regular expression string to use. Can also be a compiled
        regular expression pattern.
    :param flags: The regexp flags to use, for example re.IGNORECASE. Ignored
        if ``regex`` is not a string.
    :param str error: Error message to raise in case of a validation error.
        Can be interpolated with `{input}` and `{regex}`.
    """

    default_message = 'String does not match expected pattern.'

    def __init__(self, regex, flags=0, error=None):
        self.regex = re.compile(regex, flags) if isinstance(regex, basestring) else regex
        self.error = error or self.default_message

    def _repr_args(self):
        return 'regex={0!r}'.format(self.regex)

    def _format_error(self, value):
        return self.error.format(input=value, regex=self.regex.pattern)

    def __call__(self, value):
        if self.regex.match(value) is None:
            raise ValidationError(self._format_error(value))

        return value


class Predicate(Validator):
    """Call the specified ``method`` of the ``value`` object. The
    validator succeeds if the invoked method returns an object that
    evaluates to True in a Boolean context. Any additional keyword
    argument will be passed to the method.

    :param str method: The name of the method to invoke.
    :param str error: Error message to raise in case of a validation error.
        Can be interpolated with `{input}` and `{method}`.
    :param kwargs: Additional keyword arguments to pass to the method.
    """

    default_message = 'Invalid input.'

    def __init__(self, method, error=None, **kwargs):
        self.method = method
        self.error = error or self.default_message
        self.kwargs = kwargs

    def _repr_args(self):
        return 'method={0!r}, kwargs={1!r}'.format(self.method, self.kwargs)

    def _format_error(self, value):
        return self.error.format(input=value, method=self.method)

    def __call__(self, value):
        method = getattr(value, self.method)

        if not method(**self.kwargs):
            raise ValidationError(self._format_error(value))

        return value


class NoneOf(Validator):
    """Validator which fails if ``value`` is a member of ``iterable``.

    :param iterable iterable: A sequence of invalid values.
    :param str error: Error message to raise in case of a validation error. Can be
        interpolated using `{input}` and `{values}`.
    """

    default_message = 'Invalid input.'

    def __init__(self, iterable, error=None):
        self.iterable = iterable
        self.values_text = ', '.join(text_type(each) for each in self.iterable)
        self.error = error or self.default_message

    def _repr_args(self):
        return 'iterable={0!r}'.format(self.iterable)

    def _format_error(self, value):
        return self.error.format(
            input=value,
            values=self.values_text,
        )

    def __call__(self, value):
        try:
            if value in self.iterable:
                raise ValidationError(self._format_error(value))
        except TypeError:
            pass

        return value


class OneOf(Validator):
    """Validator which succeeds if ``value`` is a member of ``choices``.

    :param iterable choices: A sequence of valid values.
    :param iterable labels: Optional sequence of labels to pair with the choices.
    :param str error: Error message to raise in case of a validation error. Can be
        interpolated with `{input}`, `{choices}` and `{labels}`.
    """

    default_message = 'Not a valid choice.'

    def __init__(self, choices, labels=None, error=None):
        self.choices = choices
        self.choices_text = ', '.join(text_type(choice) for choice in self.choices)
        self.labels = labels if labels is not None else []
        self.labels_text = ', '.join(text_type(label) for label in self.labels)
        self.error = error or self.default_message

    def _repr_args(self):
        return 'choices={0!r}, labels={1!r}'.format(self.choices, self.labels)

    def _format_error(self, value):
        return self.error.format(
            input=value,
            choices=self.choices_text,
            labels=self.labels_text,
        )

    def __call__(self, value):
        try:
            if value not in self.choices:
                raise ValidationError(self._format_error(value))
        except TypeError:
            raise ValidationError(self._format_error(value))

        return value

    def options(self, valuegetter=text_type):
        """Return a generator over the (value, label) pairs, where value
        is a string associated with each choice. This convenience method
        is useful to populate, for instance, a form select field.

        :param valuegetter: Can be a callable or a string. In the former case, it must
            be a one-argument callable which returns the value of a
            choice. In the latter case, the string specifies the name
            of an attribute of the choice objects. Defaults to `str()`
            or `unicode()`.
        """
        valuegetter = valuegetter if callable(valuegetter) else attrgetter(valuegetter)
        pairs = zip_longest(self.choices, self.labels, fillvalue='')

        return ((valuegetter(choice), label) for choice, label in pairs)


class ContainsOnly(OneOf):
    """Validator which succeeds if ``value`` is a sequence and each element
    in the sequence is also in the sequence passed as ``choices``.

    :param iterable choices: Same as :class:`OneOf`.
    :param iterable labels: Same as :class:`OneOf`.
    :param str error: Same as :class:`OneOf`.
    """

    default_message = 'One or more of the choices you made was not acceptable.'

    def _format_error(self, value):
        value_text = ', '.join(text_type(val) for val in value)
        return super(ContainsOnly, self)._format_error(value_text)

    def __call__(self, value):
        choices = list(self.choices)

        if not value and choices:
            raise ValidationError(self._format_error(value))

        # We check list.index instead of using set.issubset so that
        # unhashable types are handled.
        for val in value:
            try:
                index = choices.index(val)
            except ValueError:
                raise ValidationError(self._format_error(value))
            else:
                del choices[index]

        return value
