/*******************************************************************************
 * Copyright (c) 2007 Bruno Medeiros and other Contributors.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Bruno Medeiros - initial implementation
 *******************************************************************************/
package melnorme.utilbox.misc;

import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue;
import static melnorme.utilbox.core.CoreUtil.areEqual;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import melnorme.utilbox.collections.Collection2;
import java.util.function.Function;
import java.util.function.Predicate;

public class ArrayUtil {
	
	public static final Object[] EMPTY_ARRAY = new Object[] {};
	
	/** @return the given array if it is non-null, an empty array otherwise. */
	public static Object[] nullToEmpty(Object[] array) {
		return array == null ? EMPTY_ARRAY : array;
	}
	
	/** @return the given array if it is non-null, an empty array otherwise with given componentType. */
	public static <T> T[] nullToEmpty(T[] array, Class<T> componentType) {
		return array == null ? create(0, componentType) : array;
	}
	
	@SuppressWarnings("unchecked")
	public static <T> T[] create(int length, Class<T> componentType) {
		return (T[]) Array.newInstance(componentType, length);
	}
	@SuppressWarnings("unchecked")
	public static <T> T[] create(Class<T> componentType, int length) {
		return (T[]) Array.newInstance(componentType, length);
	}
	
	/** Creates a new array of given length, and same component type as given similarArray. */
	@SuppressWarnings("unchecked")
	public static <T> T[] createWithSameComponentType(int length, T[] similarArray) {
		return (T[]) Array.newInstance(similarArray.getClass().getComponentType(), length);
	}
	
     /** Create an array from the given element, with the given componentType.
     * If the element is null, a zero-length array is created. */
	public static <T> T[] singletonArray(T element, Class<T> componentType) {
		if(element == null) {
			return create(componentType, 0);
		}
		T[] newArray = create(componentType, 1);
		newArray[0] = element;
		return newArray;
	}
	
	/** Create an array from the given list, with the given componentType.
     * If given list is null, a zero-length array is created. */
	public static <T> T[] createFrom(Collection<? extends T> list, Class<T> componentType) {
		if(list == null) {
			return create(componentType, 0);
		}
		return list.toArray(create(componentType, list.size()));
	}
	
	/** Create an array from the given list, with Object.class as the component type.
     * If given list is null, a zero-length array is created. */
	public static Object[] createFrom(Collection<?> list) {
		return createFrom(list, Object.class);
	}
	
	public static <E> E[] copyFrom(E[] array, Class<E> componentType) {
		E[] newArray = create(array.length, componentType);
	    System.arraycopy(array, 0, newArray, 0, array.length);
	    return newArray;
	}
	
    /** Create an array from the given list, with the given cpType as the run-time component type.
     * If the list is null, null is returned. */
	public static <T> T[] toArray(Collection<? extends T> list, Class<T> componentType) {
		if(list == null) {
			return null;
		}
		return list.toArray(create(componentType, list.size()));
	}
	
	/** Creates a new array with the given length, and of the same type as the given array. */
	public static <T> T[] copyFrom(T[] array, int newLength) {
		T[] copy = createWithSameComponentType(newLength, array);
    	System.arraycopy(array, 0, copy, 0, Math.min(array.length, newLength));
    	return copy;
	}
	
	/** Creates a copy of given array with the given newlength, and of type char[]. */
	public static char[] copyFrom(char[] array, int newLength) {
        char[] copy = (char[]) Array.newInstance(Character.TYPE, newLength);
    	System.arraycopy(array, 0, copy, 0, Math.min(array.length, newLength));
    	return copy;
	}
	
	/** Creates a copy of given array with the given newlength, and of type byte[]. */
	public static byte[] copyFrom(byte[] array, int newLength) {
		byte[] copy = (byte[]) Array.newInstance(Byte.TYPE, newLength);
    	System.arraycopy(array, 0, copy, 0, Math.min(array.length, newLength));
    	return copy;
	}
	
	/** Creates a copy of given array with the given newlength, and of type int[]. */
	public static int[] copyFrom(int[] array, int newLength) {
		int[] copy = (int[]) Array.newInstance(Integer.TYPE, newLength);
    	System.arraycopy(array, 0, copy, 0, Math.min(array.length, newLength));
    	return copy;
	}
	
	/** Creates a copy of given array, and of type int[]. */
	public static int[] copyFrom(int[] array) {
		return copyFrom(array, array.length);
	}
	
	/** Creates a copy of given array, and of type T[]. */
	public static <T> T[] copyFrom(T[] array) {
		return Arrays.copyOf(array, array.length);
	}
	
	/** Same as {@link Arrays#copyOfRange(Object[], int, int)} */
	public static <T> T[] copyOfRange(T[] original, int from, int to) {
        return Arrays.copyOfRange(original, from, to);
    }
	
	/** Same as {@link Arrays#copyOfRange(Object[], int, int, Class)} */
	public static <T,U> T[] copyOfRange(U[] original, int from, int to, Class<? extends T[]> newType) {
    	return Arrays.copyOfRange(original, from, to, newType);
    }
    
	/** Creates an int[] from given coll of Integers. */
	public static int[] createIntArray(List<? extends Integer> coll) {
		int[] array = new int[coll.size()];
		for (int i = 0; i < coll.size(); i++) {
			array[i] = coll.get(i);
		}
		return array;
	}
	
	/** Creates an array with the same size as the given list.
	 * If the list is null, a zero-length array is created. */
	public static <T> T[] newSameSize(List<?> list, Class<T> cpType) {
		if(list == null)
			return create(cpType, 0);
		else 
			return create(cpType, list.size());
	}

    /** Copies src array range [0 .. src.length] to dest array starting at destIx. */
	public static void copyToRange(byte[] src, byte[] dest, int destIx) {
		assertTrue(src.length < dest.length - destIx);
		System.arraycopy(src, 0, dest, destIx, src.length);
	}
	
	
	/** Appends an element to array, creating a new array. */
	public static <T> T[] append(T[] base, T element) {
		T[] newArray = copyFrom(base, base.length + 1);
		newArray[base.length] = element;
		return newArray;
	}
	
	/** Creates a new array with given first element prepended to given rest array. */
	@SafeVarargs
	public static <T> T[] prepend(T first, T... rest) {
		T[] newArray = createWithSameComponentType(rest.length + 1, rest);
		newArray[0] = first;
		System.arraycopy(rest, 0, newArray, 1, rest.length);
		return newArray;
	}
	
	/** Appends given array other to given array base, 
	 * creating a new array of the same runtime type as original. */
	@SafeVarargs
	public static <T> T[] concat(T[] base, T... other) {
		return concat(base, other, other.length);
	}
	
	/** Appends appendCount number of elements of given array other to given array base, 
	 * creating a new array of the same runtime type as original. */
	public static <T> T[] concat(T[] base, T[] other, int appendCount) {
		T[] newArray = copyFrom(base, base.length + appendCount);
		System.arraycopy(other, 0, newArray, base.length, appendCount);
		return newArray;
	}
	
	/** Appends appendCount number of elements of given array other to given array base */
	public static byte[] concat(byte[] base, byte[] other, int appendCount) {
		final int length = base.length;
		byte[] newArray = copyFrom(base, base.length + appendCount);
		System.arraycopy(other, 0, newArray, length, appendCount);
		return newArray;
	}
	
	/** Appends appendCount number of elements of given array other to given array base */
	public static char[] concat(char[] base, char[] other, int appendCount) {
		final int length = base.length;
		char[] newArray = copyFrom(base, base.length + appendCount);
		System.arraycopy(other, 0, newArray, length, appendCount);
		return newArray;
	}
	
	/** Removes from the given array the first element that isEqual to given objToRemove. */
	public static<T> T[] remove(T[] array, T objToRemove) {
		for (int i = 0; i < array.length; i++) {
			T elem = array[i];
			if(areEqual(elem, objToRemove))
				return removeAt(array, i);
		}
		return array;
	}
	
	
	/** Removes the element at index ix from array, creating a new array. */
	public static <T> T[] removeAt(T[] array, int ix) {
		T[] newArray = copyFrom(array, array.length - 1);
		System.arraycopy(array, 0, newArray, 0, ix);
		System.arraycopy(array, ix + 1, newArray, ix, array.length - ix - 1);	
		return newArray;
	}
	
	/** Removes the last given count elements from given array, creating a new array. */
	public static <T> T[] removeLast(T[] array, int count) {
		assertTrue(array.length >= count);
		T[] newArray = ArrayUtil.copyFrom(array, array.length - count);
		return newArray;
	}
	
	/** Removes from the given array the first element that areEqual given objToRemove. 
	 * @return a new array with elements removed, or the same array if no elements where removed. */
	public static<T> T[] removeAll(T[] array, T objToRemove) {
		int removeCount = 0;
		for (T elem : array) {
			if(areEqual(elem, objToRemove)) {
				removeCount++;
			}
		}
		if(removeCount == 0) {
			return array;
		}
		int ix = 0;
		T[] newArray = createWithSameComponentType(array.length - removeCount, array); 
		for (T elem : array) {
			if(areEqual(elem, objToRemove)) {
				continue;
			}
			newArray[ix] = elem;
			ix++;
		}
		
		return newArray;
	}

	
	/* ====================== search/index ====================== */
	
	/** @return the last element of given array, or null if array is empty. */
	public static <T> T getLastElement(T[] array) {
		if(array.length == 0) {
			return null;
		}
		return array[array.length-1];
	}
	
	/** @return the same as {@link List#indexOf(Object)}, using given array as a collection. */
	public static <T> int indexOf(T[] array, T elem) {
		// return Arrays.asList(array).indexOf(elem);
		if(elem == null) {
			for(int ix = 0; ix < array.length; ix++) {
				if(array[ix] == null)
					return ix;
			}
		} else {
			for(int ix = 0; ix < array.length; ix++) {
				if(elem.equals(array[ix]))
					return ix;
			}
		}
		return -1;
	}
	
	/** @return true if array contains an element equal to obj. */
	public static <T> boolean contains(T[] array, T obj) {
		return indexOf(array, obj) != -1;
	}
	
	/** @return the index in given array of the first occurrence of given elem, or -1 if none is found. */
	public static int indexOf(byte[] array, byte elem) {
		for (int i = 0; i < array.length; i++) {
			if(array[i] == elem)
				return i;
		}
		return -1;
	}
	public static int indexOf(char[] array, char elem) {
		for (int i = 0; i < array.length; i++) {
			if(array[i] == elem)
				return i;
		}
		return -1;
	}
	
	/** @return the index in given array of the first element that is the same as given elem, 
	 * or -1 if none is found. */
	public static <T> int indexOfIdentity(T[] array, T elem) {
		for (int i = 0; i < array.length; i++) {
			if(array[i] == elem)
				return i;
		}
		return -1;
	}
	
	/** @return true if array contains an element that matched given predicate. */
	public static <T> boolean search(T[] array, Predicate<T> predicate) {
		for(T elem: array) {
			if(predicate.test(elem))
				return true;
		}
		return false;
	}
	
	/** Is {@link #map(Object[], Function, Class)}  with klass = Object.class */
	public static <T> Object[] map(T[] array, Function<? super T, ? extends Object> evalFunction) {
		return ArrayUtil.map(array, evalFunction, Object.class);
	}
	
	/** Creates a new array, based on given array, whose elements are produced element-wise from the original 
	 * array using given evalFunction. */
	public static <T, R> R[] map(T[] array, Function<? super T, ? extends R> evalFunction, Class<R> klass) {
		R[] newArray = create(array.length, klass);
		for(int i = 0; i < newArray.length; i++) {
			newArray[i] = evalFunction.apply(array[i]);
		}
		return newArray;
	}
	
	/** Is {@link #map(Collection, IEvalFunc, Class klass)}, IEvalFunc, Class) with klass = Object.class */
	public static <T> Object[] map(Collection<T> coll, Function<? super T, ? extends Object> evalFunction) {
		return map(coll, evalFunction, Object.class);
	}
	/** Is {@link #map(Collection, IEvalFunc, Class klass)}, IEvalFunc, Class) with klass = Object.class */
	public static <T> Object[] map(Collection2<T> coll, Function<? super T, ? extends Object> evalFunction) {
		return map(coll, evalFunction, Object.class);
	}
	
	/** Creates a new array, based on given coll, whose elements are produced element-wise from the original 
	 * coll using given evalFunction. */
	public static <T, R> R[] map(Collection<T> coll, Function<? super T, ? extends R> evalFunction, Class<R> klass) {
		R[] newArray = create(coll.size(), klass);
		int i = 0;
		for(T elem : coll) {
			newArray[i] = evalFunction.apply(elem);
			i++;
		}
		return newArray;
	}
	/** Creates a new array, based on given coll, whose elements are produced element-wise from the original 
	 * coll using given evalFunction. */
	public static <T, R> R[] map(Collection2<T> coll, Function<? super T, ? extends R> evalFunction, Class<R> klass) {
		R[] newArray = create(coll.size(), klass);
		int i = 0;
		for(T elem : coll) {
			newArray[i] = evalFunction.apply(elem);
			i++;
		}
		return newArray;
	}
	
	/** Filters given array, using given predicate, creating a new array. */
	public static <T> T[] filter(T[] array, Predicate<T> predicate) {
		T[] newArray = createWithSameComponentType(array.length, array);
		assertTrue(newArray.length <= array.length);
		int newIx = 0, arrayIx = 0;
		while(arrayIx < array.length) {
			if(predicate.test(array[arrayIx])) {
				newArray[newIx] = array[arrayIx];
				newIx++; arrayIx++;
			} else {
				arrayIx++;
			}
		}
		return newIx == arrayIx ? newArray : Arrays.copyOfRange(newArray, 0, newIx);
	}
	
}