#    Copyright (C) 2017-2018  Alban Gruin
#
#    celcatsanitizer is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as published
#    by the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    celcatsanitizer is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with celcatsanitizer.  If not, see <http://www.gnu.org/licenses/>.

import datetime

from django.db import connection
from django.db.models import Count, Max
from django.db.models.functions import ExtractWeek, ExtractYear, Length
from django.http import Http404
from django.shortcuts import get_object_or_404, render
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt

from .forms import QSJPSForm
from .models import Course, Group, Room, Timetable, Year
from .utils import get_current_week, get_current_or_next_week, get_week, \
    group_courses

import edt

if connection.vendor == "postgresql":
    from django.contrib.postgres.aggregates import ArrayAgg
    from django.db.models.expressions import RawSQL


def index(request):
    years = Year.objects.order_by("name")
    return render(request, "year_list.html", {"elements": years})


def mention_list(request, year_slug):
    year = get_object_or_404(Year, slug=year_slug)
    timetables = Timetable.objects.order_by("name").filter(year=year)

    return render(request, "mention_list.html",
                  {"year": year, "elements": timetables})


def group_list(request, year_slug, timetable_slug):
    timetable = get_object_or_404(Timetable, year__slug=year_slug,
                                  slug=timetable_slug)

    start, _ = get_week(*get_current_week())
    end = start + datetime.timedelta(weeks=4)

    groups = Group.objects.get_relevant_groups(start, source=timetable.source,
                                               hidden=False)
    groups_weeks = Course.objects.get_weeks(begin__gte=start, begin__lt=end,
                                            groups__in=groups)

    for group in groups:
        for group_week in groups_weeks:
            if group.corresponds_to(group_week["groups__mention"],
                                    group_week["groups__semester"],
                                    group_week["groups__subgroup"]):
                if not hasattr(group, "weeks"):
                    group.weeks = []

                date, _ = get_week(group_week["year"], group_week["week"])
                if date not in group.weeks:
                    group.weeks.append(date)

        if hasattr(group, "weeks"):
            group.weeks.sort()

    return render(request, "group_list.html",
                  {"timetable": timetable, "groups": groups})


def groups_all(request, year_slug, timetable_slug):
    # Récupération de l’emploi du temps et du groupe
    timetable = get_object_or_404(Timetable, year__slug=year_slug,
                                  slug=timetable_slug)
    groups = Group.objects.filter(source=timetable.source).order_by("name")

    # Rendu de la page
    return render(request, "groups_all_list.html",
                  {"timetable": timetable, "elements": groups})


def group_weeks(request, year_slug, timetable_slug, group_slug):
    # Récupération de l’emploi du temps et des groupes
    timetable = get_object_or_404(Timetable, year__slug=year_slug,
                                  slug=timetable_slug)
    group = get_object_or_404(Group, slug=group_slug, source=timetable.source)

    # Groupes parents
    groups = Group.objects.get_parents(group)

    # Récupération de toutes les semaines avec des cours, sans doublons
    courses = Course.objects.filter(groups__in=groups) \
                            .order_by("year", "week") \
                            .annotate(year=ExtractYear("begin"),
                                      week=ExtractWeek("begin")) \
                            .values("year", "week") \
                            .annotate(c=Count("*"))

    # Conversion des semaines de cours en dates
    weeks = [get_week(course["year"], course["week"])[0] for course in courses]

    # Rendu
    return render(request, "group_weeks_list.html",
                  {"timetable": timetable, "group": group,
                   "elements": weeks})


def timetable_common(request, obj, year=None, week=None, timetable=None):
    current_year, current_week = get_current_or_next_week()
    is_old_timetable, provided_week = False, True

    if year is None or week is None:
        year, week = current_year, current_week
        provided_week = False
    elif (int(year), int(week)) < (current_year, current_week):
        is_old_timetable = True

    try:
        start, end = get_week(year, week)
    except ValueError:
        raise Http404

    courses = Course.objects.get_courses(obj, begin__gte=start, begin__lt=end)
    if not courses.exists() and provided_week:
        raise Http404

    # Récupération des semaines suivantes et précédentes pour les
    # afficher proprement dans l’emploi du temps
    last_course = Course.objects.get_courses(obj, begin__lt=start).last()
    last_week = getattr(last_course, "begin", None)

    next_course = Course.objects.get_courses(obj, begin__gte=end).first()
    next_week = getattr(next_course, "begin", None)

    last_update = courses.aggregate(Max("last_update"))["last_update__max"]
    grouped_courses = group_courses(courses)

    return render(request, "timetable.html",
                  {"group": obj, "courses": grouped_courses,
                   "last_update": last_update,
                   "year": year, "week": int(week),
                   "last_week": last_week,
                   "next_week": next_week,
                   "is_old_timetable": is_old_timetable,
                   "group_mode": isinstance(obj, Group),
                   "timetable": timetable})


def timetable(request, year_slug, timetable_slug, group_slug,
              year=None, week=None):
    timetable = get_object_or_404(Timetable, year__slug=year_slug,
                                  slug=timetable_slug)
    group = get_object_or_404(Group, slug=group_slug, source=timetable.source)

    return timetable_common(request, group, year, week, timetable)


def calendars(request, year_slug, timetable_slug, group_slug):
    timetable = get_object_or_404(Timetable, year__slug=year_slug,
                                  slug=timetable_slug)
    group = get_object_or_404(Group, source=timetable.source, slug=group_slug)
    groups = Group.objects.get_parents(group) \
                          .annotate(length=Length("subgroup")) \
                          .order_by("length")

    return render(request, "calendars.html",
                  {"timetable": timetable, "group": group, "groups": groups})


def rooms(request):
    # On récupère les dates allant de cette semaine à dans un mois
    start, _ = get_week(*get_current_week())
    end = start + datetime.timedelta(weeks=4)

    if connection.vendor == "postgresql":
        # Si le SGBD est PostgreSQL, on utilise une requête à base de
        # ArrayAgg. Elle présente l’avantage d’être plus rapide que la
        # requête « généraliste » et de ne pas nécessiter de
        # traitement après.  On récupère chaque salle ayant un cours
        # dans le mois à venir. Pour chacun de ses cours, on ne
        # récupère que le premier jour de la semaine, et si jamais ce
        # jour n’est pas déjà dans la liste des semaines de cours
        # (« weeks »), on l’y rajoute.
        rooms = Room.objects.filter(course__begin__gte=start,
                                    course__begin__lt=end) \
                            .order_by("name") \
                            .annotate(weeks=ArrayAgg(
                                RawSQL("date_trunc('week', edt_course.begin)",
                                       []), distinct=True))

        return render(request, "group_list.html", {"groups": rooms})

    # Récupération des salles et de toutes les semaines où elles sont
    # concernées.
    # Cette requête associe chaque salle à toutes les semaines où un
    # cours s’y déroule. Le résultat est trié par le nom de la salle
    # et par semaine.
    # TODO optimiser cette requête, elle me semble un peu lente
    rooms = Room.objects.filter(course__begin__gte=start,
                                course__begin__lt=end) \
                        .order_by("name") \
                        .annotate(year=ExtractYear("course__begin"),
                                  week=ExtractWeek("course__begin"),
                                  c=Count("*"))

    # Regroupement des semaines dans une liste de chaque objet salle
    rooms_weeks = []
    for room in rooms:
        # Si on a pas traité de salle ou que la salle courante
        # dans le résultat de la requête est différente de la dernière
        # dans la liste des salles traitées
        if len(rooms_weeks) == 0 or rooms_weeks[-1].id != room.id:
            # On lui affecte un tableau et on l’ajoute dans
            # la liste des salles à traiter
            room.weeks = []
            rooms_weeks.append(room)

        # On récupère le premier jour de la semaine
        date, _ = get_week(room.year, room.week)
        # Et on le rajoute dans la liste des semaines de la salle.
        rooms_weeks[-1].weeks.append(date)

    # Rendu de la page.
    return render(request, "group_list.html", {"groups": rooms_weeks})


def room_weeks(request, room_slug):
    room = get_object_or_404(Room, slug=room_slug)

    # Récupération des semaines de cours
    courses = Course.objects.filter(rooms=room) \
                            .order_by("year", "week") \
                            .annotate(year=ExtractYear("begin"),
                                      week=ExtractWeek("begin")) \
                            .values("year", "week") \
                            .annotate(c=Count("*"))

    weeks = [get_week(course["year"], course["week"])[0] for course in courses]

    return render(request, "room_weeks_list.html",
                  {"room": room, "elements": weeks})


def room_timetable(request, room_slug, year=None, week=None):
    room = get_object_or_404(Room, slug=room_slug)
    return timetable_common(request, room, year, week)


@csrf_exempt
def qsjps(request):
    if request.method == "POST":
        # Si on traite un formulaire, on le valide
        form = QSJPSForm(request.POST)
        if form.is_valid():
            # Formulaire validé
            day = form.cleaned_data["day"]
            begin_hour = form.cleaned_data["begin"]
            end_hour = form.cleaned_data["end"]

            begin = timezone.make_aware(datetime.datetime.combine(day,
                                                                  begin_hour))
            end = timezone.make_aware(datetime.datetime.combine(day, end_hour))

            rooms = Room.objects.qsjps(begin, end)
            return render(request, "qsjps.html",
                          {"rooms": rooms, "form": form})

        # Si le formulaire est invalide, on ré-affiche le formulaire
        # avec les erreurs
    else:
        # Si le formulaire n’a pas été soumis, on en instancie un
        # nouveau
        form = QSJPSForm()

    return render(request, "qsjps_form.html", {"form": form})


def ctx_processor(request):
    return {"celcatsanitizer_version": edt.VERSION}