package streams;

import org.junit.jupiter.api.Test;

import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import static org.assertj.core.api.Assertions.assertThat;

public class StringExercises {
    private final List<String> strings = Arrays.asList("this", "is", "a",
            "list", "of", "strings");

    @Test
    public void stringLengthSort_InnerClass() {     // Java 5, 6, 7
        strings.sort(new Comparator<String>() {
            @Override
            public int compare(String s1, String s2) {
                return s1.length() - s2.length();
            }
        });
        System.out.println(strings);
    }

    @Test
    public void stringLengthSort_lambda() {
        // Use lambda for the Comparator (reverse sort)
        strings.sort((s1, s2) -> s2.length() - s1.length());
        System.out.println(strings);

        // Use the "sorted" method on Stream
        List<String> sorted = strings.stream()
                .sorted((s1, s2) -> s1.length() - s2.length())
                .collect(Collectors.toList());
        System.out.println(sorted);
        System.out.println(strings);
    }

    private int compareStrings(String s1, String s2) {
        return s1.length() - s2.length();
    }

    @Test  // Use a lambda that calls 'compareStrings' directly
    public void stringLengthSort_methodCall() {
        List<String> sorted = strings.stream()
                .sorted((s1, s2) -> compareStrings(s1, s2))
                .collect(Collectors.toList());
        System.out.println(sorted);
    }

    @Test  // Use a method ref to 'compareStrings'
    public void stringLengthSort_methodRef() {
        List<String> sorted = strings.stream()
                .sorted(this::compareStrings)
                .collect(Collectors.toList());
        System.out.println(sorted);
    }

    @Test  // Use Comparator.comparingInt
    public void stringLengthSort_comparingInt() {
        List<String> sorted = strings.stream()
                .sorted(Comparator.comparingInt(String::length))
                .collect(Collectors.toList());
        System.out.println(sorted);
    }

    @Test
    public void demoCollectors() {
        // Get only strings of even length
        // Add them to a LinkedList
        List<String> evens = strings.stream()
                .filter(s -> s.length() % 2 == 0)
                // .collect(Collectors.toList()); // No guarantee on type of List
                // .collect(Collectors.toCollection(() -> new LinkedList<>()));
                .collect(Collectors.toCollection(LinkedList::new));
        System.out.println(evens);
        assertThat(evens).isExactlyInstanceOf(LinkedList.class);

        // Add the strings to a map of string to length
        Map<String, Integer> map = strings.stream()
                .distinct()  // get rid of duplicates
                // .collect(Collectors.toMap(s -> s, String::length));
                .collect(Collectors.toMap(Function.identity(), String::length));
        System.out.println(map);

        map = strings.stream()  // "merge" duplicates by taking just the first one
                .collect(Collectors.toMap(Function.identity(), String::length, (x, y) -> x));

        Logger logger = Logger.getLogger(StringExercises.class.getName());

        // Filter out nulls, then print even-length strings
        List<String> stringsWithNulls = Arrays.asList("this", "is", null, "a", "list", null,
                "of", "strings", null, "with", null, "nulls");
        List<String> evenLengths = stringsWithNulls.stream()
                // .filter(s -> s != null && s.length() % 2 == 0)  // short-circuiting logical AND
                // .filter(s -> s != null)
                .peek(s -> logger.fine(() -> "before null check: " + s))
                .filter(Objects::nonNull)
                .peek(s -> logger.fine(() -> "after null check, before length check: " + s))
                .filter(s -> s.length() % 2 == 0)
                .peek(s -> logger.fine(() -> "after length check: " + s))
                .collect(Collectors.toList());
        System.out.println(evenLengths);

        // Function composition
        Consumer<String> print = System.out::println;
        Consumer<String> log = logger::info;


        // Combine the two predicates and use the result to print non-null, even-length strings
        Predicate<String> nonNull = Objects::nonNull;
        Predicate<String> evenLength = s -> s.length() % 2 == 0;
        stringsWithNulls.stream()
                .filter(nonNull.and(evenLength))
                .forEach(print.andThen(log));

        // f: A -> B, g: B -> C, (g.f)(x) = g(f(x)), A -> C
    }

    // generated by GitHub Copilot, but looks good
    private <A, B, C> Function<A, C> compose(Function<A, B> f, Function<B, C> g) {
        return x -> g.apply(f.apply(x));
    }

}
