import types

from jsonschema import draft4_format_checker, ValidationError
from jsonschema.validators import Draft4Validator, extend

ERROR_TYPE = "Not valid value '{}' for type {}:{}"


def to_bool(value):
    if isinstance(value, str):
        value = value.lower()
    if value in ('true', '1', ''):
        return True
    elif value in ('false', '0'):
        return False
    else:
        raise ValueError('Not valid value for bool: {}'.format(value))


types_mapping = {
    'boolean': to_bool,
    'integer': int,
    'number': {
        None: float,
        'integer': int,
        'float': float,
        'double': float,
    },
}


def convert(name, value, sw_type, sw_format, errors):
    conv = types_mapping.get(sw_type, lambda x: x)

    if isinstance(conv, dict):
        conv = conv.get(sw_format, conv[None])

    if sw_type in ('string', 'file'):
        return value
    elif isinstance(value, (list, tuple)):
        result = []
        for i, v in enumerate(value):
            try:
                result.append(conv(v))
            except (ValueError, TypeError):
                errors['{}.{}'.format(name, i)].add(
                    ERROR_TYPE.format(v, sw_type, sw_format)
                )
        return result
    else:
        try:
            return conv(value)
        except (ValueError, TypeError):
            errors[name].add(
                ERROR_TYPE.format(value, sw_type, sw_format)
            )


class ConvertTo(BaseException):
    def __init__(self, new_value):
        self.new_value = new_value


class WithMessages(BaseException):
    def __init__(self, *messages):
        self.messages = messages


class RequiredException(ValidationError):
    def __init__(self, *args, prop, **kwargs):
        super().__init__(*args, **kwargs)
        self.prop = prop


class Validator:
    check_schema = True
    format_checker = draft4_format_checker

    @classmethod
    def factory(cls, *args, **kwargs):
        factory = extend(Draft4Validator, dict(required=cls._required))
        return factory(*args, format_checker=cls.format_checker, **kwargs)

    @staticmethod
    def _required(validator, required, instance, schema):
        if not validator.is_type(instance, "object"):
            return
        for property in required:
            if property not in instance:
                yield RequiredException("required", prop=property)

    @staticmethod
    def _raises(raises):
        r = [ConvertTo, WithMessages]
        if isinstance(raises, (list, tuple)):
            r.extend(raises)
        else:
            r.append(raises)
        return tuple(r)

    @staticmethod
    def _try_messages(v):
        if isinstance(v, types.GeneratorType):
            messages = []
            try:
                while True:
                    messages.append(next(v))
            except StopIteration as e:
                v = e.value
            if messages:
                raise WithMessages(*messages)
        return v

    @classmethod
    def converts_format(cls, name, raises=()):
        raises = cls._raises(raises)

        def wraper(f):
            @cls.format_checker.checks(name, raises)
            def conv(value):
                v = cls._try_messages(f(value))
                raise ConvertTo(v)
        return wraper

    @classmethod
    def checks_format(cls, name, raises=()):
        raises = cls._raises(raises)

        def wraper(f):
            @cls.format_checker.checks(name, raises)
            def conv(value):
                v = cls._try_messages(f(value))
                return v is not False
        return wraper

    def __init__(self, schema):
        self.schema = schema
        self.validator = self.factory(schema)
        if self.check_schema:
            self.validator.check_schema(schema)

    def validate(self, value, errors):
        for error in self.validator.descend(value, self.schema):
            if error.path:
                path = tuple(error.path)
            else:
                path = ()
            if isinstance(error.cause, ConvertTo):
                if not path:
                    return error.cause.new_value
                base = value
                *path, tail = path
                for i in path:
                    base = base[i]
                base[tail] = error.cause.new_value
                continue
            elif isinstance(error.cause, WithMessages):
                messages = error.cause.messages
            elif isinstance(error, RequiredException):
                path += error.prop,
                messages = error.message,
            else:
                messages = error.message,
            errors[path].update(messages)
        return value


COLLECTION_SEP = {'csv': ',', 'ssv': ' ', 'tsv': '\t', 'pipes': '|'}


def get_collection(source, name, collection_format, default):
    """get collection named `name` from the given `source` that
    formatted accordingly to `collection_format`.
    """
    if collection_format in COLLECTION_SEP:
        separator = COLLECTION_SEP[collection_format]
        value = source.get(name, None)
        if value is None:
            return default
        return value.split(separator)
    if collection_format == 'brackets':
        return source.getall(name + '[]', default)
    else:                       # format: multi
        return source.getall(name, default)