package net.ranides.assira.collection.query.support;

import lombok.experimental.UtilityClass;

import net.ranides.assira.collection.arrays.NativeArrayBuilder;
import net.ranides.assira.collection.maps.OpenMap;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.collection.sets.OpenSet;
import net.ranides.assira.functional.Predicates.EachPredicate;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.text.StrBuilder;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import java.util.stream.Collector;


/**
 * This is internal utility class not supposed to use by clients.
 * It has default implementations of query operations using "each" as first choice.
 */
@UtilityClass
@SuppressWarnings("MissingJavadoc")
public class BaseEach {

    public static <T> int size(CQuery<T> that) {
        BaseState.Index index = BaseState.newIndex(that);
        that.forEach(v -> index.next());
        return index.value();
    }

    public static <T> boolean isEmpty(CQuery<T> that) {
        BaseState.Terminator terminator = BaseState.newTerminator(that);
        that.whileEach(v -> {
            terminator.stop();
            return false;
        });
        return !terminator.stopped();
    }

    public static <T, A, R> R collect(CQuery<T> that, Collector<? super T, A, R> collector) {
        A out = collector.supplier().get();
        BiConsumer<A, ? super T> acc = collector.accumulator();
        that.forEach(v -> acc.accept(out, v));
        return collector.finisher().apply(out);
    }

    @SuppressWarnings("unchecked")
    public static <T, A> A[] array(CQuery<T> that, IntFunction<A[]> generator) {
        if (that.features().hasFastLength()) {
            A[] out = generator.apply(that.size());
            that.forEach((i, v) -> out[i] = (A) v);
            return out;
        } else {
            NativeArrayBuilder array = new NativeArrayBuilder(generator, 8);
            that.forEach(array::value);
            return array.toArray();
        }
    }

    public static <T> List<T> list(CQuery<T> that) {
        List<T> out = that.features().hasFastLength() ? new ArrayList<>(that.size()) : new ArrayList<>();
        that.forEach(v -> out.add(v));
        return out;
    }

    public static <T> Set<T> set(CQuery<T> that) {
        Set<T> out = that.features().hasFastLength() ? new OpenSet<>(that.size()) : new OpenSet<>();
        that.forEach(v -> out.add(v));
        return out;
    }

    public static <T> String join(CQuery<T> that) {
        StrBuilder out = new StrBuilder();
        that.forEach(v -> out.append(v));
        return out.toString();
    }

    public static <T> String join(CQuery<T> that, String joiner) {
        StrBuilder out = new StrBuilder();
        out.open("", "", joiner);
        that.forEach(v -> out.item().append(v));
        return out.toString();
    }

    public static <T, K> Map<K, T> group(CQuery<T> that, Function<? super T, ? extends K> key) {
        Map<K, T> map = new OpenMap<>();
        that.forEach(v -> {
            map.put(key.apply(v), v);
        });
        return map;
    }

    public static <T, K, V> Map<K, V> group(CQuery<T> that, Function<? super T, ? extends K> key, Function<? super T, ? extends V> value) {
        Map<K, V> map = new OpenMap<>();
        that.forEach(v -> {
            map.put(key.apply(v), value.apply(v));
        });
        return map;
    }

    public static <T> boolean matchAny(CQuery<T> that, Predicate<? super T> predicate) {
        BaseState.Terminator out = BaseState.newTerminator(that);
        that.whileEach(v -> {
            if(predicate.test(v)) {
                out.stop();
                return false;
            }
            return true;
        });
        return out.stopped();
    }

    public static <T> T reduce(CQuery<T> that, T identity, BinaryOperator<T> accumulator) {
        BaseState.Holder<T> out = BaseState.newHolder(that, identity);
        that.forEach(v -> {
            out.set(accumulator.apply(out.get(), v));
        });
        return out.get();
    }

    public static <T> T reduce(CQuery<T> that, BinaryOperator<T> accumulator) {
        BaseState.Holder<T> out = BaseState.newHolder(that);
        that.forEach(v -> {
            out.set(out.isEmpty() ? v : accumulator.apply(out.get(), v));
        });
        return out.get();
    }

    public static <T> Ref<T> first(CQuery<T> that) {
        BaseState.Holder<T> out = BaseState.newHolder(that);
        that.whileEach(v -> {
            out.set(v);
            return false;
        });
        return Ref.ofNullable(out.get(), that.message());
    }

    public static <T> Ref<T> last(CQuery<T> that) {
        BaseState.Holder<T> out = BaseState.newHolder(that);
        that.forEach(v -> {
            out.set(v);
        });
        return Ref.ofNullable(out.get());
    }

    public static <T> int indexOf(CQuery<T> that, EachPredicate<? super T> predicate) {
        BaseState.Holder<Integer> out = BaseState.newHolder(that, -1);
        that.whileEach((i,v) -> {
            if(predicate.test(i,v)) {
                out.set(i);
                return false;
            }
            return true;
        });
        return out.get();
    }

    public static <T> int lastIndexOf(CQuery<T> that, EachPredicate<? super T> predicate) {
        BaseState.Holder<Integer> out = BaseState.newHolder(that, -1);
        that.forEach((i,v) -> {
            if(predicate.test(i,v)) {
                out.set(i);
            }
        });
        return out.get();
    }
}
