package tc.oc.commons.core.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.function.BiFunction;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;

import com.google.common.collect.Lists;
import tc.oc.commons.core.IterableUtils;

public final class Streams {
    private Streams() {}

    private static final Consumer NO_OP = o -> {};
    public static void consume(Stream<?> stream) {
        stream.forEach(NO_OP);
    }

    public static <T> Stream<T> reverseStream(Iterable<T> iterable) {
        return Lists.reverse(Lists.newArrayList(iterable)).stream();
    }

    public static <T, E extends Throwable> void forEachThrows(Stream<T> stream, ThrowingConsumer<? super T, E> consumer) throws E {
        for(final Iterator<T> it = stream.iterator(); it.hasNext();) {
            consumer.acceptThrows(it.next());
        }
    }

    public static <T> void forEachWithIndex(Stream<T> stream, IndexedConsumer<T> consumer) {
        final Counter index = new Counter();
        stream.forEachOrdered(t -> consumer.accept(t, index.next()));
    }

    public static <T> Stream<T> reverse(Stream<T> stream) {
        return Lists.reverse(stream.collect(Collectors.toCollection(ArrayList::new))).stream();
    }

    public static <T> void reverseForEach(Stream<T> stream, Consumer<? super T> consumer) {
        reverse(stream).forEach(consumer);
    }

    public static <T, E extends Throwable> void reverseForEachThrows(Stream<T> stream, ThrowingConsumer<? super T, E> consumer) throws E {
        forEachThrows(reverse(stream), consumer);
    }

    public static <T> Stream<T> shuffle(Stream<T> stream, Random random) {
        final List<T> list = stream.collect(Collectors.toCollection(ArrayList::new));
        Collections.shuffle(list, random);
        return list.stream();
    }

    public static <T> Stream<T> shuffle(Stream<T> stream) {
        final List<T> list = stream.collect(Collectors.toCollection(ArrayList::new));
        Collections.shuffle(list);
        return list.stream();
    }

    public static <T> Stream<T> flatten(Stream<Stream<? extends T>> streams) {
        return (Stream<T>) streams.reduce(Stream::concat)
                                  .orElse(Stream.of());
    }

    public static <T> Stream<T> concat(Stream<? extends T>... streams) {
        return flatten(Stream.of(streams));
    }

    public static <T> Stream<T> instancesOf(Stream<?> stream, Class<T> type) {
        return (Stream<T>) stream.filter(type::isInstance);
    }

    public static <T> Stream<Class<? extends T>> subtypesOf(Stream<? extends Class<?>> stream, Class<T> type) {
        return (Stream) stream.filter(type::isAssignableFrom);
    }

    public static <T> boolean any(Stream<T> stream, Predicate<? super T> predicate) {
        return stream.filter(predicate).findAny().isPresent();
    }

    public static <T> boolean all(Stream<T> stream, Predicate<? super T> predicate) {
        return any(stream, predicate.negate());
    }

    public static <T> boolean none(Stream<T> stream, Predicate<? super T> predicate) {
        return !any(stream, predicate);
    }

    public static <T> Stream<T> copyOf(Stream<T> stream) {
        return stream.collect(Collectors.toList()).stream();
    }

    public static <T> Stream<T> copyOf(Collection<T> collection) {
        return IterableUtils.copyOf(collection).stream();
    }

    public static <T> Stream<T> append(Stream<T> stream, T... elements) {
        return Stream.concat(stream, Stream.of(elements));
    }

    public static <T> Stream<T> prepend(Stream<T> stream, T... elements) {
        return Stream.concat(Stream.of(elements), stream);
    }

    public static <T> Stream<T> remove(Stream<T> stream, T... elements) {
        return stream.filter(t -> !ArrayUtils.contains(elements, t));
    }

    public static <T> Stream<T> of(Iterator<T> iterator) {
        return StreamSupport.stream(Spliterators.spliterator(iterator, 0, Spliterator.ORDERED), false);
    }

    public static <T> Stream<T> of(Iterable<T> iterable) {
        if(iterable instanceof Collection) {
            return ((Collection<T>) iterable).stream();
        } else {
            return StreamSupport.stream(iterable.spliterator(), false);
        }
    }

    public static <T> Stream<T> ofNullable(@Nullable T t) {
        return t == null ? Stream.empty() : Stream.of(t);
    }

    public static <T> Stream<T> ofNullables(Stream<T> s) {
        return s.filter(t -> t != null);
    }

    public static <T> Stream<T> ofNullables(T... things) {
        return ofNullables(Stream.of(things));
    }

    public static <T> Stream<T> conditional(boolean condition, Stream<T> stream) {
        return condition ? stream : Stream.empty();
    }

    public static <T> Stream<T> conditional(boolean condition, T element) {
        return condition ? Stream.of(element) : Stream.empty();
    }

    public static <T> Stream<T> conditional(boolean condition, Supplier<? extends T> element) {
        return condition ? Stream.of(element.get()) : Stream.empty();
    }

    public static <T> Stream<T> compact(Stream<Optional<T>> stream) {
        return stream.filter(Optional::isPresent).map(Optional::get);
    }

    public static <T> Stream<T> compact(Optional<T>... elements) {
        return compact(Stream.of(elements));
    }

    public static <T> Stream<T> compact1(T t1, Optional<T>... elements) {
        return Stream.concat(Stream.of(t1), compact(Stream.of(elements)));
    }

    public static <T> Stream<T> compact2(T t1, T t2, Optional<T>... elements) {
        return Stream.concat(Stream.of(t1, t2), compact(Stream.of(elements)));
    }

    public static <T> Stream<T> compact3(T t1, T t2, T t3, Optional<T>... elements) {
        return Stream.concat(Stream.of(t1, t2, t3), compact(Stream.of(elements)));
    }

    public static <T, R> R reduce(Stream<T> stream, R identity, BiFunction<R, T, R> accumulator) {
        class Result { R v; }
        Result result = new Result();
        result.v = identity;
        stream.forEachOrdered(t -> result.v = accumulator.apply(result.v, t));
        return result.v;
    }

    /**
     * Test if all elements of the stream are equal to each other, according to {@link Objects#equals(Object, Object)}.
     * Returns true for an empty stream, or a stream of all nulls.
     */
    public static <T> boolean isUniform(Stream<T> stream) {
        return stream.reduce(Uniformity.EMPTY, Uniformity::add, Uniformity::combine).isUniform();
    }

    /**
     * This is surprisingly complex, but it is the simplest approach I could come up
     * with that is actually in the spirit of streams. This is actually quite efficient,
     * creating at most one temporary object per reduction thread.
     */
    private interface Uniformity<T> {
        boolean isUniform();
        Uniformity<T> add(T t);
        Uniformity<T> combine(Uniformity<T> that);

        // Initial (empty) result
        Uniformity EMPTY = new Uniformity() {
            @Override public boolean isUniform() { return true; }
            @Override public Uniformity add(Object t) { return new Intermediate(t); }
            @Override public Uniformity combine(Uniformity that) { return that; }
        };

        // Result after multiple distinct values have been seen
        Uniformity FAILED = new Uniformity() {
            @Override public boolean isUniform() { return false; }
            @Override public Uniformity add(Object t) { return this; }
            @Override public Uniformity combine(Uniformity that) { return this; }
        };

        // Result after one or more values have been seen, and they are all equal
        class Intermediate<T> implements Uniformity<T> {
            final T value;

            public Intermediate(T value) {
                this.value = value;
            }

            @Override public boolean isUniform() {
                return true;
            }

            @Override public Uniformity<T> add(T t) {
                return Objects.equals(value, t) ? this : FAILED;
            }

            @Override public Uniformity<T> combine(Uniformity<T> that) {
                if(that instanceof Intermediate) {
                    return add(((Intermediate<T>) that).value);
                } else {
                    return that.combine(this);
                }
            }
        }
    }
}
