"""CSC110 Fall 2021: Term Test 2, Question 3 (Object Oriented Design)

Module Description
==================
This module contains instructions for this question. There are TWO
parts of this question, labelled "Part (a)", "Part (b)", etc.
The comments in this file contain instructions on how to complete each part,
so please read those comments carefully.

SUBMIT THIS FILE FOR GRADING!

Copyright and Usage Information
===============================

This file is provided solely for the personal and private use of students
taking CSC110 at the University of Toronto St. George campus. All forms of
distribution of this code, whether as given or with any changes, are
expressly prohibited. For more information on copyright for CSC110 materials,
please consult our Course Syllabus.

This file is Copyright (c) 2021 Mario Badr and Tom Fairgrieve.
"""
from dataclasses import dataclass


###################################################################################################
# Modelling a Problem Domain
###################################################################################################
# In this question, you will model a new problem domain by designing classes in a similar style to
# the food delivery system we studied in lecture.

###################################################################################################
# Description of the Problem Domain
###################################################################################################
# Your client is the University of Toronto. They want new software to manage the grades of their
# students. The grade system will store:
#   1. GRADES that each have the course id (e.g., 'CSC110Y'), term id (e.g., '20219'), and score in
#   the course (i.e., an integer ranging from 0 to 100, inclusive).
#   2. STUDENTS that each have a utorid (e.g., 'astudent') and a collection of grades.

###################################################################################################
# Part (a) - Entity data classes
###################################################################################################
# Your first task is to complete the two data classes below, which represent  the two main entities
# in our computational model.  Read the provided docstrings and use them to complete each data class
# body.
#
# Do NOT add any additional instance attributes or change the class docstring.
# You do NOT need to add any representation invariants or doctest examples.

@dataclass
class Grade:
    """A grade on Acorn.

    Instance Attributes:
       - course: the course (e.g., 'CSC110Y1') this grade was achieved in.
       - term: the term (e.g., '20219') this grade was achieved in.
       - score: an integer representing the grade achieved.
    """
    course: str
    term: str
    score: int


@dataclass
class Student:
    """A student who can have courses grades on acorn.

    Instance Attributes:
        - utorid: the utorid of this student
        - grades: a dictionary mapping a course (e.g., 'CSC110Y1') to the student's Grade in that
            course.
    """
    utorid: str
    grades: dict[str, Grade]


###############################################################################
# Part (b) - Manager class
###############################################################################
# The class below is responsible for keeping track of all instances of the entities in our
# model and performing mutating operations on them.
#
# Read through the class and implement the methods that have empty bodies.
# Do NOT change anything else in the class (attributes, methods that we have implemented).


class AcornSystem:
    """A class that tracks students and their grades."""

    # Private Instance Attributes:
    #   - _students: a dictionary mapping utorids to Students

    def __init__(self, initial_students: list[Student]) -> None:
        """Initialize a new AcornSystem, adding every student in initial_students to this system.

        Precondition:
            - Every student in initial_students has a unique utorid
        """
        self._students = {}
        for student in initial_students:
            self._students[student.utorid] = student

    def student_count(self) -> int:
        """Return the number of students in this system."""
        return len(self._students)

    def add_grade(self, utorid: str, grade: Grade) -> bool:
        """Add grade to the student with utorid and return whether the grade was added successfully.

        Do not add grade if the student with utorid already has a grade assigned for that course.

        Preconditions:
            - A student with utorid exists in this system
        """
        student = self._students[utorid]

        if grade.course in student.grades:
            return False
        else:
            student.grades[grade.course] = grade
            return True

    def get_grades(self, utorid: str, term: str) -> list[tuple[str, int]]:
        """Return a list of tuples of the courses and scores that the student with utorid achieved
        in term.

        Preconditions:
            - A student with utorid exists in this system
            - The student in this system with utorid has a grade for course

        >>> acorn_system = AcornSystem([Student('astudent', {})])
        >>> acorn_system.add_grade('astudent', Grade('CLA204H1', '20209', 76))
        True
        >>> acorn_system.get_grades('astudent', '20209')
        [('CLA204H1', 76)]
        """
        return [(g.course, g.score) for g in self._students[utorid].grades.values()
                if g.term == term]

    def amend_grades(self, utorid: str, amendments: dict[str, int]) -> int:
        """Update the grades for the student with utorid based on the amendments, which maps courses
        to the amended score.

        Return the number of successful amendments made. An amendment is not successful if the
        student has no grade on record for the course. No mutation should occur for students who
        have no grade on record for course.

        Preconditions:
            - A student with utorid exists in this system

        >>> acorn_system = AcornSystem([Student('astudent', {})])
        >>> acorn_system.add_grade('astudent', Grade('CLA204H1', '20209', 76))
        True
        >>> acorn_system.amend_grades('astudent', {'CLA204H1': 80})
        1
        >>> acorn_system.get_grades('astudent', '20209')
        [('CLA204H1', 80)]
        """
        grades = self._students[utorid].grades
        count = 0
        for course in amendments:
            if course in grades:
                grades[course].score = amendments[course]
                count += 1
        return count

    def calculate_averages(self, utorids: list[str]) -> dict[str, float]:
        """Return a dictionary mapping a student's utorid to the average grade they achieved for
        all courses completed.

        Preconditions:
            - Every utorid in amendments is a student in this system

        >>> acorn_system = AcornSystem([Student('astudent', {}), Student('bstudent', {})])
        >>> acorn_system.add_grade('astudent', Grade('CLA204H1', '20209', 76))
        True
        >>> acorn_system.calculate_averages(['astudent'])
        {'astudent': 76.0}
        >>> acorn_system.add_grade('astudent', Grade('CLA205H1', '20219', 98))
        True
        >>> acorn_system.calculate_averages(['astudent'])
        {'astudent': 87.0}
        >>> acorn_system.add_grade('bstudent', Grade('CLA205H1', '20219', 98))
        True
        >>> temp = acorn_system.calculate_averages(['astudent', 'bstudent'])
        >>> temp == {'astudent': 87.0, 'bstudent': 98.0}
        True
        """
        return {u: sum(g.score for g in self._students[u].grades.values())
                / len(self._students[u].grades) for u in utorids}
