from django.shortcuts import render, redirect, get_object_or_404
from django.views import generic
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.http import HttpResponseRedirect, JsonResponse, HttpResponse
from django.urls import reverse, reverse_lazy

from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required
from django.contrib.sessions.backends.base import SessionBase

from django.core.serializers.json import DjangoJSONEncoder
from django.core.serializers import serialize
from django.core import serializers

from django.utils.html import strip_tags
from django.db.models import Q
from channels.db import database_sync_to_async
from django.db.models.query import EmptyQuerySet

from .models import Canvas, CanvasTag, Idea, IdeaComment, Project
from .forms import SignUpForm, IdeaForm, CommentForm, AddUserForm
from . import consumers

from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer, AsyncWebsocketConsumer, AsyncConsumer

import django.utils.timezone

# TODO: change serialization methods from needing to pass a singleton list to accepting a single model instance (each marked below)

'''
TODO: nullable owner field, blank admins & users - this is for a 'blank' project that 'blank' canvasses use, which the trial user
uses. The selected trial canvas is currently downloaded as any other canvas, and a new idea is created like other ones, but the idea
is not added to the canvas model in the database. It is just so that a blank idea can be sent back to the trial user and appended
to the list of ideas in the front-end. This is why View functions, when testing for user permissions, also check for project.title == blank-project

This approach was written as a 'quick-fix', a better solution should be investigated.
'''


##################################################################################################################################
#                                                           CANVAS VIEWS                                                         #
##################################################################################################################################



def new_canvas(request, canvas_type):
    creator = request.user

    if creator.is_authenticated:

        split_url = request.META.get('HTTP_REFERER').split('/')
        project_pk = split_url[len(split_url) - 2]
        project = Project.objects.get(pk=project_pk)

        # canvas_type integer: 0 for Ethics, 1 for Business, 2 for Privacy
        canvas = Canvas(canvas_type=canvas_type, project=project)
        canvas.save()
        canvas.title =  f'New Canvas {canvas.pk} (Ethics)' if canvas_type == 0 else f'New Canvas {canvas.pk} (Business)' if canvas_type == 1 else f'New Canvas {canvas.pk} (Privacy)'
        canvas.save()

        return redirect(canvas.get_absolute_url()) # bring user to the canvas page for the newly created canvas

    else:
        # NOTE: code below relates to blank project
        # check that a blank canvas exists - this will be used to render a blank canvas for the anonymous user to interact with

        if Project.objects.filter(title='blank-project').exists() == False:
            project = Project(title='blank-project')
            project.save()

        else:
            project = Project.objects.get(title='blank-project', is_public=False)

            if canvas_type == 0:

                if Canvas.objects.filter(title='blank-ethics').exists():
                    return redirect(Canvas.objects.get(title='blank-ethics').get_absolute_url())
                else :
                # if there is no blank canvas, create one. set public to false so that it remains blank
                    canvas = Canvas(title='blank-ethics', canvas_type=canvas_type, project=project)
                    canvas.save()
                    return redirect(canvas.get_absolute_url()) # bring user to the canvas page for the newly created canvas

            elif canvas_type == 1:
                if Canvas.objects.filter(title='blank-business').exists():
                    return redirect(Canvas.objects.get(title='blank-business').get_absolute_url())
                else :
                # if there is no blank canvas, create one. set public to false so that it remains blank
                    canvas = Canvas(title='blank-business', canvas_type=canvas_type, project=project)
                    canvas.save()
                    return redirect(canvas.get_absolute_url()) # bring user to the canvas page for the newly created canvas

            elif canvas_type == 2:
                if Canvas.objects.filter(title='blank-privacy').exists():
                    return redirect(Canvas.objects.get(title='blank-business').get_absolute_url())
                else :
                # if there is no blank canvas, create one. set public to false so that it remains blank
                    canvas = Canvas(title='blank-privacy', canvas_type=canvas_type, project=project)
                    canvas.save()
                    return redirect(canvas.get_absolute_url()) # bring user to the canvas page for the newly created canvas


def new_project(request):
    creator = request.user

    project = Project(owner=creator)
    project.save()

    project.title = f'Project {project.pk}'
    project.admins.add(creator)
    project.users.add(creator)
    project.save()

    return redirect(project.get_absolute_url())



def delete_canvas(request, pk):
    '''
    Function for deleting a canvas
    '''
    user = request.user
    canvas = Canvas.objects.get(pk = pk)
    tags = canvas.tags.all()

    for tag in tags:
        tag.canvas_set.remove(canvas)
        tag.idea_set.remove(idea__canvas__pk=canvas)
        tag.save()

    if (not admin_permission(user, canvas.project) or (project.title == 'blank-project')):
        return HttpResponse('Forbidden', status=403)

    canvas.delete()
    return redirect(request.META.get('HTTP_REFERER'))

def delete_project(request, pk):
    '''
    Function for deleting a canvas
    '''
    user = request.user
    project = Project.objects.get(pk = pk)

    if (project.owner != user):
        return HttpResponse('Forbidden', status=403)

    project.delete()
    return redirect(request.META.get('HTTP_REFERER'))

##################################################################################################################################
#                                                   CANVAS CLASS-BASED VIEWS                                                     #
##################################################################################################################################


class ProjectListView(LoginRequiredMixin, generic.ListView):
    model = Project

    def get_context_data(self, **kwargs):
        '''
        This function's purpose is to separate the public from the private canvases
        '''
        logged_in_user = self.request.user
        context = super().get_context_data(**kwargs)

        filter_kwargs = {'is_public': True}
        user_filter_kwargs = {'users': logged_in_user}
        admin_filter_kwargs = {'admins': logged_in_user}

        # public projects are those where public is true
        public = Project.objects.filter(**filter_kwargs)
        # private projects where the user is either the owner or a collaborator on the canvas
        private = Project.objects.exclude(**filter_kwargs)

        my_private = (
            private.filter(**admin_filter_kwargs) | private.filter(**user_filter_kwargs)
        ).distinct()

        all_projects = (
            Project.objects.filter(**filter_kwargs) | private.filter(**admin_filter_kwargs) | private.filter(**user_filter_kwargs)
        ).distinct()

        context['public_projects'] = public
        context['private_projects'] = my_private
        context['all_projects'] = all_projects

        return context

class ProjectDetailView(generic.DetailView):
    model = Project

    def get(self, request, pk):

        logged_in_user = self.request.user
        project = Project.objects.get(pk=pk)

        if (not user_permission(logged_in_user, project)):
            return {
                'error': 401,
                'response': 'unauthorized'
            }

        if request.is_ajax():
            json_users = '""'
            json_admins = '""'

            users = project.users.all()
            admins = project.admins.all()
            current = [logged_in_user]

            json_admins = serialize(
                'json',
                admins,
                cls = UserModelEncoder
            )

            json_users = serialize(
                'json',
                users,
                cls = UserModelEncoder
            )

            json_self=serialize(
                'json',
                current,
                cls=UserModelEncoder
            )
            # single user - remove enclosing square brackets
            json_self = json_self[1:-1]

            data = {
                'admins': json_admins,
                'users': json_users,
                'loggedInUser': json_self,

            }
            return JsonResponse(data, safe = False)


        else:
            canvas_list = Canvas.objects.filter(project=project)

            return render(
                request,
                'catalog/project_detail.html',
                {
                    'user': logged_in_user,
                    'canvases': canvas_list
                }
            )

class CanvasDetailView(generic.DetailView):
    model = Canvas

    def get(self, request, pk):
        '''
        function for post requests, sent by a canvas on loading
        purpose is to return the canvas information as a JSON
        '''
        logged_in_user = request.user

        canvas_pk = pk
        canvas = Canvas.objects.get(pk = canvas_pk)
        project = canvas.project

        # no user permission and the canvas isn't the blank one
        if (not user_permission(logged_in_user, project) and 'blank-' not in canvas.title):
            return {
                'error': 401,
                'response': 'unauthorized'
            }

        if request.is_ajax():
            # initialise every json_object as the empty string
            null_tag = CanvasTag(label=None)
            json_comments = '""'
            json_ideas = '""'
            json_tags = '""'
            json_self = '""'
            json_users = '""'
            json_admins = '""'

            if (logged_in_user.is_authenticated):
                current = [logged_in_user]

            else:
                current = project.users.none()
                comments = "''"


            ideas = Idea.objects.filter(canvas=canvas)
            # every tag attached to an idea in this current canvas
            tags = CanvasTag.objects.filter(idea_set__in=ideas).distinct()
            # every tag in the entire project
            all_tags = CanvasTag.objects.filter(canvas_set__in=Canvas.objects.filter(project=project)).distinct()

            # initialise these as empty lists - they will become lists of lists as each tag may have several tagged ideas and canvases
            tagged_ideas_json = []
            tagged_canvases_json = []

            for t in tags:
                '''
                Only need the tagged ideas and canvasses for the tags that will actually be rendered ie the tags in the current canvas
                '''
                tagged_ideas_json.append(
                    serialize(
                        'json',
                        t.idea_set.all(),
                        cls=IdeaEncoder
                    )
                )

                tagged_canvases_json.append(
                    serialize(
                        'json',
                        t.canvas_set.all(),
                        cls=CanvasEncoder
                    )
                )

            comments = IdeaComment.objects.filter(idea__in=ideas)
            # need the users list for the comment authors, when the comment is parsed the PK of the user is what's used for the user FK, not the user object itself
            users = project.users.all()
            # also need the admins for enabling or disabling certain buttons
            admins = project.admins.all()

            if tags:
                json_tags = serialize(
                    'json',
                    tags,
                    cls = CanvasTagEncoder
                )

            else:
                # a null tag is used for conditionally rendering the tag Vue element

                # TODO: change serialization method from needing to pass a singleton list to accepting a single model instance
                json_tags = serialize(
                    'json',
                    [null_tag],
                    cls = CanvasTagEncoder
                )
                # singular tag, replace enclosing square brackets with curly brackets
                json_tags = json_tags[1:-1]

            if all_tags:
                json_all_tags = serialize(
                    'json',
                    all_tags,
                    cls = CanvasTagEncoder
                )

            else:
                # a null tag is used for conditionally rendering the tag Vue element
                tag = CanvasTag(label=None)
                # TODO: change serialization method from needing to pass a singleton list to accepting a single model instance
                json_all_tags = serialize(
                    'json',
                    [null_tag],
                    cls = CanvasTagEncoder
                )
                # singular tag, remove enclosing square brackets
                json_all_tags = json_all_tags[1:-1]

            if comments:
                json_comments = serialize(
                    'json',
                    comments,
                    cls = IdeaCommentEncoder
                )

            # TODO: change serialization method from needing to pass a singleton list to accepting a single model instance
            # NB: current assigned above as either empty string if Anon. user or [logged_in_user] if is_authenticated
            json_self=serialize(
                'json',
                current,
                cls=UserModelEncoder
            )

            if logged_in_user.is_authenticated:
                # singular user, remove enclosing square brackets
                json_self = json_self[1:-1]


            # TODO: change serialization method from needing to pass a singleton list to accepting a single model instance
            json_canvas=serialize(
                'json',
                [canvas],
                cls=CanvasEncoder
            )

            # singular canvas, remove enclosing square brackets
            json_canvas = json_canvas[1:-1]


            json_users=serialize(
                'json',
                users,
                cls=UserModelEncoder
            )

            json_admins=serialize(
                'json',
                admins,
                cls=UserModelEncoder
            )

            # only serialise ideas if they exist
            if ideas:
                json_ideas = serialize(
                    'json',
                    ideas,
                    cls = IdeaEncoder
                )


            data = {
                'ideas': json_ideas,
                'comments': json_comments,
                'tags': json_tags,
                'allTags': json_all_tags,
                'loggedInUser': json_self,
                'allTaggedIdeas': tagged_ideas_json,
                'taggedCanvases': tagged_canvases_json,
                'thisCanvas': json_canvas,
                'projectPK': project.pk,
                'users': json_users,
                'admins': json_admins,
            }

            return JsonResponse(data, safe = False)
        else:
            '''
            This 'else' is for the initial page load. document.onload currently triggers the AJAX GET request which gets all
            the relevant canvas information as JSON objects
            '''
            return render(
                request,
                'catalog/canvas_detail.html',
                {
                    'user': logged_in_user,
                    'project': project,
                },
            )


##################################################################################################################################
#                                                         IDEA VIEWS                                                             #
##################################################################################################################################

def new_trial_idea(request):
    '''
    NOTE: BLANK CANVAS, TRIAL USER IDEA ADDITION
    '''
    if request.method == 'POST':
        print("..")
        canvas_pk = request.POST['canvas_pk']
        try:
            canvas = Canvas.objects.get(pk=canvas_pk)
        except Canvas.DoesNotExist:
            return HttpResponse('Canvas does not exist', status=404)

        logged_in_user = request.user

        project = canvas.project
        # can't add ideas if the canvas is unavailable or if the blank canvas is being edited to by an authenticated user
        if (not user_permission(logged_in_user, project) and ('blank-' not in canvas.title and logged_in_user.is_authenticated)):
            return HttpResponse('Unauthorized', status=401)

        category = request.POST['idea_category']

        idea = Idea(
            canvas = canvas,
            category = category,
            text = '',
            title = f'Canvas {canvas_pk} TRIAL IDEA'
        )
        idea.save()

        # TODO: change serialization method from needing to pass a singleton list to accepting a single model instance
        return_idea = serialize(
            'json',
            [idea],
            cls=IdeaEncoder
        )
        idea.delete()

        # singular idea, remove enclosing square brackets
        return_idea = return_idea[1:-1]
        data = {
            'idea': return_idea,
        }

        return JsonResponse(data)

def delete_trial_idea(idea_pk):
    '''
    NOTE: BLANK CANVAS, TRIAL USER IDEA DELETION - IMM
    '''
    Idea.objects.get(pk=idea_pk).delete()



def new_idea(request):
    '''
    Creation of a new idea. This gets the id for the canvas in which it is created from the calling URL
    '''
    if request.method == 'POST':
        canvas_pk = request.POST['canvas_pk']
        category = request.POST['idea_category']
        logged_in_user = request.user


        try:
            canvas = Canvas.objects.get(pk = canvas_pk)
            project = canvas.project
        except:
            Canvas.DoesNotExist
            return HttpResponse('Canvas does not exist', status=404)

            Project.DoesNotExist
            return HttpResponse('Project does not exist', status=404)

        # can't add ideas if the canvas is unavailable or if the blank canvas is being edited to by an authenticated user
        if (not user_permission(logged_in_user, project) and ('blank-' not in canvas.title and logged_in_user.is_authenticated)):
            return HttpResponse('Unauthorized', status=401)



        idea = Idea(
            canvas = canvas,
            category = category,
            text = ''
        )
        idea.save()
        # This is so I can click on it in the django admin - should probably delete later
        idea.title = f'Canvas {canvas_pk} Idea {idea.pk}'
        idea.save()


        # TODO: change serialization method from needing to pass a singleton list to accepting a single model instance
        return_idea = serialize(
            'json',
            [idea],
            cls=IdeaEncoder
        )
        # singular idea, remove enclosing square brackets
        return_idea = return_idea[1:-1]

        channel_layer = get_channel_layer()

        room_name = canvas_pk + "_idea"
        room_group_name = 'canvas_%s' %room_name

        data = {
            'function': request.POST['function'],
            'idea': return_idea,
        }

        async_to_sync(channel_layer.group_send)(
            room_group_name,
            {
                'type': 'channel_message',
                'data': data
            }
        )

        return HttpResponse(status=200)



def delete_idea(request):
    '''
    Deletion of an idea
    '''
    if request.method == 'POST':
        try:
            logged_in_user = request.user
            idea_pk = request.POST['idea_pk']

            idea = Idea.objects.get(pk=idea_pk)
            canvas = idea.canvas
            project = canvas.project

        except:
            Idea.DoesNotExist
            return HttpResponse('Idea does not exist', status=404)

            Canvas.DoesNotExist
            return HttpResponse('Canvas does not exist', status=404)

            Project.DoesNotExist
            return HttpResponse('Project does not exist', status=404)

        # can't remove ideas if the canvas is unavailable or if the blank canvas is being edited by an authenticated user
        if (not user_permission(logged_in_user, project) and ('blank-' not in canvas.title and logged_in_user.is_authenticated)):
            return HttpResponse('Unauthorized', status=401)

        category = idea.category

        # get every tag associated with the idea
        tags = idea.tags.all()
        removed_tags = []

        json_tagged_canvases = []
        json_tagged_ideas = []
        json_tags = []

        # iterate through the tags, removing the idea from each
        for tag in tags:
            tag.idea_set.remove(idea)
            tag.save()

            # check if any ideas remain
            updated_ideas = tag.idea_set.filter(canvas=canvas).distinct()

            # if no idaes remain, remove the canvas from the tag's canvas set as well
            if not updated_ideas:
                tag.canvas_set.remove(canvas)
                tag.save()
                canvas.save()

            removed_tags.append(tag)


        return_tag_data = []

        for tag in removed_tags:

            json_tagged_canvases=(
                serialize(
                    'json',
                    tag.canvas_set.all(),
                    cls=CanvasEncoder
                )
            )

            json_tagged_ideas=(
                serialize(
                    'json',
                    tag.idea_set.all(),
                    cls=IdeaEncoder
                )
            )

            json_tags=(
                serialize(
                    'json',
                    [tag],
                    cls = CanvasTagEncoder
                )
            )
            json_tags = json_tags[1:-1]

            tag_data = {
                'taggedCanvases': json_tagged_canvases,
                'taggedIdeas': json_tagged_ideas,
                'tags': json_tags,
            }

            return_tag_data.append(tag_data)

        # TODO: change serialization method from needing to pass a singleton list to accepting a single model instance
        return_idea = serialize(
            'json',
            [idea],
            cls=IdeaEncoder
        )
        # singular idea, remove enclosing square brackets
        return_idea = return_idea[1:-1]

        idea.delete()


        data = {
            'function': request.POST['function'],
            'returnTagData': return_tag_data,
            'idea': return_idea,
            'ideaCategory': category,
            'ideaListIndex': request.POST['idea_list_index']
        }

        channel_layer = get_channel_layer()

        room_name = f"{canvas.pk}_idea"
        room_group_name = 'canvas_%s' %room_name

        async_to_sync(channel_layer.group_send)(
            room_group_name,
            {
                'type': 'channel_message',
                'data': data
            }
        )

        return HttpResponse(status=200)




def edit_idea(request):
    '''
    Update of an idea
    '''
    if request.method == 'POST':
        try:
            logged_in_user = request.user
            idea_pk = request.POST['idea_pk']
            input_text = request.POST['input_text']

            idea = Idea.objects.get(pk = idea_pk)
            canvas = idea.canvas
            project = canvas.project

        except:
            Idea.DoesNotExist
            return HttpResponse('Idea does not exist', status=404)

            Canvas.DoesNotExist
            return HttpResponse('Canvas does not exist', status=404)

            Project.DoesNotExist
            return HttpResponse('Project does not exist', status=404)


        current_tags_in_idea = idea.tags.all()

        old_text = idea.text

        if (not user_permission(logged_in_user, project) or (project.title == 'blank-project')):
            return HttpResponse('Unauthorized', status=401)

        input_text = strip_tags(input_text)

        updated_tags_in_idea = []

        new_tags = []
        new_tags_canvas_set = []
        new_tags_idea_set = []

        removed_tags = []
        removed_tags_canvas_set = []
        removed_tags_idea_set = []

        # check if any of the tags are implicitly removed or inserted by their labels no longer occurring in the idea or newly occurring in the idea respectively
        for temp_canvas in project.canvas_set.all():
            for tag in temp_canvas.tags.all():

                # if the tag is in the input_text, add it. RHS of and operation is to keep the list elements unique
                if tag.label in input_text and tag not in updated_tags_in_idea:
                    updated_tags_in_idea.append(tag)

                # if it was in the old text and no longer occurs, remove it
                elif tag.label in old_text and tag.label not in input_text:
                    removed_tags.append(tag)


        # update the tags field in canvas by setting them - implicitly remove the removed tags
        canvas.tags.set(updated_tags_in_idea)
        canvas.save()

        # the same for idea's tags field
        idea.tags.set(updated_tags_in_idea)
        idea.text = input_text
        idea.save()

        new_return_tag_data = []
        removed_return_tag_data = []

        # TODO: change serialization method from needing to pass a singleton list to accepting a single model instance
        return_idea = serialize(
            'json',
            [idea],
            cls=IdeaEncoder
        )
        return_idea = return_idea[1:-1]



        for tag in updated_tags_in_idea:

            new_tags=(
                serialize(
                    'json',
                    [tag],
                    cls = CanvasTagEncoder
                )
            )
            new_tags = new_tags[1:-1]

            new_tags_canvas_set=(
                serialize(
                    'json',
                    tag.canvas_set.all(),
                    cls=CanvasEncoder
                )
            )

            new_tags_idea_set=(
                serialize(
                    'json',
                    tag.idea_set.all(),
                    cls=IdeaEncoder
                )
            )


            tag_data = {
                'newTag': new_tags,
                'newTaggedCanvases': new_tags_canvas_set,
                'newTaggedIdeas': new_tags_idea_set,
            }

            new_return_tag_data.append(tag_data)

        for tag in removed_tags:

            # TODO: change serialization method from needing to pass a singleton list to accepting a single model instance
            removed_tags=(
                serialize(
                    'json',
                    [tag],
                    cls = CanvasTagEncoder
                )
            )
            removed_tags = removed_tags[1:-1]

            removed_tags_canvas_set=(
                serialize(
                    'json',
                    tag.canvas_set.all(),
                    cls=CanvasEncoder
                )
            )

            removed_tags_idea_set=(
                serialize(
                    'json',
                    tag.idea_set.all(),
                    cls=IdeaEncoder
                )
            )

            tag_data = {
                'removedTag': removed_tags,
                'removedTaggedCanvases': removed_tags_canvas_set,
                'removedTaggedIdeas': removed_tags_idea_set,
            }

            removed_return_tag_data.append(tag_data)

        data = {
            'function': request.POST['function'],
            'removedReturnTagData': removed_return_tag_data,
            'newReturnTagData': new_return_tag_data,
            'idea': return_idea,
            'oldText': old_text,
            'ideaCategory': idea.category,
            'ideaListIndex': request.POST['idea_list_index'],
        }

        channel_layer = get_channel_layer()

        room_name = f"{canvas.pk}_idea"
        room_group_name = 'canvas_%s' %room_name

        async_to_sync(channel_layer.group_send)(
            room_group_name,
            {
                'type': 'channel_message',
                'data': data
            }
        )

        return HttpResponse(status=200)




##################################################################################################################################
#                                                       COMMENT VIEWS                                                            #
##################################################################################################################################


def new_comment(request):
    if request.method == 'POST':
        logged_in_user = request.user
        idea_pk = request.POST['idea_pk']
        input_text = request.POST['input_text']


        try:
            idea = Idea.objects.get(pk=idea_pk)
            canvas = idea.canvas
            project = canvas.project

        except:
            Idea.DoesNotExist
            return HttpResponse('Idea does not exist.', status=404)

            Canvas.DoesNotExist
            return HttpResponse('Canvas does not exist.', status=404)

            Project.DoesNotExist
            return HttpResponse('Project does not exist.', status=404)


        if (not user_permission(logged_in_user, project) or (project.title == 'blank-project')):
            return HttpResponse('Unauthorized', status=401)


        text = input_text
        text = strip_tags(text)

        comment = IdeaComment(
            user = logged_in_user,
            text = text,
            idea = idea
        )
        comment.save()

        # TODO: change serialization method from needing to pass a singleton list to accepting a single model instance
        json_comment = serialize(
            'json',
            [comment],
            cls = IdeaCommentEncoder
        )

        # singular comment, remove enclosing square brackets
        json_comment = json_comment[1:-1]

        data = {
            'function': request.POST['function'],
            'comment': json_comment,
            'ideaCategory': idea.category,
            'ideaListIndex': request.POST['idea_list_index']
        }

        channel_layer = get_channel_layer()

        room_name = f"{canvas.pk}_comment"
        room_group_name = 'canvas_%s' %room_name

        async_to_sync(channel_layer.group_send)(
            room_group_name,
            {
                'type': 'channel_message',
                'data': data
            }
        )

        return HttpResponse(status=200)


def delete_comment(request):
    '''
    Deletion of a comment
    '''
    if request.method == 'POST':
        logged_in_user = request.user
        comment_pk = request.POST['comment_pk']

        try:
            comment = IdeaComment.objects.get(pk=comment_pk)
            canvas = comment.idea.canvas
            project = canvas.project

        except:
            IdeaComment.DoesNotExist
            return HttpResponse('Comment does not exist.', status=404)

            Canvas.DoesNotExist
            return HttpResponse('Canvas does not exist.', status=404)

            Project.DoesNotExist
            return HttpResponse('Project does not exist.', status=404)


        if (not admin_permission(logged_in_user, project) or (project.title == 'blank-project')):
            return HttpResponse('Forbidden.', status=403)


        category = comment.idea.category
        comment.delete()


        data = {
            'function': request.POST['function'],
            'ideaCategory': category,
            'ideaListIndex': request.POST['idea_list_index'],
            'commentListIndex': request.POST['comment_list_index'],
        }

        channel_layer = get_channel_layer()

        room_name = f"{canvas.pk}_comment"
        room_group_name = 'canvas_%s' %room_name

        async_to_sync(channel_layer.group_send)(
            room_group_name,
            {
                'type': 'channel_message',
                'data': data
            }
        )

        return HttpResponse(status=200)



def single_comment_resolve(request):
    '''
    Resolution of a single comment
    '''
    if request.method == 'POST':
        logged_in_user = request.user
        comment_pk = request.POST['comment_pk']

        try:
            comment = IdeaComment.objects.get(pk=comment_pk)
            canvas = comment.idea.canvas
            project = canvas.project

        except:
            IdeaComment.DoesNotExist
            return HttpResponse('Comment does not exist.', status=404)

            Canvas.DoesNotExist
            return HttpResponse('Canvas does not exist.', status=404)

            Project.DoesNotExist
            return HttpResponse('Project does not exist.', status=404)


        if (not admin_permission(logged_in_user, project) or (project.title == 'blank-project')):
            return HttpResponse('Forbidden.', status=403)

        category = comment.idea.category
        comment.resolved = True
        comment.save()

        data = {
            'function': request.POST['function'],
            'ideaCategory': category,
            'ideaListIndex': request.POST['idea_list_index'],
            'commentListIndex': request.POST['comment_list_index'],
        }

        channel_layer = get_channel_layer()

        room_name = f"{canvas.pk}_comment"
        room_group_name = 'canvas_%s' %room_name

        async_to_sync(channel_layer.group_send)(
            room_group_name,
            {
                'type': 'channel_message',
                'data': data
            }
        )

        return HttpResponse(status=200)



def all_comment_resolve(request):
    '''
    Resolution of comments - mark all as resolved
    '''
    if request.method == 'POST':
        logged_in_user = request.user
        idea_pk = request.POST['idea_pk']

        try:
            idea = Idea.objects.get(pk = idea_pk)
            canvas = idea.canvas
            project = canvas.project
        except:
            Idea.DoesNotExist
            return HttpResponse('Idea does not exist.', status=404)

            Canvas.DoesNotExist
            return HttpResponse('Canvas does not exist.', status=404)

            Project.DoesNotExist
            return HttpResponse('Project does not exist.', status=404)


        if (not admin_permission(logged_in_user, project) or (project.title == 'blank-project')):
            return HttpResponse('Forbidden.', status=403)

        IdeaComment.objects.all().filter(idea = idea).update(resolved=True)


        data = {
            'function': request.POST['function'],
            'ideaCategory': idea.category,
            'ideaListIndex': request.POST['idea_list_index'],
        }

        channel_layer = get_channel_layer()

        room_name = f"{canvas.pk}_comment"
        room_group_name = 'canvas_%s' %room_name

        async_to_sync(channel_layer.group_send)(
            room_group_name,
            {
                'type': 'channel_message',
                'data': data
            }
        )

        return HttpResponse(status=200)


##################################################################################################################################
#                                           COLLABORATOR AND LANDING PAGE VIEWS                                                  #
##################################################################################################################################


def index(request):
    return render(request, 'index.html')



def register(request):
    if request.method == 'POST':
        form = SignUpForm(request.POST)

        if form.is_valid():
            username = form.cleaned_data['name']
            email = form.cleaned_data['email']
            password = form.cleaned_data['password']

            newUser = User.objects.create_user(
                username = username,
                email = email,
                password = password
            )

            return HttpResponseRedirect(reverse('index'))

    else:
        form = SignUpForm(initial = {
            'name': '',
            'email': '',
            'password': '',
            'password2': '',
        })


    return render(
        request,
        'catalog/register.html',
        {'form': form}
    )

def add_user(request):
    '''
    Function for addition of user to project
    '''
    if request.method == 'POST':
        project_pk = request.POST['project_pk']

        try:
            project = Project.objects.get(pk=project_pk)
        except:
            Project.DoesNotExist
            return HttpResponse('Project does not exist.', status=404)

        name = request.POST['name']

        logged_in_user = request.user
        # check is admin
        if (not admin_permission(logged_in_user, project)):
            return HttpResponse('Forbidden.', status=403)


        else:
            try:
                user = User.objects.get(username=name)
            except:
                User.DoesNotExist
                return HttpResponse('User does not exist.', status=404)

            if user in project.users.all() or user in project.admins.all():
                reply = ''

                if user is logged_in_user:
                    reply = 'Error: you\'re already a collaborator, you can\'t add yourself!'

                else:
                    reply = 'Error: ' + name + ' is already a collaborator!'

                return HttpResponse(reply, status=500)


            project.users.add(user)

            # TODO: change serialization method from needing to pass a singleton list to accepting a single model instance
            json_user = serialize(
                'json',
                [user],
                cls = UserModelEncoder
            )
            # singular user - remove enclosing square brackets
            json_user = json_user[1:-1]

            data = {
                'function': request.POST['function'],
                'user': json_user,
            }

            channel_layer = get_channel_layer()

            room_name = project_pk + "_collab"
            room_group_name = 'project_%s' %room_name

            async_to_sync(channel_layer.group_send)(
                room_group_name,
                {
                    'type': 'channel_message',
                    'data': data
                }
            )

            return HttpResponse(status=200)

def delete_user(request):
    '''
    Function for deleting a user from the project.
    '''
    if request.method == 'POST':
        project_pk = request.POST['project_pk']

        try:
            project = Project.objects.get(pk=project_pk)
        except:
            Project.DoesNotExist
            return HttpResponse('Project does not exist.', status=404)

        logged_in_user = request.user
        # check is admin
        if (not admin_permission(logged_in_user, project)):
            return HttpResponse('Forbidden', status=403)


        try:
            user = User.objects.get(pk=request.POST['user_pk'])
        except:
            User.DoesNotExist
            return HttpResponse('User does not exist.', status=404)

        if user not in project.users.all():
            reply = 'Error: ' + name + ' is not a collaborator'
            return HttpResponse(reply, status=500)

        admins = project.admins.all()

        # if there is one admin who is the logged-in user, do not allow them to
        # delete themselves. It's implied that if there's one admin, the logged_in
        # user is that admin, as earlier it is checked that the logged_in user
        # is in the project admin set
        if (len(admins) == 1 and user in admins):
            reply = 'Error: You are the only admin, you may not delete yourself!'
            return HttpResponse(reply, status=500)

        victim_is_admin = "false"
        # if the user is also an admin, remove them from that field also
        if user in admins:
            victim_is_admin = "true"
            project.admins.remove(user)

        project.users.remove(user)

        data = {
            'function': request.POST['function'],
            'userListIndex': request.POST['user_list_index'],
            'victimIsAdmin': victim_is_admin,
        }

        channel_layer = get_channel_layer()

        room_name = project_pk + "_collab"
        room_group_name = 'project_%s' %room_name

        async_to_sync(channel_layer.group_send)(
            room_group_name,
            {
                'type': 'channel_message',
                'data': data
            }
        )

        return HttpResponse(status=200)


def promote_user(request):
    '''
    Function for promoting a user to admin status
    '''
    if request.method == 'POST':
        project_pk = request.POST['project_pk']
        try:
            project = Project.objects.get(pk=project_pk)
        except:
            Project.DoesNotExist
            return HttpResponse('Project does not exist', status=404)

        logged_in_user = request.user
        # check is admin
        if (not admin_permission(logged_in_user, project)):
                return HttpResponse('Forbidden', status=403)

        try:
            user = User.objects.get(pk=request.POST['user_pk'])
        except:
            User.DoesNotExist
            return HttpResponse('User does not exist', status=404)


        name_str = user.username
        admins = project.admins.all()

        # check presence in admin set
        if user in admins:
            # additionally check the user isn't trying to promote themselves
            if user is logged_in_user:
                name_str = 'you are'
            else:
                name_str = name_str + ' is '
            reply = 'Error: ' + name_str + ' already an admin!'

            return HttpResponse(reply, status=500)

        project.admins.add(user)

        # TODO: change serialization method from needing to pass a singleton list to accepting a single model instance
        json_user = serialize(
            'json',
            [user],
            cls = UserModelEncoder
        )
        # singular user - remove enclosing square brackets
        json_user = json_user[1:-1]

        data = {
            'function': request.POST['function'],
            'admin': json_user,
        }

        channel_layer = get_channel_layer()

        room_name = project_pk + "_collab"
        room_group_name = 'project_%s' %room_name

        async_to_sync(channel_layer.group_send)(
            room_group_name,
            {
                'type': 'channel_message',
                'data': data
            }
        )

        return HttpResponse(status=200)


def demote_user(request):
    '''
    Function to delete a user from the admin field - this is for demotion only.
    For complete deletion, call delete user
    '''
    if request.method == 'POST':
        project_pk = request.POST['project_pk']
        try:
            project = Project.objects.get(pk=project_pk)
        except:
            Project.DoesNotExist
            return HttpResponse('Project does not exist', status=404)

        logged_in_user = request.user
        # check is admin
        if (not admin_permission(logged_in_user, project)):
                return HttpResponse('Forbidden', status=403)

        try:
            user = User.objects.get(pk=request.POST['user_pk'])
        except:
            User.DoesNotExist
            return HttpResponse('User does not exist', status=404)

        admins = project.admins.all()
        # Can't delete a non-existent admin
        if user not in admins:
            reply = 'Error: ' + name + ' is not an admin'
            return HttpResponse(reply, status=500)

        # if there is one admin who is the logged-in user, do not allow them to
        # delete themselves
        if len(admins) == 1:
            reply = 'Error: You are the only admin, you may not demote yourself!'
            return HttpResponse(reply, status=500)

        project.admins.remove(user)

        data = {
            'function': request.POST['function'],
            'adminListIndex': request.POST['admin_list_index']
        }

        channel_layer = get_channel_layer()

        room_name = project_pk + "_collab"
        room_group_name = 'project_%s' %room_name

        async_to_sync(channel_layer.group_send)(
            room_group_name,
            {
                'type': 'channel_message',
                'data': data
            }
        )

        return HttpResponse(status=200)


def toggle_public(request):
    if request.method == 'POST':
        project_pk = request.POST['project_pk']

        try:
            project = Project.objects.get(pk=project_pk)
        except:
            Project.DoesNotExist
            return HttpResponse('Project does not exist', status=404)

        logged_in_user = request.user
        # check is admin
        if (not admin_permission(logged_in_user, project)):
                return HttpResponse('Forbidden', status=403)

        project.is_public = not(project.is_public)
        project.save()

        data = {
            'function': request.POST['function'],
        }

        channel_layer = get_channel_layer()

        room_name = project_pk + "_collab"
        room_group_name = 'project_%s' %room_name

        async_to_sync(channel_layer.group_send)(
            room_group_name,
            {
                'type': 'channel_message',
                'data': data
            }
        )

        return HttpResponse(status=200)




##################################################################################################################################
#                                                           TAG VIEWS                                                            #
##################################################################################################################################

def add_tag(request):
    '''
    ADDITION OF NEW TAG
    '''
    if request.method == 'POST':
        try:
            canvas = Canvas.objects.get(pk=request.POST['canvas_pk'])
            print(canvas)
            project = canvas.project

        except:
            Canvas.DoesNotExist
            return HttpResponse('Canvas does not exist.', status=404)

            Project.DoesNotExist
            return HttpResponse('Project does not exist.', status=404)

        logged_in_user = request.user
        label = request.POST['label']

        if (not user_permission(logged_in_user, project) or (project.title == 'blank-project')):
            return HttpResponse('Unauthorized', status=401)

        # check existence of tag within project - avoid duplicating tags
        if CanvasTag.objects.filter(label=label, canvas_set__project=project).exists():
            tag = CanvasTag.objects.get(label=label)

            tag_canvas_set = tag.canvas_set.all()

            if canvas not in tag_canvas_set:
                tag.canvas_set.add(canvas)


        else:
            # only create tag if it doesn't exist anywhere visible to the user
            tag = CanvasTag(label=label)
            tag.save()
            tag.canvas_set.add(canvas)

        tag.save()
        canvas.save()

        # check every canvas for presence of new tag's label in those canvases on creation of new tag
        for canvas in project.canvas_set.all():
            ideas = Idea.objects.filter(canvas=canvas)

            for idea in ideas:

                if tag.label in idea.text:

                    if idea not in tag.idea_set.all():
                        tag.idea_set.add(idea)
                        idea.save()

                        # skip the below step if the above is false
                        if canvas not in tag.canvas_set.all():
                            canvas.save()
                            tag.canvas_set.add(canvas)
                            canvas.save()

                        # save tag if modifications made
                        tag.save()


        tags = CanvasTag.objects.filter(canvas_set__project=project).distinct()
        json_tagged_canvases = []
        json_tagged_ideas = []


        # for t in tags:
        json_tagged_canvases.append(
            serialize(
                'json',
                tag.canvas_set.all().order_by('-id'),
                cls=CanvasEncoder
            )
        )


        json_tagged_ideas.append(
            serialize(
                'json',
                tag.idea_set.all().order_by('canvas'),
                cls=IdeaEncoder
            )
        )

        # TODO: change serialization method from needing to pass a singleton list to accepting a single model instance
        json_tag = serialize(
            'json',
            [tag],
            cls = CanvasTagEncoder
        )
        # singular tag - remove enclosing square brackets
        json_tag = json_tag[1:-1]

        data = {
            'function': request.POST['function'],
            'taggedCanvases': json_tagged_canvases,
            'taggedIdeas': json_tagged_ideas,
            'tag': json_tag,
        }

        channel_layer = get_channel_layer()

        room_name = f"{project.pk}_tag"
        room_group_name = 'project_%s' %room_name

        async_to_sync(channel_layer.group_send)(
            room_group_name,
            {
                'type': 'channel_message',
                'data': data
            }
        )

        return HttpResponse(status=200)


def delete_tag(request):
    '''
    DELETION OF TAG
    '''
    if request.method == 'POST':

        try:
            canvas = Canvas.objects.get(pk=request.POST['canvas_pk'])
            project = canvas.project

        except:
            Canvas.DoesNotExist
            return HttpResponse('Canvas does not exist.', status=404)

            Project.DoesNotExist
            return HttpResponse('Project does not exist.', status=404)

        logged_in_user = request.user
        label = request.POST['label']

        if (not user_permission(logged_in_user, project) or (project.title == 'blank-project')):
            return HttpResponse('Unauthorized', status=401)

        try:
            tag = CanvasTag.objects.get(label=label, canvas_set=canvas)
        except:
            CanvasTag.DoesNotExist
            return HttpResponse('Tag does not exist.', status=404)


        CanvasTag.objects.filter(label=label, canvas_set__project=project).delete()

        # delete any tags that aren't attached to a canvas: they are never useful
        CanvasTag.objects.filter(canvas_set=None).delete()

        # TODO: change serialization method from needing to pass a singleton list to accepting a single model instance
        json_tag = serialize(
            'json',
            [tag],
            cls = CanvasTagEncoder
        )
        # singular tag - remove enclosing square brackets
        json_tag = json_tag[1:-1]

        data = {
            'function': request.POST['function'],
            'tag': json_tag,
        }

        channel_layer = get_channel_layer()

        room_name = f"{project.pk}_tag"
        room_group_name = 'project_%s' %room_name

        async_to_sync(channel_layer.group_send)(
            room_group_name,
            {
                'type': 'channel_message',
                'data': data
            }
        )

        return HttpResponse(status=200)


##################################################################################################################################
#                                                   MISCELLANEOUS FUNCTIONS                                                      #
##################################################################################################################################


def search_canvas_for_tag(tag, canvas):
    '''
    check for presence of tag in canvas
    '''
    ideas = Idea.objects.filter(canvas=canvas)

    for idea in ideas:

        if tag.label in idea.text:

            if idea not in tag.idea_set.all():
                tag.idea_set.add(idea)

                # skip the below step if the above is false
                if canvas not in tag.canvas_set.all():
                    canvas.save()
                    tag.canvas_set.add(canvas)

                # save tag if modifications made
                tag.save()



def user_permission(logged_in_user, project):
    return ((logged_in_user in project.users.all()) or (project.is_public == True))

def admin_permission(logged_in_user, project):
    return ((logged_in_user in project.admins.all()) or (project.is_public == True))

class IdeaEncoder(DjangoJSONEncoder):
    def default(self, obj):
        if isinstance(obj, Idea):
            return str(obj)
        return super().default(obj)

class IdeaCommentEncoder(DjangoJSONEncoder):
    def default(self, obj):
        if isinstance(obj, IdeaComment):
            return str(obj)
        return super().default(obj)

class CanvasTagEncoder(DjangoJSONEncoder):
    def default(self, obj):
        if isinstance(obj, CanvasTag):
            return str(obj)
        return super().default(obj)

class CanvasEncoder(DjangoJSONEncoder):
    def default(self, obj):
        if isinstance(obj, Canvas):
            return str(obj)
        return super().default(obj)

class ProjectModelEncoder(DjangoJSONEncoder):
    def default(self, obj):
        if isinstance(obj, Project):
            return str(obj)
        return super().default(obj)

class UserModelEncoder(DjangoJSONEncoder):
    def default(self, obj):
        if isinstance(obj, CanvasTag):
            return str(obj)
        return super().default(obj)

    class Meta:
        model = User
        exclude = ('password',)
