import re
from collections import OrderedDict
from django import forms
from django.db import models
from django.template import loader
try:
    from formtools.wizard.storage import get_storage
    from formtools.wizard.forms import ManagementForm
    from formtools.wizard.views import StepsHelper
except:
    # work for django<1.8
    from django.contrib.formtools.wizard.storage import get_storage
    from django.contrib.formtools.wizard.forms import ManagementForm
    from django.contrib.formtools.wizard.views import StepsHelper

from django.utils import six
from django.utils.encoding import smart_text
from django.utils.module_loading import import_string
from django.forms import ValidationError
from django.forms.models import modelform_factory

from xadmin.sites import site
from xadmin.views import BaseAdminPlugin, ModelFormAdminView


def normalize_name(name):
    new = re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', name)
    return new.lower().strip('_')


class WizardFormPlugin(BaseAdminPlugin):

    wizard_form_list = None
    wizard_for_update = False

    storage_name = 'formtools.wizard.storage.session.SessionStorage'
    form_list = None
    initial_dict = None
    instance_dict = None
    condition_dict = None
    file_storage = None

    def _get_form_prefix(self, step=None):
        if step is None:
            step = self.steps.current
        obj = self.get_form_list().keys()
        if six.PY3:
            obj = [s for s in obj]
        return 'step_%d' % obj.index(step)

    def get_form_list(self):
        if not hasattr(self, '_form_list'):
            init_form_list = OrderedDict()

            assert len(
                self.wizard_form_list) > 0, 'at least one form is needed'

            for i, form in enumerate(self.wizard_form_list):
                init_form_list[smart_text(form[0])] = form[1]

            self._form_list = init_form_list

        return self._form_list

    # Plugin replace methods
    def init_request(self, *args, **kwargs):
        if self.request.is_ajax() or ("_ajax" in self.request.GET) or not hasattr(self.request, 'session') or (args and not self.wizard_for_update):
            # update view
            return False
        return bool(self.wizard_form_list)

    def prepare_form(self, __):
        # init storage and step helper
        self.prefix = normalize_name(self.__class__.__name__)
        self.storage = get_storage(
            self.storage_name, self.prefix, self.request,
            getattr(self, 'file_storage', None))
        self.steps = StepsHelper(self)
        self.wizard_goto_step = False

        if self.request.method == 'GET':
            self.storage.reset()
            self.storage.current_step = self.steps.first

            self.admin_view.model_form = self.get_step_form()
        else:
            # Look for a wizard_goto_step element in the posted data which
            # contains a valid step name. If one was found, render the requested
            # form. (This makes stepping back a lot easier).
            wizard_goto_step = self.request.POST.get('wizard_goto_step', None)
            if wizard_goto_step and int(wizard_goto_step) < len(self.get_form_list()):
                obj = self.get_form_list().keys()
                if six.PY3:
                    obj = [s for s in obj]
                self.storage.current_step = obj[int(wizard_goto_step)]
                self.admin_view.model_form = self.get_step_form()
                self.wizard_goto_step = True
                return

            # Check if form was refreshed
            management_form = ManagementForm(
                self.request.POST, prefix=self.prefix)

            if not management_form.is_valid():
                raise ValidationError(
                    'ManagementForm data is missing or has been tampered.')

            form_current_step = management_form.cleaned_data['current_step']
            if (form_current_step != self.steps.current and
                    self.storage.current_step is not None):
                # form refreshed, change current step
                self.storage.current_step = form_current_step

            # get the form for the current step
            self.admin_view.model_form = self.get_step_form()

    def get_form_layout(self, __):
        attrs = self.get_form_list()[self.steps.current]
        if type(attrs) is dict and 'layout' in attrs:
            self.admin_view.form_layout = attrs['layout']
        else:
            self.admin_view.form_layout = None
        return __()

    def get_step_form(self, step=None):
        if step is None:
            step = self.steps.current
        attrs = self.get_form_list()[step]
        if type(attrs) in (list, tuple):
            return modelform_factory(self.model, form=forms.ModelForm,
                                     fields=attrs, formfield_callback=self.admin_view.formfield_for_dbfield)
        elif type(attrs) is dict:
            if attrs.get('fields', None):
                return modelform_factory(self.model, form=forms.ModelForm,
                                         fields=attrs['fields'], formfield_callback=self.admin_view.formfield_for_dbfield)
            if attrs.get('callback', None):
                callback = attrs['callback']
                if callable(callback):
                    return callback(self)
                elif hasattr(self.admin_view, str(callback)):
                    return getattr(self.admin_view, str(callback))(self)
        elif issubclass(attrs, forms.BaseForm):
            return attrs
        return None

    def get_step_form_obj(self, step=None):
        if step is None:
            step = self.steps.current
        form = self.get_step_form(step)
        return form(prefix=self._get_form_prefix(step),
                    data=self.storage.get_step_data(step),
                    files=self.storage.get_step_files(step))

    def get_form_datas(self, datas):
        datas['prefix'] = self._get_form_prefix()
        if self.request.method == 'POST' and self.wizard_goto_step:
            datas.update({
                'data': self.storage.get_step_data(self.steps.current),
                'files': self.storage.get_step_files(self.steps.current)
            })
        return datas

    def valid_forms(self, __):
        if self.wizard_goto_step:
            # goto get_response directly
            return False
        return __()

    def _done(self):
        cleaned_data = self.get_all_cleaned_data()
        exclude = self.admin_view.exclude

        opts = self.admin_view.opts
        instance = self.admin_view.org_obj or self.admin_view.model()

        file_field_list = []
        for f in opts.fields:
            if not f.editable or isinstance(f, models.AutoField) \
                    or not f.name in cleaned_data:
                continue
            if exclude and f.name in exclude:
                continue
            # Defer saving file-type fields until after the other fields, so a
            # callable upload_to can use the values from other fields.
            if isinstance(f, models.FileField):
                file_field_list.append(f)
            else:
                f.save_form_data(instance, cleaned_data[f.name])

        for f in file_field_list:
            f.save_form_data(instance, cleaned_data[f.name])

        instance.save()

        for f in opts.many_to_many:
            if f.name in cleaned_data:
                f.save_form_data(instance, cleaned_data[f.name])

        self.admin_view.new_obj = instance

    def save_forms(self, __):
        # if the form is valid, store the cleaned data and files.
        form_obj = self.admin_view.form_obj
        self.storage.set_step_data(self.steps.current, form_obj.data)
        self.storage.set_step_files(self.steps.current, form_obj.files)

        # check if the current step is the last step
        if self.steps.current == self.steps.last:
            # no more steps, render done view
            return self._done()

    def save_models(self, __):
        pass

    def save_related(self, __):
        pass

    def get_context(self, context):
        context.update({
            "show_save": False,
            "show_save_as_new": False,
            "show_save_and_add_another": False,
            "show_save_and_continue": False,
        })
        return context

    def get_response(self, response):
        self.storage.update_response(response)
        return response

    def post_response(self, __):
        if self.steps.current == self.steps.last:
            self.storage.reset()
            return __()

        # change the stored current step
        self.storage.current_step = self.steps.next

        self.admin_view.form_obj = self.get_step_form_obj()
        self.admin_view.setup_forms()

        return self.admin_view.get_response()

    def get_all_cleaned_data(self):
        """
        Returns a merged dictionary of all step cleaned_data dictionaries.
        If a step contains a `FormSet`, the key will be prefixed with formset
        and contain a list of the formset cleaned_data dictionaries.
        """
        cleaned_data = {}
        for form_key, attrs in self.get_form_list().items():
            form_obj = self.get_step_form_obj(form_key)
            if form_obj.is_valid():
                if type(attrs) is dict and 'convert' in attrs:
                    callback = attrs['convert']
                    if callable(callback):
                        callback(self, cleaned_data, form_obj)
                    elif hasattr(self.admin_view, str(callback)):
                        getattr(self.admin_view,
                                str(callback))(self, cleaned_data, form_obj)
                elif isinstance(form_obj.cleaned_data, (tuple, list)):
                    cleaned_data.update({
                        'formset-%s' % form_key: form_obj.cleaned_data
                    })
                else:
                    cleaned_data.update(form_obj.cleaned_data)
        return cleaned_data

    def get_cleaned_data_for_step(self, step):
        """
        Returns the cleaned data for a given `step`. Before returning the
        cleaned data, the stored values are being revalidated through the
        form. If the data doesn't validate, None will be returned.
        """
        if step in self.get_form_list():
            form_obj = self.get_step_form_obj(step)
            if form_obj.is_valid():
                return form_obj.cleaned_data
        return None

    def get_next_step(self, step=None):
        """
        Returns the next step after the given `step`. If no more steps are
        available, None will be returned. If the `step` argument is None, the
        current step will be determined automatically.
        """
        if step is None:
            step = self.steps.current
        obj = self.get_form_list().keys()
        if six.PY3:
            obj = [s for s in obj]
        key = obj.index(step) + 1
        if len(obj) > key:
            return obj[key]
        return None

    def get_prev_step(self, step=None):
        """
        Returns the previous step before the given `step`. If there are no
        steps available, None will be returned. If the `step` argument is
        None, the current step will be determined automatically.
        """
        if step is None:
            step = self.steps.current
        obj = self.get_form_list().keys()
        if six.PY3:
            obj = [s for s in obj]
        key = obj.index(step) - 1
        if key >= 0:
            return obj[key]
        return None

    def get_step_index(self, step=None):
        """
        Returns the index for the given `step` name. If no step is given,
        the current step will be used to get the index.
        """
        if step is None:
            step = self.steps.current
        obj = self.get_form_list().keys()
        if six.PY3:
            obj = [s for s in obj]
        return obj.index(step)

    def block_before_fieldsets(self, context, nodes):
        context = context.update(dict(self.storage.extra_data))
        context['wizard'] = {
            'steps': self.steps,
            'management_form': ManagementForm(prefix=self.prefix, initial={
                'current_step': self.steps.current,
            }),
        }
        nodes.append(loader.render_to_string('xadmin/blocks/model_form.before_fieldsets.wizard.html', context))

    def block_submit_line(self, context, nodes):
        context = context.update(dict(self.storage.extra_data))
        context['wizard'] = {
            'steps': self.steps
        }

        nodes.append(loader.render_to_string('xadmin/blocks/model_form.submit_line.wizard.html', context))

site.register_plugin(WizardFormPlugin, ModelFormAdminView)