# coding=utf-8
from .utils import FakeMultiDict, get_obj_value, set_obj_value


class FormSet(object):

    """Open set of forms. This is intended to be used as in two different ways:
    A. As a field in another form
    B. As a independent form generator

    :param form_class:
        The base form class.

    :param data:
        Used to pass data coming from the enduser, usually `request.form`,
        `request.POST` or equivalent.

    :param obj:
        If `data` is empty or not provided, this object is checked for
        attributes matching field names.

    :param files:
        Used to pass files coming from the enduser, usually `request.files`,
        or equivalent.

    """
    _forms = None
    _errors = None
    _named_errors = None
    missing_objs = None
    has_changed = False

    def __init__(
            self, form_class, data=None, objs=None, files=None,
            locale='en', tz='utc', create_new=True, name='',
            backref=None, parent=None):
        self._form_class = form_class
        self._locale = locale
        self._tz = tz
        self._name = name
        self._create_new = bool(create_new)
        backref = backref or parent
        self._backref = backref
        self._forms = []
        self._errors = {}
        self._named_errors = {}
        self.missing_objs = []
        self.has_changed = False
        self._objs = objs

        if (data or objs or files):
            self._init(data, objs, files)

    def reset(self):
        # Reset sub-forms
        for subform in self._forms:
            subform.reset()

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

    def __iter__(self):
        """Iterate the bound forms of this set.
        """
        return iter(self._forms)

    def __nonzero__(self):
        return len(self._forms) > 0

    @property
    def form(self):
        return self.get_empty_form(1)

    def get_form(self, index):
        """Returns the n-index form, where index is 1-based.
        the form'll be filled with data if it exists or empty if not
        """
        index = max(1, index)
        if len(self._forms) >= index:
            return self._forms[index - 1]
        return self.get_empty_form(index)

    def get_empty_form(self, index):
        """Returns an empty n-index form, where index is 1-based.
        """
        return self._form_class(prefix=self._get_fullname(index))

    def _init(self, data=None, objs=None, files=None):
        self._errors = {}
        self._named_errors = {}
        self.has_changed = False

        data = data or {}
        if not hasattr(data, 'getlist'):
            data = FakeMultiDict(data)
        files = files or {}
        if not hasattr(files, 'getlist'):
            files = FakeMultiDict(files)
        objs = objs or []
        try:
            iter(objs)
        except TypeError:
            objs = [objs]

        forms = []
        missing_objs = []
        num = 0

        for i, obj in enumerate(objs, 1):
            num = i
            fullname = self._get_fullname(num)
            if has_been_deleted(data, fullname):
                missing_objs.append(obj)
                continue

            f = self._form_class(
                data, obj=obj, files=files,
                locale=self._locale, tz=self._tz,
                prefix=fullname, backref=self._backref
            )
            forms.append(f)
        num += 1

        if data and self._create_new:
            forms = self._find_new_forms(
                forms, num, data, files,
                locale=self._locale, tz=self._tz
            )

        self._forms = forms
        self.missing_objs = missing_objs

        # Deattach the missing objects from their former parent
        if self._backref:
            for mo in missing_objs:
                if get_obj_value(mo, self._backref, None):
                    set_obj_value(mo, self._backref, None)

        # Delete the missing objects if possible
        if missing_objs:
            for mo in missing_objs:
                if hasattr(mo, 'db'):
                    mo.db.session.delete(mo)

    def as_dict(self):
        return [form.as_dict() for form in self._forms]

    def _get_fullname(self, num):
        return '{name}.{num}'.format(
            name=self._name or self._form_class.__name__.lower(),
            num=num
        )

    def _find_new_forms(self, forms, num, data, files, locale, tz):
        """Acknowledge new forms created client-side.
        """
        fullname = self._get_fullname(num)
        while has_data(data, fullname) or has_data(files, fullname):
            f = self._form_class(
                data, files=files, locale=locale, tz=tz,
                prefix=fullname, backref=self._backref
            )
            forms.append(f)
            num += 1
            fullname = self._get_fullname(num)
        return forms

    def is_valid(self):
        self._errors = {}
        self._named_errors = {}
        self.has_changed = False
        errors = {}
        named_errors = {}

        for name, form in enumerate(self._forms, 1):
            if not form.has_input_data:
                self.has_changed = True
                continue
            else:
                if not form.is_valid():
                    errors[name] = form._errors
                    named_errors.update(form._named_errors)
                    continue
                if form.has_changed:
                    self.has_changed = True
        if errors:
            self._errors = errors
            self._named_errors = named_errors
            return False
        return True

    def save(self, backref_obj):
        return [
            form.save(backref_obj) for form in self._forms
            if form.has_input_data
        ]


def has_data(d, fullname):
    """Test if any of the `keys` of the `d` dictionary starts with `fullname`.
    """
    fullname = r'%s-' % (fullname, )
    for k in d:
        if not k.startswith(fullname):
            continue
        return True
    return False


def has_been_deleted(d, fullname):
    return '{}__deleted'.format(fullname) in d
