from __future__ import unicode_literals

import operator

from packaging import version
import wtforms
from wtforms import widgets
from wtforms.fields import Field, SelectField, SelectFieldBase
from wtforms.validators import ValidationError

IS_WTFORMS_LESS_THEN_3_1_0 = version.parse(wtforms.__version__) < version.parse("3.1.0")


class AJAXSelectField(Field):
    """
    Simple class to convert primary key to ORM objects
    for SQLAlchemy and fab normal processing on add and update
    This WTF field class is prepared to be used in related views or directly on forms.

    :param label: The label to render on form
    :param validators: A list of form validators
    :param: datamodel: An initialized SQLAInterface with a model
    :param: col_name: The column that maps to the model
    :param: is_related:
        If the model column is a relationship or direct on
        this case use col_name with the pk
    """

    def __init__(
        self,
        label=None,
        validators=None,
        datamodel=None,
        col_name=None,
        is_related=True,
        **kwargs
    ):
        super(AJAXSelectField, self).__init__(label, validators, **kwargs)
        self.datamodel = datamodel
        self.col_name = col_name
        self.is_related = is_related

    def process_data(self, value):
        """
        Process the Python data applied to this field and store the result.
        This will be called during form construction by the form's `kwargs` or
        `obj` argument.

        Converting ORM object to primary key for client form.

        :param value: The python object containing the value to process.
        """
        if value:
            if self.is_related:
                self.data = self.datamodel.get_related_interface(
                    self.col_name
                ).get_pk_value(value)
            else:
                self.data = self.datamodel.get(value)
        else:
            self.data = None

    def process_formdata(self, valuelist):
        """
        Process data received over the wire from a form.
        This will be called during form construction with data supplied
        through the `formdata` argument.

        Converting primary key to ORM for server processing.

        :param valuelist: A list of strings to process.
        """
        if valuelist:
            if self.is_related:
                self.data = self.datamodel.get_related_interface(self.col_name).get(
                    valuelist[0]
                )
            else:
                self.data = self.datamodel.get(valuelist[0])


class QuerySelectField(SelectFieldBase):
    """
    Based on WTForms QuerySelectField
    """

    widget = widgets.Select()

    def __init__(
        self,
        label=None,
        validators=None,
        query_func=None,
        get_pk_func=None,
        get_label=None,
        allow_blank=False,
        blank_text="",
        **kwargs
    ):
        super(QuerySelectField, self).__init__(label, validators, **kwargs)
        self.query_func = query_func
        self.get_pk_func = get_pk_func

        if get_label is None:
            self.get_label = lambda x: x
        elif isinstance(get_label, str):
            self.get_label = operator.attrgetter(get_label)
        else:
            self.get_label = get_label

        self.allow_blank = allow_blank
        self.blank_text = blank_text
        self._object_list = None

    def _get_data(self):
        if self._formdata is not None:
            for pk, obj in self._get_object_list():
                if pk == self._formdata:
                    self._set_data(obj)
                    break
        return self._data

    def _set_data(self, data):
        self._data = data
        self._formdata = None

    data = property(_get_data, _set_data)

    def _get_object_list(self):
        if self._object_list is None:
            objs = self.query_func()
            self._object_list = list((str(self.get_pk_func(obj)), obj) for obj in objs)
        return self._object_list

    def iter_choices(self):
        if self.allow_blank:
            if IS_WTFORMS_LESS_THEN_3_1_0:
                yield ("__None", self.blank_text, self.data is None)
            else:
                yield ("__None", self.blank_text, self.data is None, {})

        for pk, obj in self._get_object_list():
            if IS_WTFORMS_LESS_THEN_3_1_0:
                yield (pk, self.get_label(obj), obj == self.data)
            else:
                yield (pk, self.get_label(obj), obj == self.data, {})

    def process_formdata(self, valuelist):
        if valuelist:
            if self.allow_blank and valuelist[0] == "__None":
                self.data = None
            else:
                self._data = None
                self._formdata = valuelist[0]

    def pre_validate(self, form):
        data = self.data
        if data is not None:
            for pk, obj in self._get_object_list():
                if data == obj:
                    break
            else:
                raise ValidationError(self.gettext("Not a valid choice"))
        elif self._formdata or not self.allow_blank:
            raise ValidationError(self.gettext("Not a valid choice"))


class QuerySelectMultipleField(QuerySelectField):
    """
    Very similar to QuerySelectField with the difference that this will
    display a multiple select. The data property will hold a list with ORM
    model instances and will be an empty list when no value is selected.
    If any of the items in the data list or submitted form data cannot be
    found in the query, this will result in a validation error.
    """

    widget = widgets.Select(multiple=True)

    def __init__(self, label=None, validators=None, default=None, **kwargs):
        if default is None:
            default = []
        super(QuerySelectMultipleField, self).__init__(
            label, validators, default=default, **kwargs
        )
        if kwargs.get("allow_blank", False):
            import warnings

            warnings.warn(
                "allow_blank=True does not do anything for QuerySelectMultipleField."
            )
        self._invalid_formdata = False

    def _get_data(self):
        formdata = self._formdata
        if formdata is not None:
            data = []
            for pk, obj in self._get_object_list():
                if not formdata:
                    break
                elif pk in formdata:
                    formdata.remove(pk)
                    data.append(obj)
            if formdata:
                self._invalid_formdata = True
            self._set_data(data)
        return self._data

    def _set_data(self, data):
        self._data = data
        self._formdata = None

    data = property(_get_data, _set_data)

    def iter_choices(self):
        for pk, obj in self._get_object_list():
            if IS_WTFORMS_LESS_THEN_3_1_0:
                yield (pk, self.get_label(obj), obj in self.data)
            else:
                yield (pk, self.get_label(obj), obj in self.data, {})

    def process_formdata(self, valuelist):
        self._formdata = set(valuelist)

    def pre_validate(self, form):
        if self._invalid_formdata:
            raise ValidationError(self.gettext("Not a valid choice"))
        elif self.data:
            obj_list = list(x[1] for x in self._get_object_list())
            if not isinstance(self.data, list):
                self.data = [self.data]
            for v in self.data:
                if v not in obj_list:
                    raise ValidationError(self.gettext("Not a valid choice"))


class EnumField(SelectField):
    """Selection field for Sqlalchemy Enum type.

    The meaning of enum_class and enums is the same as for
    attributes on sqlalchemy.types.Enum:

    :param enum_class: either None or a subclass of Python enum.Enum
    :param enums: a sequence of strings, if enum_class is not Null than it should be
                  `list(enum_class.__members__)`
    """

    def __init__(
        self, enum_class, enums, label=None, validators=None, default=None, **kwargs
    ):
        self._enum_class = enum_class
        self._enums = enums

        # Column(Enum(enum.Enum)) case
        if enum_class is not None:
            labels = [
                str(enum_class.__members__[enum_member].value) for enum_member in enums
            ]

            def coerce(value):
                if value is None:
                    return None
                elif isinstance(value, enum_class):
                    return value
                else:
                    return enum_class.__members__[value]

        # Column(Enum(*enums)) case
        else:
            labels = enums

            def coerce(value):
                if value is None:
                    return None
                return str(value)

        choices = list(zip(enums, labels))

        super(EnumField, self).__init__(
            label=label,
            validators=validators,
            default=default,
            coerce=coerce,
            choices=choices,
            **kwargs
        )

    def pre_validate(self, form):
        for v, _ in self.choices:
            if self.data == self.coerce(v):
                break
        else:
            raise ValueError(self.gettext("Not a valid choice"))
