import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.function.BinaryOperator;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class Ch5 {
    public void all() {
        findUniqueCharacters();
        makePairs();
        findingMatching();
        reducing();
        primitiveStreamSpecializations_Ch5_6();
        pythagoreanTriples();
    }

    private void pythagoreanTriples() {
        //a*a+b*b=c*c <=> (3,4,5) is a valid Pythagorean triple


    }

    private void primitiveStreamSpecializations_Ch5_6() {
        List<Dish> menu = Dish.createMenu();

        //MapToInt => Can use sum(), max(), min(), average(), etc.
        int sumOfCalories = menu.stream()
                .mapToInt(Dish::getCalories)
                .sum();
        System.out.println("Sum of calories " + sumOfCalories);

        //Convert from IntStream to Stream<Integers>. "Each int will be boxed to an Integer":
        IntStream intStream = menu.stream()
                .mapToInt(Dish::getCalories);
        Stream<Integer> stream = intStream.boxed();

        stream.forEach(s->System.out.println("Boxed to stream: " + s));

        //OptionalInt
        OptionalInt maxCalories = menu.stream()
                .mapToInt(Dish::getCalories)
                .max();
        int max = maxCalories.orElse(1);
        System.out.println("Max calories are " + max);

        //Numeric ranges:
        IntStream evenNumbers = IntStream.rangeClosed(1,10)
                .filter(n->n%2==0);

        //Apply one of these terminal operations:
        /*System.out.print("RangeClosed INcludes the range: " );
        evenNumbers.forEach(s->System.out.print(s + " "));
        System.out.println("");*/
        System.out.println("Number of integers in rangeClosed example " + evenNumbers.count());

        IntStream evenNumbers2 = IntStream.range(1,10)
                .filter(n->n%2==0);
        /*System.out.print("Range EXcludes the range: " );
        evenNumbers2.forEach(s->System.out.print(s + " "));
        System.out.println("");*/
        System.out.println("Number of integers in range example " + evenNumbers2.count());

    }

    private void reducing() {
        List<Integer> numbers = Arrays.asList(1,2,3,4);

        //Using reduce:
        int sum = numbers.stream().reduce(0,(a,b)->a+b);
        int product = numbers.stream().reduce(1,(a,b)-> a*b);
        System.out.println("Sum is " + sum + " and product is " + product);

        //Or with BinaryOperators, to make it more obvious that the argument is the only thing that differs
        BinaryOperator<Integer> sumOp = (m,n) ->m+n;
        BinaryOperator<Integer> multOp = (m,n) ->m*n;
        int sum2 = numbers.stream().reduce(0,sumOp);
        int mult2 = numbers.stream().reduce(1,multOp);
        System.out.println("Sum is " + sum2 + " and product is " + mult2);

        //Or using method reference:
        int sum1 = numbers.stream().reduce(0,Integer::sum);
        BinaryOperator<Integer> sum1Op = Integer::sum;


        //Find the maxium value
        BinaryOperator<Integer> maxLambda = (a,b)->a<b?b:a;
        Optional<Integer> max1 = numbers.stream().reduce(maxLambda);
        //Equivalent to
        Optional<Integer> max2 = numbers.stream().reduce(Integer::max);
        System.out.println("Max value is " + max1.get() + " or " + max2.get());

        //Count number of dishes using map and reduce:
        List<Dish> menu = Dish.createMenu();
        int noOfDishes = menu.stream()
                .map(d->1)
                .reduce(0,Integer::sum);
        System.out.println("number of dishes: " + noOfDishes);
        long count = menu.stream().count();
        System.out.println("or by using count: " + count); //But this is not as efficient adn not suitable for parallellism!!
        //To handle parallelism:
        int parallell = menu.parallelStream()
                .map(d->1)
                .reduce(0,Integer::sum);
        System.out.println("Parallell - number of dishes: " + parallell);
        int parallellSum = numbers.parallelStream()
                .reduce(0,Integer::sum);
        System.out.println("Parallell - sum of numbers: " + parallellSum);
    }

    private void findingMatching() {
        List<Dish> menu = Dish.createMenu();

        if(menu.stream().anyMatch(Dish::isVegetarian)){
            System.out.println("There is at least one vegetarian dish");
        }

        if (menu.stream().allMatch(d->d.getCalories()<1000)) {
            System.out.println("All dishes are rather healthy");
        }
        if ( menu.stream().noneMatch(d->d.getCalories()>=1000)) {
            System.out.println("An other way to find out that all dishes are rather healthy");
        }

        Optional<Dish> optionalDishPresent = menu.stream()
                .filter(d->d.isVegetarian())
                .findAny();
        optionalDishPresent.ifPresent(System.out::println);
        if(optionalDishPresent.isPresent()) {
            System.out.println("OptionalDishPresent: Is present");
        } else {
            System.out.println("Not present");
        }
        System.out.println(optionalDishPresent.get());

        Optional<Dish> optionalDishNotPresent = menu.stream()
                .filter(d->d.isVegetarian() && d.getCalories()>1000)
                .findAny();
        optionalDishNotPresent.ifPresent(System.out::println); //Does not execute the code
        if(optionalDishNotPresent.isPresent()) {
            System.out.println("Is present");
        } else {
            System.out.println("optionalDishNotPresent: Not present");
        }

        try {
            System.out.println(optionalDishNotPresent.get());
        } catch (Exception NoSuchElementException) {
            System.out.println("No such element exception caught");
        }

    }

    private void makePairs() {
        List<Integer> numbers1 = Arrays.asList(1,2,3);
        List<Integer> numbers2 = Arrays.asList(3,4);

        Stream<int[]> pairStream = numbers1.stream()
                .flatMap(i->numbers2.stream()
                            .map(j->new int[]{i,j}));

        List<int[]> pairs = pairStream.collect(Collectors.toList());

        pairs.stream().forEach(i -> System.out.println("Pair " + i[0] + "," + i[1]));
        System.out.println("pairs: " + pairs.size());

        //Extended example:
        System.out.println("Extended example. Finding all sums that are 6 - not what I want... Redo...");
        pairs.stream()
                .map(i-> (i[0]+i[1]))
                .filter(i->i%3==0)
                .forEach(System.out::println);

        List<int[]> pairs2 = numbers1.stream()
                .flatMap(i->
                        numbers2.stream()
                                .filter(j->(i+j)%3==0)
                                .map(j->new int[]{i,j}))
                .collect(Collectors.toList());
        pairs2.stream().forEach(i->System.out.println("Pair sums dividable by 3: " + i[0] + "," + i[1]));

    }

    public void findUniqueCharacters() {
        //Ex 1
        String[] arrayOfWords = {"Goodbye", "World"};
        Stream<String> streamOfWords = Arrays.stream(arrayOfWords);

        List<String> res = streamOfWords
                .map(word->word.split(""))
                .flatMap(Arrays::stream)
                .distinct()
                .collect(Collectors.toList());

        System.out.println("Unique characters of all words: " + res);

        //Ex 2
        String s = "Goodbye";
        System.out.println("The word is: " +  s);

        String[] sArray = new String[]{s};
        System.out.println("First/only string in sArray: " + sArray[0]);
        Stream<String> wordAsStream = Arrays.stream(sArray);
        List<String> strippedWord = wordAsStream
                .map(w->w.split(""))
                .flatMap(Arrays::stream)
                .distinct()
                .collect(Collectors.toList());

        System.out.println("strippedWord: " + strippedWord + " contains " + strippedWord.size() + " characters");
    }
}
