/*
 * @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.IntComparator;
import net.ranides.assira.collection.Swapper;
import net.ranides.assira.collection.arrays.NativeArray.NativeComparator;
import net.ranides.assira.collection.iterators.RandomAccessIterator;
import net.ranides.assira.generic.CompareUtils;

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 NativeArray wrapper.
 */
@SuppressWarnings({
    "PMD.AvoidReassigningParameters"
})
public final class NativeArrayUtils {

    private NativeArrayUtils() {
        // utility class
    }

    /**
     * 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 <T> component
     */
    public static <T> int collect(Iterator<? extends T> iterator, NativeArray target, int begin, int end) {
        NativeArrayAllocator.ensureFromTo(target, begin, end);
        int j = end;
        while (j-- != 0 && iterator.hasNext()) {
            target.set(begin++, iterator.next());
        }
        return end - j - 1;
    }

    /**
     * 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 <T> component
     */
    public static <T> int collect(Iterator<? extends T> iterator, NativeArray target) {
        return collect(iterator, target, 0, target.size());
    }

    /**
     * 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 <T> T
     */
    public static <T> void fill(NativeArray array, T value) {
        array.set(0, array.size(), 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 <T> component
     */
    public static <T> void fill(NativeArray array, int begin, int end, T value) {
        NativeArrayAllocator.ensureFromTo(array, begin, end);
        array.set(begin, end, value);
    }

    /**
     * Reverses the order of the elements in the specified array, but only inside specified range.
     *
     * Please note, that NativeArray implements Swapper - this method can be used in other contexts too.
     *
     * @param swapper the array to be reversed.
     * @param begin begin
     * @param end end
     * @return array
     *
     * @param <S> array
     */
    public static <S extends Swapper> S reverse(S swapper, int begin, int end){
        while(begin < --end){
            swapper.swap(begin++, end);
        }
        return swapper;
    }

    /**
     * Reverses the order of the elements in the specified array, but only inside specified range.
     *
     * Please note, that NativeArray implements Swapper - this method can be used in other contexts too.
     *
     * @param swapper the array to be reversed.
     * @param size size
     * @return array
     *
     * @param <S> array
     */
    public static <S extends Swapper> S reverse(S swapper, int size) {
        return reverse(swapper, 0, size);
    }

    /**
     * Reverses the order of the elements in the specified array.
     *
     * @param array the array to be reversed.
     * @return array
     */
    public static NativeArray reverse(NativeArray array) {
        reverse(array, array.size());
        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.
     */
    public static boolean equals(NativeArray array1, NativeArray array2) {
        return array1.equals(array2);
    }

    /**
     * Copies elements before index, and returns as new array
     *
     * @param values values
     * @param index index of last element to copy
     * @return new array
     */
    public static NativeArray head(NativeArray values, int index) {
        return slice(values, 0, index);
    }

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

    /**
     * Copies elements from [index] position (including), and returns as new array
     *
     * @param values values
     * @param index index
     * @return new array
     */
    public static NativeArray tail(NativeArray values, int index) {
        return slice(values, index, values.size());
    }

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

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

    /**
     * Copies elements from begin (including) to end (excluding)
     *
     * @param values values
     * @param begin begin (including)
     * @param end end (excluding)
     * @return new array
     */
    public static NativeArray slice(NativeArray values, int begin, int end) {
        int size = end - begin;
        NativeArray result = values.allocate(size);
        NativeArrayUtils.copy(values, begin, result, 0, size);
        return result;
    }

    /**
     * Shifts all elements by offset positions. Last elements are moved to begin.
     *
     * @param array array
     * @param offset offset
     * @return the same array
     */
    public static NativeArray rotate(NativeArray array, int offset) {
        int n = array.size();
        offset = offset % n;
        if( offset > 0) {
            int a = n - offset; 
            reverse(array, 0, a);
            reverse(array, a, n);
            reverse(array, 0, n);
        } 
        else if( offset < 0) {
            rotate(array, offset+n);
        }
        return array;
    }
    
    private static <T> int ifind(NativeArray values, T value, int begin, Comparator<? super T> cmp) {
        NativeComparator icmp = values.rcmp(value, cmp);
        int i = begin, n = values.size();
        while(i<n && icmp.compare(i) < 0) {
            i++;
        }
        return i;
    }

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

    /**
     * Shuffles all elements inside range [0;size) using provided swapper and random generator.
     * It runs in linear time, it makes [length] swaps.
     *
     * Please note, that NativeArray implements Swapper, so it can be passed to this function, if necessary.
     *
     * @param random random
     * @param size size
     * @param swapper swapper
     */
    public static void shuffle(Random random, int size, Swapper swapper) {
        for(int i=size; i>1; i--) {
            swapper.swap(i-1, random.nextInt(i));
        }
    }

    /**
     * Returns length of array.
     *
     * @param array array
     * @return int
     */
    public static int size(NativeArray array) {
        return array.size();
    }

    /**
     * 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
     * @return dst
     */
    @SuppressWarnings("SuspiciousSystemArraycopy")
    public static NativeArray copy(NativeArray src, int srcpos, NativeArray dst, int dstpos, int size) {
        System.arraycopy(src.array(), srcpos, dst.array(), dstpos, size);
        return dst;
    }

    /**
     * Copies array.
     *
     * @param array array
     * @return new array
     */
    public static NativeArray copy(NativeArray array) {
        int n = array.size();
        return copy(array, 0, NativeArrayAllocator.forPrototype(array, n), 0, n);
    }

    /**
     * Concatenates two arrays.
     * @param a array
     * @param b array
     * @return new array
     */
    public static NativeArray concat(NativeArray a, NativeArray b) {
        int as = a.size();
        int bs = b.size();
        NativeArray result = a.allocate(as+bs);
        copy(a, 0, result, 0, as);
        copy(b, 0, result, as, bs);
        return result;
    }

    /**
     * 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
     * @return new array
     */
    public static NativeArray append(NativeArray a, Object value) {
        int as = a.size();
        NativeArray result = a.allocate(as + 1);
        copy(a, 0, result, 0, as);
        result.set(as, value);
        return result;
    }

    /**
     * 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
     * @return new array
     */
    public static NativeArray prepend(NativeArray a, Object value) {
        int as = a.size();
        NativeArray result = a.allocate(as + 1);
        copy(a, 0, result, 1, as);
        result.set(0, value);
        return result;
    }

    /**
     * Removes first element from array and moves all elements to the left.
     * Note that last element in array will be duplicated.
     * @param a a
     * @param <T> T
     * @return removed value
     */
    @SuppressWarnings("unchecked")
    public static <T> T shift(NativeArray a) {
        int n = a.size();
        if(n == 0) {
            return null;
        }
        Object value = a.get(0);
        for(int i=1; i<n; i++) {
            a.move(i-1, i);
        }
        return (T)value;
    }

    /**
     * Removes last element from array and moves all elements to the right.
     * Note that first element in array will be duplicated.
     * @param a a
     * @param <T> T
     * @return T
     */
    @SuppressWarnings("unchecked")
    public static <T> T unshift(NativeArray a) {
        int n = a.size();
        if(n == 0) {
            return null;
        }
        Object value = a.get(n-1);
        for(int i=n-1; i>0; i--) {
            a.move(i, i-1);
        }
        return (T)value;
    }

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

    /**
     * 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
     *
     * @param array array
     * @param begin begin
     * @param end end
     * @param <T> T
     * @return iterator
     */
    @SuppressWarnings("unchecked")
    public static <T> ListIterator<T> iterator(NativeArray array, int begin, int end) {
        return new RandomAccessIterator<T>() {
            @Override
            protected int size() {
                return end-begin;
            }

            @Override
            protected T get(int index) {
                return (T)array.get(begin+index);
            }

            @Override
            protected void set(int index, T value) {
                array.set(begin+index, value);
            }
            
        };
    }

    /**
     * Compares two arrays using specified comparison function.
     * Returns true, if arrays are equal.
     *
     * @param a array
     * @param b array
     * @param cmp comparision function
     * @param <T> component
     * @return bool
     */
    public static <T> boolean equals(NativeArray a, NativeArray b, Comparator<? super T> cmp) {
        if(a.size() != b.size()) {
            return false;
        }
        IntComparator icmp = a.comparator(b, cmp);
        for(int i=0, n=a.size(); i<n; i++) {
            if( 0 != icmp.compare(i,i)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Compares two arrays using specified comparison function.
     * Returns true, if arrays are equal.
     *
     * @param a array
     * @param b array
     * @param predicate comparision function
     * @param <T> component
     * @return bool
     */
    public static <T> boolean equals(NativeArray a, NativeArray b, BiPredicate<? super T,? super T> predicate) {
        if(a.size() != b.size()) {
            return false;
        }
        Comparator<T> vcmp = (av,bv) -> predicate.test(av, bv) ? 0 : -1;
        IntComparator icmp = a.comparator(b, vcmp);
        for(int i=0, n=a.size(); i<n; i++) {
            if( 0 != icmp.compare(i,i)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Compares two arrays. Elements must be comparable.
     *
     * @param a array
     * @param b array
     * @return bool
     */
    public static int compare(NativeArray a, NativeArray b) {
        return icompare(a.size(), b.size(), a.comparator(b));
    }

    /**
     * Compares two arrays using specified comparator.
     *
     * @param a array
     * @param b array
     * @param cmp comparator
     * @param <T> component
     * @return int
     */
    public static <T> int compare(NativeArray a, NativeArray b, Comparator<? super T> cmp) {
        return icompare(a.size(), b.size(), a.comparator(b, cmp));
    }
    
    private static <T> int icompare(int a, int b, IntComparator cmp) {
        for(int i=0, n=Math.min(a,b); i<n; i++) {
            int ic = cmp.compare(i,i);
            if(ic != 0) {
                return ic;
            }
        }
        return CompareUtils.cmp(a,b);
    }
    
}
