/*
 * ###############################################################
 * Installation instructions:
 * 1. Create a fork of the MiniCP git repository:
 *    https://inginious.org/course/minicp-s/minicp-install-1
 *
 * 2. Make a local clone of the forked repository.
 *
 * 3. Set up your IDE (IntelliJ or Eclipse) for your cloned
 *    repository: https://minicp.readthedocs.io/en/latest/intro.html
 *
 * 4. Using the IDE you set up, create the file
 *    "LambdaExpressionTest.java" at "test/java/minicp/util/"
 *    in your cloned repository.
 *
 * 5. Paste the contents of this file into the created java file.
 *
 * ###############################################################
 */
/*
 * mini-cp is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License  v3
 * as published by the Free Software Foundation.
 *
 * mini-cp is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY.
 * See the GNU Lesser General Public License  for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with mini-cp. If not, see http://www.gnu.org/licenses/lgpl-3.0.en.html
 *
 * Copyright (c)  2018. by Laurent Michel, Pierre Schaus, Pascal Van Hentenryck
 *
 * These are some lambda warmup exercises for Combinatorial Optimisation and Constraint Programming (course 1DL441).
 *
 * Made by Frej Knutar Lewander 2021
 *
 */

package minicp.util;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.*;

import static org.junit.jupiter.api.Assertions.*;

public class LambdaExpressionTest {

    private List<Integer> integers;
    private List<String> strings;

    @BeforeEach
    public void setUp() {
        integers = Arrays.asList(9, -6, 4, -6, -5, 0, 9, 4, -3, 4);
        strings = Arrays.asList(
                "jYt4Tq5b", "13Jl4w", "CUdwAnxgVz", "", "v", "GGqHztUmw", "RNt59s9Z", "Reg", "QQyq", "dGrm7U1XHZ");
    }

    /**
     * Returns a list consisting of the elements of collection for which predicate holds.
     * @param collection The collection containing the elements that predicate is applied to.
     * @param predicate The predicate that is applied to each element in collection.
     * @return The filtered list.
     */
    private static <T> List<T> filter(Collection<T> collection, Predicate<T> predicate) {
        List<T> result = new ArrayList<>(collection.size());
        for (T element : collection) {
            if (predicate.test(element)) {
                result.add(element);
            }
        }
        return result;
    }

    /**
     * Returns the list consisting of the results of applying the given function to the elements of collection.
     * @param collection The collection containing the elements the function is applied to.
     * @param fun The function that is applied to each element in collection.
     * @return The new list.
     */
    private static <T, R> List<R> map(Collection<T> collection, Function<T, R> fun) {
        List<R> result = new ArrayList<>(collection.size());
        for (T element : collection) {
            result.add(fun.apply(element));
        }
        return result;
    }

    /**
     * Performs a reduction on the elements of collection, using the provided initial value and an associative
     * accumulation function, and returns the reduced value.
     * @param collection The collection containing the elements to be reduced.
     * @param initial The initial accumulated value
     * @param fun The accumulation function
     * @return The result of the reduction.
     */
    private static <T, R> R reduce(Collection<T> collection, R initial, BiFunction<R, T, R> fun) {
        R accumulator = initial;
        for (T element : collection) {
            accumulator = fun.apply(accumulator, element);
        }
        return accumulator;
    }

    @Test
    public void filterLambdas() {
        // Create a lambda that returns true if the input number is 0 or greater, and false otherwise.
        Predicate<Integer> isNonNegative = i -> i >= 0;

        List<Integer> filteredIntegers = filter(integers, isNonNegative);
        assertArrayEquals(new Integer[] { 9, 4, 0, 9, 4, 4 }, filteredIntegers.toArray(new Integer[0]));

        // Create a lambda that returns true if the input string contains the upper-case letter G, and false
        // otherwise.
        Predicate<String> containsG = s -> s.contains("G");

        List<String> filteredStrings = filter(strings, containsG);
        assertArrayEquals(new String[] { "GGqHztUmw", "dGrm7U1XHZ" }, filteredStrings.toArray(new String[0]));

    }

    @Test
    public void mapLambdas() {
        // Create a lambda that returns the square of the input integer.
        Function<Integer, Integer> square = i -> i*i;

        List<Integer> squaredIntegers = map(integers, square);
        assertArrayEquals(new Integer[] { 81, 36, 16, 36, 25, 0, 81, 16, 9, 16 },
                squaredIntegers.toArray(new Integer[0]));

        // Create a lambda that given a string s truncates all characters after the third character of s.
        // For example; the input "ab" returns "ab", while the input "cdef" returns "cde".
        Function<String, String> stringTruncator = s -> s.length() <= 3 ? s : s.substring(0, 3);

        List<String> truncatedStrings = map(strings, stringTruncator);
        assertArrayEquals(new String[] { "jYt", "13J", "CUd", "", "v", "GGq", "RNt", "Reg", "QQy", "dGr" },
                truncatedStrings.toArray(new String[0]));
    }

    @Test
    public void reduceLambdas() {
        // Create a lambda that sums over the inputs.
        BiFunction<Integer, Integer, Integer> sum = (accumulator, cur) -> accumulator + cur;
        assertEquals(10, (int) reduce(integers, 0, sum));

        // Create a lambda that returns the greatest value.
        BiFunction<Integer, Integer, Integer> max = (accumulator, cur) -> Math.max(accumulator, cur);
        assertEquals(9, (int) reduce(integers, Integer.MIN_VALUE, max));

        // Create a lambda that given the inputs accumulator and cur returns cur if accumulator is null, otherwise
        // concatenates accumulator and cur with a comma and a space (i.e., accumulator + ", " + cur).
        BiFunction<String, String, String> commaConcat = (accumulator, cur) -> accumulator == null ? cur : (accumulator + ", " + cur);
        assertEquals("jYt4Tq5b, 13Jl4w, CUdwAnxgVz, , v, GGqHztUmw, RNt59s9Z, Reg, QQyq, dGrm7U1XHZ",
                reduce(strings, null, commaConcat));

    }

    @Test
    public void combineLambdas() {
        // Combine filter, map, and reduce calls with lambdas you created earlier to
        // 1. Remove all negative integers,
        // 2. square the remaining integers, and finally
        // 3. converting and comma concatenating the squared integers.

        // Create any needed lambdas

        String result = reduce(
                map(
                        map(filter(integers, i -> i >= 0),
                                i -> i*i
                        ),
                        i -> Integer.toString(i)
                ),
                null,
                (accumulator, cur) -> accumulator == null ? cur : (accumulator + ", " + cur)
        );

        assertEquals("81, 16, 0, 81, 16, 16", result);
    }

    @Test
    public void difficultLambdas() {
        // Create a lambda that given an integer input n returns a truncation lambda function that given a string s
        // truncates all characters after the first n characters.
        Function<Integer, Function<String, String>> stringTruncatorFactory = i -> (s -> s.length() <= i ? s : s.substring(0, i));

        assertArrayEquals(new String[] { "jYt", "13J", "CUd", "", "v", "GGq", "RNt", "Reg", "QQy", "dGr" },
                map(strings, stringTruncatorFactory.apply(3)).toArray(new String[0]));

        assertArrayEquals(new String[] { "", "", "", "", "", "", "", "", "", "" },
                map(strings, stringTruncatorFactory.apply(0)).toArray(new String[0]));

        assertArrayEquals(new String[] { "jYt4Tq5b", "13Jl4w", "CUdwAnxgVz", "", "v", "GGqHztUmw", "RNt59s9Z", "Reg",
                "QQyq", "dGrm7U1XHZ" }, map(strings, stringTruncatorFactory.apply(10)).toArray(new String[0]));


        // Create a supplier that returns the next element in the integers List field.
        // Hint: use nextIndex to keep track of the index of the next element of integers that is to be returned.
        AtomicInteger nextIndex = new AtomicInteger(0);
        Supplier<Integer> nextInteger = () -> integers.get(nextIndex.getAndIncrement());

        for (int val : integers) {
            assertEquals(val, (int) nextInteger.get());
        }
    }
}