package net.ranides.assira.collection.arrays;

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

/**
 * Fluent builder for array creation
 * It supports batch insertions.
 * It supports optional sorting list at the end
 *
 * It can construct arrays of primitive types, if you pass primitive component type.
 *
 * Because it supports primitives, it is not generic and you must make correct cast on your own.
 */
public class NativeArrayBuilder {

    private static final int INITIAL_SIZE = 8;

    private int count;
    private NativeArray array;
    private Comparator<Object> cmp;

    /**
     * Creates array builder with provided component type.
     * It allows creating arrays of primitive type.
     * Other methods will do necessary boxing and unboxing operations.
     *
     * It reserves buffer of some default, unspecifed size, but greater than zero.
     *
     * @param component component
     */
    public NativeArrayBuilder(Class<?> component) {
        this(component, INITIAL_SIZE);
    }

    /**
     * Creates array builder with provided generic compoment type.
     * Generator passed to constructor is used to construct correct array of initial size.
     *
     * Please note, that this generator is not used later for reallocation.
     * Internal buffer is resized by creating new array of detected component.
     *
     * It reserves buffer of some default, unspecifed size, but greater than zero.
     *
     * @param generator generator
     * @param <A> component type
     */
    public <A> NativeArrayBuilder(IntFunction<A[]> generator) {
        this(generator, INITIAL_SIZE);
    }

    /**
     * Creates array builder with provided component type.
     * It allows creating arrays of primitive type.
     * Other methods will do necessary boxing and unboxing operations.
     *
     * It reserves buffer of provided size.
     *
     * @param component component
     * @param size size
     */
    public NativeArrayBuilder(Class<?> component, int size) {
        this.array = NativeArray.allocate(component, size);
        this.count = 0;
    }

    /**
     * Creates array builder with provided generic component type.
     * Generator passed to constructor is used to construct correct array of initial size.
     *
     * Please note, that this generator is not used later for reallocation.
     * Internal buffer is resized by creating new array of detected component.
     *
     * It reserves buffer of provided size.
     *
     * @param generator generator
     * @param size size
     * @param <A> A
     */
    public <A> NativeArrayBuilder(IntFunction<A[]> generator, int size) {
        this.array = NativeArray.wrap(generator.apply(size));
        this.count = 0;
    }

    /**
     * Returns internal buffer used by builder.
     *
     * Returned array is sorted, if builder has defined comparator.
     * Returned array can be filled only partially, please check {@link #size} method.
     *
     * @return NativeArray
     */
    public NativeArray buffer() {
        if(cmp != null) {
            ArraySort.quickSort(array, 0, count, cmp);
        }
        return array.$array();
    }

    /**
     * Returns number of elements currently inserted into buffer.
     * Can be used to decide which elements returned by {@link #buffer} are valid.
     * @return int
     */
    public int size() {
        return count;
    }

    /**
     * Returns constructed array.
     * Optionally it sorts values using comparator provider earlier.
     *
     * Please note, that returned array can share data with builder.
     *
     * Be carefull, because type of returned array is inferred.
     *
     * @param <A> array
     * @return array
     */
    public <A> A toArray() {
        if(cmp != null) {
            ArraySort.quickSort(array, 0, count, cmp);
        }
        return NativeArrayAllocator.resize(array, count).$array();
    }

    /**
     * Inserts new value.
     *
     * @param value value
     * @return this
     */
    public NativeArrayBuilder value(Object value) {
        array = NativeArrayAllocator.grow(array, count +1);
        array.set(count++, value);
        return this;
    }

    /**
     * Inserts more than one value at once.
     *
     * @param value value
     * @param values values
     * @return this
     */
    public final NativeArrayBuilder values(Object value, Object... values) {
        int ds = values.length;
        array = NativeArrayAllocator.grow(array, count+ ds + 1);
        array.set(count++, value);
        for(Object v : values) {
            array.set(count++, v);
        }
        return this;
    }

    /**
     * Inserts more than one value at once.
     *
     * @param values values
     * @return this
     */
    public NativeArrayBuilder values(Object[] values) {
        int ds = values.length;
        array = NativeArrayAllocator.grow(array, count+ ds);
        for(Object v : values) {
            array.set(count++, v);
        }
        return this;
    }

    /**
     * Inserts more than one value at once.
     *
     * @param values values
     * @return this
     */
    public NativeArrayBuilder values(Collection<?> values) {
        int ds = values.size();
        array = NativeArrayAllocator.grow(array, count+ ds);
        for(Object v : values) {
            array.set(count++, v);
        }
        return this;
    }

    /**
     * Inserts more than one value at once.
     *
     * @param values values
     * @return this
     */
    public NativeArrayBuilder values(Iterable<?> values) {
        for(Object v : values) {
            array = NativeArrayAllocator.grow(array, count+ 1);
            array.set(count++, v);
        }
        return this;
    }

    /**
     * Inserts more than one value at once.
     *
     * @param values values
     * @return this
     */
    public NativeArrayBuilder values(Stream<?> values) {
        values.spliterator().forEachRemaining(this::value);
        return this;
    }

    /**
     * Inserts value conditionaly (if predicate accepts it).
     *
     * @param predicate predicate
     * @param value value
     * @param <P> P
     * @return this
     */
    public <P> NativeArrayBuilder valueIf(Predicate<P> predicate, P value) {
        if(predicate.test(value)) {
            value(value);
        }
        return this;
    }

    /**
     * Inserts more than one value conditionaly (if predicate accepts it).
     *
     * @param predicate predicate
     * @param value value
     * @param values values
     * @param <P> P
     * @return this
     */
    @SafeVarargs
    public final <P> NativeArrayBuilder valuesIf(Predicate<P> predicate, P value, P... values) {
        array = NativeArrayAllocator.grow(array, count + 1 + values.length);
        return valueIf(predicate, value).valuesIf(predicate, values);
    }

    /**
     * Inserts more than one value conditionaly (if predicate accepts it).
     *
     * @param predicate predicate
     * @param values values
     * @param <P> P
     * @return this
     */
    public <P> NativeArrayBuilder valuesIf(Predicate<P> predicate, P[] values) {
        array = NativeArrayAllocator.grow(array, count + values.length);
        for(P value : values) {
            if(predicate.test(value)) {
                value(value);
            }
        }
        return this;
    }

    /**
     * Inserts more than one value conditionaly (if predicate accepts it).
     *
     * @param predicate predicate
     * @param values values
     * @param <P> P
     * @return this
     */
    public <P> NativeArrayBuilder valuesIf(Predicate<P> predicate, Collection<P> values) {
        array = NativeArrayAllocator.grow(array, count + 1 + values.size());
        for(P value : values) {
            if(predicate.test(value)) {
                value(value);
            }
        }
        return this;
    }

    /**
     * Inserts more than one value conditionaly (if predicate accepts it).
     *
     * @param predicate predicate
     * @param values values
     * @param <P> P
     * @return this
     */
    public <P> NativeArrayBuilder values(Predicate<P> predicate, Iterable<P> values) {
        for(P value : values) {
            if(predicate.test(value)) {
                value(value);
            }
        }
        return this;
    }

    /**
     * Defines sorting order.
     * Builder will sort all inserted values at the end of construction.
     *
     * That means, sorting will be done by methods: {@link #toArray()} and {@link #buffer()}
     *
     * @param cmp cmp
     * @return this
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    public NativeArrayBuilder sorted(Comparator<?> cmp) {
        if(this.cmp != null) {
            this.cmp = this.cmp.thenComparing((Comparator)cmp);
        } else {
            this.cmp = (Comparator)cmp;
        }
        return this;
    }

}
