"""
Forms used throughout the DISBi app.
"""
# standard library
from collections import namedtuple

# third-party
from more_itertools import unique_everseen

# Django
from django import forms
from django.conf import settings
from django.db.models.fields.related import ForeignKey
from django.utils.translation import ugettext_lazy as _

# DISBi
from disbi.utils import camelize, construct_none_displayer, get_choices


# ----------------------------- functions -----------------------------

def make_ChoiceField(model, attribute, label=None, empty_choice=None):
    """
    Return a ChoiceField and the maxiumn number of choices.
    
    The ChoiceField contains all distinct values of an attribute of model.
    Addtionally a NULL option is inserted as first choice.
    
    Args:
        model (Model): The model from which the field should be constructed.
        attribute (Field): The attribute of the model. 
        label (str): The label used when displaying the form (default None).
        empty_choice (tuple): 2-tuple of a value label pair, used for enabling
            the user to choose an empty condition, e.g. no stress.
    
    Returns:
        ChoiceField: A Choicefield based on the available options in the DB.
        int: The number of availablbe options.
        
    Raises:
        IndexError: Raises error when no entries are found in the DB. 
    """
    
    try:
        entries = model.objects.values_list(attribute, flat=True).distinct()
        max_num = len(entries)
        model_field = model._meta.get_field(attribute)
        # Format human-readable label according to type
        if model_field.choices:
            # The field is a choice field. Use DB and display names as 
            # specified in the models.
            choices = list(zip(get_choices(model_field.choices, style='db'), 
                               get_choices(model_field.choices, style='display')))
        
        
        elif isinstance(entries[0], str):
            # The field is a free form string field.
            # Split on SEPARATOR, flatten list and make it unique.
            entries = [e.split(settings.DISBI['SEPARATOR']) for e in entries if e]
            entries = sum(entries, [])
            entries = list(unique_everseen(entries))
            entries = [e for e in entries if e != settings.DISBI['EMPTY_STR']]
            choices = [(e, e) for e in entries if e]
            
        else:
            # The field is something else. DB and display values are the same.
            choices = [(e, e) for e in entries if e]
        
        if empty_choice is not None:
            choices.append(empty_choice)
        
        choices.insert(0, (None, construct_none_displayer(entries)))
        # Use label if given
        if label is not None:
            field = forms.ChoiceField(choices=choices, label=label)
        else:
            field = forms.ChoiceField(choices=choices)
        return field, max_num
    except IndexError:
        raise Exception('No entries for field {}'.format(attribute))
        

def construct_direct_select_form(model):
    """
    Construct a form for directly selecting model instances.
    
    Args:
        model (models.Model): A Django model, for which the form is constructed.
    """
    cls_name = model.__name__ + 'Form'
    entries = model.objects.all()
    mymax_num = len(entries)
    myselect_field = forms.ModelChoiceField(queryset=entries)
    form = type(cls_name, (forms.Form,), {
                model.__name__.lower(): myselect_field,
                'max_num': mymax_num,}
            )
    return form

def construct_foreign_select_field(model, field):
    """
    Construct a form for directly selecting related model instances.
    
    Args:
        model (models.Model): A Django model, for which the form is constructed.
        field (fields.related.ForeignKey): The field of the realated instances.
    """
    entries = field.related_model.objects.filter(pk__in=
                model.objects.values_list(field.name, flat=True).distinct()
            )
    max_num = len(entries)
    select_field = forms.ModelChoiceField(queryset=entries)
    return select_field, max_num 




def construct_modelfieldsform(model, exclude=['id'], direct_select=False):
    """
    Construct forms based on a model and available values in the DB.
    
    Args:
        model (models.Model): A Django model, for which the forms are constructed.
        
    Keyword Args:
        exclude (iterable): Fields for which no forms should be constructed.
        direct_select (bool): Determines whether a form for directly selecting
            model instances is constructed.
    
    Returns:
        list: A list of namedtuples with the form classes and the prefix. 
    """
    NamedFormClass = namedtuple('NamedFormClass', ['classname', 'prefix'])
    formclasses = []
    if direct_select:
        form = construct_direct_select_form(model)
        formclasses.append(NamedFormClass(form, model.__name__.lower()))
    
    fields = [f for f in model._meta.get_fields() if f.concrete]
    for field in fields:
        if field.name not in exclude:
            # Construct class name and form label.
            cls_name = camelize(field.name) + 'Form'
            mylabel = field.verbose_name\
                      if field.verbose_name != field.name.replace('_', ' ')\
                      else None
            # If dealing with a foreign key field, get the related objects.
            if isinstance(field, ForeignKey):
                myselect_field, mymax_num = construct_foreign_select_field(model, field)
            # When dealing else with a normal field use make_Choicefield().
            else:
                # Check whether an empty value representation was added to the model.
                try:
                    myempty_choice = (settings.DISBI['EMPTY_STR'], field.di_empty)