# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from __future__ import absolute_import
import random
import string

from datetime import timedelta
from django.utils import timezone

from django import forms
from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _
from django.utils.safestring import mark_safe
from django.forms.util import flatatt
from six.moves import range
try:
    from django.utils.encoding import force_unicode
except ImportError:
    def force_unicode(x):
        return(x)
from django.utils.html import escape, conditional_escape
from django.core.urlresolvers import resolve, Resolver404

from itertools import chain

from wiki import models
from wiki.conf import settings
from wiki.editors import getEditor
from wiki.core.diff import simple_merge
from django.forms.widgets import HiddenInput
from wiki.core.plugins.base import PluginSettingsFormMixin
from django.contrib.auth.forms import UserCreationForm
from wiki.core import permissions

from wiki.core.compat import get_user_model
User = get_user_model()


class SpamProtectionMixin():

    """Check a form for spam. Only works if properties 'request' and 'revision_model' are set."""

    revision_model = models.ArticleRevision

    def check_spam(self):
        """Check that user or IP address does not perform content edits that
        are not allowed.

        current_revision can be any object inheriting from models.BaseRevisionMixin
        """
        request = self.request
        user = None
        ip_address = None
        if request.user.is_authenticated():
            user = request.user
        else:
            ip_address = request.META.get('REMOTE_ADDR', None)

        if not (user or ip_address):
            raise forms.ValidationError(
                ugettext(
                    'Spam protection failed to find both a logged in user and an IP address.'))

        def check_interval(from_time, max_count, interval_name):
            from_time = timezone.now(
            ) - timedelta(minutes=settings.REVISIONS_MINUTES_LOOKBACK)
            revisions = self.revision_model.objects.filter(
                created__gte=from_time,
            )
            if user:
                revisions = revisions.filter(user=user)
            if ip_address:
                revisions = revisions.filter(ip_address=ip_address)
            revisions = revisions.count()
            if revisions >= max_count:
                raise forms.ValidationError(
                    ugettext('Spam protection: You are only allowed to create or edit %(revisions)d article(s) per %(interval_name)s.') % {
                        'revisions': max_count,
                        'interval_name': interval_name,
                    })

        if not settings.LOG_IPS_ANONYMOUS:
            return
        if request.user.has_perm('wiki.moderator'):
            return

        from_time = timezone.now(
        ) - timedelta(minutes=settings.REVISIONS_MINUTES_LOOKBACK)
        if request.user.is_authenticated():
            per_minute = settings.REVISIONS_PER_MINUTES
        else:
            per_minute = settings.REVISIONS_PER_MINUTES_ANONYMOUS
        check_interval(
            from_time,
            per_minute,
            _('minute') if settings.REVISIONS_MINUTES_LOOKBACK == 1 else (
                _('%d minutes') %
                settings.REVISIONS_MINUTES_LOOKBACK),
        )

        from_time = timezone.now() - timedelta(minutes=60)
        if request.user.is_authenticated():
            per_hour = settings.REVISIONS_PER_MINUTES
        else:
            per_hour = settings.REVISIONS_PER_MINUTES_ANONYMOUS
        check_interval(from_time, per_hour, _('hour'))


class CreateRootForm(forms.Form):

    title = forms.CharField(
        label=_('Title'),
        help_text=_(
            'Initial title of the article. May be overridden with revision titles.'))
    content = forms.CharField(
        label=_('Type in some contents'),
        help_text=_(
            'This is just the initial contents of your article. After creating it, you can use more complex features like adding plugins, meta data, related articles etc...'),
        required=False, widget=getEditor().get_widget())  # @UndefinedVariable


class EditForm(forms.Form, SpamProtectionMixin):

    title = forms.CharField(label=_('Title'),)
    content = forms.CharField(
        label=_('Contents'),
        required=False,
        widget=getEditor().get_widget())  # @UndefinedVariable

    summary = forms.CharField(
        label=_('Summary'),
        help_text=_(
            'Give a short reason for your edit, which will be stated in the revision log.'),
        required=False)

    current_revision = forms.IntegerField(
        required=False,
        widget=forms.HiddenInput())

    def __init__(self, request, current_revision, *args, **kwargs):

        self.request = request
        self.no_clean = kwargs.pop('no_clean', False)
        self.preview = kwargs.pop('preview', False)
        self.initial_revision = current_revision
        self.presumed_revision = None
        if current_revision:
            initial = {'content': current_revision.content,
                       'title': current_revision.title,
                       'current_revision': current_revision.id}
            initial.update(kwargs.get('initial', {}))

            # Manipulate any data put in args[0] such that the current_revision
            # is reset to match the actual current revision.
            data = None
            if len(args) > 0:
                data = args[0]
                args = args[1:]
            if data is None:
                data = kwargs.get('data', None)
            if data:
                self.presumed_revision = data.get('current_revision', None)
                if not str(
                        self.presumed_revision) == str(
                        self.initial_revision.id):
                    newdata = {}
                    for k, v in list(data.items()):
                        newdata[k] = v
                    newdata['current_revision'] = self.initial_revision.id
                    newdata['content'] = simple_merge(
                        self.initial_revision.content,
                        data.get(
                            'content',
                            ""))
                    newdata['title'] = current_revision.title
                    kwargs['data'] = newdata
                else:
                    # Always pass as kwarg
                    kwargs['data'] = data

            kwargs['initial'] = initial

        super(EditForm, self).__init__(*args, **kwargs)

    def clean(self):
        cd = self.cleaned_data
        if self.no_clean or self.preview:
            return cd
        if not str(self.initial_revision.id) == str(self.presumed_revision):
            raise forms.ValidationError(
                ugettext(
                    'While you were editing, someone else changed the revision. Your contents have been automatically merged with the new contents. Please review the text below.'))
        if cd['title'] == self.initial_revision.title and cd[
                'content'] == self.initial_revision.content:
            raise forms.ValidationError(
                ugettext('No changes made. Nothing to save.'))
        self.check_spam()
        return cd


class SelectWidgetBootstrap(forms.Select):

    """
    http://twitter.github.com/bootstrap/components.html#buttonDropdowns
    Needs bootstrap and jquery
    """

    def __init__(self, attrs={}, choices=(), disabled=False):
        attrs['class'] = 'btn-group pull-left btn-group-form'
        self.disabled = disabled
        self.noscript_widget = forms.Select(attrs={}, choices=choices)
        super(SelectWidgetBootstrap, self).__init__(attrs, choices)

    def __setattr__(self, k, value):
        super(SelectWidgetBootstrap, self).__setattr__(k, value)
        if k != 'attrs' and k != 'disabled':
            self.noscript_widget.__setattr__(k, value)

    def render(self, name, value, attrs=None, choices=()):
        if value is None:
            value = ''
        final_attrs = self.build_attrs(attrs, name=name)
        output = [
            """<div%(attrs)s>"""
            """    <button class="btn btn-group-label%(disabled)s" type="button">%(label)s</button>"""
            """    <button class="btn btn-default dropdown-toggle%(disabled)s" type="button" data-toggle="dropdown">"""
            """        <span class="caret"></span>"""
            """    </button>"""
            """    <ul class="dropdown-menu">"""
            """        %(options)s"""
            """    </ul>"""
            """    <input type="hidden" name="%(name)s" value="" class="btn-group-value" />"""
            """</div>"""
            """<noscript>%(noscript)s</noscript>""" %
            {'attrs': flatatt(final_attrs),
             'options': self.render_options(choices, [value]),
             'label': _('Select an option'),
             'name': name, 'disabled': ' disabled' if self.disabled else '',
             'noscript': self.noscript_widget.render(
                 name, value, {},
                 choices)}]
        return mark_safe('\n'.join(output))

    def render_option(self, selected_choices, option_value, option_label):
        option_value = force_unicode(option_value)
        selected_html = (
            option_value in selected_choices) and ' selected="selected"' or ''
        return '<li><a href="javascript:void(0)" data-value="%s"%s>%s</a></li>' % (
            escape(option_value), selected_html,
            conditional_escape(force_unicode(option_label)))

    def render_options(self, choices, selected_choices):
        # Normalize to strings.
        selected_choices = set([force_unicode(v) for v in selected_choices])
        output = []
        for option_value, option_label in chain(self.choices, choices):
            if isinstance(option_label, (list, tuple)):
                output.append(
                    '<li class="divider" label="%s"></li>' %
                    escape(
                        force_unicode(option_value)))
                for option in option_label:
                    output.append(
                        self.render_option(
                            selected_choices,
                            *option))
            else:
                output.append(
                    self.render_option(
                        selected_choices,
                        option_value,
                        option_label))
        return '\n'.join(output)

    class Media(forms.Media):

        js = ("wiki/js/forms.js",)


class TextInputPrepend(forms.TextInput):

    def __init__(self, *args, **kwargs):
        self.prepend = kwargs.pop('prepend', "")
        super(TextInputPrepend, self).__init__(*args, **kwargs)

    def render(self, *args, **kwargs):
        html = super(TextInputPrepend, self).render(*args, **kwargs)
        return mark_safe(
            '<div class="input-group"><span class="input-group-addon">%s</span>%s</div>' %
            (self.prepend, html))


class CreateForm(forms.Form, SpamProtectionMixin):

    def __init__(self, request, urlpath_parent, *args, **kwargs):
        super(CreateForm, self).__init__(*args, **kwargs)
        self.request = request
        self.urlpath_parent = urlpath_parent

    title = forms.CharField(label=_('Title'),)
    slug = forms.SlugField(
        label=_('Slug'),
        help_text=_(
            "This will be the address where your article can be found. Use only alphanumeric characters and - or _. Note that you cannot change the slug after creating the article."),
        max_length=models.URLPath.SLUG_MAX_LENGTH)
    content = forms.CharField(
        label=_('Contents'),
        required=False,
        widget=getEditor().get_widget())  # @UndefinedVariable

    summary = forms.CharField(
        label=_('Summary'),
        help_text=_("Write a brief message for the article's history log."),
        required=False)

    def clean_slug(self):
        slug = self.cleaned_data['slug']
        if slug.startswith("_"):
            raise forms.ValidationError(
                ugettext('A slug may not begin with an underscore.'))
        if slug == 'admin':
            raise forms.ValidationError(
                ugettext("'admin' is not a permitted slug name."))

        if settings.URL_CASE_SENSITIVE:
            already_existing_slug = models.URLPath.objects.filter(
                slug=slug,
                parent=self.urlpath_parent)
        else:
            slug = slug.lower()
            slug = slug.replace('-', '_')
            already_existing_slug = models.URLPath.objects.filter(
                slug__iexact=slug,
                parent=self.urlpath_parent)
        if already_existing_slug:
            already_urlpath = already_existing_slug[0]
            if already_urlpath.article and already_urlpath.article.current_revision.deleted:
                raise forms.ValidationError(
                    ugettext('A deleted article with slug "%s" already exists.') %
                    already_urlpath.slug)
            else:
                raise forms.ValidationError(
                    ugettext('A slug named "%s" already exists.') %
                    already_urlpath.slug)

        if settings.CHECK_SLUG_URL_AVAILABLE:
            try:
                # Fail validation if URL resolves to non-wiki app
                match = resolve(self.urlpath_parent.path + '/' + slug + '/')
                if match.app_name != 'wiki':
                    raise forms.ValidationError(
                        ugettext('This slug conflicts with an existing URL.'))
            except Resolver404:
                pass

        return slug

    def clean(self):
        self.check_spam()
        return self.cleaned_data


class DeleteForm(forms.Form):

    def __init__(self, *args, **kwargs):
        self.article = kwargs.pop('article')
        self.has_children = kwargs.pop('has_children')
        super(DeleteForm, self).__init__(*args, **kwargs)

    confirm = forms.BooleanField(required=False,
                                 label=_('Yes, I am sure'))
    purge = forms.BooleanField(
        widget=HiddenInput(),
        required=False, label=_('Purge'),
        help_text=_(
            'Purge the article: Completely remove it (and all its contents) with no undo. Purging is a good idea if you want to free the slug such that users can create new articles in its place.'))
    revision = forms.ModelChoiceField(models.ArticleRevision.objects.all(),
                                      widget=HiddenInput(), required=False)

    def clean(self):
        cd = self.cleaned_data
        if not cd['confirm']:
            raise forms.ValidationError(ugettext('You are not sure enough!'))
        if cd['revision'] != self.article.current_revision:
            raise forms.ValidationError(
                ugettext(
                    'While you tried to delete this article, it was modified. TAKE CARE!'))
        return cd


class PermissionsForm(PluginSettingsFormMixin, forms.ModelForm):

    locked = forms.BooleanField(
        label=_('Lock article'),
        help_text=_('Deny all users access to edit this article.'),
        required=False)

    settings_form_headline = _('Permissions')
    settings_order = 5
    settings_write_access = False

    owner_username = forms.CharField(
        required=False,
        label=_('Owner'),
        help_text=_('Enter the username of the owner.'))
    group = forms.ModelChoiceField(
        models.Group.objects.all(),
        empty_label=_('(none)'),
        label=_('Group'),
        required=False)
    if settings.USE_BOOTSTRAP_SELECT_WIDGET:
        group.widget = SelectWidgetBootstrap()

    recursive = forms.BooleanField(
        label=_('Inherit permissions'),
        help_text=_(
            'Check here to apply the above permissions (excluding group and owner of the article) recursively to articles below this one.'),
        required=False)

    recursive_owner = forms.BooleanField(
        label=_('Inherit owner'),
        help_text=_(
            'Check here to apply the ownership setting recursively to articles below this one.'),
        required=False)

    recursive_group = forms.BooleanField(
        label=_('Inherit group'),
        help_text=_(
            'Check here to apply the group setting recursively to articles below this one.'),
        required=False)

    def get_usermessage(self):
        if self.changed_data:
            return _('Permission settings for the article were updated.')
        else:
            return _(
                'Your permission settings were unchanged, so nothing saved.')

    def __init__(self, article, request, *args, **kwargs):
        self.article = article
        self.user = request.user
        self.request = request
        kwargs['instance'] = article
        kwargs['initial'] = {'locked': article.current_revision.locked}

        super(PermissionsForm, self).__init__(*args, **kwargs)

        self.can_change_groups = False
        self.can_assign = False

        if permissions.can_assign(article, request.user):
            self.can_assign = True
            self.can_change_groups = True
            self.fields['group'].queryset = models.Group.objects.all()
        elif permissions.can_assign_owner(article, request.user):
            self.fields['group'].queryset = models.Group.objects.filter(
                user=request.user)
            self.can_change_groups = True
        else:
            # Quick-fix...
            # Set the group dropdown to readonly and with the current
            # group as only selectable option
            self.fields['group'] = forms.ModelChoiceField(
                queryset=models.Group.objects.filter(
                    id=self.instance.group.id)
                if self.instance.group else models.Group.objects.none(),
                empty_label=_('(none)'),
                required=False, widget=SelectWidgetBootstrap(
                    disabled=True)
                if settings.USE_BOOTSTRAP_SELECT_WIDGET else forms.Select(
                    attrs={'disabled': True}))
            self.fields['group_read'].widget = forms.HiddenInput()
            self.fields['group_write'].widget = forms.HiddenInput()

        if not self.can_assign:
            self.fields['owner_username'].widget = forms.TextInput(
                attrs={
                    'readonly': 'true'})
            self.fields['recursive'].widget = forms.HiddenInput()
            self.fields['recursive_group'].widget = forms.HiddenInput()
            self.fields['recursive_owner'].widget = forms.HiddenInput()
            self.fields['locked'].widget = forms.HiddenInput()

        self.fields['owner_username'].initial = getattr(
            article.owner,
            User.USERNAME_FIELD) if article.owner else ""

    def clean_owner_username(self):
        if self.can_assign:
            username = self.cleaned_data['owner_username']
            if username:
                try:
                    kwargs = {User.USERNAME_FIELD: username}
                    user = User.objects.get(**kwargs)
                except User.DoesNotExist:
                    raise forms.ValidationError(
                        ugettext('No user with that username'))
            else:
                user = None
        else:
            user = self.article.owner
        return user

    def save(self, commit=True):
        article = super(PermissionsForm, self).save(commit=False)

        # Alter the owner according to the form field owner_username
        # TODO: Why not rename this field to 'owner' so this happens
        # automatically?
        article.owner = self.cleaned_data['owner_username']

        # Revert any changes to group permissions if the
        # current user is not allowed (see __init__)
        # TODO: Write clean methods for this instead!
        if not self.can_change_groups:
            article.group = self.article.group
            article.group_read = self.article.group_read
            article.group_write = self.article.group_write

        if self.can_assign:
            if self.cleaned_data['recursive']:
                article.set_permissions_recursive()
            if self.cleaned_data['recursive_owner']:
                article.set_owner_recursive()
            if self.cleaned_data['recursive_group']:
                article.set_group_recursive()
            if self.cleaned_data[
                    'locked'] and not article.current_revision.locked:
                revision = models.ArticleRevision()
                revision.inherit_predecessor(self.article)
                revision.set_from_request(self.request)
                revision.automatic_log = _('Article locked for editing')
                revision.locked = True
                self.article.add_revision(revision)
            elif not self.cleaned_data['locked'] and article.current_revision.locked:
                revision = models.ArticleRevision()
                revision.inherit_predecessor(self.article)
                revision.set_from_request(self.request)
                revision.automatic_log = _('Article unlocked for editing')
                revision.locked = False
                self.article.add_revision(revision)

        article.save()

    class Meta:
        model = models.Article
        fields = (
            'locked',
            'owner_username',
            'recursive_owner',
            'group',
            'recursive_group',
            'group_read',
            'group_write',
            'other_read',
            'other_write',
            'recursive')
        widgets = {}


class DirFilterForm(forms.Form):

    query = forms.CharField(
        widget=forms.TextInput(
            attrs={
                'placeholder': _('Filter...'),
                'class': 'search-query'}),
        required=False)


class SearchForm(forms.Form):

    q = forms.CharField(
        widget=forms.TextInput(
            attrs={
                'placeholder': _('Search...'),
                'class': 'search-query'}),
        required=False)


class UserCreationForm(UserCreationForm):
    email = forms.EmailField(required=True)

    def __init__(self, *args, **kwargs):
        super(UserCreationForm, self).__init__(*args, **kwargs)

        # Add honeypots
        self.honeypot_fieldnames = "address", "phone"
        self.honeypot_class = ''.join(
            random.choice(string.ascii_uppercase + string.digits)
            for __ in range(10))
        self.honeypot_jsfunction = 'f' + ''.join(
            random.choice(string.ascii_uppercase + string.digits)
            for __ in range(10))

        for fieldname in self.honeypot_fieldnames:
            self.fields[fieldname] = forms.CharField(
                widget=forms.TextInput(attrs={'class': self.honeypot_class}),
                required=False,
            )

    def clean(self):
        cd = super(UserCreationForm, self).clean()
        for fieldname in self.honeypot_fieldnames:
            if cd[fieldname]:
                raise forms.ValidationError(
                    "Thank you, non-human visitor. Please keep trying to fill in the form.")
        return cd

    class Meta:
        model = User
        fields = ("username", "email")
