import java.util.*;
import java.util.function.*;
import java.util.stream.*;

/**
 * LambdaStreamDemo.java
 * Demonstrates Lambda Expressions and the Stream API for MSc students.
 * Covers: functional interfaces, Predicate, Function, Consumer, Supplier,
 *         method references, Stream operations (filter, map, sorted, collect,
 *         reduce, groupingBy, partitioningBy), and parallel streams.
 *
 * Compile: javac LambdaStreamDemo.java
 * Run:     java LambdaStreamDemo
 */
public class LambdaStreamDemo {

    // --- Data class ---
    static class Student {
        String name;
        String department;
        double gpa;
        int age;

        Student(String name, String department, double gpa, int age) {
            this.name = name;
            this.department = department;
            this.gpa = gpa;
            this.age = age;
        }

        @Override
        public String toString() {
            return String.format("%s (dept=%s, gpa=%.1f, age=%d)", name, department, gpa, age);
        }
    }

    // ================================================================
    // 1. Custom functional interface with a single abstract method
    // ================================================================
    @FunctionalInterface
    interface MathOperation {
        double operate(double a, double b);
    }

    // ================================================================
    // 2. Helper that accepts a functional interface
    // ================================================================
    static double compute(double x, double y, MathOperation op) {
        return op.operate(x, y);
    }

    // ================================================================
    // 3. Static method used as a method reference target
    // ================================================================
    static boolean isHonors(Student s) {
        return s.gpa >= 3.7;
    }

    // ================================================================
    // Main
    // ================================================================
    public static void main(String[] args) {

        // --- Sample data ---
        List<Student> students = Arrays.asList(
            new Student("Ali",    "CS",    3.8, 24),
            new Student("Fatma",  "CS",    3.5, 23),
            new Student("Omar",   "IS",    3.9, 25),
            new Student("Aisha",  "CS",    3.2, 22),
            new Student("Hassan", "IS",    3.7, 26),
            new Student("Zara",   "Math",  3.6, 23),
            new Student("Yusuf",  "Math",  3.1, 24),
            new Student("Mwana",  "CS",    3.95, 22),
            new Student("Juma",   "IS",    2.9, 27),
            new Student("Salma",  "Math",  3.4, 25)
        );

        // ============================================================
        // Part A: Lambda Expressions & Functional Interfaces
        // ============================================================
        System.out.println("=== Custom Functional Interface (MathOperation) ===");
        MathOperation add      = (a, b) -> a + b;
        MathOperation subtract = (a, b) -> a - b;
        MathOperation multiply = (a, b) -> a * b;
        System.out.println("10 + 3 = " + compute(10, 3, add));
        System.out.println("10 - 3 = " + compute(10, 3, subtract));
        System.out.println("10 * 3 = " + compute(10, 3, multiply));
        // Inline lambda
        System.out.println("10 / 3 = " + compute(10, 3, (a, b) -> a / b));
        System.out.println();

        // --- Predicate<T> ---
        System.out.println("=== Predicate<T> ===");
        Predicate<Student> highGpa = s -> s.gpa >= 3.5;
        Predicate<Student> csStudent = s -> s.department.equals("CS");
        // Compose predicates with and()
        Predicate<Student> csAndHigh = csStudent.and(highGpa);
        System.out.println("CS students with GPA >= 3.5:");
        students.stream().filter(csAndHigh).forEach(s -> System.out.println("  " + s));
        System.out.println();

        // --- Function<T, R> ---
        System.out.println("=== Function<T, R> ===");
        Function<Student, String> toLabel = s -> s.name + " [" + s.department + "]";
        System.out.println("Labels:");
        students.forEach(s -> System.out.println("  " + toLabel.apply(s)));
        System.out.println();

        // --- Consumer<T> ---
        System.out.println("=== Consumer<T> ===");
        Consumer<Student> printGpa = s ->
            System.out.println("  " + s.name + " -> GPA " + s.gpa);
        System.out.println("GPA report:");
        students.forEach(printGpa);
        System.out.println();

        // --- Supplier<T> ---
        System.out.println("=== Supplier<T> ===");
        Supplier<List<Student>> emptyList = ArrayList::new; // method reference
        List<Student> fresh = emptyList.get();
        System.out.println("New empty list size: " + fresh.size());
        System.out.println();

        // --- Method References ---
        System.out.println("=== Method References ===");
        // Static method reference
        System.out.println("Honors students (static method ref):");
        students.stream()
                .filter(LambdaStreamDemo::isHonors) // reference to static method
                .map(s -> s.name)
                .forEach(name -> System.out.println("  " + name));
        System.out.println();

        // ============================================================
        // Part B: Stream API
        // ============================================================
        System.out.println("=== Stream: filter + map + sorted + collect ===");
        List<String> topNames = students.stream()
            .filter(s -> s.gpa >= 3.5)
            .sorted(Comparator.comparingDouble((Student s) -> s.gpa).reversed())
            .map(s -> s.name + " (" + s.gpa + ")")
            .collect(Collectors.toList());
        System.out.println("Top students (GPA >= 3.5, sorted desc):");
        topNames.forEach(n -> System.out.println("  " + n));
        System.out.println();

        // --- reduce ---
        System.out.println("=== Stream: reduce ===");
        double totalGpa = students.stream()
            .map(s -> s.gpa)
            .reduce(0.0, Double::sum);
        System.out.printf("Total GPA sum: %.2f%n", totalGpa);
        System.out.printf("Average GPA:   %.2f%n", totalGpa / students.size());

        OptionalDouble avgGpa = students.stream()
            .mapToDouble(s -> s.gpa)
            .average();
        System.out.printf("Average (via mapToDouble): %.2f%n", avgGpa.orElse(0));
        System.out.println();

        // --- groupingBy ---
        System.out.println("=== Stream: groupingBy ===");
        Map<String, List<Student>> byDept = students.stream()
            .collect(Collectors.groupingBy(s -> s.department));
        byDept.forEach((dept, list) -> {
            System.out.println("  " + dept + ":");
            list.forEach(s -> System.out.println("    " + s));
        });
        System.out.println();

        // --- groupingBy with downstream collector ---
        System.out.println("=== Stream: groupingBy + averagingDouble ===");
        Map<String, Double> avgByDept = students.stream()
            .collect(Collectors.groupingBy(
                s -> s.department,
                Collectors.averagingDouble(s -> s.gpa)
            ));
        avgByDept.forEach((dept, avg) ->
            System.out.printf("  %s -> avg GPA %.2f%n", dept, avg));
        System.out.println();

        // --- partitioningBy ---
        System.out.println("=== Stream: partitioningBy ===");
        Map<Boolean, List<Student>> partition = students.stream()
            .collect(Collectors.partitioningBy(s -> s.gpa >= 3.5));
        System.out.println("GPA >= 3.5 (true):");
        partition.get(true).forEach(s -> System.out.println("  " + s));
        System.out.println("GPA <  3.5 (false):");
        partition.get(false).forEach(s -> System.out.println("  " + s));
        System.out.println();

        // --- Parallel Stream ---
        System.out.println("=== Parallel Stream ===");
        long start = System.nanoTime();
        double parallelSum = students.parallelStream()
            .mapToDouble(s -> {
                // Simulate some work
                try { Thread.sleep(50); } catch (InterruptedException ignored) {}
                return s.gpa;
            })
            .sum();
        long parallelTime = System.nanoTime() - start;

        start = System.nanoTime();
        double sequentialSum = students.stream()
            .mapToDouble(s -> {
                try { Thread.sleep(50); } catch (InterruptedException ignored) {}
                return s.gpa;
            })
            .sum();
        long sequentialTime = System.nanoTime() - start;

        System.out.printf("Parallel   sum=%.2f  time=%d ms%n", parallelSum, parallelTime / 1_000_000);
        System.out.printf("Sequential sum=%.2f  time=%d ms%n", sequentialSum, sequentialTime / 1_000_000);
        System.out.println("Parallel stream can be faster for CPU-intensive or blocking work.");

        System.out.println("\n--- LambdaStreamDemo complete ---");
    }
}
