// Closures.java — lambdas capturing variables: effectively final, state and pitfalls.
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.*;

public class Closures {

    // A factory that returns a function "remembering" its argument
    static Function<Double, Double> discountOf(double percent) {
        return price -> price - price * percent / 100;    // captures 'percent'
    }

    // A counter whose state lives inside the closure
    static Supplier<Integer> counter() {
        AtomicInteger count = new AtomicInteger();          // the reference is effectively final
        return count::incrementAndGet;
    }

    public static void main(String[] args) {
        // 1. Capturing a local variable
        String customer = "Acme";                           // effectively final: never reassigned
        Runnable greet = () -> System.out.println("Hello " + customer);
        greet.run();

        // customer = "Globex";   // <- uncommenting this line breaks the lambda above:
        //                        //    "local variables referenced from a lambda expression
        //                        //     must be final or effectively final"

        // 2. Functions that remember their configuration
        Function<Double, Double> tenOff = discountOf(10);
        Function<Double, Double> quarterOff = discountOf(25);
        System.out.println(tenOff.apply(200.0) + " " + quarterOff.apply(200.0));   // 180.0 150.0

        // 3. Closures with their own state
        Supplier<Integer> next = counter();
        Supplier<Integer> other = counter();
        System.out.println(next.get() + " " + next.get() + " " + other.get());     // 1 2 1

        // 4. Counting inside a lambda: you can't do count++ on a local int...
        List<String> lines = List.of("ok", "error", "ok", "error", "error");
        // int errors = 0; lines.forEach(l -> { if (l.equals("error")) errors++; });  // won't compile
        AtomicInteger errors = new AtomicInteger();
        lines.forEach(l -> { if (l.equals("error")) errors.incrementAndGet(); });
        System.out.println("Errors: " + errors.get());                               // 3
        // ...but a stream says it more clearly:
        System.out.println("Errors: " + lines.stream().filter("error"::equals).count());

        // 5. The loop-variable trap (and why Java forbids it)
        List<Supplier<Integer>> suppliers = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            int copy = i;                                    // a fresh, effectively final copy each time
            suppliers.add(() -> copy * 10);
        }
        suppliers.forEach(s -> System.out.print(s.get() + " "));                     // 0 10 20
        System.out.println();

        // 6. Capturing 'this' in an instance context
        new Closures().run();
    }

    private final String name = "Closures demo";

    void run() {
        Runnable r = () -> System.out.println("Inside " + this.name);   // lambdas share 'this'
        r.run();
    }
}
