package net.ranides.assira.collection.lists;

import lombok.RequiredArgsConstructor;

import net.ranides.assira.collection.arrays.ArrayAllocator;
import net.ranides.assira.collection.arrays.ArraySort;
import net.ranides.assira.collection.arrays.ArrayUtils;

import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Stream;

/**
 * Fluent builder for list creation
 * It supports batch insertions.
 * It supports optional sorting list at the end
 * @param <T> T
 */
public class ListBuilder<T> {

    private int count = 0;
    private Object[] list = new Object[8];
    private Comparator<T> cmp;

    /**
     * Returns constructed list.
     * Optionally it sorts values using comparator provider earlier.
     *
     * Please note, that returned list shares data with builder.
     *
     * @return list
     */
    @SuppressWarnings({"unchecked"})
    public List<T> list() {
        if(cmp != null) {
            ArraySort.quickSort(list, 0, count, cmp);
        }
        return (List<T>)new Slice(list, count);
    }

    @SuppressWarnings({"unchecked"})
    private <P extends T> ListBuilder<P> that() {
        return (ListBuilder<P>)this;
    }

    /**
     * Inserts new value.
     * Infers target type of constructed list.
     *
     * @param value value
     * @param <P> P
     * @return this
     */
    public <P extends T> ListBuilder<P> value(P value) {
        list = ArrayAllocator.grow(list, count+1);
        list[count++] = value;
        return that();
    }

    /**
     * Inserts more than one value at once.
     * Infers target type of constructed list.
     *
     * @param value value
     * @param values values
     * @param <P> P
     * @return this
     */
    @SafeVarargs
    public final <P extends T> ListBuilder<P> values(P value, P... values) {
        int ds = values.length;
        list = ArrayAllocator.grow(list, count+ ds + 1);
        list[count++] = value;
        ArrayUtils.copy(values, 0, list, count, ds);
        count += ds;
        return that();
    }

    /**
     * Inserts more than one value at once.
     * Infers target type of constructed list.
     *
     * @param values values
     * @param <P> P
     * @return this
     */
    public <P extends T> ListBuilder<P> values(P[] values) {
        int ds = values.length;
        list = ArrayAllocator.grow(list, count+ ds);
        ArrayUtils.copy(values, 0, list, count, ds);
        count += ds;
        return that();
    }

    /**
     * Inserts more than one value at once.
     * Infers target type of constructed list.
     *
     * @param values values
     * @param <P> P
     * @return this
     */
    public <P extends T> ListBuilder<P> values(Collection<P> values) {
        int ds = values.size();
        list = ArrayAllocator.grow(list, count+ ds);
        for(Object v : values) {
            list[count++] = v;
        }
        return that();
    }

    /**
     * Inserts more than one value at once.
     * Infers target type of constructed list.
     *
     * @param values values
     * @param <P> P
     * @return this
     */
    public <P extends T> ListBuilder<P> values(Iterable<P> values) {
        for(Object v : values) {
            list = ArrayAllocator.grow(list, count+ 1);
            list[count++] = v;
        }
        return that();
    }

    /**
     * Inserts more than one value at once.
     * Infers target type of constructed list.
     *
     * @param values values
     * @param <P> P
     * @return this
     */
    public <P extends T> ListBuilder<P> values(Stream<P> values) {
        values.spliterator().forEachRemaining(this::value);
        return that();
    }

    /**
     * Inserts value conditionaly (if predicate accepts it).
     * Infers target type of constructed list.
     *
     * @param predicate predicate
     * @param value value
     * @param <P> P
     * @return this
     */
    public <P extends T> ListBuilder<P> valueIf(Predicate<P> predicate, P value) {
        if(predicate.test(value)) {
            value(value);
        }
        return that();
    }

    /**
     * Inserts more than one value conditionaly (if predicate accepts it).
     * Infers target type of constructed list.
     *
     * @param predicate predicate
     * @param value value
     * @param values values
     * @param <P> P
     * @return this
     */
    @SafeVarargs
    public final <P extends T> ListBuilder<P> valuesIf(Predicate<P> predicate, P value, P... values) {
        list = ArrayAllocator.grow(list, count + 1 + values.length);
        return valueIf(predicate, value).valuesIf(predicate, values);
    }

    /**
     * Inserts values conditionaly (if predicate accepts it).
     * Infers target type of constructed list.
     *
     * @param predicate predicate
     * @param values values
     * @param <P> P
     * @return this
     */
    public <P extends T> ListBuilder<P> valuesIf(Predicate<P> predicate, P[] values) {
        list = ArrayAllocator.grow(list, count + values.length);
        for(P value : values) {
            if(predicate.test(value)) {
                value(value);
            }
        }
        return that();
    }

    /**
     * Inserts values conditionaly (if predicate accepts it).
     * Infers target type of constructed list.
     *
     * @param predicate predicate
     * @param values values
     * @param <P> P
     * @return this
     */
    public <P extends T> ListBuilder<P> valuesIf(Predicate<P> predicate, Collection<P> values) {
        list = ArrayAllocator.grow(list, count + 1 + values.size());
        for(P value : values) {
            if(predicate.test(value)) {
                value(value);
            }
        }
        return that();
    }

    /**
     * Inserts values conditionaly (if predicate accepts it).
     * Infers target type of constructed list.
     *
     * @param predicate predicate
     * @param values values
     * @param <P> P
     * @return this
     */
    public <P extends T> ListBuilder<P> values(Predicate<P> predicate, Iterable<P> values) {
        for(P value : values) {
            if(predicate.test(value)) {
                value(value);
            }
        }
        return that();
    }

    /**
     * Defines sorting order.
     * Builder will sort all inserted values at the end of construction.
     *
     * @param cmp cmp
     * @return this
     */
    public ListBuilder<T> sorted(Comparator<T> cmp) {
        if(this.cmp != null) {
            this.cmp = this.cmp.thenComparing(cmp);
        } else {
            this.cmp = cmp;
        }
        return that();
    }

    @RequiredArgsConstructor
    private static final class Slice extends VirtualList<Object> {

        private final Object[] data;
        private final int size;

        @Override
        public int size() {
            return size;
        }

        @Override
        public Object get(int index) {
            return data[index];
        }
    }

}
