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

import net.ranides.assira.collection.iterators.IntIterator;

import java.lang.reflect.Array;
import java.util.Comparator;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Random;
import java.util.function.BiPredicate;

/**
 * Common utility method for arrays.
 */
@SuppressWarnings({
    "PMD.AvoidReassigningParameters"
})
public final class ArrayUtils {

    /**
     * Unwraps an iterator into an array starting at a given offset for a given
     * number of elements.
     *
     * <P>This method iterates over the given type-specific iterator and stores
     * the elements returned, up to a maximum of
     * {@code length}, in the given array starting at
     * {@code offset}. The number of actually unwrapped elements is
     * returned (it may be less than
     * {@code max} if the iterator emits less than
     * {@code max} elements).
     *
     * @param iterator a type-specific iterator.
     * @param target an array to contain the output of the iterator.
     * @param begin the first element of the array to be returned.
     * @param end the maximum number of elements to collect.
     * @return the number of elements unwrapped.
     *
     * @param <A> array
     * @param <T> component
     */
    public static <A,T> int collect(Iterator<? extends T> iterator, A target, int begin, int end) {
        if($isint(target)) {
            return $collect(IntIterator.$wrap(iterator), (int[])target, begin, end);
        } else {
            return NativeArrayUtils.collect(iterator, NativeArray.wrap(target), begin, end);
        }
    }

    private static <A> boolean $isint(A array) {
        return null!=array && int[].class.equals(array.getClass());
    }
    
    /**
     * Unwraps an iterator into an array.
     *
     * <P>This method iterates over the given type-specific iterator and stores
     * the elements returned in the given array. The iteration will stop when
     * the iterator has no more elements or when the end of the array has been
     * reached.
     *
     * @param iterator a type-specific iterator.
     * @param target an array to contain the output of the iterator.
     * @return the number of elements unwrapped.
     *
     * @param <A> array
     * @param <T> component
     */
    public static <A,T> int collect(Iterator<? extends T> iterator, A target) {
        if($isint(target)) {
            int[] array = (int[])target;
            return $collect(IntIterator.$wrap(iterator), array, 0, array.length);
        } else {
            return NativeArrayUtils.collect(iterator, NativeArray.wrap(target));
        }
    }
    
    static int $collect(IntIterator iterator, int[] target, int begin, int end) {
        NativeArrayAllocator.ensureFromTo(target.length, begin, end);
        int j = end;
        while (j-- != 0 && iterator.hasNext()) {
            target[begin++] = iterator.nextInt();
        }
        return end - j - 1;
    }
        
    /**
     * Fills the given array with the given value.
     *
     * <P>This method uses a backward loop. It is significantly faster than the
     * corresponding method in {@link java.util.Arrays}.
     *
     * @param array an array.
     * @param value the new value for all elements of the array.
     *
     * @param <A> array
     * @param <T> component
     */
    public static <A,T> void fill(A array, T value) {
        NativeArrayUtils.fill(NativeArray.wrap(array), value);
    }

    /**
     * Fills a portion of the given array with the given value.
     *
     * <P>If possible (i.e.,
     * {@code from} is 0) this method uses a backward loop. In this case,
     * it is significantly faster than the corresponding method in
     * {@link java.util.Arrays}.
     *
     * @param array an array.
     * @param begin the starting index of the portion to fill (inclusive).
     * @param end the end index of the portion to fill (exclusive).
     * @param value the new value for all elements of the specified portion of
     * the array.
     *
     * @param <A> array
     * @param <T> component
     */
    public static <A,T> void fill(A array, int begin, int end, T value) {
        NativeArrayUtils.fill(NativeArray.wrap(array), begin, end, value);
    }

    /**
     * Reverses the order of the elements in the specified array, but only inside specified range.
     *
     * @param array the array to be reversed.
     * @param begin begin
     * @param end end
     * @return {@code a}.
     *
     * @param <A> array
     */
    public static <A> A reverse(A array, int begin, int end){
        NativeArrayUtils.reverse(NativeArray.wrap(array), begin, end);
        return array;
    }

    /**
     * Reverses the order of the elements in the specified array.
     *
     * @param array the array to be reversed.
     * @return {@code a}.
     *
     * @param <A> inferred type of array
     */
    public static <A> A reverse(A array) {
        NativeArrayUtils.reverse(NativeArray.wrap(array));
        return array;
    }
    
    /**
     * Returns true if the two arrays are elementwise equal.
     *
     * @param array1 an array.
     * @param array2 another array.
     * @return true if the two arrays are of the same length, and their elements
     * are equal.
     *
     * @param <A> inferred type of array
     */
    public static <A> boolean equals(A array1, A array2) {
        return NativeArrayUtils.equals(NativeArray.wrap(array1), NativeArray.wrap(array2));
    }

    /**
     * Copies elements before index, and returns as new array
     *
     * @param array array
     * @param index index of last element to copy
     * @param <A> array
     * @return new array
     */
    public static <A> A head(A array, int index) {
        return NativeArrayUtils.head(NativeArray.wrap(array), index).$array();
    }

    /**
     * Finds position of [value] and copies all elements before it.
     * It uses cmp function for search.
     *
     * @param array array
     * @param value value
     * @param cmp cmp
     * @param <A> array
     * @param <T> component
     * @return new array
     */
    public static <A,T> A head(A array, T value, Comparator<? super T> cmp) {
        return NativeArrayUtils.head(NativeArray.wrap(array), value, cmp).$array();
    }

    /**
     * Copies elements from [index] position (including), and returns as new array
     *
     * @param array array
     * @param index index
     * @param <A> array
     * @return new array
     */
    public static <A> A tail(A array, int index) {
        return NativeArrayUtils.tail(NativeArray.wrap(array), index).$array();
    }

    /**
     * Finds position of [value] and copies all elements from this position (including).
     * It uses cmp function for search.
     *
     * @param array array
     * @param value value
     * @param cmp cmp
     * @param <A> array
     * @param <T> component
     * @return new array
     */
    public static <A,T> A tail(A array, T value, Comparator<? super T> cmp) {
        return NativeArrayUtils.tail(NativeArray.wrap(array), value, cmp).$array();
    }

    /**
     * Finds [begin] and [end] elements and copies content between them, including [begin].
     * It uses cmp function for search.
     *
     * @param values array
     * @param begin first value (include)
     * @param end last value (exclude)
     * @param cmp cmp
     * @param <A> array
     * @param <T> component
     * @return new array
     */
    public static <A,T> A slice(A values, T begin, T end, Comparator<? super T> cmp) {
        return NativeArrayUtils.slice(NativeArray.wrap(values), begin, end, cmp).$array();
    }

    /**
     * Copies elements from begin (including) to end (excluding)
     *
     * @param array array
     * @param begin begin (including)
     * @param end end (excluding)
     * @param <A> array
     * @return new array
     */
    public static <A> A slice(A array, int begin, int end) {
        return NativeArrayUtils.slice(NativeArray.wrap(array), begin, end).$array();
    }

    /**
     * Shifts all elements by offset positions. Last elements are moved to begin.
     *
     * @param array array
     * @param offset offset
     * @param <A> array
     * @return the same array
     */
    public static <A> A rotate(A array, int offset) {
        return NativeArrayUtils.rotate(NativeArray.wrap(array), offset).$array();
    }

    /**
     * Shuffles all elements inside array using provided random generator.
     * It runs in linear time, it makes [length] swaps.
     *
     * @param random random
     * @param array array
     * @param <A> array
     * @return the same array
     */
    public static <A> A shuffle(Random random, A array) {
        return NativeArrayUtils.shuffle(random, NativeArray.wrap(array)).$array();
    }

    /**
     * Returns length of array. null-safe.
     *
     * @param array array
     * @param <A> array
     * @return int
     */
    public static <A> int size(A array) {
        return null == array ? 0 : Array.getLength(array);
    }

    /**
     * Copies source array into destination array.
     * It uses System.arraycopy.
     * The only difference is that is returns destination as result.
     *
     * @param src array
     * @param srcpos srcpos
     * @param dst array
     * @param dstpos dstpos
     * @param size size
     * @param <A> array
     * @return dst
     */
    @SuppressWarnings("SuspiciousSystemArraycopy")
    public static <A> A copy(A src, int srcpos, A dst, int dstpos, int size) {
        System.arraycopy(src, srcpos, dst, dstpos, size);
        return dst;
    }

    /**
     * Copies array.
     *
     * @param array array
     * @param <A> array
     * @return new array
     */
    public static <A> A copy(A array) {
        if(null == array) {
            return null;
        }
        int n = Array.getLength(array);
        return copy(array, 0, ArrayAllocator.forPrototype(array, n), 0, n);
    }

    /**
     * Concatenates two arrays.
     * @param a array
     * @param b array
     * @param <A> array
     * @return new array
     */
    public static <A> A concat(A a, A b) {
        return NativeArrayUtils.concat(NativeArray.wrap(a), NativeArray.wrap(b)).$array();
    }

    /**
     * Creates new array with value appended at the end of source array.
     * New array is always one element longer that original.
     *
     * @param a array
     * @param value value
     * @param <A> array
     * @return new array
     */
    public static <A> A append(A a, Object value) {
        return NativeArrayUtils.append(NativeArray.wrap(a), value).$array();
    }

    /**
     * Creates new array with value inserted at the begining of source array.
     * New array is always one element longer that original.
     *
     * @param a array
     * @param value value
     * @param <A> array
     * @return new array
     */
    public static <A> A prepend(A a, Object value) {
        return NativeArrayUtils.prepend(NativeArray.wrap(a), value).$array();
    }

    /**
     * Removes first element from array and moves all elements to the left.
     * Note that last element in array will be duplicated.
     * @param array array
     * @param <A> array
     * @param <T> component
     * @return removed element or null
     */
    public static <A,T> T shift(A array) {
        return NativeArrayUtils.shift(NativeArray.wrap(array));
    }

    /**
     * Removes last element from array and moves all elements to the right.
     * Note that first element in array will be duplicated.
     * @param array array
     * @param <A> array
     * @param <T> component
     * @return removed element or null
     */
    public static <A,T> T unshift(A array) {
        return NativeArrayUtils.unshift(NativeArray.wrap(array));
    }

    /**
     * Creates iterator for specified array.
     * Iterator supports "set" operation, but not "add" or "remove".
     * Please note, that component type is inferred from result, not deduced from argument.
     *
     * @param array array
     * @param <A> array
     * @param <T> array
     * @return iterator
     */
    public static <A,T> ListIterator<T> iterator(A array) {
        return NativeArrayUtils.iterator(NativeArray.wrap(array));
    }

    /**
     * Creates iterator for specified array, but only for specified range.
     * Iterator supports "set" operation, but not "add" or "remove".
     * Please note, that component type is inferred from result, not deduced from argument.
     *
     * @param array array
     * @param begin begin
     * @param end end
     * @param <A> array
     * @param <T> component
     * @return iterator
     */
    public static <A,T> ListIterator<T> iterator(A array, int begin, int end) {
        return NativeArrayUtils.iterator(NativeArray.wrap(array), begin, end);
    }

    /**
     * Compares two arrays using specified comparison function.
     * Returns true, if arrays are equal.
     *
     * @param a array
     * @param b array
     * @param predicate comparision function
     * @param <A> array
     * @param <T> component
     * @return bool
     */
    public static <A,T> boolean equals(A a, A b, BiPredicate<T,T> predicate) {
        return NativeArrayUtils.equals(NativeArray.wrap(a), NativeArray.wrap(b), predicate);
    }

    /**
     * Compares two arrays. Elements must be comparable.
     *
     * @param a array
     * @param b array
     * @param <A> array
     * @return bool
     */
    public static <A> int compare(A a, A b) {
        return NativeArrayUtils.compare(NativeArray.wrap(a), NativeArray.wrap(b));
    }

    /**
     * Compares two arrays using specified comparator.
     *
     * @param a array
     * @param b array
     * @param cmp comparator
     * @param <A> array
     * @param <T> component
     * @return int
     */
    public static <A,T> int compare(A a, A b, Comparator<T> cmp) {
        return NativeArrayUtils.compare(NativeArray.wrap(a), NativeArray.wrap(b), cmp);
    }
}
