"""
Jason Yau 
10/31/2025

Use to quickly run all student source code from Webcourses on Eustis and output a csv using stdout.

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

Usage:

Go to assignments, P4, 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 (p4.py) to temp Eustis folder like so:
scp -r <path of p4.py> <YOUR_NID>@eustis.eecs.ucf.edu:~/temp/

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

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

Run the python script which outputs a csv using stdout. You can copy the file over to your local machine via rsync and open with Excel.
python3 ~/temp/p4.py > ~/temp/p4.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
    
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 3
    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):
        self.id = id
        self.name = name
        self.source_code_files = 0
        self.passed_cases = 0
        self.case_points = 0
        self.comment = ""
    
    def __repr__(self):
        return f"\"{self.name}\",\"{self.case_points}\",\"{self.passed_cases}\",\"{self.comment}\""

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])

sample_cases_zip = glob.glob(f"{script_directory}/p4-sample.zip")
if len(sample_cases_zip) != 1:
    print(f"p4-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}/p4-secret.zip")
if len(secret_cases_zip) != 1:
    print(f"p4-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}/p4-*/ranklist*.in"))
output_files = sorted(glob.glob(f"{extracted_path}/p4-*/ranklist*.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]

print(f"\"student name\",\"points from test cases\",\"passed cases\",\"comment to write\"")

source_code_files = sorted(glob.glob(f"{extracted_path}/*.c")+glob.glob(f"{extracted_path}/*.C"))
for source_code_file in 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 > 2:
        student.comment += f"\n\nStudent submitted more than 2 source code files 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 student.source_code_files == 2:
        student.comment += "\n\n"
    if "qs" in student_file_name:
        student.comment += f"---------\nExecution Points for Quick Sort Program:\n"
    elif "ms" in student_file_name:
        student.comment += f"---------\nExecution Points for Merge Sort Program:\n"
    else:
        student.comment += f"---------\nExecution Points for Program #{student.source_code_files}:\n"
    
    if compiled:
        for i in range(len(input_files)):
            with open(input_files[i], "r") as fin:
                try:
                    execute_result = subprocess.run([executable], stdin=fin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5, text=True, universal_newlines=True, encoding='mac_roman')
                    if not execute_result.stdout or not execute_result.stdout.strip():
                        student.comment += f"Test Case #{i+1}: FAILED (BLANK OUTPUT) (+0)\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 curr_case_points == 3:
                        student.comment += f"Test Case #{i+1}: PASSED (+{curr_case_points})\n"
                        passed_cases += 1
                    else:
                        student.comment += f"Test Case #{i+1}: FAILED (+0)\n"
                except subprocess.TimeoutExpired:
                    student.comment += f"Test case #{i+1}: FAILED (TIME LIMIT EXCEEDED) (+0)\n"
    else:
        student.comment += "Did not compile.\n"

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

    

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"
    if student.source_code_files == 1:
        student.comment = f"Student only submitted one source code file.\n"+student.comment
    if student.source_code_files >= 1:
        student.comment += "\n--------"
    student.comment += f"\n\nTotal Execution Points: {student.case_points}/{2*len(input_files)*3}\n"
    print(student)

shutil.rmtree(extracted_path)
