from typing import List, Union

from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.forms.widgets import FILE_INPUT_CONTRADICTION

from .value import (
    LocalizedFileValue,
    LocalizedIntegerValue,
    LocalizedStringValue,
    LocalizedValue,
)
from .widgets import (
    AdminLocalizedIntegerFieldWidget,
    LocalizedCharFieldWidget,
    LocalizedFieldWidget,
    LocalizedFileWidget,
)


class LocalizedFieldForm(forms.MultiValueField):
    """Form for a localized field, allows editing the field in multiple
    languages."""

    widget = LocalizedFieldWidget
    field_class = forms.fields.CharField
    value_class = LocalizedValue

    def __init__(
        self, *args, required: Union[bool, List[str]] = False, **kwargs
    ):
        """Initializes a new instance of :see:LocalizedFieldForm."""

        fields = []

        # Do not print initial value in html in the form of a hidden input. This will result in loss of information
        kwargs["show_hidden_initial"] = False

        for lang_code, _ in settings.LANGUAGES:
            field_options = dict(
                required=required
                if type(required) is bool
                else (lang_code in required),
                label=lang_code,
            )
            fields.append(self.field_class(**field_options))

        super(LocalizedFieldForm, self).__init__(
            fields,
            required=required if type(required) is bool else True,
            require_all_fields=False,
            *args,
            **kwargs
        )

        # set 'required' attribute for each widget separately
        for field, widget in zip(self.fields, self.widget.widgets):
            widget.is_required = field.required

    def compress(self, value: List[str]) -> LocalizedValue:
        """Compresses the values from individual fields into a single
        :see:LocalizedValue instance.

        Arguments:
            value:
                The values from all the widgets.

        Returns:
            A :see:LocalizedValue containing all
            the value in several languages.
        """

        localized_value = self.value_class()

        for (lang_code, _), value in zip(settings.LANGUAGES, value):
            localized_value.set(lang_code, value)

        return localized_value


class LocalizedCharFieldForm(LocalizedFieldForm):
    """Form for a localized char field, allows editing the field in multiple
    languages."""

    widget = LocalizedCharFieldWidget
    value_class = LocalizedStringValue


class LocalizedTextFieldForm(LocalizedFieldForm):
    """Form for a localized text field, allows editing the field in multiple
    languages."""

    value_class = LocalizedStringValue


class LocalizedIntegerFieldForm(LocalizedFieldForm):
    """Form for a localized integer field, allows editing the field in multiple
    languages."""

    widget = AdminLocalizedIntegerFieldWidget
    value_class = LocalizedIntegerValue


class LocalizedFileFieldForm(LocalizedFieldForm, forms.FileField):
    """Form for a localized file field, allows editing the field in multiple
    languages."""

    widget = LocalizedFileWidget
    field_class = forms.fields.FileField
    value_class = LocalizedFileValue

    def clean(self, value, initial=None):
        """Most part of this method is a copy of
        django.forms.MultiValueField.clean, with the exception of initial value
        handling (this need for correct processing FileField's).

        All original comments saved.
        """
        if initial is None:
            initial = [None for x in range(0, len(value))]
        else:
            if not isinstance(initial, list):
                initial = self.widget.decompress(initial)

        clean_data = []
        errors = []
        if not value or isinstance(value, (list, tuple)):
            is_empty = [v for v in value if v not in self.empty_values]
            if (not value or not is_empty) and (not initial or not is_empty):
                if self.required:
                    raise ValidationError(
                        self.error_messages["required"], code="required"
                    )
        else:
            raise ValidationError(
                self.error_messages["invalid"], code="invalid"
            )
        for i, field in enumerate(self.fields):
            try:
                field_value = value[i]
            except IndexError:
                field_value = None

            try:
                field_initial = initial[i]
            except IndexError:
                field_initial = None

            if (
                field_value in self.empty_values
                and field_initial in self.empty_values
            ):
                if self.require_all_fields:
                    # Raise a 'required' error if the MultiValueField is
                    # required and any field is empty.
                    if self.required:
                        raise ValidationError(
                            self.error_messages["required"], code="required"
                        )
                elif field.required:
                    # Otherwise, add an 'incomplete' error to the list of
                    # collected errors and skip field cleaning, if a required
                    # field is empty.
                    if field.error_messages["incomplete"] not in errors:
                        errors.append(field.error_messages["incomplete"])
                    continue
            try:
                clean_data.append(field.clean(field_value, field_initial))
            except ValidationError as e:
                # Collect all validation errors in a single list, which we'll
                # raise at the end of clean(), rather than raising a single
                # exception for the first error we encounter. Skip duplicates.
                errors.extend(m for m in e.error_list if m not in errors)
        if errors:
            raise ValidationError(errors)

        out = self.compress(clean_data)
        self.validate(out)
        self.run_validators(out)
        return out

    def bound_data(self, data, initial):
        bound_data = []
        if initial is None:
            initial = [None for x in range(0, len(data))]
        else:
            if not isinstance(initial, list):
                initial = self.widget.decompress(initial)
        for d, i in zip(data, initial):
            if d in (None, FILE_INPUT_CONTRADICTION):
                bound_data.append(i)
            else:
                bound_data.append(d)
        return bound_data