import matplotlib.pyplot as plt
import scipy.stats
from pathlib import Path
from typing_extensions import final
import pandas as pd
import numpy as np

# Path object to current working directory
CURRENT_DIR = Path.cwd()
# and create one for the data folder
DATA_DIR = CURRENT_DIR / "data"


roster = pd.read_csv(DATA_DIR / "roster.csv")
roster = pd.read_csv(DATA_DIR / "roster.csv",
                     converters={"NetID": str.lower,
                                 "Email Address": str.lower},
                     usecols=["Section", "Email Address", "NetID"],
                     index_col="NetID"
                     )


hw_exam_grades = pd.read_csv(DATA_DIR / "hw_exam_grades.csv",
                             converters={"SID": str.lower},
                             usecols=lambda title: "Submission" not in title,
                             index_col="SID")

quiz_grades = pd.DataFrame()

for file_path in DATA_DIR.glob("quiz_*_grades.csv"):
    quiz_name = " ".join(file_path.stem.title().split("_")[0:2])
    quiz = pd.read_csv(file_path,
                       converters={"Email": str.lower},
                       index_col=["Email"],
                       usecols=["Email", "Grade"]).rename(columns={"Grade": quiz_name})
    # Concatenate along the columns with axis=1 (rather than stacking the dataframe)
    quiz_grades = pd.concat([quiz_grades, quiz], axis=1)


# Merge the two dataframes on the index.
final_df = pd.merge(roster, hw_exam_grades, left_index=True, right_index=True)

# or merge on the index via .join (default behaviour is via the indice)
final_df = roster.join(hw_exam_grades)

# and finally merge the quiz grades in
final_df = pd.merge(final_df, quiz_grades,
                    left_on="Email Address", right_index=True)


# We have some NaNs in the dataframe, we'll presume the student didn't submit it and set the grade to 0 in those cases.
final_df = final_df.fillna(0)


# the weights for the grades
weights = pd.Series(
    {
        "Exam 1 Score": 0.05,
        "Exam 2 Score": 0.10,
        "Exam 3 Score": 0.15,
        "Quiz Score": 0.30,
        "Homework Score": 0.40


    }
)


# Compute the final grades
n_exams = 3
for n in range(1, n_exams + 1):
    final_df[f'Exam {n} Score'] = final_df[f'Exam {n}'] / \
        final_df[f'Exam {n} - Max Points']


# homework grades
final_df[[x for x in final_df.columns if 'Home' in x and 'Max' in x]]

# we have two options:
# - total score (sum all the raw scores and max points independently, then calculate the ratio/percentage)
# - average score (divide each raw score by its respective maximum points, then average the resulting ratios)

# total score will favour students who did well on assignments worth more points
# average score favours students who performed consistently

# example
# hw = [25, 85, 33, 95]
# hw_max = [50, 90, 50, 100]
# by_total_score = sum(hw) / sum(hw_max)
# by_average_score = sum([a/b for a,b in zip(hw, hw_max)]) / len(hw)


# Let's calculate both and then take the max

hw_max_cols = sorted(
    [x for x in final_df.columns if 'Home' in x and 'Max' in x])
hw_cols = sorted(
    [x for x in final_df.columns if 'Home' in x and 'Max' not in x])

hw_score_by_total = final_df[hw_cols].sum(
    axis=1) / final_df[hw_max_cols].sum(axis=1)
final_df['HW by Total'] = hw_score_by_total


# this is what we want to calculate
# final_df[hw_cols] / final_df[hw_max_cols].set_axis(hw_cols, axis=1)
# make it pretty by creating a dataframe where the names of the columns match the other
hw_max_data = final_df[hw_max_cols].set_axis(hw_cols, axis=1)

hw_score_by_average = (final_df[hw_cols] /
                       hw_max_data).sum(axis=1) / len(hw_cols)
final_df['HW by Average'] = hw_score_by_average

final_df['Homework Score'] = final_df[[
    'HW by Total', 'HW by Average']].max(axis=1)


# and then computing for the quizzes
quiz_scores = final_df.filter(regex=r"^Quiz \d$", axis=1)
n_quiz = quiz_scores.shape[1]
quiz_max_points = pd.Series(
    {'Quiz 1': 11, 'Quiz 2': 15, 'Quiz 3': 17, 'Quiz 4': 14, 'Quiz 5': 12}
)

quiz_score_by_total = quiz_scores.sum(axis=1) / quiz_max_points.sum()
quiz_score_by_average = (quiz_scores / quiz_max_points).sum(axis=1) / n_quiz

final_df['Quiz Score'] = pd.concat(
    [quiz_score_by_total, quiz_score_by_average], axis=1).max(axis=1)


# final scores
weights = pd.Series(
    {
        'Exam 1 Score': 0.05,
        'Exam 2 Score': 0.10,
        'Exam 3 Score': 0.15,
        'Quiz Score': 0.30,
        'Homework Score': 0.40
    }
)

final_df['Final Score'] = (final_df[weights.index] * weights).sum(axis=1)
final_df['Ceiling Score'] = np.ceil(final_df['Final Score'] * 100)


def get_letter_grade(score):
    if score >= 90:
        return 'A'
    elif score >= 80:
        return 'B'
    elif score >= 70:
        return 'C'
    elif score >= 60:
        return 'D'
    else:
        return 'F'


letter_grades = final_df['Ceiling Score'].map(get_letter_grade)

final_df['Final Grade'] = pd.Categorical(
    letter_grades, categories=['F', 'D', 'C', 'B', 'A'], ordered=True
)


# Saving to CSV
cols_to_write = ['Last Name', 'First Name',
                 'Email Address', 'Ceiling Score', 'Final Grade']

# Create a group by column(s) in this case by Section
final_df.groupby("Section")
# show groups by .group and get specific group by .get_group(n)
for section, df in final_df.groupby("Section"):
    # returns a tuple of the grouping identifier and the dataframe
    section_file = DATA_DIR / f'section_{section}_grades.csv'
    df[cols_to_write].sort_values(
        by=["Last Name", "First Name"]).to_csv(section_file)


# Distribution
grade_counts = final_df['Final Grade'].value_counts().sort_index()
grade_counts.plot.bar()


# normally distributed?
grade_mean = final_df['Final Score'].mean()
grade_std = final_df['Final Score'].std()

x = np.linspace(grade_mean - 5*grade_std, grade_mean+5*grade_std, 200)
y = scipy.stats.norm.pdf(x, loc=grade_mean, scale=grade_std)

plt.plot(x, y, label='Normal Distribution', linewidth=3)

final_df['Final Score'].plot.hist(bins=20, label='Grade Distribution')
final_df['Final Score'].plot.density(
    linewidth=3, label='Kernel Density Estimate')

plt.legend()

plt.show()
# Looks like this is a fairly average class
