# -*- coding: utf-8 -*-
# Code for Life
#
# Copyright (C) 2015, Ocado Innovation Limited
#
# This program 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.
#
# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
#
# ADDITIONAL TERMS – Section 7 GNU General Public Licence
#
# This licence does not grant any right, title or interest in any “Ocado” logos,
# trade names or the trademark “Ocado” or any other trademarks or domain names
# owned by Ocado Innovation Limited or the Ocado group of companies or any other
# distinctive brand features of “Ocado” as may be secured from time to time. You
# must not distribute any modification of this program using the trademark
# “Ocado” or claim any affiliation or association with Ocado or its employees.
#
# You are not authorised to use the name Ocado (or any of its trade names) or
# the names of any author or contributor in advertising or for publicity purposes
# pertaining to the distribution of this program, without the prior written
# authorisation of Ocado.
#
# Any propagation, distribution or conveyance of this program must include this
# copyright notice and these terms. You must not misrepresent the origins of this
# program; modified versions of the program must be marked as such and not
# identified as the original program.
from __future__ import division
from datetime import timedelta

from django.http import Http404
from django.shortcuts import render
from django.template import RequestContext

import game.messages as messages
import game.permissions as permissions
from game.views.scoreboard_csv import scoreboard_csv
from helper import renderError
from game.forms import ScoreboardForm
from game.models import Level, Attempt, sort_levels
from portal.models import Class, Teacher, Student

Single_Level_Header = ['Class', 'Name', 'Score', 'Total Time', 'Start Time', 'Finish Time']
Multiple_Levels_Header = ['Class', 'Name', 'Total Score', 'Total Time', 'Progress']


def scoreboard(request):
    """ Renders a page with students' scores. A teacher can see the the visible classes in their
        school. Student's view is restricted to their class if their teacher enabled the
        scoreboard for said class.
    """
    if not permissions.can_see_scoreboard(request.user):
        return render_no_permission_error(request)

    user = User(request.user.userprofile)
    users_classes = classes_for(user)

    class_ids = set(map(int, request.POST.getlist('classes')))
    level_ids = set(map(int, request.POST.getlist('levels')))

    if user.is_independent_student():
        return render_no_permission_error(request)

    if is_teacher_with_no_classes_assigned(user, users_classes):
        return render_no_permission_error(request)

    if not is_valid_request(user, class_ids):
        raise Http404

    form = ScoreboardForm(request.POST or None, classes=users_classes)

    if request.method == 'POST' and form.is_valid():
        student_data, headers = scoreboard_data(user, level_ids, class_ids)
    else:
        student_data = []
        headers = []

    csv_export = 'export' in request.POST

    if csv_export:
        return scoreboard_csv(student_data, sorted_levels_by(level_ids))
    else:
        return scoreboard_view(request, form, student_data, headers)


def render_no_permission_error(request):
    return renderError(request, messages.noPermissionTitle(), messages.noPermissionScoreboard())


def is_teacher_with_no_classes_assigned(user, users_classes):
    return user.is_teacher() and len(users_classes) == 0


def success_response(form, student_data, headers):
    return form, student_data, headers, None


def classes_for(user):
    if user.is_teacher():
        teachers = Teacher.objects.filter(school=user.teacher.school)
        return Class.objects.filter(teacher__in=teachers)
    elif user.is_student():
        class_ = user.student.class_field
        return Class.objects.filter(id=class_.id)


def scoreboard_view(request, form, student_data, headers):
    context = RequestContext(request, {
        'form': form,
        'student_data': student_data,
        'headers': headers,
    })
    return render(request, 'game/scoreboard.html', context_instance=context)


def scoreboard_data(user, level_ids, class_ids):
    classes = Class.objects.filter(id__in=class_ids)

    students = students_visible_to_user(user, classes)

    # If there are more than one level to show, show the total score, total time and score of each level
    # Otherwise, show the details of the level (the score, total time, start time and end time)
    return data_and_headers_for(students, level_ids)


def data_and_headers_for(students, level_ids):
    levels_sorted = sorted_levels_by(level_ids)

    score_for_multiple_levels_is_displayed = len(levels_sorted) > 1

    level_names = map(str, levels_sorted)

    if score_for_multiple_levels_is_displayed:
        headers = Multiple_Levels_Header + level_names
        student_data = multiple_students_multiple_levels(students, levels_sorted)
    else:
        headers = Single_Level_Header
        student_data = multiple_students_one_level(students, first(levels_sorted))

    return student_data, headers


def sorted_levels_by(level_ids):
    return sort_levels(Level.objects.filter(id__in=level_ids))


def first(elements):
    if len(elements) == 0:
        raise ValueError("Collection is empty")
    return next(iter(elements))


def are_classes_viewable_by_teacher(class_ids, user):
    teachers = Teacher.objects.filter(school=user.teacher.school)
    classes_in_teachers_school = Class.objects.filter(teacher__in=teachers).values_list('id', flat=True)
    for class_id in class_ids:
        is_authorised = class_id in classes_in_teachers_school
        if not is_authorised:
            return False
    return True


def authorised_student_access(class_, class_ids):
    return len(class_ids) == 1 and next(iter(class_ids)) == class_


def students_visible_to_student(student):
    class_ = student.class_field
    if is_viewable(class_):
        return class_.students.all().select_related('class_field', 'user__user')
    else:
        return [student]


def students_visible_to_user(user, classes):
    if user.is_teacher():
        return students_of_classes(classes)
    elif user.is_student():
        student = user.student
        return students_visible_to_student(student)


def students_of_classes(classes):
    return Student.objects.filter(class_field__in=classes).select_related('class_field', 'user__user')


def is_valid_request(user, class_ids):
    if len(class_ids) == 0:
        return True
    elif user.is_teacher():
        return are_classes_viewable_by_teacher(class_ids, user)
    elif user.is_student():
        return authorised_student_access(user.student.class_field.id, class_ids)
    else:
        return False
    return True


def one_row(student, level):
    best_attempt = Attempt.objects.filter(level=level, student=student, is_best_attempt=True).first()
    if best_attempt:
        total_score = best_attempt.score if best_attempt.score is not None else ''
        return StudentRow(student=student,
                          total_score=total_score,
                          start_time=best_attempt.start_time,
                          finish_time=best_attempt.finish_time,
                          total_time=chop_miliseconds(best_attempt.finish_time - best_attempt.start_time))
    else:
        return StudentRow(student=student)


# Return rows of student object with values for progress bar and scores of each selected level
def multiple_students_multiple_levels(students, levels_sorted):
    result = map(lambda student: student_row(levels_sorted, student), students)
    return result


def student_row(levels_sorted, student):
    threshold = 0.5

    num_levels = len(levels_sorted)
    num_all = num_finished = num_attempted = num_started = 0
    total_score = 0.0
    scores = []
    times = []
    progress = (0.0, 0.0, 0.0)
    best_attempts = Attempt.objects.filter(level__in=levels_sorted,
                                           student=student,
                                           is_best_attempt=True).select_related('level')
    if best_attempts:
        attempts_dict = {best_attempt.level.id: best_attempt for best_attempt in best_attempts}
        for level in levels_sorted:
            attempt = attempts_dict.get(level.id)
            if attempt:
                num_all += 1
                max_score = 10 if attempt.level.disable_route_score else 20
                if attempt.score:
                    if attempt.score / max_score >= threshold:
                        num_finished += 1
                    else:
                        num_attempted += 1
                else:
                    num_started += 1

                total_score += attempt.score if attempt.score is not None else 0

                elapsed_time = attempt.elapsed_time()
                times.append(chop_miliseconds(elapsed_time))
                # '-' is used to show that the student has started the level but has not submitted any attempts

                scores.append(attempt.score if attempt.score is not None else '-')
            else:
                times.append(timedelta(0))
                scores.append("")

        progress = compute_proportions(num_levels, num_started, num_attempted, num_finished)
    else:
        scores.extend([''] * num_levels)

    total_time = sum(times, timedelta())

    row = StudentRow(student=student,
                     total_time=total_time,
                     total_score=total_score,
                     progress=progress,
                     scores=scores)
    return row


def compute_proportions(num_levels, num_started, num_attempted, num_finished):
    return (num_started / num_levels) * 100, (num_attempted / num_levels) * 100, (num_finished / num_levels) * 100


# Returns rows of student object with score, start time, end time of the level
def multiple_students_one_level(students, level):
    student_data = []

    for student in students:
        student_data.append(one_row(student, level))

    return student_data


def is_viewable(class_):
    return class_.classmates_data_viewable


def chop_miliseconds(delta):
    return delta - timedelta(microseconds=delta.microseconds)


class StudentRow:
    def __init__(self, *args, **kwargs):
        student = kwargs.get('student')
        self.class_field = student.class_field
        self.name = student.user.user.first_name
        self.id = student.id
        self.total_time = kwargs.get('total_time', timedelta(0))
        self.total_score = kwargs.get('total_score', 0.0)
        self.progress = kwargs.get('progress', (0.0, 0.0, 0.0))
        self.scores = kwargs.get('scores', [])
        self.start_time = kwargs.get('start_time', "")
        self.finish_time = kwargs.get('finish_time', "")


class User:
    def __init__(self, profile):
        self.profile = profile
        if self.is_teacher():
            self.teacher = profile.teacher

        if self.is_student():
            self.student = profile.student

    def is_student(self):
        return hasattr(self.profile, 'student') and not self.profile.student.is_independent()

    def is_teacher(self):
        return hasattr(self.profile, 'teacher')

    def is_independent_student(self):
        return hasattr(self.profile, 'student') and self.student.is_independent()
