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

import net.ranides.assira.collection.arrays.ArrayUtils;
import net.ranides.assira.collection.iterators.IterableUtils;
import net.ranides.assira.collection.iterators.IteratorUtils;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.collection.query.CQueryAbstract;
import net.ranides.assira.collection.query.support.BaseFeatures;
import net.ranides.assira.generic.ValueUtils;

import java.util.Arrays;
import java.util.Iterator;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;

/**
 * Composite query, collecting many queries inside.
 * It reports "feature" if all queries support it.
 * Used by {@link CQuery#append(CQuery)} operation.
 *
 * @param <T> T
 */
public class CQAppend<T> extends CQueryAbstract<T> {

    private final CQuery<T>[] array;

    private Supplier<? extends RuntimeException> message;

    private int flags;

    /**
     * Merges two queries into one composite.
     * It is public starting point used by clients.
     *
     * @param a a
     * @param b b
     */
    @SuppressWarnings("unchecked")
    public CQAppend(CQuery<T> a, CQuery<T> b) {
        this.array = new CQuery[]{a, b};
        this.flags = BaseFeatures.and(a, b);
        this.message = ValueUtils.or(a.message(), b.message());
    }

    /**
     * Merges two queries into one composite.
     * It is private starting point used by CQAppend to optimize CQAppend grow.
     *
     * @param a a
     * @param b b
     */
    public CQAppend(CQAppend<T> a, CQuery<T> b) {
        this.array = ArrayUtils.append(a.array, b);
        this.flags = BaseFeatures.and(a, b);
        this.message = ValueUtils.or(a.message(), b.message());
    }

    @Override
    public Supplier<? extends RuntimeException> message() {
        return message;
    }

    @Override
    public CQuery<T> message(Supplier<? extends RuntimeException> message) {
        this.message = message;
        return this;
    }

    @Override
    public boolean hasFastEach() {
        return (flags & EACH) != 0;
    }

    @Override
    public boolean hasFastStream() {
        return (flags & STREAM) != 0;
    }

    @Override
    public boolean hasFastIterator() {
        return (flags & ITERATOR) != 0;
    }

    @Override
    public boolean hasFastLength() {
        return (flags & LENGTH) != 0;
    }

    @Override
    public boolean hasFastList() {
        return false;
    }

    @Override
    public boolean isParallel() {
        return (flags & PARALLEL) != 0;
    }

    @Override
    public CQuery<T> parallel() {
        flags |= PARALLEL;
        for(CQuery<T> q : array) {
            q.parallel();
        }
        return this;
    }

    @Override
    public CQuery<T> sequential() {
        flags &= ~PARALLEL;
        for(CQuery<T> q : array) {
            q.sequential();
        }
        return this;
    }

    @Override
    public Stream<T> stream() {
        return arrayStream().flatMap(CQuery::stream);
    }

    @Override
    public Iterator<T> iterator() {
        return IteratorUtils.withCause(IterableUtils.flat(Arrays.asList(array)).iterator(), message);
    }

    @Override
    public int size() {
        return arrayStream().mapToInt(CQuery::size).sum();
    }

    @Override
    public CQuery<T> append(CQuery<T> next) {
        return new CQAppend<>(this, next);
    }

    @Override
    public void forEach(Consumer<? super T> consumer) {
        if(isParallel()) {
            arrayStream().forEach(q -> q.forEach(consumer));
        } else {
            for (CQuery<T> q : array) {
                q.forEach(consumer);
            }
        }
    }

    @Override
    public boolean whileEach(Predicate<? super T> consumer) {
        if(isParallel()) {
            return arrayStream().allMatch(q -> q.whileEach(consumer));
        } else {
            for (CQuery<T> q : array) {
                if (!q.whileEach(consumer)) {
                    return false;
                }
            }
        }
        return true;
    }

    private Stream<CQuery<T>> arrayStream() {
        if(isParallel()) {
            return Arrays.stream(array).parallel();
        } else {
            return Arrays.stream(array);
        }
    }

}
