import logging

from flask_wtf import FlaskForm
from wtforms import (
    BooleanField,
    DateField,
    DateTimeField,
    DecimalField,
    FloatField,
    IntegerField,
    StringField,
    TextAreaField,
)
from wtforms import validators

from .fields import EnumField, QuerySelectField, QuerySelectMultipleField
from .fieldwidgets import (
    BS3TextAreaFieldWidget,
    BS3TextFieldWidget,
    DatePickerWidget,
    DateTimePickerWidget,
    Select2ManyWidget,
    Select2Widget,
)
from .models.mongoengine.fields import MongoFileField, MongoImageField
from .upload import (
    BS3FileUploadFieldWidget,
    BS3ImageUploadFieldWidget,
    FileUploadField,
    ImageUploadField,
)
from .validators import Unique

try:
    from wtforms.fields.core import _unset_value as unset_value
except Exception:
    from wtforms.utils import unset_value  # noqa: F401

log = logging.getLogger(__name__)


class FieldConverter(object):
    """
        Helper class that converts model fields into WTForm fields

        it has a conversion table with type method checks from model
        interfaces, these methods are invoked with a column name
    """

    conversion_table = (
        ("is_image", ImageUploadField, BS3ImageUploadFieldWidget),
        ("is_file", FileUploadField, BS3FileUploadFieldWidget),
        ("is_gridfs_file", MongoFileField, BS3FileUploadFieldWidget),
        ("is_gridfs_image", MongoImageField, BS3ImageUploadFieldWidget),
        ("is_text", TextAreaField, BS3TextAreaFieldWidget),
        ("is_binary", TextAreaField, BS3TextAreaFieldWidget),
        ("is_string", StringField, BS3TextFieldWidget),
        ("is_integer", IntegerField, BS3TextFieldWidget),
        ("is_numeric", DecimalField, BS3TextFieldWidget),
        ("is_float", FloatField, BS3TextFieldWidget),
        ("is_boolean", BooleanField, None),
        ("is_date", DateField, DatePickerWidget),
        ("is_datetime", DateTimeField, DateTimePickerWidget),
    )

    def __init__(
        self, datamodel, colname, label, description, validators, default=None
    ):
        self.datamodel = datamodel
        self.colname = colname
        self.label = label
        self.description = description
        self.validators = validators
        self.default = default

    def convert(self):
        # sqlalchemy.types.Enum inherits from String, therefore `is_enum` must be
        # checked before checking for `is_string`:
        if getattr(self.datamodel, "is_enum")(self.colname):
            col_type = self.datamodel.list_columns[self.colname].type
            return EnumField(
                enum_class=col_type.enum_class,
                enums=col_type.enums,
                label=self.label,
                description=self.description,
                validators=self.validators,
                widget=Select2Widget(),
                default=self.default,
            )
        for type_marker, field, widget in self.conversion_table:
            if getattr(self.datamodel, type_marker)(self.colname):
                if widget:
                    return field(
                        self.label,
                        description=self.description,
                        validators=self.validators,
                        widget=widget(),
                        default=self.default,
                    )
                else:
                    return field(
                        self.label,
                        description=self.description,
                        validators=self.validators,
                        default=self.default,
                    )
        log.error("Column %s Type not supported" % self.colname)


class GeneralModelConverter(object):
    """
        Returns a form from a model only one public exposed
        method 'create_form'
    """

    def __init__(self, datamodel):
        self.datamodel = datamodel

    @staticmethod
    def _get_validators(col_name, validators_columns):
        return validators_columns.get(col_name, [])

    @staticmethod
    def _get_description(col_name, description_columns):
        return description_columns.get(col_name, "")

    @staticmethod
    def _get_label(col_name, label_columns):
        return label_columns.get(col_name, "")

    def _get_related_query_func(self, col_name, filter_rel_fields):
        if filter_rel_fields:
            if col_name in filter_rel_fields:
                datamodel = self.datamodel.get_related_interface(col_name)
                filters = datamodel.get_filters().add_filter_list(
                    filter_rel_fields[col_name]
                )
                return lambda: datamodel.query(filters)[1]
        return lambda: self.datamodel.get_related_interface(col_name).query()[1]

    def _get_related_pk_func(self, col_name):
        return lambda obj: self.datamodel.get_related_interface(col_name).get_pk_value(
            obj
        )

    def _convert_many_to_one(
        self,
        col_name,
        label,
        description,
        lst_validators,
        filter_rel_fields,
        form_props,
    ):
        """
            Creates a WTForm field for many to one related fields,
            will use a Select box based on a query. Will only
            work with SQLAlchemy interface.
        """
        query_func = self._get_related_query_func(col_name, filter_rel_fields)
        get_pk_func = self._get_related_pk_func(col_name)
        extra_classes = None
        allow_blank = True
        if not self.datamodel.is_nullable(col_name):
            lst_validators.append(validators.DataRequired())
            allow_blank = False
        else:
            lst_validators.append(validators.Optional())
        form_props[col_name] = QuerySelectField(
            label,
            description=description,
            query_func=query_func,
            get_pk_func=get_pk_func,
            allow_blank=allow_blank,
            validators=lst_validators,
            widget=Select2Widget(extra_classes=extra_classes),
        )
        return form_props

    def _convert_many_to_many(
        self,
        col_name,
        label,
        description,
        lst_validators,
        filter_rel_fields,
        form_props,
    ):
        query_func = self._get_related_query_func(col_name, filter_rel_fields)
        get_pk_func = self._get_related_pk_func(col_name)
        allow_blank = True
        form_props[col_name] = QuerySelectMultipleField(
            label,
            description=description,
            query_func=query_func,
            get_pk_func=get_pk_func,
            allow_blank=allow_blank,
            validators=lst_validators,
            widget=Select2ManyWidget(),
        )
        return form_props

    def _convert_simple(self, col_name, label, description, lst_validators, form_props):
        # Add Validator size
        max = self.datamodel.get_max_length(col_name)
        min = self.datamodel.get_min_length(col_name)
        if max != -1 or min != -1:
            lst_validators.append(validators.Length(max=max, min=min))
        # Add Validator is null
        if not self.datamodel.is_nullable(col_name):
            lst_validators.append(validators.InputRequired())
        else:
            lst_validators.append(validators.Optional())
        # Add Validator is unique
        if self.datamodel.is_unique(col_name):
            lst_validators.append(Unique(self.datamodel, col_name))
        default_value = self.datamodel.get_col_default(col_name)
        fc = FieldConverter(
            self.datamodel,
            col_name,
            label,
            description,
            lst_validators,
            default=default_value,
        )
        form_props[col_name] = fc.convert()
        return form_props

    def _convert_col(
        self,
        col_name,
        label,
        description,
        lst_validators,
        filter_rel_fields,
        form_props,
    ):
        if self.datamodel.is_relation(col_name):
            if self.datamodel.is_relation_many_to_one(
                col_name
            ) or self.datamodel.is_relation_one_to_one(col_name):
                return self._convert_many_to_one(
                    col_name,
                    label,
                    description,
                    lst_validators,
                    filter_rel_fields,
                    form_props,
                )
            elif self.datamodel.is_relation_many_to_many(
                col_name
            ) or self.datamodel.is_relation_one_to_many(col_name):
                return self._convert_many_to_many(
                    col_name,
                    label,
                    description,
                    lst_validators,
                    filter_rel_fields,
                    form_props,
                )
            else:
                log.warning("Relation {0} not supported".format(col_name))
        else:
            return self._convert_simple(
                col_name, label, description, lst_validators, form_props
            )

    def create_form(
        self,
        label_columns=None,
        inc_columns=None,
        description_columns=None,
        validators_columns=None,
        extra_fields=None,
        filter_rel_fields=None,
    ):
        """
            Converts a model to a form given

            :param label_columns:
                A dictionary with the column's labels.
            :param inc_columns:
                A list with the columns to include
            :param description_columns:
                A dictionary with a description for cols.
            :param validators_columns:
                A dictionary with WTForms validators ex::

                    validators={'personal_email':EmailValidator}

            :param extra_fields:
                A dictionary containing column names and a WTForm
                Form fields to be added to the form, these fields do not
                 exist on the model itself ex::

                    extra_fields={'some_col':BooleanField('Some Col', default=False)}

            :param filter_rel_fields:
                A filter to be applied on relationships
        """
        label_columns = label_columns or {}
        inc_columns = inc_columns or []
        description_columns = description_columns or {}
        validators_columns = validators_columns or {}
        extra_fields = extra_fields or {}
        form_props = {}
        for col_name in inc_columns:
            if col_name in extra_fields:
                form_props[col_name] = extra_fields.get(col_name)
            else:
                self._convert_col(
                    col_name,
                    self._get_label(col_name, label_columns),
                    self._get_description(col_name, description_columns),
                    self._get_validators(col_name, validators_columns),
                    filter_rel_fields,
                    form_props,
                )
        return type("DynamicForm", (DynamicForm,), form_props)


class DynamicForm(FlaskForm):
    """
        Refresh method will force select field to refresh
    """

    @classmethod
    def refresh(self, obj=None):
        form = self(obj=obj)
        return form
