package com.googlecode.totallylazy.collections;

import com.googlecode.totallylazy.functions.Binary;
import com.googlecode.totallylazy.functions.Function1;
import com.googlecode.totallylazy.functions.Monoid;
import com.googlecode.totallylazy.functions.Reducer;
import com.googlecode.totallylazy.Sequence;
import com.googlecode.totallylazy.Strings;
import com.googlecode.totallylazy.functions.Unary;
import com.googlecode.totallylazy.numbers.Integers;
import org.junit.Test;

import java.util.Map;

import static com.googlecode.totallylazy.Assert.assertThat;
import static com.googlecode.totallylazy.collections.SelectionTest.Concat.concat;
import static com.googlecode.totallylazy.predicates.Predicates.is;
import static com.googlecode.totallylazy.predicates.Predicates.where;
import static com.googlecode.totallylazy.Sequences.one;
import static com.googlecode.totallylazy.Sequences.sequence;
import static com.googlecode.totallylazy.Unchecked.cast;
import static com.googlecode.totallylazy.collections.PersistentMap.constructors.map;

public class SelectionTest {
    Keyword<String> name = Keyword.keyword();
    Keyword<Integer> age = Keyword.keyword();

    PersistentMap<String, Object> dan = map(name.key(), "Dan", age.key(), 21);
    PersistentMap<String, Object> matt = map(name.key(), "Matt", age.key(), 22);
    PersistentMap<String, Object> bob = map(name.key(), "Bob", age.key(), 22);
    Sequence<PersistentMap<String, Object>> data = sequence(dan, matt, bob);


    @Test
    public void canFilter() throws Exception {
        assertThat(data.filter(where(name, is("Dan"))), is(one(dan)));
    }

    @Test
    public void canSelect() throws Exception {
        assertThat(data.map(name), is(sequence("Dan", "Matt", "Bob")));
        assertThat(data.map(select(name)), is(sequence(map("name", "Dan"), map("name", "Matt"), map("name", "Bob"))));
    }

    @Test
    public void canReduce() throws Exception {
        Aggregate<String, String> minimumName = Aggregate.aggregate(name, Strings.minimum);
        Aggregate<Integer, Integer> minimumAge = Aggregate.aggregate(age, Integers.minimum());
        assertThat(data.reduce(minimumAge), is(21));
        assertThat(data.reduce(minimumName), is("Bob"));
        assertThat(data.reduce(select(minimumName, minimumAge)), is(map("name", "Bob", "age", 21)));
    }

    @Test
    public void supportsConcatination() throws Exception {
        Keyword<String> composite = composite(concat, name, age);
        assertThat(data.filter(where(name, is("Dan"))).map(composite), is(sequence("Dan21")));
        assertThat(data.filter(where(name, is("Dan"))).map(select(composite)), is(one(map(composite.key(), "Dan21"))));
    }

    @Test
    public void supportsGroupByAndConcatication() throws Exception {
        Aggregate<String, String> join = Aggregate.aggregate(name, concat);
        assertThat(data.groupBy(age).map(group -> group.reduce(join)), is(sequence("Dan", "MattBob")));
    }

    @Test
    public void supportsUppercase() throws Exception {
        Keyword<String> upperCase = compose(name, String::toUpperCase);
        assertThat(data.filter(where(name, is("Dan"))).map(upperCase), is(sequence("DAN")));
        assertThat(data.filter(where(name, is("Dan"))).map(select(upperCase)), is(one(map(upperCase.key(), "DAN"))));
    }

    private <T, R> Keyword<R> compose(Keyword<T> keyword, Function1<T, R> function) {
        return new Keyword<R>() {
            @Override
            public String key() {
                return function.toString() + "(" + keyword.key() + ")";
            }

            @Override
            public Class<R> forClass() {
                return null;
            }

            @Override
            public R call(Map<String, Object> map) throws Exception {
                return function.call(keyword.call(map));
            }
        };
    }

    @SafeVarargs
    public final <T, R> Keyword<R> composite(Reducer<? super T, R> reducer, Keyword<? extends T>... functions) {
        return new Keyword<R>() {
            Sequence<Keyword<? extends T>> keywords = sequence(functions);

            @Override
            public Class<R> forClass() {
                return null;
            }

            @Override
            public String key() {
                return keywords.map(Keyword::key).toString(reducer.toString() + "(", ",", ")");
            }

            @Override
            public R call(Map<String, Object> map) throws Exception {
                return keywords.fold(reducer.identity(), (a, k) -> reducer.call(a, k.apply(map)));
            }
        };
    }

    private Projection<PersistentMap<String, Object>> select(Selection... selections) {
        return Projection.projection(map(), (seed, row) ->
                sequence(selections).fold(seed, (accumulator, selection) -> selection.select(row, accumulator)));
    }

    interface Projection<T> extends Monoid<T>, Unary<T> {
        @Override
        default T call(T t) throws Exception {
            return call(identity(), t);
        }

        static <T> Projection<T> projection(T identity, Binary<T> binary) {
            return new Projection<T>() {
                @Override
                public T call(T t, T t2) throws Exception {
                    return binary.call(t, t2);
                }

                @Override
                public T identity() {
                    return identity;
                }
            };
        }
    }

    enum Concat implements Reducer<Object, String> {
        concat;

        @Override
        public String call(String s, Object o) throws Exception {
            return s + o;
        }

        @Override
        public String identity() {
            return "";
        }
    }

    interface Aggregate<T, R> extends Reducer<PersistentMap<String, Object>, R>, Selection {
        Keyword<? extends T> keyword();

        Reducer<? super T, R> reducer();

        static <T, R> Aggregate<T, R> aggregate(Keyword<? extends T> keyword, Reducer<? super T, R> reducer) {
            return new Aggregate<T, R>() {
                @Override
                public Keyword<? extends T> keyword() {
                    return keyword;
                }

                @Override
                public Reducer<? super T, R> reducer() {
                    return reducer;
                }
            };
        }

        @Override
        default R call(R seed, PersistentMap<String, Object> data) throws Exception {
            return reducer().call(seed, keyword().call(data));
        }

        @Override
        default R identity() {
            return reducer().identity();
        }

        @Override
        default PersistentMap<String, Object> select(PersistentMap<String, Object> source, PersistentMap<String, Object> destination) {
            T sourceValue = keyword().apply(source);
            R value = sourceValue == null ? identity() : cast(sourceValue);
            R reduced = apply(value, destination);
            return destination.insert(keyword().key(), reduced);
        }
    }
}