# coding=utf-8
"""
This module contains a collection of fields classes
"""
import warnings

from abc import ABCMeta
from collections import Iterable
from itertools import chain
from datetime import datetime

from flyforms.common import is_set, jsonify_types, UNSET, FrozenDict
from flyforms.validators import *
from flyforms.validators import Validator
from flyforms.compatibility import string_types, with_metaclass, NoneType

__all__ = (
    "StringField",
    "EmailField",
    "IntField",
    "FloatField",
    "BooleanField",
    "Ip4Field",
    "ListField",
    "ArrayField",
    "DatetimeField",
    "DictField"
)


class Field(with_metaclass(ABCMeta, object)):
    """
    This the base class for all Fields.
    Fields instances reflect and validate data.
    """

    value_types = (type,)  # valid types of field value

    def __init__(self, required=True, choices=(), validators=(), **kwargs):
        """
        :param required: boolean flag is this field required or can be empty
        :type required: bool

        :param choices: iterable object contains possible values of this field
        :type choices: iterable

        :param validators: the additional validators for field
        :type validators: list of callable

        :param default: the default value of the field
        :type default: instance of value_types

        :raises TypeError: if passed arguments invalid
        """

        # Define validators container
        self.base_validators = []

        # Add required validation, if necessary
        if required:
            self.base_validators.append(RequiredValidator())
        self.required = required

        # Check given default value
        self.default = kwargs.get("default", UNSET)
        if not isinstance(self.default, self.value_types) and self.default is not UNSET:
            raise TypeError(
                "Bad default value type. \nExpected {}, got {}".format(self.value_types, type(self.default))
            )
        if required and self.default is not UNSET:
            warnings.warn(
                "Warning in field %s: default value given for required field. I'll be skipped.", RuntimeWarning
            )
            self.default = UNSET

        # Add type validation
        self.base_validators.append(TypedValidator(self.value_types))

        # Check is given choices iterable object
        if not isinstance(choices, Iterable):
            raise TypeError("Choices should be an iterable object")

        # Check choices type
        for choice in choices:
            if not isinstance(choice, self.value_types):
                raise TypeError(
                    "Bad value type in choices. \nExpected %s, got %s" % (
                        self.value_types,
                        type(choice)
                    )
                )

        # Add choices validation, if necessary
        if choices:
            self.base_validators.append(EntryValidator(choices))

        # Clean given list of validators, if necessary
        if not isinstance(validators, Iterable):
            validators = (validators,)

        # Check is given validators are callable object and extend validators list
        for validator in validators:
            if not isinstance(validator, Validator):
                raise TypeError("Validator should be a child of Validator superclass")
        self.custom_validators = validators

    def validate(self, value):
        """
        Validates given value via defined set of :code:`Validators`

        :param value: the value to validate
        """
        warnings.simplefilter('always', DeprecationWarning)
        warnings.warn("This method deprecated and will be removed in v1.0.0")
        warnings.simplefilter('ignore', DeprecationWarning)

        # Check is value set
        if not self.required and not is_set(value):
            return

        # Run all validators
        for validator in self.validators:
            validator(value)

    def is_valid(self, value):
        """
        The 'silent' variant of value validation.

        :param value: the value to validate

        :return: True if given value is valid, otherwise - False
        """

        warnings.simplefilter('always', DeprecationWarning)
        warnings.warn("This method deprecated and will be removed in v1.0.0")
        warnings.simplefilter('ignore', DeprecationWarning)

        # Check is value set
        if not self.required and not is_set(value):
            return True

        # Check value via validators
        for validator in self.validators:
            if not validator.is_valid(value):
                return False
        return True

    @property
    def validators(self):
        """
        Chain, contains :code:`base_validators` and :code:`custom_validators`
        """
        return chain(self.base_validators, self.custom_validators)

    def bind(self, value):
        """
        Validates and given value via defined set of :code:`Validators` and returns it
        If value is mutable obj (for example :code:`list`) it'll be converted to immutable (for example :code:`tuple`)

        :param value: the value to bind

        :param name: field name (optional)
        :type name: str

        :return: given value or :code:`field.default` if value is :py:data:`.UNSET`
        """
        # Check is value set
        if not self.required and not is_set(value):
            return self.default

        # Run all validators
        for validator in self.validators:
            validator(value)

        return value


class StringField(Field):
    """
    Reflects Python strings
    """

    value_types = string_types  # valid types of field value

    def __init__(self, min_length=None, max_length=None, regex="", **kwargs):
        """
        :param required: boolean flag is this field required
        :type required: bool

        :param min_length: the minimum length of the string
        :type min_length: int or None

        :param max_length: the maximum length of the string
        :type max_length: int or None

        :param regex: the regular expression to validate
        :type regex: str or regexp

        :param choices: iterable object contains possible values of this field
        :type choices: iterable

        :param validators: the additional validators for field
        :type validators: list of callable

        :param default: the default value of the field
        :type default: instance of value_types
        """
        super(StringField, self).__init__(**kwargs)

        if min_length:
            self.base_validators.append(MinLengthValidator(min_length, False))

        if max_length:
            self.base_validators.append(MaxLengthValidator(max_length, False))

        if regex:
            self.base_validators.append(RegexValidator(regex))


class EmailField(Field):
    """
    Reflects Python string corresponding to an email
    """

    value_types = string_types  # valid types of field value

    def __init__(self, **kwargs):
        """
        :param required: boolean flag is this field required
        :type required: bool

        :param choices: iterable object contains possible values of this field
        :type choices: iterable

        :param validators: the additional validators for field
        :type validators: list of callable

        :param default: the default value of the field
        :type default: instance of value_types
        """
        super(EmailField, self).__init__(**kwargs)

        self.base_validators.append(EmailValidator())


class IntField(Field):
    """
    Reflects Python integer (:code:`int`)
    """

    value_types = (int, NoneType)  # valid types of field value

    def __init__(self, min_value=None, max_value=None, **kwargs):
        """
        :param required: boolean flag is this field required
        :type required: bool

        :param min_value: the minimum valid value

        :param max_value: the maximum valid value

        :param choices: iterable object contains possible values of this field
        :type choices: iterable

        :param validators: the additional validators for field
        :type validators: list of callable

        :param default: the default value of the field
        :type default: instance of value_types
        """
        super(IntField, self).__init__(**kwargs)

        if min_value is not None:
            self.base_validators.append(MinValueValidator(min_value, False))
        if max_value is not None:
            self.base_validators.append(MaxValueValidator(max_value, False))


class FloatField(IntField):
    """
    Reflects Python float (:code:`float`)
    """

    value_types = (float, NoneType)  # valid types of field value


class BooleanField(Field):
    """
    Reflects Python boolean (:code:`bool`)
    """

    value_types = (bool, NoneType)  # valid types of field value

    def __init__(self, **kwargs):
        """
        :param required: boolean flag is this field required
        :type required: bool

        :param validators: the additional validators for field
        :type validators: list of callable

        :param default: the default value of the field
        :type default: instance of value_types

        """
        super(BooleanField, self).__init__(**kwargs)


class Ip4Field(Field):
    """
    Reflects Python string corresponding to an IPv4 address
    """

    value_types = string_types  # valid types of field value

    def __init__(self, **kwargs):
        """
        :param required: boolean flag is this field required
        :type required: bool

        :param choices: iterable object contains possible values of this field
        :type choices: iterable

        :param validators: the additional validators for field
        :type validators: list of callable

        :param default: the default value of the field
        :type default: instance of value_types

        """
        super(Ip4Field, self).__init__(**kwargs)

        self.base_validators.append(Ip4AddressValidator())


class ListField(Field):
    """
    Reflects iterable Python objects
    """
    value_types = Iterable

    def __init__(self, min_length=None, max_length=None, jsonify=True, **kwargs):
        """
        :param required: boolean flag is this field required or can be empty
        :type required: bool

        :param min_length: minimum iterable length
        :type min_length: int

        :param max_length: maximum iterable length
        :type max_length: int

        :param jsonify: if passed :code:`item_type` should be one of :py:data:`.jsonify_types`
        :type jsonify: bool

        :param validators: the additional validators for field
        :type validators: list of callable

        :param default: the default value of the field
        :type default: instance of value_types

        .. versionadded:: 0.2.0
        """

        super(ListField, self).__init__(**kwargs)

        if min_length:
            self.base_validators.append(MinLengthValidator(min_length, False))

        if max_length:
            self.base_validators.append(MaxLengthValidator(max_length, False))

        if jsonify:
            self.base_validators.append(JsonItemTypedValidator())

    def bind(self, value):
        value = super(ListField, self).bind(value)
        return tuple(value)


class ArrayField(ListField):
    """

    .. versionadded:: 0.2.0

    Reflects iterable objects where each item same type
    """

    def __init__(self, item_type, jsonify=True, **kwargs):
        """
        :param item_type: type of each item in the list

        :param min_length: minimum iterable length
        :type min_length: int

        :param max_length: maximum iterable length
        :type max_length: int

        :param jsonify: if passed :code:`item_type` should be one of :py:data:`.jsonify_types`
        :type jsonify: bool

        :param required: boolean flag is this field required or can be empty
        :type required: bool

        :param validators: the additional validators for field
        :type validators: list of callable

        :param default: the default value of the field
        :type default: instance of value_types
        """

        if jsonify and item_type not in jsonify_types:
            raise TypeError("Type %s is not supported for JSON encoding and decoding operations" % item_type)

        super(ArrayField, self).__init__(jsonify=jsonify, **kwargs)

        self.base_validators.append(ItemTypedValidator(item_type))


class DatetimeField(StringField):
    """
    .. versionadded:: 0.3.0

    A datetime field.

    Parse string contains datetime via datetime.strptime
    """

    def __init__(self, required=True, fmt="%Y-%m-%d %H:%M:%S", now=False):
        """
        :param fmt: datetime format
        :type fmt: str

        :param now: if passed the default value will be datetime.now()
        :type now: bool

        .. versionadded:: 0.3.0

        """

        super(DatetimeField, self).__init__(required=required)

        self.fmt = fmt

        if now:
            self.default = datetime.now().strftime(self.fmt)

    def bind(self, value):
        """
        Validates given datetime and wraps it into :py:class:`datetime.datetime`
        """
        try:
            return datetime.strptime(super(DatetimeField, self).bind(value), self.fmt)
        except ValueError as error:
            raise ValidationError(str(error))


class DictField(Field):
    """

    .. versionadded:: 0.3.0

    Reflects Python dict
    """

    value_types = dict

    def __init__(self, schema, **kwargs):
        """
        :param schema: template to dict validation
        :type schema: dict

        :param required: boolean flag is this field required or can be empty
        :type required: bool

        :param validators: the additional validators for field
        :type validators: list of callable

        :param default: the default value of the field
        :type default: instance of value_types
        """

        if not isinstance(schema, dict):
            raise TypeError("Schema for DictField should be dict instance")

        for key, value in schema.items():
            if not isinstance(value, Field):
                raise TypeError("Bad field %s in schema. Expected Field subclass instance, got %s" % type(value))

        super(DictField, self).__init__(**kwargs)
        self.schema = schema

    def bind(self, value):
        """
        Validates given dict and wraps it into :py:class:`.FrozenDict`
        """
        d = super(DictField, self).bind(value)
        assert isinstance(d, dict)
        if d is not self.default:
            for key, value in d.items():
                if key not in self.schema:
                    raise ValidationError("Unknown key }".format(key))
                d[key] = self.schema[key].bind(value)
        return FrozenDict(**d)
