import urllib
from django.contrib.auth import get_permission_codename
from django.contrib.auth.models import Permission
from django.utils.translation import ugettext as _
from django.utils.encoding import force_text
from django.contrib.admin.utils import quote
from django.core.urlresolvers import reverse
from wagtail.wagtailcore.models import Page


class PermissionHelper(object):
    """
    Provides permission-related helper functions to help determine what a 
    user can do with a 'typical' model (where permissions are granted
    model-wide).
    """

    def __init__(self, model):
        self.model = model
        self.opts = model._meta

    def get_all_model_permissions(self):
        return Permission.objects.filter(
            content_type__app_label=self.opts.app_label,
            content_type__model=self.opts.model_name,
        )

    def has_specific_permission(self, user, codename):
        return user.has_perm("%s.%s" % (self.opts.app_label, codename))

    def has_any_permissions(self, user):
        """
        Return a boolean to indicate whether the supplied user has any
        permissions at all on the associated model
        """
        for perm in self.get_all_model_permissions():
            if self.has_specific_permission(user, perm.codename):
                return True
        return False

    def has_add_permission(self, user):
        """
        For typical models, whether or not a user can add an object depends
        on their permissions on that model
        """
        return self.has_specific_permission(
            user, get_permission_codename('add', self.opts))

    def has_edit_permission(self, user):
        """
        For typical models, whether or not a user can edit an object depends
        on their permissions on that model
        """
        return self.has_specific_permission(
            user, get_permission_codename('change', self.opts))

    def has_delete_permission(self, user):
        """
        For typical models, whether or not a user can delete an object depends
        on their permissions on that model
        """
        return self.has_specific_permission(
            user, get_permission_codename('delete', self.opts))

    def has_list_permission(self, user):
        return self.has_any_permissions(user)

    def can_edit_object(self, user, obj):
        """
        Used from within templates to decide what functionality to allow
        for a specific object. For typical models, we just return the
        model-wide permission.
        """
        return self.has_edit_permission(user)

    def can_delete_object(self, user, obj):
        """
        Used from within templates to decide what functionality to allow
        for a specific object. For typical models, we just return the
        model-wide permission.
        """
        return self.has_delete_permission(user)

    def can_unpublish_object(self, user, obj):
        """
        'Unpublishing' isn't really a valid option for models not extending
        Page, so we always return False
        """
        return False

    def can_copy_object(self, user, obj):
        """
        'Copying' isn't really a valid option for models not extending
        Page, so we always return False
        """
        return False


class PagePermissionHelper(PermissionHelper):
    """
    Provides permission-related helper functions to help determine what 
    a user can do with a model extending Wagtail's Page model. It differs
    from `PermissionHelper`, because model-wide permissions aren't really
    relevant. We generally need to determine permissions on an 
    object-specific basis.
    """

    def get_valid_parent_pages(self, user):
        """
        Identifies possible parent pages for the current user by first looking
        at allowed_parent_page_models() on self.model to limit options to the
        correct type of page, then checking permissions on those individual
        pages to make sure we have permission to add a subpage to it.
        """
        # Start with empty qs
        parents_qs = Page.objects.none()

        # Add pages of the correct type
        for pt in self.model.allowed_parent_page_models():
            pt_items = Page.objects.type(pt)
            parents_qs = parents_qs | pt_items

        # Exclude pages that we can't add subpages to
        for page in parents_qs.all():
            if not page.permissions_for_user(user).can_add_subpage():
                parents_qs = parents_qs.exclude(pk=page.pk)

        return parents_qs

    def has_list_permssion(self, user):
        """
        For models extending Page, permitted actions are determined by
        permissions on individual objects. Rather than check for change
        permissions on every object individually (which would be quite
        resource intensive), we simply always allow the list view to be
        viewed, and limit further functionality when relevant.
        """
        return True

    def has_add_permission(self, user):
        """
        For models extending Page, whether or not a page of this type can be
        added somewhere in the tree essentially determines the add permission,
        rather than actual model-wide permissions
        """
        return self.get_valid_parent_pages(user).count() > 0

    def can_edit_object(self, user, obj):
        perms = obj.permissions_for_user(user)
        return perms.can_edit()

    def can_delete_object(self, user, obj):
        perms = obj.permissions_for_user(user)
        return perms.can_delete()

    def can_unpublish_object(self, user, obj):
        perms = obj.permissions_for_user(user)
        return obj.live and perms.can_unpublish()

    def can_copy_object(self, user, obj):
        parent_page = obj.get_parent()
        return parent_page.permissions_for_user(user).can_publish_subpage()


def get_url_pattern(model_meta, action=None):
    if not action:
        return r'^modeladmin/%s/%s/$' % (
            model_meta.app_label, model_meta.model_name)
    return r'^modeladmin/%s/%s/%s/$' % (
        model_meta.app_label, model_meta.model_name, action)


def get_object_specific_url_pattern(model_meta, action):
    return r'^modeladmin/%s/%s/%s/(?P<object_id>[-\w]+)/$' % (
        model_meta.app_label, model_meta.model_name, action)


def get_url_name(model_meta, action='index'):
    return '%s_%s_modeladmin_%s/' % (
        model_meta.app_label, model_meta.model_name, action)    


class ButtonHelper(object):

    default_button_classnames = ['button']
    add_button_classnames = ['bicolor', 'icon', 'icon-plus']
    inspect_button_classnames = []
    edit_button_classnames = []
    delete_button_classnames = ['no']

    def __init__(self, model, permission_helper, user,
                 inspect_view_enabled=False):
        self.user = user
        self.model = model
        self.opts = model._meta
        self.permission_helper = permission_helper
        self.inspect_view_enabled = inspect_view_enabled
        self.model_name = force_text(self.opts.verbose_name).lower()

    def finalise_classname(self, classnames_add=[], classnames_exclude=[]):
        combined = self.default_button_classnames + classnames_add
        finalised = [cn for cn in combined if cn not in classnames_exclude]
        return ' '.join(finalised)

    def get_action_url(self, action='index', pk=None):
        kwargs = {}
        if pk and action not in ('create', 'index'):
            kwargs.update({'object_id': pk})
        return reverse(get_url_name(self.opts, action), kwargs=kwargs)

    def show_add_button(self):
        return self.permission_helper.has_add_permission(self.user)

    def add_button(self, classnames_add=[], classnames_exclude=[]):
        classnames = self.add_button_classnames + classnames_add
        cn = self.finalise_classname(classnames, classnames_exclude)
        return {
            'url': self.get_action_url('create'),
            'label': _('Add %s') % self.model_name,
            'classname': cn,
            'title': _('Add a new %s') % self.model_name,
        }

    def inspect_button(self, pk, classnames_add=[], classnames_exclude=[]):
        classnames = self.inspect_button_classnames + classnames_add
        cn = self.finalise_classname(classnames, classnames_exclude)
        return {
            'url': self.get_action_url('inspect', pk),
            'label': _('Inspect'),
            'classname': cn,
            'title': _('View details for this %s') % self.model_name,
        }

    def edit_button(self, pk, classnames_add=[], classnames_exclude=[]):
        classnames = self.edit_button_classnames + classnames_add
        cn = self.finalise_classname(classnames, classnames_exclude)
        return {
            'url': self.get_action_url('edit', pk),
            'label': _('Edit'),
            'classname': cn,
            'title': _('Edit this %s') % self.model_name,
        }

    def delete_button(self, pk, classnames_add=[], classnames_exclude=[]):
        classnames = self.delete_button_classnames + classnames_add
        cn = self.finalise_classname(classnames, classnames_exclude)
        return {
            'url': self.get_action_url('confirm_delete', pk),
            'label': _('Delete'),
            'classname': cn,
            'title': _('Delete this %s') % self.model_name,
        }

    def get_buttons_for_obj(self, obj, exclude=[], classnames_add=[],
                            classnames_exclude=[]):
        ph = self.permission_helper
        pk = quote(getattr(obj, self.opts.pk.attname))
        btns = []
        if('inspect' not in exclude and self.inspect_view_enabled):
            btns.append(
                self.inspect_button(pk, classnames_add, classnames_exclude)
            )
        if('edit' not in exclude and ph.can_edit_object(self.user, obj)):
            btns.append(
                self.edit_button(pk, classnames_add, classnames_exclude)
            )
        if('delete' not in exclude and ph.can_delete_object(self.user, obj)):
            btns.append(
                self.delete_button(pk, classnames_add, classnames_exclude)
            )
        return btns


class PageButtonHelper(ButtonHelper):

    unpublish_button_classnames = []
    copy_button_classnames = []

    def unpublish_button(self, pk, classnames_add=[], classnames_exclude=[]):
        classnames = self.unpublish_button_classnames + classnames_add
        cn = self.finalise_classname(classnames, classnames_exclude)
        return {
            'url': self.get_action_url('unpublish', pk),
            'label': _('Unpublish'),
            'classname': cn,
            'title': _('Unpublish this %s') % self.model_name,
        }

    def copy_button(self, pk, classnames_add=[], classnames_exclude=[]):
        classnames = self.copy_button_classnames + classnames_add
        cn = self.finalise_classname(classnames, classnames_exclude)
        return {
            'url': self.get_action_url('copy', pk),
            'label': _('Copy'),
            'classname': cn,
            'title': _('Copy this %s') % self.model_name,
        }

    def get_buttons_for_obj(self, obj, exclude=[], classnames_add=[],
                            classnames_exclude=[]):
        user = self.user
        ph = self.permission_helper
        pk = quote(getattr(obj, self.opts.pk.attname))
        btns = []
        if('inspect' not in exclude and self.inspect_view_enabled):
            btns.append(
                self.inspect_button(pk, classnames_add, classnames_exclude)
            )
        if('edit' not in exclude and ph.can_edit_object(user, obj)):
            btns.append(
                self.edit_button(pk, classnames_add, classnames_exclude)
            )
        if('copy' not in exclude and ph.can_copy_object(user, obj)):
            btns.append(
                self.copy_button(pk, classnames_add, classnames_exclude)
            )
        if('unpublish' not in exclude and ph.can_unpublish_object(user, obj)):
            btns.append(
                self.unpublish_button(pk, classnames_add, classnames_exclude)
            )
        if('delete' not in exclude and ph.can_delete_object(user, obj)):
            btns.append(
                self.delete_button(pk, classnames_add, classnames_exclude)
            )
        return btns