/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.collection.query;

import net.ranides.assira.collection.HashFunction;
import net.ranides.assira.collection.iterators.IterableUtils;
import net.ranides.assira.collection.query.base.CQArray;
import net.ranides.assira.collection.query.base.CQList;
import net.ranides.assira.collection.query.derived.*;
import net.ranides.assira.collection.query.support.BaseState;
import net.ranides.assira.collection.query.support.BaseEach;
import net.ranides.assira.collection.query.support.BaseFeatures;
import net.ranides.assira.collection.query.support.BaseIterable;
import net.ranides.assira.collection.query.support.BaseList;
import net.ranides.assira.collection.query.support.BaseFlat;
import net.ranides.assira.collection.query.support.BaseStream;
import net.ranides.assira.collection.sets.CustomSet;
import net.ranides.assira.collection.sets.OpenSet;
import net.ranides.assira.functional.Consumers;
import net.ranides.assira.functional.FunctionUtils;
import net.ranides.assira.functional.Functions;
import net.ranides.assira.functional.PredicateUtils;
import net.ranides.assira.functional.Predicates;
import net.ranides.assira.generic.CompareUtils;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.generic.SerializationUtils;
import net.ranides.assira.generic.TypeToken;
import net.ranides.assira.generic.ValueUtils;
import net.ranides.assira.reflection.*;
import net.ranides.assira.reflection.util.ClassUtils;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.Spliterator;
import java.util.TreeSet;
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.Collectors;

/**
 * Base abstract class for all CQuery implementations.
 *
 * This class does not declare any native fast operations on their own,
 * but most implemented methods inspects features to select best strategy.
 *
 * @param <T> T
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public abstract class CQueryAbstract<T> implements CQuery<T>, CQueryFeatures {

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

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

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

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

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

    @Override
    public int flags() {
        return BaseFeatures.flags(features());
    }

    @Override
    public CQueryFeatures features() {
        return this;
    }

    @Override
    public CQuery<T> parallel(boolean enabled) {
        return enabled ? parallel() : sequential();
    }

    @Override
    public CQuery<T> sequential(boolean enabled) {
        return enabled ? sequential() : parallel();
    }

    @Override
    public Spliterator<T> spliterator() {
        if(hasFastStream()) {
            return BaseStream.spliterator(this);
        }
        return BaseIterable.spliterator(this);
    }

    @Override
    public <A, R> R collect(Collector<? super T, A, R> collector) {
        if(!hasFastStream()) {
            if(hasFastEach()) {
                return BaseEach.collect(this, collector);
            }
            if(hasFastIterator()) {
                return BaseIterable.collect(this, collector);
            }
        }
        return BaseStream.collect(this, collector);
    }

    @Override
    @SuppressWarnings("unchecked")
    public T[] array() {
        return (T[])array(Object[]::new);
    }

    @Override
    public <A> A[] array(IntFunction<A[]> generator) {
        if(!hasFastStream()) {
            if(hasFastEach()) {
                return BaseEach.array(this, generator);
            }
            if(hasFastIterator()) {
                return BaseIterable.array(this, generator);
            }
            if(hasFastList()) {
                return BaseList.array(this, generator);
            }
        }
        return BaseStream.array(this, generator);
    }

    @Override
    public List<T> list() {
        if(!hasFastStream()) {
            if(hasFastEach()) {
                return BaseEach.list(this);
            }
            if(hasFastIterator()) {
                return BaseIterable.list(this);
            }
        }
        return BaseStream.list(this);
    }

    @Override
    public Set<T> set() {
        if(hasFastList()) {
            return BaseList.set(this);
        }
        if(!hasFastStream()) {
            if(hasFastEach()) {
                return BaseEach.set(this);
            }
            if(hasFastIterator()) {
                return BaseIterable.set(this);
            }
        }
        return BaseStream.set(this);
    }

    @Override
    public final String join() {
        if(!hasFastStream()) {
            if(hasFastEach()) {
                return BaseEach.join(this);
            }
            if(hasFastIterator()) {
                return BaseIterable.join(this);
            }
        }
        return map(String::valueOf).collect(Collectors.joining());
    }

    @Override
    public final String join(String joiner) {
        if(!hasFastStream()) {
            if(hasFastEach()) {
                return BaseEach.join(this, joiner);
            }
            if(hasFastIterator()) {
                return BaseIterable.join(this, joiner);
            }
        }
        return map(String::valueOf).collect(Collectors.joining(joiner));
    }

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

    @Override
    public CQuery<T> fetch() {
        return new CQList<>(isParallel(), collect(Collectors.toList()));
    }

    @Override
    public CQuery<T> prefetch() {
        return new CQPrefetch<>(this, message());
    }

    @SuppressWarnings("unchecked")
    @Override
    public CQuery<T> cache(Path filename) throws IOException {
        Object[] data;
        if(Files.exists(filename)) {
            data = SerializationUtils.read(new TypeToken<T[]>() {}, filename);
        } else {
            data = array();
            SerializationUtils.write(filename, data);
        }
        return (CQArray<T>)new CQArray<>(data).parallel(isParallel());
    }

    @Override
    public CQuery<T> limit(Predicate<? super T> p) {
        return new CQLimit<>(this, p);
    }

    @Override
    public CQuery<T> limit(int n) {
        return slice(0, n);
    }

    @Override
    public CQuery<T> skip(int n) {
        return slice(n, Integer.MAX_VALUE);
    }

    @Override
    public CQuery<T> slice(int begin, int end) {
        return end<1 ? CQuery.empty() : new CQSlice<>(this, begin, end);
    }

    @Override
    public <K> Map<K, T> group(Function<? super T, ? extends K> key) {
        if(!hasFastStream()) {
            if(hasFastEach()) {
                return BaseEach.group(this, key);
            }
            if(hasFastIterator()) {
                return BaseIterable.group(this, key);
            }
        }
        return BaseStream.group(this, key);
    }

    @Override
    public <K, V> Map<K, V> group(Function<? super T, ? extends K> key, Function<? super T, V> value) {
        if(!hasFastStream()) {
            if(hasFastEach()) {
                return BaseEach.group(this, key, value);
            }
            if(hasFastIterator()) {
                return BaseIterable.group(this, key, value);
            }
        }
        return BaseStream.group(this, key, value);
    }

    @Override
    public boolean isEmpty() {
        if(hasFastLength()) {
            return size() == 0;
        }
        if(hasFastEach()) {
            return BaseEach.isEmpty(this);
        }
        return BaseIterable.isEmpty(this);
    }

    @Override
    public CQuery<T> filter(Predicate<? super T> p) {
        return new CQFilter<>(this, p);
    }

    @Override
    public CQuery<T> filterEach(Predicates.EachPredicate<? super T> p) {
        return new CQFilterEach<>(this, p);
    }

    @Override
    public <R> CQuery<R> filter(Class<R> p) {
        Class<R> type = ClassUtils.box(p);
        return filter(type::isInstance).map(type::cast);
    }

    @Override
    public <R> CQuery<R> filter(IClass<R> p) {
        return filter(p::isInstance).map(p::cast);
    }

    @Override
    public final CQuery<T> discard(Predicate<? super T> p) {
        return filter(p.negate());
    }

    @Override
    public final CQuery<T> discardEach(Predicates.EachPredicate<? super T> p) {
        return filterEach(p.negate());
    }

    @Override
    public final CQuery<T> discard(Class<?> p) {
        return discard(p::isInstance);
    }

    @Override
    public final CQuery<T> discard(IClass<?> p) {
        return discard(p::isInstance);
    }

    @Override
    public <R> CQuery<R> map(Function<? super T, R> f) {
        return new CQMap<>(this, f);
    }

    @Override
    public <R> CQuery<R> map(Class<R> f) {
        return map(f::cast);
    }

    @Override
    public <R> CQuery<R> map(IClass<R> f) {
        return map(f::cast);
    }

    @Override
    public <R> CQuery<R> mapOptional(Function<? super T, Ref<R>> f) {
        return new CQMapOptional<>(this, f);
    }

    @Override
    public <R> CQuery<R> mapEach(Functions.EachFunction<? super T, R> f) {
        return new CQMapEach<>(this, f);
    }

    @Override
    public <R> CQuery<R> flat(Function<? super T, CQuery<R>> f) {
        return BaseFlat.flat(this, f);
    }

    @Override
    public <R> CQuery<R> flatIterable(Function<? super T, Iterable<R>> f) {
        return BaseFlat.flatIterable(this, f);
    }

    @Override
    public <R> CQuery<R> flatArray(Function<? super T, R[]> f) {
        return BaseFlat.flatArray(this, f);
    }

    @Override
    public CQuery<T> mapIf(Predicate<? super T> p, Function<? super T, T> f) {
        return map(v -> !p.test(v) ? v : f.apply(v));
    }

    @Override
    public CQuery<T> mapEachIf(Predicates.EachPredicate<? super T> p, Functions.EachFunction<? super T, T> f) {
        return mapEach((i, v) -> !p.test(i, v) ? v : f.apply(i,v));
    }

    @Override
    public CQuery<T> mapOptionalIf(Predicate<? super T> p, Function<? super T, Ref<T>> f) {
        return mapOptional(v -> !p.test(v) ? Ref.of(v, message()) : f.apply(v));
    }

    @Override
    public CQuery<T> flatIf(Predicate<? super T> p, Function<? super T, CQuery<T>> f) {
        return BaseFlat.flatIf(this, p, f);
    }

    @Override
    public CQuery<T> flatIterableIf(Predicate<? super T> p, Function<? super T, Iterable<T>> f) {
        return BaseFlat.flatIterableIf(this, p, f);
    }

    @Override
    public CQuery<T> flatArrayIf(Predicate<? super T> p, Function<? super T, T[]> f) {
        return BaseFlat.flatArrayIf(this, p, f);
    }

    @Override
    public CQuery<T> deepFlatIf(Predicate<? super T> p, Function<? super T, CQuery<T>> f) {
        return BaseFlat.deepFlatIf(this, p, f);
    }

    @Override
    public CQuery<T> deepFlatIterableIf(Predicate<? super T> p, Function<? super T, Iterable<T>> f) {
        return BaseFlat.deepFlatIterableIf(this, p, f);
    }

    @Override
    public CQuery<T> deepFlatArrayIf(Predicate<? super T> p, Function<? super T, T[]> f) {
        return BaseFlat.deepFlatArrayIf(this, p, f);
    }

    @Override
    public CQuery<T> treeFlatIf(Predicate<? super T> p, Function<? super T, CQuery<T>> f) {
        return BaseFlat.treeFlatIf(this, p, f);
    }

    @Override
    public CQuery<T> treeFlatIterableIf(Predicate<? super T> p, Function<? super T, Iterable<T>> f) {
        return BaseFlat.treeFlatIterableIf(this, p, f);
    }

    @Override
    public CQuery<T> treeFlatArrayIf(Predicate<? super T> p, Function<? super T, T[]> f) {
        return BaseFlat.treeFlatArrayIf(this, p, f);
    }

    @Override
    public T reduce(T identity, BinaryOperator<T> accumulator) {
        if(!hasFastStream()) {
            if(hasFastEach()) {
                return BaseEach.reduce(this, identity, accumulator);
            }
            if(hasFastIterator()) {
                return BaseIterable.reduce(this, identity, accumulator);
            }
        }
        return BaseStream.reduce(this, identity, accumulator);
    }

    @Override
    public T reduce(BinaryOperator<T> accumulator) {
        if(!hasFastStream()) {
            if(hasFastEach()) {
                return BaseEach.reduce(this, accumulator);
            }
            if(hasFastIterator()) {
                return BaseIterable.reduce(this, accumulator);
            }
        }
        T out = BaseStream.reduce(this, accumulator);
        if(out == null) {
            throw ValueUtils.or(message(), (Supplier<NoSuchElementException>)NoSuchElementException::new).get();
        }
        return out;
    }

    @Override
    public T min(Comparator<? super T> cmp) {
        return reduce((a, b) -> cmp.compare(a, b) < 0 ? a : b);
    }

    @Override
    public T max(Comparator<? super T> cmp) {
        return reduce((a, b) -> cmp.compare(a, b) > 0 ? a : b);
    }

    @Override
    public Ref<T> single() {
        return BaseIterable.single(this);
    }

    @Override
    public <X extends Throwable> Ref<T> single(Supplier<X> ambigous) throws X {
        return BaseIterable.single(this, ambigous);
    }

    @Override
    public Ref<T> first() {
        if(hasFastList()) {
            return BaseList.first(this);
        }
        if(!hasFastStream()) {
            if(hasFastIterator()) {
                return BaseIterable.first(this);
            }
            if(hasFastEach()) {
                return BaseEach.first(this);
            }
        }
        return BaseStream.first(this);
    }

    @Override
    public Ref<T> last() {
        if(hasFastList()) {
            return BaseList.last(this);
        }
        if(hasFastLength()) {
            int n = size();
            return n> 0 ? skip(n -1).first() : Ref.empty(message());
        }
        if(hasFastEach()) {
            return BaseEach.last(this);
        }
        return BaseIterable.last(this);
    }

    @Override
    public Ref<T> at(int index) {
        if(hasFastList()) {
            return BaseList.at(this, index);
        }
        return skip(index).first();
    }

    @Override
    public CQuery<T> distinct() {
        return new CQFilter<>(this, () -> new OpenSet<>()::add);
    }

    @Override
    public CQuery<T> distinct(HashFunction<T> eq) {
        return new CQFilter<>(this, () -> new CustomSet<>(eq)::add);
    }

    @Override
    public CQuery<T> distinct(Comparator<? super T> cmp) {
        return new CQFilter<>(this, () -> new TreeSet<>(cmp)::add);
    }

    @Override
    public CQuery<T> sort() {
        return sort(CompareUtils.comparator());
    }

    @Override
    public CQuery<T> sort(Comparator<? super T> cmp) {
        return new CQSort<>(message(), this, cmp);
    }

    @Override
    public CQuery<T> reverse() {
        return new CQReverse<>(message(), this);
    }

    @Override
    public void forEach(Consumer<? super T> consumer) {
        whileEach(v -> {
            consumer.accept(v);
            return true;
        });
    }

    @Override
    public void forEach(Consumers.EachConsumer<? super T> consumer) {
        whileEach((i,v) -> {
            consumer.accept(i, v);
            return true;
        });
    }

    @Override
    public boolean whileEach(Predicate<? super T> consumer) {
        if(hasFastEach()) {
            return whileEach((i,v) -> consumer.test(v));
        }
        if(hasFastIterator()) {
            return BaseIterable.whileEach(this, consumer);
        }
        if(hasFastList()) {
            return BaseList.whileEach(this, consumer);
        }
        return BaseStream.whileEach(this, consumer);
    }

    @Override
    public boolean whileEach(Predicates.EachPredicate<? super T> consumer) {
        if(hasFastEach()) {
            BaseState.Index index = BaseState.newIndex(this);
            return whileEach(v -> consumer.test(index.next(), v));
        }
        if(hasFastIterator()) {
            return BaseIterable.whileEach(this, consumer);
        }
        if(hasFastList()) {
            return BaseList.whileEach(this, consumer);
        }
        return BaseStream.whileEach(this, consumer);
    }

    @Override
    public final CQuery<T> onEach(Consumer<? super T> consumer) {
        return map(FunctionUtils.asFunction(consumer));
    }

    @Override
    public final CQuery<T> onEach(Consumers.EachConsumer<? super T> consumer) {
        return mapEach(FunctionUtils.asEachFunction(consumer));
    }

    @Override
    public final void into(Collection<? super T> collection) {
        if (hasFastList()) {
            collection.addAll(list());
        } else if (hasFastEach()) {
            forEach(collection::add);
        } else {
            IterableUtils.collect(this, collection);
        }
    }

    @Override
    public boolean matchAny(Predicate<? super T> predicate) {
        if(!hasFastStream()) {
            if(hasFastEach()) {
                return BaseEach.matchAny(this, predicate);
            }
            if(hasFastIterator()) {
                return BaseIterable.matchAny(this, predicate);
            }
        }
        return BaseStream.matchAny(this, predicate);
    }

    @Override
    public final boolean matchAll(Predicate<? super T> predicate) {
        return !matchAny(predicate.negate());
    }

    @Override
    public final boolean matchNone(Predicate<? super T> predicate) {
        return !matchAny(predicate);
    }

    @Override
    public int indexOf(T value) {
        if(hasFastList()) {
            return list().indexOf(value);
        } else {
            return indexOf(v -> Objects.equals(v, value));
        }
    }

    @Override
    public int indexOf(Predicate<? super T> predicate) {
        return indexOfEach((i,v) -> predicate.test(v));
    }

    @Override
    public int indexOfEach(Predicates.EachPredicate<? super T> predicate) {
        if(!hasFastStream()) {
            if(hasFastEach()) {
                return BaseEach.indexOf(this, predicate);
            }
            if(hasFastIterator()) {
                return BaseIterable.indexOf(this, predicate);
            }
        }
        return BaseStream.indexOf(this, predicate);
    }

    @Override
    public int lastIndexOf(T value) {
        if(hasFastList()) {
            return list().lastIndexOf(value);
        } else {
            return lastIndexOf(v -> Objects.equals(v, value));
        }
    }

    @Override
    public int lastIndexOf(Predicate<? super T> predicate) {
        return lastIndexOfEach((i,v) -> predicate.test(v));
    }

    @Override
    public int lastIndexOfEach(Predicates.EachPredicate<? super T> predicate) {
        if(!hasFastStream()) {
            if(hasFastEach()) {
                return BaseEach.lastIndexOf(this, predicate);
            }
            if(hasFastIterator()) {
                return BaseIterable.lastIndexOf(this, predicate);
            }
        }
        return BaseStream.lastIndexOf(this, predicate);
    }

    @SuppressWarnings("unchecked")
    @Override
    public final <R> R apply(Function<? extends CQuery<? super T>, R> function) {
        return (R)FunctionUtils.raw(function).apply(this);
    }

    @Override
    public final boolean test(Predicate<? extends CQuery<? super T>> function) {
        return PredicateUtils.raw(function).test(this);
    }

}
