# Source code for flyforms.form

    # coding=utf-8
    from inspect import isclass

    from flyforms.validators import ValidationError
    from flyforms.common import UNSET
    from flyforms.fields import Field
    from flyforms.compatibility import with_metaclass

    __all__ = ("Form", "validate_schema")

    class FormMeta(type):
        """
        The metaclass for Form and it's subclasses
        """

        def __new__(mcs, name, bases, dct):

            fields = []  # create a container for Form's fields names
            for attr, val in dct.items():  # walk through class attributes
                if isinstance(val, Field):  # find all Field instances
                    fields.append(attr)  # catch them
                    dct[attr] = FormField(attr, val)  # and replace with descriptor

            # Update class attributes
            dct["_fields"] = set(fields)
            cls = super(FormMeta, mcs).__new__(mcs, name, bases, dct)  # create new class

            # Get all fields names from MRO
            _fields = set()  # prepare container for Form fields names
            for base in cls.__mro__:  # walk through the MRO
                _fields |= getattr(base, "_fields", set())  # get fields names from each base
            cls._fields = _fields  # update Form's fields
            return cls

    class FormField(object):
        def __init__(self, name, field_obj):
            if not isinstance(field_obj, Field):
                raise TypeError("You should bind FormField with Field subclass instance, not {}".format(type(field_obj)))
            self.name = name
            self.field = field_obj

        def __get__(self, instance, owner):
            if isinstance(instance, Form) and issubclass(owner, Form):
                return instance.raw_data.get(self.name, self.field.default)
            if instance is None and issubclass(owner, Form):
                return self.field
            raise AttributeError("You can\'t use FormField without Form")

        def __set__(self, instance, value):
            if not isinstance(instance, Form):
                raise AttributeError("You can\'t use FormField without Form")
            if instance.raw_data.get(self.name, UNSET) is not UNSET:
                raise AttributeError("You can\'t overwrite already bound field {}!".format(self.name))
            try:
                value = self.field.bind(value)
            except ValidationError as e:
                instance.errors[self.name] = str(e)
                value = UNSET
            instance.raw_data[self.name] = value

        def __delete__(self, instance):
            raise AttributeError("You can\'t delete Form fields!")

    class Form(with_metaclass(FormMeta, object)):
        """
        The root class for all Forms
        """

        _fields = set()

        def __init__(self, **data):
            """
            :param data: additional data to form
            :type data: dict

            When a Form is instantiated you can access given data via instance attributes or get everything at once
            using :py:attr:`.data` attribute
            """
            self.raw_data = {}  # create a container for Form data
            self.errors = {}  # create a container for Form's errors

            for field_name in self._fields:  # walk through class fields
                setattr(self, field_name, data.pop(field_name, UNSET))  # and try to set up them
            for unk in data:  # if some extra fields given it's an error
                self.errors[unk] = "Unknown field %s in data for Form %s" % (unk, self.__class__.__name__)

        @property
        def is_bound(self):
            """
            Checks is Form instance bound.
            Returns True if all fields is bound. Otherwise, False.
            """
            return UNSET not in self.raw_data.values()

        @property
        def is_valid(self):
            """
            Checks is Form instance valid.
            Returns there are no errors. Otherwise, False.
            """
            return self.errors == {}

        @property
        def data(self):
            """
            Returns :code:`dict` with bound data (if you need all data even :py:data:`.UNSET` values -
            use :py:attr:`.raw_data`)
            """
            return {field: self.raw_data[field] for field in self._fields if self.raw_data[field] is not UNSET}

    def validate_schema(form_cls, **data):
        """
        This function validates given data via given :py:class:`.Form` subclass
        without defined Form instantiation.

        :param form_cls: user-defined Form
        :type form_cls: :py:class:`.Form`

        :param data: data to validate

        :return: boolean flag is data valid for given :code:`form_cls`

        :raises TypeError: if given :code:`form_cls` is not class
            or not :py:class:`.Form` subclass
        """
        if not isclass(form_cls) or not issubclass(form_cls, Form):
            raise TypeError("You should pass Form subclass, not instance as first arg")

        for field_name in form_cls._fields:
            field_obj = getattr(form_cls, field_name)
            if not field_obj.is_valid(data.pop(field_name, UNSET)):
                return False
        if data:
            return False
        return True