import tempfile
import subprocess
import contextlib

from django import forms
from django.views.generic import (
    FormView,
    DetailView,
    TemplateView,
)
from django.views.generic.detail import SingleObjectMixin
from django.shortcuts import get_object_or_404
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.core.exceptions import PermissionDenied
from django.core import urlresolvers
from django.contrib.admin.widgets import AdminSplitDateTime
from django.http import Http404

from bs4 import BeautifulSoup

from widgy.views.base import AuthorizedMixin
from widgy.utils import build_url

DIFF_ENABLED = bool(getattr(settings, 'DAISYDIFF_JAR_PATH', False))


class CommitForm(forms.Form):
    PUBLISH_RADIO_CHOICES = (
        ('now', _('Immediately')),
        ('later', _('Later')),
    )

    message = forms.CharField(widget=forms.Textarea,
                              label=_('Notes (optional)'),
                              required=False)
    publish_radio = forms.ChoiceField(label=_('Publish at'),
                                      widget=forms.RadioSelect,
                                      choices=PUBLISH_RADIO_CHOICES,
                                      initial=PUBLISH_RADIO_CHOICES[0][0],
                                      )
    publish_at = forms.DateTimeField(widget=AdminSplitDateTime,
                                     initial=timezone.now,
                                     localize=True,
                                     required=False)

    def get_publish_at(self):
        if self.cleaned_data['publish_radio'] == 'now':
            publish_at = timezone.now()
        else:
            publish_at = self.cleaned_data['publish_at']
        return publish_at

    def commit(self, obj, user):
        return obj.commit(user,
                          message=self.cleaned_data['message'],
                          publish_at=self.get_publish_at())


class RevertForm(CommitForm):
    def commit(self, obj, user):
        return obj.tracker.revert_to(
            commit=obj,
            user=user,
            message=self.cleaned_data['message'],
            publish_at=self.get_publish_at(),
        )


class VersionTrackerMixin(SingleObjectMixin):
    def get_queryset(self):
        return self.site.get_version_tracker_model().objects.all().select_related(
            'head',
            'head__root_node',
            'working_copy')

    def get_preview_urls(self, root_node):
        for owner in self.object.owners:
            if hasattr(owner, 'get_action_links'):
                for link in owner.get_action_links(root_node):
                    if link['type'] == 'preview':
                        yield link

    def get_diff_urls(self, before_node, after_node):
        if DIFF_ENABLED:
            for a, b in zip(self.get_preview_urls(before_node), self.get_preview_urls(after_node)):
                yield build_url(self.site.reverse(self.site.diff_view),
                                before=a['url'],
                                after=b['url'])


class PopupView(object):
    def get_context_data(self, **kwargs):
        kwargs['is_popup'] = True
        kwargs['request'] = self.request
        return super(PopupView, self).get_context_data(**kwargs)


class CommitView(PopupView, AuthorizedMixin, VersionTrackerMixin, FormView):
    template_name = 'widgy/commit.html'
    form_class = CommitForm
    permission_error_message = _("You don't have permission to commit.")

    def get_context_data(self, **kwargs):
        self.object = self.get_object()
        kwargs = super(CommitView, self).get_context_data(**kwargs)
        kwargs['title'] = _('Commit Changes')
        kwargs['tracker'] = self.object
        kwargs['site'] = self.site
        kwargs['commit_url'] = self.site.reverse(self.site.commit_view,
                                                 kwargs={'pk': self.object.pk})
        if not self.has_permission(self.object):
            kwargs['permission_error_message'] = self.permission_error_message

        if self.object.head:
            kwargs['diff_urls'] = self.get_diff_urls(self.object.working_copy,
                                                     self.object.head.root_node)

        # lazy because the template doesn't always use it
        kwargs['changed_anything'] = lambda: self.object.has_changes()

        return kwargs

    def has_permission(self, object):
        return self.site.has_add_permission(self.request, object.commit_model)

    def form_valid(self, form):
        obj = self.get_object()

        if not self.has_permission(obj):
            raise PermissionDenied(self.permission_error_message)

        form.commit(obj, self.request.user)
        # because this is a popup, we shouldn't redirect.
        return self.response_class(
            request=self.request,
            template='widgy/commit_success.html',
            context=self.get_context_data(),
        )


class ResetView(PopupView, AuthorizedMixin, VersionTrackerMixin, TemplateView):
    template_name = 'widgy/reset.html'
    permission_error_message = _("You don't have permission to reset.")

    def get_context_data(self, **kwargs):
        self.object = self.get_object()
        kwargs = super(ResetView, self).get_context_data(**kwargs)
        kwargs['title'] = _('Reset')
        kwargs['tracker'] = self.object
        kwargs['changed_anything'] = self.object.has_changes()
        kwargs['has_commits'] = bool(self.object.head)
        kwargs['reset_url'] = self.request.get_full_path()

        if not self.has_permission(self.object):
            kwargs['permission_error_message'] = self.permission_error_message

        return kwargs

    def has_permission(self, object):
        # We don't really add a commit while resetting, but this permission is
        # used for everything commit related.
        return self.site.has_add_permission(self.request, object.commit_model)

    def post(self, request, *args, **kwargs):
        version_tracker = self.get_object()

        if not self.has_permission(version_tracker):
            raise PermissionDenied(self.permission_error_message)

        version_tracker.reset()
        return self.response_class(
            request=self.request,
            template='widgy/reset_success.html',
            context=self.get_context_data(),
        )


class HistoryView(AuthorizedMixin, VersionTrackerMixin, DetailView):
    template_name = 'widgy/history.html'

    def get_context_data(self, **kwargs):
        kwargs = super(HistoryView, self).get_context_data(**kwargs)
        kwargs['site'] = self.site
        kwargs['commits'] = self.object.get_history_list()
        for commit in kwargs['commits']:
            if commit.parent_id:
                commit.diff_urls = self.get_diff_urls(commit.root_node, commit.parent.root_node)
        return kwargs


class RevertView(PopupView, AuthorizedMixin, VersionTrackerMixin, FormView):
    template_name = 'widgy/revert.html'
    pk_url_kwarg = 'commit_pk'
    form_class = RevertForm
    permission_error_message = _("You don't have permission to revert.")

    def get_context_data(self, **kwargs):
        kwargs['title'] = _('Revert Commit')
        kwargs['commit'] = self.object
        kwargs['tracker'] = self.object.tracker
        kwargs['site'] = self.site
        kwargs = super(RevertView, self).get_context_data(**kwargs)
        kwargs['revert_url'] = self.site.reverse(
            self.site.revert_view,
            kwargs={'pk': self.object.tracker.pk, 'commit_pk': self.object.pk})

        if not self.has_permission(self.object):
            kwargs['permission_error_message'] = self.permission_error_message

        return kwargs

    def get_object(self, queryset=None):
        if queryset is None:
            queryset = self.get_queryset()
        vt = get_object_or_404(queryset, pk=self.kwargs['pk'])
        commit = get_object_or_404(vt.commits.select_related('root_node'),
                                   pk=self.kwargs['commit_pk'])
        commit.tracker = vt
        return commit

    def get_form_kwargs(self):
        self.object = self.get_object()
        kwargs = super(RevertView, self).get_form_kwargs()
        kwargs['initial'] = {
            'message': _('Revert to version %(version)s') % {'version': self.object},
        }
        return kwargs

    def has_permission(self, object):
        return self.site.has_add_permission(self.request, object)

    def form_valid(self, form):
        commit = self.get_object()

        if not self.has_permission(commit):
            raise PermissionDenied(self.permission_error_message)

        form.commit(commit, user=self.request.user)

        return self.response_class(
            request=self.request,
            template='widgy/commit_success.html',
            context=self.get_context_data(),
        )


class DiffView(AuthorizedMixin, TemplateView):
    template_name = 'widgy/diff.html'

    def call_view_from_url(self, url):
        view, args, kwargs = urlresolvers.resolve(url)
        return view(self.request, *args, **kwargs).rendered_content

    def get_context_data(self, **kwargs):
        kwargs = super(DiffView, self).get_context_data(**kwargs)
        try:
            before_url = self.request.GET['before']
            after_url = self.request.GET['after']
        except KeyError:
            raise Http404

        before = self.call_view_from_url(before_url)
        after = self.call_view_from_url(after_url)

        kwargs['diff'] = daisydiff(before, after)

        return kwargs


def daisydiff(before, after):
    """
    Given two strings of html documents in a and b, return a string containing
    html representing the diff between a and b. Requires java and daisydiff.
    """
    files = [tempfile.NamedTemporaryFile() for i in range(3)]
    with contextlib.nested(*files) as (f_a, f_b, f_out):
        f_a.write(before.encode('utf-8'))
        f_b.write(after.encode('utf-8'))

        f_a.flush()
        f_b.flush()

        proc = subprocess.Popen(stdout=subprocess.PIPE, args=[
            'java',
            '-jar',
            settings.DAISYDIFF_JAR_PATH,
            f_b.name,
            f_a.name,
            '--file=' + f_out.name,
        ])
        proc.communicate()
        retcode = proc.poll()
        if retcode:
            assert False, "Daisydiff returned %s" % retcode

        diff_html = f_out.read().decode('utf-8')

        # remove the daisydiff chrome so we can add our own
        parsed = BeautifulSoup(diff_html)
        body = parsed.find('body')
        for i in body.find_all(recursive=False)[:6]:
            i.extract()
        return unicode(body)
