"""
Jason Yau 
12/06/2025

Use to quickly run all student source code from Webcourses on Eustis and creates a CSV file called p6.csv.

====================================

Usage:

Go to assignments, P6, Download submissions. A file named submissions.zip (or similar) will download

Copy submissions.zip file to temp Eustis folder like so (This assumes ~/temp/ exists already. If not, you will need to create the directory.): 
scp -r <path of submissions.zip> <YOUR_NID>@eustis.eecs.ucf.edu:~/temp/

Go to gradebook, Export, Export Entire Gradebook. A csv will download.

Copy *Grades-COP3502C*.csv file to temp Eustis folder like so (This assumes ~/temp/ exists already. If not, you will need to create the directory.): 
scp -r <path of *Grades-COP3502C*.csv> <YOUR_NID>@eustis.eecs.ucf.edu:~/temp/


Copy this python script (p6.py) to temp Eustis folder like so:
scp -r <path of p6.py> <YOUR_NID>@eustis.eecs.ucf.edu:~/temp/

Copy both test cases zip file to temp Eustis folder like so:
scp -r <path of p6-sample.zip> <YOUR_NID>@eustis.eecs.ucf.edu:~/temp/
scp -r <path of p6-secret.zip> <YOUR_NID>@eustis.eecs.ucf.edu:~/temp/

ssh into eustis
ssh <YOUR_NID>@eustis.eecs.ucf.edu

Run the python script which creates a CSV file called p6.csv. You can copy the file over to your local machine via rsync and open with Excel.
cd ~/temp
python3 p6.py
# Ex of rsync: sudo rsync -acv <YOUR_NID>@eustis.eecs.ucf.edu:~/temp/p6.csv .

====================================

Make sure to delete all created files afterwards to be safe.
"""


import os
import glob
from zipfile import ZipFile
import shutil
import subprocess
import re
from collections import Counter
import csv
import sys
import time

points_per_case = 6
time_limit_seconds = 4
    
def grade_case(output: str, expected: str, case_input: str) -> int:
    out_lines = output.splitlines()
    out_lines = list(filter(lambda line: line and line.strip(), out_lines))
    exp_lines = expected.splitlines()
    if len(out_lines) != len(exp_lines):
        return 0
    matching_lines = 0
    for i in range(len(out_lines)):
        if out_lines[i] == exp_lines[i]:
            matching_lines += 1
    if matching_lines == len(out_lines):
        return points_per_case
    else:
        return 0

script_path = os.path.abspath(__file__)
script_directory = os.path.dirname(script_path)

extracted_path = f"{script_directory}/extracted"
if (len(glob.glob(extracted_path)) != 0):
    shutil.rmtree(extracted_path)

submissions_zip = glob.glob(f"{script_directory}/submissions*.zip")
if len(submissions_zip) != 1:
    print(f"There were {len(submissions_zip)} submissions*.zip found. Exiting.")
    exit(1)
with ZipFile(submissions_zip[0], 'r') as zip_object:
    zip_object.extractall(extracted_path)


grades_csv = glob.glob(f"{script_directory}/*Grades-COP3502C*.csv")
if len(grades_csv) != 1:
    print(f"There were {len(grades_csv)} *Grades-COP3502C*.csv found. Exiting.")
    exit(1)

class Student:
    def __init__(self, id: str, name: str, lab_section: str):
        self.id = id
        self.name = name
        self.source_code_files = 0
        self.passed_cases = 0
        self.case_points = 0
        self.comment = ""
        self.lab_section = lab_section
    
    def __repr__(self):
        return f"\"{self.name}\",\"{self.case_points}\",\"{self.passed_cases}\",\"{self.comment}\",\"{self.lab_section}\""

students = {}
with open(grades_csv[0], 'r') as csv_file:
    csv_reader = csv.reader(csv_file)
    next(csv_reader)
    next(csv_reader)
    for row in csv_reader:
        students[row[1]] = Student(row[1], row[0], row[5].split(' ')[-1])

sample_cases_zip = glob.glob(f"{script_directory}/p6-sample.zip")
if len(sample_cases_zip) != 1:
    print(f"p6-sample.zip not found. Exiting.")
    exit(1)
with ZipFile(sample_cases_zip[0], 'r') as zip_object:
    zip_object.extractall(extracted_path)

secret_cases_zip = glob.glob(f"{script_directory}/p6-secret.zip")
if len(secret_cases_zip) != 1:
    print(f"p6-secret.zip not found. Exiting.")
    exit(1)
with ZipFile(secret_cases_zip[0], 'r') as zip_object:
    zip_object.extractall(extracted_path)

input_files = sorted(glob.glob(f"{extracted_path}/p6-*/wordleheap*.in"))
output_files = sorted(glob.glob(f"{extracted_path}/p6-*/wordleheap*.out"))
# Make sure we don't screw this part up.
assert len(input_files) == len(output_files)
for i in range(len(input_files)):
    assert input_files[i].split(".in")[0] in output_files[i].split(".out")[0]

cases_to_points = {}

source_code_files = sorted(glob.glob(f"{extracted_path}/*.c")+glob.glob(f"{extracted_path}/*.C"))
for source_code_i, source_code_file in enumerate(source_code_files):
    file_name = os.path.basename(source_code_file)
    student_name = file_name.split('_')[0]
    student_file_name = file_name.split('_')[len(file_name.split('_'))-1]
    late = "LATE" in file_name.upper()
    student_id = file_name.split('_')[1]
    if student_id == "LATE":
        student_id = file_name.split('_')[2]

    student = students[student_id]
    assert student is not None

    executable = f"{source_code_file.split(".c")[0]}.exe"
    gcc_result = subprocess.run(["gcc", source_code_file, "-o", executable], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    compiled = gcc_result.returncode == 0

    case_points = 0
    passed_cases = 0
    
    student.source_code_files += 1
    if student.source_code_files > 1:
        student.comment += f"\n\nStudent submitted more than 1 source code file in their final submission. Skipped judging: {file_name}\n"
        continue
    
    student_file_name = ""
    for token in reversed(file_name.split('_')):
        if token.isdigit():
            break
        student_file_name = student_file_name+token
    
    if compiled:
        for i in range(len(input_files)):
            with open(input_files[i], "r") as fin:
                try:
                    start_time = time.perf_counter()
                    execute_result = subprocess.run([executable], stdin=fin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=4, text=True, universal_newlines=True, encoding='mac_roman')
                    elapsed_time = time.perf_counter()-start_time
                    if not execute_result.stdout or not execute_result.stdout.strip():
                        if execute_result.returncode != 0:
                            student.comment += f"Test case #{i+1}: FAILED (+0), reason: program crashed with exit code {execute_result.returncode}, execution time: {elapsed_time:.4f}/{time_limit_seconds:.4f} seconds\n"
                        else:
                            student.comment += f"Test case #{i+1}: FAILED (+0), reason: blank output, execution time: {elapsed_time:.4f}/{time_limit_seconds:.4f} seconds\n"
                        continue
                    curr_case_points = grade_case(execute_result.stdout, open(output_files[i], "r").read(), open(input_files[i], "r").read())
                    case_points += curr_case_points
                    if i not in cases_to_points:
                        cases_to_points[i] = 0
                    cases_to_points[i] += curr_case_points
                    if curr_case_points == points_per_case:
                        student.comment += f"Test case #{i+1}: PASSED (+{curr_case_points}), execution time: {elapsed_time:.4f}/{time_limit_seconds:.4f} seconds\n"
                        passed_cases += 1
                    elif execute_result.returncode != 0:
                        student.comment += f"Test case #{i+1}: FAILED (+0), reason: program crashed with exit code {execute_result.returncode}, execution time: {elapsed_time:.4f}/{time_limit_seconds:.4f} seconds\n"
                    else:
                        student.comment += f"Test case #{i+1}: FAILED (+0), reason: incorrect output, execution time: {elapsed_time:.4f}/{time_limit_seconds:.4f} seconds\n"
                except subprocess.TimeoutExpired:
                    student.comment += f"Test case #{i+1}: FAILED (+0), reason: time limit exceeded, execution time: {time_limit_seconds:.4f}/{time_limit_seconds:.4f} seconds\n"
    else:
        student.comment += "Did not compile.\n"

    student.comment += f"Points from Test Cases: {case_points}/{len(input_files)*points_per_case}\n"
    student.comment += f"Test Cases Passed: {passed_cases}/{len(input_files)}\n"
    student.case_points += case_points
    student.passed_cases += passed_cases

    if (source_code_i+1)%5 == 0:
        print(f"Processed {source_code_i+1}/{len(source_code_files)} source code files.")

    

with open("p6.csv", "w") as output_csv:
    output_csv.write(f"\"student name\",\"points from test cases\",\"passed cases\",\"comment to write\",\"lab section\"\n")
    for id in students:
        student = students[id]
        if student.source_code_files == 0:
            student.comment += f"Student did not submit any source code files.\n"
        student.comment += f"\n\nTotal Execution Points: {student.case_points}/{len(input_files)*points_per_case}\n"
        output_csv.write(str(student)+"\n")
print("\n")
for i in range(0, 7):
    if i == 6:
        print(f"Number of students with {i*10}-{i*10} execution points: {len(list(filter(lambda student: student.case_points >= i*10 and student.case_points <= i*10, students.values())))}")
    else:
        print(f"Number of students with {i*10}-{i*10+9} execution points: {len(list(filter(lambda student: student.case_points >= i*10 and student.case_points <= i*10+9, students.values())))}")
print("\n")
for i in range(0, 11):
    print(f"Number of students who passed {i} test cases: {len(list(filter(lambda student: student.passed_cases == i, students.values())))}")
print("\n")
for i in cases_to_points:
    print(f"Test case {i+1}: {((cases_to_points[i]/(points_per_case*len(students.keys())))*100):.2f}%")

shutil.rmtree(extracted_path)
