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

import lombok.experimental.UtilityClass;
import net.ranides.assira.collection.arrays.NativeArrayBuilder;
import net.ranides.assira.collection.iterators.IterableUtils;
import net.ranides.assira.collection.iterators.IteratorUtils;
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.Consumers;
import net.ranides.assira.functional.Predicates.EachPredicate;
import net.ranides.assira.functional.special.Fold;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.text.StringUtils;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
 * This is internal utility class not supposed to use by clients.
 * It has default implementations of query operations using only Iterable contract.
 */
@UtilityClass
@SuppressWarnings("MissingJavadoc")
public class BaseIterable {

    public static <T> Spliterator<T> spliterator(CQuery<T> that) {
        if(that.features().hasFastLength()) {
            return Spliterators.spliterator(that.iterator(), that.size(), 0);
        }
        return Spliterators.spliteratorUnknownSize(that.iterator(), 0);
    }

    public static <T> Stream<T> stream(CQuery<T> that) {
        return StreamSupport.stream(spliterator(that), false);
    }

    public static <T> int size(CQuery<T> that) {
        return IteratorUtils.size(that.iterator());
    }

    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();
        for (T value : that) {
            acc.accept(out, value);
        }
        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());
            int i = 0;
            for (T v : that) {
                out[i++] = (A) v;
            }
            return out;
        } else {
            NativeArrayBuilder array = new NativeArrayBuilder(generator, 8);
            for (T v : that) {
                array.value(v);
            }
            return array.toArray();
        }
    }

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

    public static <T> String join(CQuery<T> that) {
        return StringUtils.join(that, "");
    }

    public static <T> String join(CQuery<T> that, String joiner) {
        return StringUtils.join(that, joiner);
    }

    public static <T, K> Map<K, T> group(CQuery<T> that, Function<? super T, ? extends K> key) {
        Map<K,T> map = new OpenMap<>();
        for(T v : that) {
            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<>();
        for(T v : that) {
            map.put(key.apply(v), value.apply(v));
        }
        return map;
    }

    public static <T> boolean isEmpty(CQuery<T> that) {
        return !that.iterator().hasNext();
    }

    public static <T> Ref<T> single(CQuery<T> that) {
        Iterator<T> i = that.iterator();
        if(!i.hasNext()) {
            return Ref.empty(that.message());
        }
        T out = i.next();
        if(i.hasNext()) {
            return Ref.empty(that.message());
        }
        return Ref.ofNullable(out, that.message());
    }

    public static <T, X extends Throwable> Ref<T> single(CQuery<T> that, Supplier<? extends X> ambigous) throws X {
        Iterator<T> i = that.iterator();
        if(!i.hasNext()) {
            return Ref.empty(that.message());
        }
        T out = i.next();
        if(i.hasNext()) {
            throw ambigous.get();
        }
        return Ref.ofNullable(out, that.message());
    }

    public static <T,R> Iterator<R> mapOptional(CQuery<T> that, Function<? super T, Ref<R>> f) {
        Iterator<Ref<R>> a = IteratorUtils.map(that.iterator(), f);
        Iterator<Ref<R>> b = IteratorUtils.filter(a, Ref::isPresent);
        return IteratorUtils.withCause(IteratorUtils.map(b, Ref::get), that.message());
    }

    public static <T> boolean matchAny(CQuery<T> that, Predicate<? super T> predicate) {
        for (T t : that) {
            if(predicate.test(t)) {
                return true;
            }
        }
        return false;
    }

    public static <T> void forEach(Iterable<T> that, Consumer<? super T> consumer) {
        for (T t : that) {
            consumer.accept(t);
        }
    }

    public static <T> void forEach(Iterable<T> that, Consumers.EachConsumer<? super T> consumer) {
        int index = 0;
        for (T t : that) {
            consumer.accept(index++, t);
        }
    }

    public static <T> boolean whileEach(Iterable<T> that, Predicate<? super T> consumer) {
        for (T t : that) {
            if( !consumer.test(t)) {
                return false;
            }
        }
        return true;
    }

    public static <T> boolean whileEach(Iterable<T> that, EachPredicate<? super T> consumer) {
        int index = 0;
        for (T t : that) {
            if( !consumer.test(index++, t)) {
                return false;
            }
        }
        return true;
    }

    public static <T> T reduce(CQuery<T> that, T identity, BinaryOperator<T> accumulator) {
        return Fold.apply(that.iterator(), identity, accumulator);
    }

    public static <T> T reduce(CQuery<T> that, BinaryOperator<T> accumulator) {
        Iterator<T> i = that.iterator();
        return i.hasNext() ? Fold.apply(i, accumulator) : null;
    }

    public static <T> Ref<T> first(CQuery<T> that) {
        return IterableUtils.first(that).withMessage(that.message());
    }

    public static <T> Ref<T> last(CQuery<T> that) {
        return IterableUtils.last(that).withMessage(that.message());
    }

    public static <T> int indexOf(CQuery<T> that, EachPredicate<? super T> predicate) {
        Iterator<T> itr = that.iterator();

        for (int i = 0; itr.hasNext(); i++) {
            if(predicate.test(i, itr.next())) {
                return i;
            }
        }
        return -1;
    }

    public static <T> int lastIndexOf(CQuery<T> that, EachPredicate<? super T> predicate) {
        Iterator<T> itr = that.iterator();
        int found = -1;

        for (int i = 0; itr.hasNext(); i++) {
            if(predicate.test(i, itr.next())) {
                found = i;
            }
        }
        return found;
    }
}
