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

import lombok.experimental.UtilityClass;
import net.ranides.assira.collection.arrays.ArrayUtils;
import net.ranides.assira.collection.iterators.IteratorUtils;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.collection.query.base.CQAbstractFilter;
import net.ranides.assira.system.RuntimeConfiguration;

import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;

/**
 * Utility class which implements {@code flat} operation of all kinds of queries.
 *
 * Please note, that flat streams does not returns almost any fast features,
 * because flat operation is almost always slow.
 *
 * Streams created by BaseFlat implement all operations: stream, iterator, each and list,
 * altough none of them is marked as "fast".
 *
 * We implement 6 versions because we have 6 flattening functions:
 * query, iterable, array, both conditional and unconditional.
 */
@UtilityClass
public class BaseFlat {

    /**
     * Creates new flat stream which tries to apply flattening operation to every core method:
     * stream, iterator, each, list.
     *
     * @param that that
     * @param f f
     * @param <T> T
     * @param <R> R
     * @return stream
     */
    public static <T, R> CQuery<R> flat(CQuery<T> that, Function<? super T, CQuery<R>> f) {
        class CQFlat extends CQAbstractSplit<T,R> {
            public CQFlat(CQuery<T> source) {
                super(source);
            }

            @Override
            public CQuery<R> parallel() {
                return new CQFlat(source.parallel());
            }

            @Override
            public CQuery<R> sequential() {
                return new CQFlat(source.sequential());
            }

            @Override
            public Stream<R> stream() {
                if (RuntimeConfiguration.FIX_FLAT_STREAM()) {
                    return BaseIterable.stream(this);
                }
                return source.stream().flatMap(s -> f.apply(s).stream());
            }

            @Override
            public Iterator<R> iterator() {
                return IteratorUtils.flat(IteratorUtils.map(source.iterator(), s -> f.apply(s).iterator()));
            }

            @Override
            public boolean whileEach(Predicate<? super R> consumer) {
                return source.whileEach(s -> f.apply(s).whileEach(consumer));
            }

            @Override
            public List<R> list() {
                return BaseStream.list(this);
            }
        };
        return new CQFlat(that);
    }

    /**
     * Creates new flat stream which tries to apply flattening operation to every core method:
     * stream, iterator, each, list.
     *
     * @param that that
     * @param f f
     * @param <T> T
     * @param <R> R
     * @return stream
     */
    public static <T,R> CQuery<R> flatIterable(CQuery<T> that, Function<? super T, Iterable<R>> f) {
        class CQFlatIterable extends CQAbstractSplit<T,R> {
            public CQFlatIterable(CQuery<T> source) {
                super(source);
            }

            @Override
            public CQuery<R> parallel() {
                return new CQFlatIterable(source.parallel());
            }

            @Override
            public CQuery<R> sequential() {
                return new CQFlatIterable(source.sequential());
            }

            @Override
            public Stream<R> stream() {
                if (RuntimeConfiguration.FIX_FLAT_STREAM()) {
                    return BaseIterable.stream(this);
                }
                return source.stream().flatMap(s -> IteratorUtils.stream(f.apply(s).iterator()));
            }

            @Override
            public Iterator<R> iterator() {
                return IteratorUtils.flat(IteratorUtils.map(source.iterator(), s -> f.apply(s).iterator()));
            }

            @Override
            public boolean whileEach(Predicate<? super R> consumer) {
                return source.whileEach(s -> BaseIterable.whileEach(f.apply(s), consumer));
            }

        };
        return new CQFlatIterable(that);
    }

    /**
     * Creates new flat stream which tries to apply flattening operation to every core method:
     * stream, iterator, each, list.
     *
     * @param that that
     * @param f f
     * @param <T> T
     * @param <R> R
     * @return stream
     */
    public static <T,R> CQuery<R> flatArray(CQuery<T> that, Function<? super T, R[]> f) {
        class CQFlatArray extends CQAbstractSplit<T,R> {
            public CQFlatArray(CQuery<T> source) {
                super(source);
            }

            @Override
            public CQuery<R> parallel() {
                return new CQFlatArray(source.parallel());
            }

            @Override
            public CQuery<R> sequential() {
                return new CQFlatArray(source.sequential());
            }

            @Override
            public Stream<R> stream() {
                if (RuntimeConfiguration.FIX_FLAT_STREAM()) {
                    return BaseIterable.stream(this);
                }
                return source.stream().flatMap(s -> Stream.of(f.apply(s)));
            }

            @Override
            public Iterator<R> iterator() {
                return IteratorUtils.flat(IteratorUtils.map(source.iterator(), s -> ArrayUtils.iterator(f.apply(s))));
            }

            @Override
            public boolean whileEach(Predicate<? super R> consumer) {
                boolean parallel = source.features().isParallel();
                return source.whileEach(s -> BaseArray.whileEach(f.apply(s), parallel, consumer));
            }

        };

        return new CQFlatArray(that);
    }

    /**
     * Creates new flat stream which tries to apply conditional flattening operation to every core method:
     * stream, iterator, each, list.
     *
     * @param that that
     * @param p p
     * @param f f
     * @return stream
     * @param <T> T
     */
    public static <T> CQuery<T> flatIf(CQuery<T> that, Predicate<? super T> p, Function<? super T, CQuery<T>> f) {
        class CQFlatIf extends CQAbstractSplit<T,T> {
            public CQFlatIf(CQuery<T> source) {
                super(source);
            }

            @Override
            public CQuery<T> parallel() {
                return new CQFlatIf(source.parallel());
            }

            @Override
            public CQuery<T> sequential() {
                return new CQFlatIf(source.sequential());
            }

            @Override
            public Stream<T> stream() {
                if (RuntimeConfiguration.FIX_FLAT_STREAM()) {
                    return BaseIterable.stream(this);
                }
                return source.stream().flatMap(s -> p.test(s) ? f.apply(s).stream() : Stream.of(s));
            }

            @Override
            public Iterator<T> iterator() {
                return IteratorUtils.flatIf(source.iterator(), p, item -> f.apply(item).iterator());
            }

            @Override
            public boolean whileEach(Predicate<? super T> consumer) {
                return source.whileEach(s -> {
                    return p.test(s) ? f.apply(s).whileEach(consumer) : consumer.test(s);
                });
            }
        };

        return new CQFlatIf(that);
    }

    /**
     * Creates new flat stream which tries to apply conditional flattening operation to every core method:
     * stream, iterator, each, list.
     *
     * @param that that
     * @param p p
     * @param f f
     * @return stream
     * @param <T> T
     */
    public static <T> CQuery<T> flatIterableIf(CQuery<T> that, Predicate<? super T> p, Function<? super T, Iterable<T>> f) {
        class CQFlatIterableIf extends CQAbstractSplit<T,T> {
            public CQFlatIterableIf(CQuery<T> source) {
                super(source);
            }

            @Override
            public CQuery<T> parallel() {
                return new CQFlatIterableIf(source.parallel());
            }

            @Override
            public CQuery<T> sequential() {
                return new CQFlatIterableIf(source.sequential());
            }

            @Override
            public Stream<T> stream() {
                if (RuntimeConfiguration.FIX_FLAT_STREAM()) {
                    return BaseIterable.stream(this);
                }
                return source.stream().flatMap(s -> {
                    return p.test(s) ? IteratorUtils.stream(f.apply(s).iterator()) : Stream.of(s);
                });
            }

            @Override
            public Iterator<T> iterator() {
                return IteratorUtils.flatIf(source.iterator(), p, item -> f.apply(item).iterator());
            }

            @Override
            public boolean whileEach(Predicate<? super T> consumer) {
                return source.whileEach(s -> {
                    return p.test(s) ? BaseIterable.whileEach(f.apply(s), consumer) : consumer.test(s);
                });
            }

        };

        return new CQFlatIterableIf(that);
    }

    /**
     * Creates new flat stream which tries to apply conditional flattening operation to every core method:
     * stream, iterator, each, list.
     *
     * @param that that
     * @param p p
     * @param f f
     * @return stream
     * @param <T> T
     */
    public static <T> CQuery<T> flatArrayIf(CQuery<T> that, Predicate<? super T> p, Function<? super T, T[]> f) {
        class CQFlatArrayIf extends CQAbstractSplit<T,T> {
            public CQFlatArrayIf(CQuery<T> source) {
                super(source);
            }

            @Override
            public CQuery<T> parallel() {
                return new CQFlatArrayIf(source.parallel());
            }

            @Override
            public CQuery<T> sequential() {
                return new CQFlatArrayIf(source.sequential());
            }

            @Override
            public Stream<T> stream() {
                if (RuntimeConfiguration.FIX_FLAT_STREAM()) {
                    return BaseIterable.stream(this);
                }
                return source.stream().flatMap(s -> {
                    return p.test(s) ? Stream.of(f.apply(s)) : Stream.of(s);
                });
            }

            @Override
            public Iterator<T> iterator() {
                return IteratorUtils.flat(IteratorUtils.map(source.iterator(), s -> {
                    return p.test(s) ? ArrayUtils.iterator(f.apply(s)) : IteratorUtils.singletonIterator(s);
                }));
            }

            @Override
            public boolean whileEach(Predicate<? super T> consumer) {
                boolean parallel = source.features().isParallel();
                return source.whileEach(s -> {
                    return p.test(s) ? BaseArray.whileEach(f.apply(s), parallel, consumer) : consumer.test(s);
                });
            }

        };

        return new CQFlatArrayIf(that);
    }

    public static <T> CQuery<T> deepFlatIf(CQuery<T> that, Predicate<? super T> p, Function<? super T, CQuery<T>> f) {
        return CQuery.from().iterable(() -> IteratorUtils.deepFlatIf(that.iterator(), p, item -> f.apply(item).iterator()));
    }

    public static <T> CQuery<T> deepFlatIterableIf(CQuery<T> that, Predicate<? super T> p, Function<? super T, Iterable<T>> f) {
        return CQuery.from().iterable(() -> IteratorUtils.deepFlatIf(that.iterator(), p, item -> f.apply(item).iterator()));
    }

    public static <T> CQuery<T> deepFlatArrayIf(CQuery<T> that, Predicate<? super T> p, Function<? super T, T[]> f) {
        return CQuery.from().iterable(() -> IteratorUtils.deepFlatIf(that.iterator(), p, item -> ArrayUtils.iterator(f.apply(item))));
    }

    public static <T> CQuery<T> treeFlatIf(CQuery<T> that, Predicate<? super T> p, Function<? super T, CQuery<T>> f) {
        return CQuery.from().iterable(() -> IteratorUtils.treeFlatIf(that.iterator(), p, item -> f.apply(item).iterator()));
    }

    public static <T> CQuery<T> treeFlatIterableIf(CQuery<T> that, Predicate<? super T> p, Function<? super T, Iterable<T>> f) {
        return CQuery.from().iterable(() -> IteratorUtils.treeFlatIf(that.iterator(), p, item -> f.apply(item).iterator()));
    }

    public static <T> CQuery<T> treeFlatArrayIf(CQuery<T> that, Predicate<? super T> p, Function<? super T, T[]> f) {
        return CQuery.from().iterable(() -> IteratorUtils.treeFlatIf(that.iterator(), p, item -> ArrayUtils.iterator(f.apply(item))));
    }

    private static abstract class CQAbstractSplit<T,R> extends CQAbstractFilter<T,R> {

        public CQAbstractSplit(CQuery<T> source) {
            super(source);
        }

        @Override
        public boolean hasFastStream() {
            return !RuntimeConfiguration.FIX_FLAT_STREAM() && source.features().hasFastStream();
        }
    }

}
