from crispy_forms.bootstrap import Container
from crispy_forms.exceptions import DynamicError
from crispy_forms.layout import Fieldset, MultiField


class LayoutSlice:
    # List of layout objects that need args passed first before fields
    args_first = (Fieldset, MultiField, Container)

    def __init__(self, layout, key):
        self.layout = layout
        if isinstance(key, int):
            self.slice = slice(key, key + 1, 1)
        else:
            self.slice = key

    def wrapped_object(self, LayoutClass, fields, *args, **kwargs):
        """
        Returns a layout object of type `LayoutClass` with `args` and `kwargs` that
        wraps `fields` inside.
        """
        if args:
            if isinstance(fields, list):
                fields = tuple(fields)
            else:
                fields = (fields,)

            if LayoutClass in self.args_first:
                arguments = args + fields
            else:
                arguments = fields + args

            return LayoutClass(*arguments, **kwargs)
        else:
            if isinstance(fields, list):
                return LayoutClass(*fields, **kwargs)
            else:
                return LayoutClass(fields, **kwargs)

    def pre_map(self, function):
        """
        Iterates over layout objects pointed in `self.slice` executing `function` on them.
        It passes `function` penultimate layout object and the position where to find last one
        """
        if isinstance(self.slice, slice):
            for i in range(*self.slice.indices(len(self.layout.fields))):
                function(self.layout, i)

        elif isinstance(self.slice, list):
            # A list of pointers  Ex: [[[0, 0], 'div'], [[0, 2, 3], 'field_name']]
            for pointer in self.slice:
                positions = pointer.positions

                # If it's pointing first level
                if len(positions) == 1:
                    function(self.layout, positions[-1])
                else:
                    layout_object = self.layout.fields[positions[0]]
                    for i in positions[1:-1]:
                        layout_object = layout_object.fields[i]

                    try:
                        function(layout_object, positions[-1])
                    except IndexError:
                        # We could avoid this exception, recalculating pointers.
                        # However this case is most of the time an undesired behavior
                        raise DynamicError(
                            "Trying to wrap a field within an already wrapped field, \
                            recheck your filter or layout"
                        )

    def wrap(self, LayoutClass, *args, **kwargs):
        """
        Wraps every layout object pointed in `self.slice` under a `LayoutClass` instance with
        `args` and `kwargs` passed.
        """

        def wrap_object(layout_object, j):
            layout_object.fields[j] = self.wrapped_object(LayoutClass, layout_object.fields[j], *args, **kwargs)

        self.pre_map(wrap_object)

    def wrap_once(self, LayoutClass, *args, **kwargs):
        """
        Wraps every layout object pointed in `self.slice` under a `LayoutClass` instance with
        `args` and `kwargs` passed, unless layout object's parent is already a subclass of
        `LayoutClass`.
        """

        def wrap_object_once(layout_object, j):
            if not isinstance(layout_object, LayoutClass):
                layout_object.fields[j] = self.wrapped_object(LayoutClass, layout_object.fields[j], *args, **kwargs)

        self.pre_map(wrap_object_once)

    def wrap_together(self, LayoutClass, *args, **kwargs):
        """
        Wraps all layout objects pointed in `self.slice` together under a `LayoutClass`
        instance with `args` and `kwargs` passed.
        """
        if isinstance(self.slice, slice):
            # The start of the slice is replaced
            start = self.slice.start if self.slice.start is not None else 0
            self.layout.fields[start] = self.wrapped_object(
                LayoutClass, self.layout.fields[self.slice], *args, **kwargs
            )

            # The rest of places of the slice are removed, as they are included in the previous
            for i in reversed(range(*self.slice.indices(len(self.layout.fields)))):
                if i != start:
                    del self.layout.fields[i]

        elif isinstance(self.slice, list):
            raise DynamicError("wrap_together doesn't work with filter, only with [] operator")

    def map(self, function):
        """
        Iterates over layout objects pointed in `self.slice` executing `function` on them
        It passes `function` last layout object
        """
        if isinstance(self.slice, slice):
            for i in range(*self.slice.indices(len(self.layout.fields))):
                function(self.layout.fields[i])

        elif isinstance(self.slice, list):
            # A list of pointers  Ex: [[[0, 0], 'div'], [[0, 2, 3], 'field_name']]
            for pointer in self.slice:
                positions = pointer.positions

                layout_object = self.layout.fields[positions[0]]
                for i in positions[1:]:
                    previous_layout_object = layout_object
                    layout_object = layout_object.fields[i]

                # If update_attrs is applied to a string, we call to its wrapping layout object
                if function.__name__ == "update_attrs" and isinstance(layout_object, str):
                    function(previous_layout_object)
                else:
                    function(layout_object)

    def update_attributes(self, **original_kwargs):
        """
        Updates attributes of every layout object pointed in `self.slice` using kwargs
        """

        def update_attrs(layout_object):
            kwargs = original_kwargs.copy()
            if hasattr(layout_object, "attrs"):
                if "css_class" in kwargs:
                    if "class" in layout_object.attrs:
                        layout_object.attrs["class"] += " %s" % kwargs.pop("css_class")
                    else:
                        layout_object.attrs["class"] = kwargs.pop("css_class")
                layout_object.attrs.update(kwargs)

        self.map(update_attrs)
