// A student records system: search, sort, group, and report.

record Student(int id, String name, int grade, List<Integer> scores) {}

/// Returns the mean of a list of numbers, or 0.0 if it is empty.
double average(List<Integer> numbers) {
    if (numbers.isEmpty()) {
        return 0.0;
    }
    int total = 0;
    for (int number : numbers) {
        total += number;
    }
    return (double) total / numbers.size();
}

/// Returns the letter grade for a numeric average.
String letterGrade(double score) {
    if (score >= 90) {
        return "A";
    } else if (score >= 80) {
        return "B";
    } else if (score >= 70) {
        return "C";
    } else if (score >= 60) {
        return "D";
    }
    return "F";
}

/// Binary search of a list SORTED BY ID. Returns the student, or null.
Student findById(ArrayList<Student> students, int studentId) {
    int low = 0;
    int high = students.size() - 1;

    while (low <= high) {
        int middle = (low + high) / 2;
        Student student = students.get(middle);
        if (student.id() == studentId) {
            return student;
        } else if (student.id() < studentId) {
            low = middle + 1;
        } else {
            high = middle - 1;
        }
    }

    return null;
}

/// Returns every student whose name contains the text, ignoring case.
ArrayList<Student> searchByName(ArrayList<Student> students, String text) {
    ArrayList<Student> matches = new ArrayList<>();
    for (Student student : students) {
        if (student.name().toLowerCase().contains(text.toLowerCase())) {
            matches.add(student);
        }
    }
    return matches;
}

/// Returns a map from each grade level to its students, grades in order.
Map<Integer, ArrayList<Student>> groupByGrade(ArrayList<Student> students) {
    Map<Integer, ArrayList<Student>> groups = new TreeMap<>();
    for (Student student : students) {
        if (!groups.containsKey(student.grade())) {
            groups.put(student.grade(), new ArrayList<>());
        }
        groups.get(student.grade()).add(student);
    }
    return groups;
}

/// Prints every student, ranked by average, highest first.
void printRoster(ArrayList<Student> students) {
    ArrayList<Student> ranked = new ArrayList<>(students);
    ranked.sort(Comparator.comparing((Student s) -> average(s.scores())).reversed());

    IO.println(String.format("%-2s%-5s%-18s%-3s%4s%6s",
            "#", "ID", "Name", "Gr", "Avg", "Grade"));
    IO.println("-".repeat(38));
    for (int i = 0; i < ranked.size(); i++) {
        Student student = ranked.get(i);
        double mean = average(student.scores());
        IO.println(String.format("%-2d%-5d%-18s%-3d%4.1f%6s",
                i + 1, student.id(), student.name(), student.grade(),
                mean, letterGrade(mean)));
    }
    IO.println("-".repeat(38));
}

/// Prints a summary for each grade level.
void printGradeSummary(ArrayList<Student> students) {
    IO.println();
    IO.println("By grade level");
    Map<Integer, ArrayList<Student>> groups = groupByGrade(students);
    for (int grade : groups.keySet()) {
        ArrayList<Student> members = groups.get(grade);
        double total = 0;
        for (Student student : members) {
            total += average(student.scores());
        }
        IO.println(String.format("  Grade %d: %d students, average %.1f",
                grade, members.size(), total / members.size()));
    }
}

/// Prints statistics for the whole class.
void printClassStats(ArrayList<Student> students) {
    double total = 0;
    int passing = 0;
    Student best = students.get(0);
    for (Student student : students) {
        double mean = average(student.scores());
        total += mean;
        if (mean >= 60) {
            passing++;
        }
        if (mean > average(best.scores())) {
            best = student;
        }
    }

    IO.println();
    IO.println("Class statistics");
    IO.println("  Students:      " + students.size());
    IO.println(String.format("  Class average: %.1f", total / students.size()));
    IO.println(String.format("  Top student:   %s (%.1f)",
            best.name(), average(best.scores())));
    IO.println("  Passing:       " + passing + " of " + students.size());
}

void main() {
    ArrayList<Student> students = new ArrayList<>(List.of(
            new Student(1004, "Ada Lovelace", 9, List.of(88, 92, 85)),
            new Student(1001, "Grace Hopper", 10, List.of(95, 91, 98)),
            new Student(1007, "Alan Turing", 9, List.of(45, 62, 58)),
            new Student(1002, "Katherine Johnson", 11, List.of(99, 97, 94)),
            new Student(1009, "Edsger Dijkstra", 10, List.of(71, 68, 75))));

    printRoster(students);
    printGradeSummary(students);
    printClassStats(students);

    ArrayList<Student> byId = new ArrayList<>(students);
    byId.sort(Comparator.comparing(student -> student.id()));
    Student found = findById(byId, 1007);
    IO.println();
    IO.println("Lookup 1007:   " + (found != null ? found.name() : "not found"));
    IO.println("Lookup 9999:   " + findById(byId, 9999));

    ArrayList<Student> matches = searchByName(students, "a");
    IO.println("Names with 'a': " + matches.size());
}

// From Groundwork, chapter 12 (ch12-recursion-and-problem-solving), box 18.
// This example is from Groundwork, licensed CC BY-NC-SA 4.0.
// Run with: java ch12_18.java
