
"""
This file contains the generic, assorted views that don't fall under one of
the other applications. Views are django's way of processing e.g. html
templates on the fly.

"""
from collections import OrderedDict

from django.contrib.admin.sites import site
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import authenticate
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.admin.views.decorators import staff_member_required
from django.core.exceptions import PermissionDenied
from django.db.models.functions import Lower
from django.http import HttpResponseBadRequest, HttpResponseRedirect, Http404
from django.shortcuts import render
from django.urls import reverse, reverse_lazy
from django.views.generic import View, TemplateView, ListView, DetailView, FormView
from django.views.generic.base import RedirectView
from django.views.generic.edit import CreateView, UpdateView, DeleteView

from evennia import SESSION_HANDLER
from evennia.objects.models import ObjectDB
from evennia.accounts.models import AccountDB
from evennia.utils import class_from_module, logger
from evennia.web.website.forms import *

from django.contrib.auth import login
from django.utils.text import slugify

_BASE_CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS


def _shared_login(request):
    """
    Handle the shared login between website and webclient.

    """
    csession = request.session
    account = request.user
    website_uid = csession.get("website_authenticated_uid", None)
    webclient_uid = csession.get("webclient_authenticated_uid", None)

    if not csession.session_key:
        # this is necessary to build the sessid key
        csession.save()

    if account.is_authenticated():
        # Logged into website
        if not website_uid:
            # fresh website login (just from login page)
            csession["website_authenticated_uid"] = account.id
            if webclient_uid is None:
                # auto-login web client
                csession["webclient_authenticated_uid"] = account.id

    elif webclient_uid:
        # Not logged into website, but logged into webclient
        if not website_uid:
            csession["website_authenticated_uid"] = account.id
            account = AccountDB.objects.get(id=webclient_uid)
            try:
                # calls our custom authenticate, in web/utils/backend.py
                authenticate(autologin=account)
                login(request, account)
            except AttributeError:
                logger.log_trace()


def _gamestats():
    # Some misc. configurable stuff.
    # TODO: Move this to either SQL or settings.py based configuration.
    fpage_account_limit = 4

    # A QuerySet of the most recently connected accounts.
    recent_users = AccountDB.objects.get_recently_connected_accounts()[:fpage_account_limit]
    nplyrs_conn_recent = len(recent_users) or "none"
    nplyrs = AccountDB.objects.num_total_accounts() or "none"
    nplyrs_reg_recent = len(AccountDB.objects.get_recently_created_accounts()) or "none"
    nsess = SESSION_HANDLER.account_count()
    # nsess = len(AccountDB.objects.get_connected_accounts()) or "no one"

    nobjs = ObjectDB.objects.all().count()
    nrooms = ObjectDB.objects.filter(db_location__isnull=True).exclude(db_typeclass_path=_BASE_CHAR_TYPECLASS).count()
    nexits = ObjectDB.objects.filter(db_location__isnull=False, db_destination__isnull=False).count()
    nchars = ObjectDB.objects.filter(db_typeclass_path=_BASE_CHAR_TYPECLASS).count()
    nothers = nobjs - nrooms - nchars - nexits

    pagevars = {
        "page_title": "Front Page",
        "accounts_connected_recent": recent_users,
        "num_accounts_connected": nsess or "no one",
        "num_accounts_registered": nplyrs or "no",
        "num_accounts_connected_recent": nplyrs_conn_recent or "no",
        "num_accounts_registered_recent": nplyrs_reg_recent or "no one",
        "num_rooms": nrooms or "none",
        "num_exits": nexits or "no",
        "num_objects": nobjs or "none",
        "num_characters": nchars or "no",
        "num_others": nothers or "no"
    }
    return pagevars


def to_be_implemented(request):
    """
    A notice letting the user know that this particular feature hasn't been
    implemented yet.
    """

    pagevars = {
        "page_title": "To Be Implemented...",
    }

    return render(request, 'tbi.html', pagevars)


@staff_member_required
def evennia_admin(request):
    """
    Helpful Evennia-specific admin page.
    """
    return render(
        request, 'evennia_admin.html', {
            'accountdb': AccountDB})


def admin_wrapper(request):
    """
    Wrapper that allows us to properly use the base Django admin site, if needed.
    """
    return staff_member_required(site.index)(request)
    
#
# Class-based views
#

class EvenniaIndexView(TemplateView):
    # Display this HTML page
    template_name = 'website/index.html'
    
    # Display these variables on it
    def get_context_data(self, **kwargs):
        # Call the base implementation first to get a context object
        context = super(EvenniaIndexView, self).get_context_data(**kwargs)
        
        # Add game statistics and other pagevars
        context.update(_gamestats())
        
        return context

class EvenniaCreateView(CreateView):
    
    @property
    def page_title(self):
        return 'Create %s' % self.model._meta.verbose_name.title()
        
class EvenniaDetailView(DetailView):
    
    @property
    def page_title(self):
        return '%s Detail' % self.model._meta.verbose_name.title()

class EvenniaUpdateView(UpdateView):
    
    @property
    def page_title(self):
        return 'Update %s' % self.model._meta.verbose_name.title()

class EvenniaDeleteView(DeleteView):
    
    @property
    def page_title(self):
        return 'Delete %s' % self.model._meta.verbose_name.title()
        
#
# Object views
#
    
class ObjectDetailView(EvenniaDetailView):
    
    model = class_from_module(settings.BASE_OBJECT_TYPECLASS)
    access_type = 'view'
    template_name = 'website/object_detail.html'
    attributes = ['name', 'desc']
    
    def get_context_data(self, **kwargs):
        """
        Adds an 'attributes' list to the context consisting of the attributes
        specified at the class level, in the order provided.
        
        Django views do not provide a way to reference dynamic attributes, so
        we have to grab them all before we render the template.
        
        """
        context = super(ObjectDetailView, self).get_context_data(**kwargs)
        
        obj = self.get_object()
        attribute_list = OrderedDict()
        
        for attribute in self.attributes:
            if attribute in self.model._meta._property_names:
                attribute_list[attribute.title()] = getattr(obj, attribute, '')
            else:
                attribute_list[attribute.title()] = getattr(obj.db, attribute, '')
                
        context['attribute_list'] = attribute_list
            
        return context
    
    def get_object(self, queryset=None):
        """
        Override of Django hook.
        
        Evennia does not natively store slugs, so where a slug is provided, 
        calculate the same for the object and make sure it matches.
        
        """
        if not queryset:
            queryset = self.get_queryset()
        
        # Get the object, ignoring all checks and filters
        obj = self.model.objects.get(pk=self.kwargs.get('pk'))
        
        # Check if this object was requested in a valid manner
        if slugify(obj.name) != self.kwargs.get(self.slug_url_kwarg):
            raise HttpResponseBadRequest(u"No %(verbose_name)s found matching the query" %
              {'verbose_name': queryset.model._meta.verbose_name})
        
        # Check if account has permissions to access object
        account = self.request.user
        if not obj.access(account, self.access_type):
            raise PermissionDenied(u"You are not authorized to %s this object." % self.access_type)
            
        # Get the object, based on the specified queryset
        obj = super(ObjectDetailView, self).get_object(queryset)
        
        return obj
        
class ObjectCreateView(LoginRequiredMixin, EvenniaCreateView):
    
    model = class_from_module(settings.BASE_OBJECT_TYPECLASS)
        
class ObjectDeleteView(LoginRequiredMixin, ObjectDetailView, EvenniaDeleteView):
    
    model = class_from_module(settings.BASE_OBJECT_TYPECLASS)
    access_type = 'delete'
    template_name = 'website/object_confirm_delete.html'
    
    def delete(self, request, *args, **kwargs):
        """
        Calls the delete() method on the fetched object and then
        redirects to the success URL.
        
        We extend this so we can capture the name for the sake of confirmation.
        """
        obj = str(self.get_object())
        response = super(ObjectDeleteView, self).delete(request, *args, **kwargs)
        messages.success(request, "Successfully deleted '%s'." % obj)
        return response
    
class ObjectUpdateView(LoginRequiredMixin, ObjectDetailView, EvenniaUpdateView):
    
    model = class_from_module(settings.BASE_OBJECT_TYPECLASS)
    access_type = 'edit'
    
    def get_success_url(self):
        if self.success_url: return self.success_url
        return self.object.web_get_detail_url()
    
    def get_initial(self):
        """
        Override of Django hook.
        
        Prepopulates form field values based on object db attributes as well as 
        model field values.
        
        """
        # Get the object we want to update
        obj = self.get_object()
        
        # Get attributes
        data = {k:getattr(obj.db, k, '') for k in self.form_class.base_fields}
        
        # Get model fields
        data.update({k:getattr(obj, k, '') for k in self.form_class.Meta.fields})
        
        return data
        
    def form_valid(self, form):
        """
        Override of Django hook.
        
        Updates object attributes based on values submitted.
        
        This method is only called if all values for the fields submitted
        passed form validation, so at this point we can assume the data is 
        validated and sanitized.
        
        """
        # Get the values submitted after they've been cleaned and validated
        data = {k:v for k,v in form.cleaned_data.items() if k not in self.form_class.Meta.fields}
        
        # Update the object attributes
        for key, value in data.items():
            setattr(self.object.db, key, value)
            messages.success(self.request, "Successfully updated '%s' for %s." % (key, self.object))
            
        # Do not return super().form_valid; we don't want to update the model
        # instance, just its attributes.
        return HttpResponseRedirect(self.get_success_url())
        
#
# Account views
#

class AccountMixin(object):
    
    model = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
    form_class = AccountForm

class AccountCreateView(AccountMixin, ObjectCreateView):
    
    template_name = 'website/registration/register.html'
    success_url = reverse_lazy('login')
    
    def form_valid(self, form):
        
        username = form.cleaned_data['username']
        password = form.cleaned_data['password1']
        email = form.cleaned_data.get('email', '')
        
        # Create account
        account, errs = self.model.create(
            username=username, 
            password=password,
            email=email,)
        
        # If unsuccessful, get messages passed to session.msg
        if not account:
            [messages.error(self.request, err) for err in errs]
            return self.form_invalid(form)
            
        messages.success(self.request, "Your account '%s' was successfully created! You may log in using it now." % account.name)
        return HttpResponseRedirect(self.success_url)
        
#
# Character views
#
        
class CharacterMixin(object):
    
    model = class_from_module(settings.BASE_CHARACTER_TYPECLASS)
    form_class = CharacterForm
    success_url = reverse_lazy('character-manage')
    
    def get_queryset(self):
        # Get IDs of characters owned by account
        ids = [getattr(x, 'id') for x in self.request.user.characters if x]
        
        # Return a queryset consisting of those characters
        return self.model.objects.filter(id__in=ids).order_by(Lower('db_key'))
        
class CharacterPuppetView(LoginRequiredMixin, CharacterMixin, RedirectView, ObjectDetailView):
    
    def get_redirect_url(self, *args, **kwargs):
        # Get the requested character, if it belongs to the authenticated user
        char = self.get_object()
        next = self.kwargs.get('next', self.success_url)
        
        if char:
            self.request.session['puppet'] = int(char.pk)
            messages.success(self.request, "You become '%s'!" % char)
        else:
            self.request.session['puppet'] = None
            messages.error(self.request, "You cannot become '%s'." % char)
            
        return next
        
class CharacterManageView(LoginRequiredMixin, CharacterMixin, ListView):

    paginate_by = 10
    template_name = 'website/character_manage_list.html'
    page_title = 'Manage Characters'
        
class CharacterUpdateView(CharacterMixin, ObjectUpdateView):
    
    form_class = CharacterUpdateForm
    template_name = 'website/character_form.html'
    
class CharacterDetailView(CharacterMixin, ObjectDetailView):
    pass

class CharacterDeleteView(CharacterMixin, ObjectDeleteView):
    pass
        
class CharacterCreateView(CharacterMixin, ObjectCreateView):

    template_name = 'website/character_form.html'
    
    def form_valid(self, form):
        # Get account ref
        account = self.request.user
        character = None
        
        # Get attributes from the form
        self.attributes = {k: form.cleaned_data[k] for k in form.cleaned_data.keys()}
        charname = self.attributes.pop('db_key')
        description = self.attributes.pop('desc')
        
        # Create a character
        try:
            character, errors = self.model.create(charname, account, description=description)
            
            # Assign attributes from form
            [setattr(character.db, key, value) for key,value in self.attributes.items()]
            character.db.creator_id = account.id
            character.save()
            account.save()
            
        except Exception as e:
            messages.error(self.request, "There was an error creating your character. If this problem persists, contact an admin.")
            logger.log_trace()
            return self.form_invalid(form)
        
        if character:
            messages.success(self.request, "Your character '%s' was created!" % character.name)
            return HttpResponseRedirect(self.success_url)
        else:
            messages.error(self.request, "Your character could not be created. Please contact an admin.")
            return self.form_invalid(form)
