/*
 * Copyright (c) 2008, Harald Kuhr
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.twelvemonkeys.util;

import com.twelvemonkeys.lang.Validate;

import java.lang.reflect.Array;
import java.util.*;

import static com.twelvemonkeys.lang.Validate.isTrue;
import static com.twelvemonkeys.lang.Validate.notNull;

/**
 * A utility class with some useful collection-related functions.
 *
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @author <A href="mailto:eirik.torske@twelvemonkeys.no">Eirik Torske</A>
 * @author last modified by $Author: haku $
 * @version $Id: com/twelvemonkeys/util/CollectionUtil.java#3 $
 * @see Collections
 * @see Arrays
 */
public final class CollectionUtil {

    /**
     * Testing only.
     *
     * @param pArgs command line arguents
     */
    @SuppressWarnings({"UnusedDeclaration", "UnusedAssignment", "unchecked"})
    public static void main(String[] pArgs) {
        int howMany = 1000;

        if (pArgs.length > 0) {
            howMany = Integer.parseInt(pArgs[0]);
        }
        long start;
        long end;

        /*
      int[] intArr1 = new int[] {
        0, 1, 2, 3, 4, 5, 6, 7, 8, 9
      };
      int[] intArr2 = new int[] {
        10, 11, 12, 13, 14, 15, 16, 17, 18, 19
      };
      start = System.currentTimeMillis();

      for (int i = 0; i < howMany; i++) {
        intArr1 = (int[]) mergeArrays(intArr1, 0, intArr1.length, intArr2, 0, intArr2.length);

      }
      end = System.currentTimeMillis();

      System.out.println("mergeArrays: " + howMany + " * " + intArr2.length + "  ints took " + (end - start) + " milliseconds (" + intArr1.length
                         + ")");
    */

        ////////////////////////////////
        String[] stringArr1 = new String[]{
            "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"
        };

        /*
        String[] stringArr2 = new String[] {
          "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"
        };

        start = System.currentTimeMillis();
        for (int i = 0; i < howMany; i++) {
          stringArr1 = (String[]) mergeArrays(stringArr1, 0, stringArr1.length, stringArr2, 0, stringArr2.length);

        }
        end = System.currentTimeMillis();
        System.out.println("mergeArrays: " + howMany + " * " + stringArr2.length + "  Strings took " + (end - start) + " milliseconds ("
                           + stringArr1.length + ")");


          start   = System.currentTimeMillis();
          while (intArr1.length > stringArr2.length) {
            intArr1 = (int[]) subArray(intArr1, 0, intArr1.length - stringArr2.length);

          }
          end = System.currentTimeMillis();

          System.out.println("subArray: " + howMany + " * " + intArr2.length + "  ints took " + (end - start) + " milliseconds (" + intArr1.length
                             + ")");

          start = System.currentTimeMillis();
          while (stringArr1.length > stringArr2.length) {
            stringArr1 = (String[]) subArray(stringArr1, stringArr2.length);

          }
          end = System.currentTimeMillis();
          System.out.println("subArray: " + howMany + " * " + stringArr2.length + "  Strings took " + (end - start) + " milliseconds ("
                             + stringArr1.length + ")");

    */
        stringArr1 = new String[]{
            "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen",
            "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"
        };
        System.out.println("\nFilterIterators:\n");
        List list = Arrays.asList(stringArr1);
        Iterator iter = new FilterIterator(list.iterator(), new FilterIterator.Filter() {

            public boolean accept(Object pElement) {
                return ((String) pElement).length() > 5;
            }
        });

        while (iter.hasNext()) {
            String str = (String) iter.next();

            System.out.println(str + " has more than 5 letters!");
        }
        iter = new FilterIterator(list.iterator(), new FilterIterator.Filter() {

            public boolean accept(Object pElement) {
                return ((String) pElement).length() <= 5;
            }
        });

        while (iter.hasNext()) {
            String str = (String) iter.next();

            System.out.println(str + " has less than, or exactly 5 letters!");
        }
        start = System.currentTimeMillis();

        for (int i = 0; i < howMany; i++) {
            iter = new FilterIterator(list.iterator(), new FilterIterator.Filter() {

                public boolean accept(Object pElement) {
                    return ((String) pElement).length() <= 5;
                }
            });
            while (iter.hasNext()) {
                iter.next();
                System.out.print("");
            }
        }

//        end = System.currentTimeMillis();
//        System.out.println("Time: " + (end - start) + " ms");
//        System.out.println("\nClosureCollection:\n");
//        forEach(list, new Closure() {
//
//            public void execute(Object pElement) {
//
//                String str = (String) pElement;
//
//                if (str.length() > 5) {
//                    System.out.println(str + " has more than 5 letters!");
//                }
//                else {
//                    System.out.println(str + " has less than, or exactly 5 letters!");
//                }
//            }
//        });
//        start = System.currentTimeMillis();
//        for (int i = 0; i < howMany; i++) {
//            forEach(list, new Closure() {
//
//                public void execute(Object pElement) {
//
//                    String str = (String) pElement;
//
//                    if (str.length() <= 5) {
//                        System.out.print("");
//                    }
//                }
//            });
//        }
//        end = System.currentTimeMillis();
//        System.out.println("Time: " + (end - start) + " ms");
    }

    // Disallow creating objects of this type
    private CollectionUtil() {}

    /**
     * Merges two arrays into a new array. Elements from array1 and array2 will
     * be copied into a new array, that has array1.length + array2.length
     * elements.
     *
     * @param pArray1 First array
     * @param pArray2 Second array, must be compatible with (assignable from)
     *                the first array
     * @return A new array, containing the values of array1 and array2. The
     *         array (wrapped as an object), will have the length of array1 +
     *         array2, and can be safely cast to the type of the array1
     *         parameter.
     * @see #mergeArrays(Object,int,int,Object,int,int)
     * @see java.lang.System#arraycopy(Object,int,Object,int,int)
     */
    public static Object mergeArrays(Object pArray1, Object pArray2) {
        return mergeArrays(pArray1, 0, Array.getLength(pArray1), pArray2, 0, Array.getLength(pArray2));
    }

    /**
     * Merges two arrays into a new array. Elements from pArray1 and pArray2 will
     * be copied into a new array, that has pLength1 + pLength2 elements.
     *
     * @param pArray1  First array
     * @param pOffset1 the offset into the first array
     * @param pLength1 the number of elements to copy from the first array
     * @param pArray2  Second array, must be compatible with (assignable from)
     *                 the first array
     * @param pOffset2 the offset into the second array
     * @param pLength2 the number of elements to copy from the second array
     * @return A new array, containing the values of pArray1 and pArray2. The
     *         array (wrapped as an object), will have the length of pArray1 +
     *         pArray2, and can be safely cast to the type of the pArray1
     *         parameter.
     * @see java.lang.System#arraycopy(Object,int,Object,int,int)
     */
    @SuppressWarnings({"SuspiciousSystemArraycopy"})
    public static Object mergeArrays(Object pArray1, int pOffset1, int pLength1, Object pArray2, int pOffset2, int pLength2) {
        Class class1 = pArray1.getClass();
        Class type = class1.getComponentType();

        // Create new array of the new length
        Object array = Array.newInstance(type, pLength1 + pLength2);

        System.arraycopy(pArray1, pOffset1, array, 0, pLength1);
        System.arraycopy(pArray2, pOffset2, array, pLength1, pLength2);
        return array;
    }

    /**
     * Creates an array containing a subset of the original array.
     * If the sub array is same length as the original
     * ({@code pStart == 0}), the original array will be returned.
     *
     * @param pArray the original array
     * @param pStart the start index of the original array
     * @return a subset of the original array, or the original array itself,
     *         if {@code pStart} is 0.
     *
     * @throws IllegalArgumentException if {@code pArray} is {@code null} or
     *         if {@code pArray} is not an array.
     * @throws ArrayIndexOutOfBoundsException if {@code pStart} &lt; 0
     */
    public static Object subArray(Object pArray, int pStart) {
        return subArray(pArray, pStart, -1);
    }

    /**
     * Creates an array containing a subset of the original array.
     * If the sub array is same length as the original
     * ({@code pStart == 0}), the original array will be returned.
     *
     * @param <T> the type of array
     * @param pArray the original array
     * @param pStart the start index of the original array
     * @return a subset of the original array, or the original array itself,
     *         if {@code pStart} is 0.
     *
     * @throws IllegalArgumentException if {@code pArray} is {@code null}
     * @throws ArrayIndexOutOfBoundsException if {@code pStart} &lt; 0
     */
    public static <T> T[] subArray(T[] pArray, int pStart) {
        return subArray(pArray, pStart, -1);
    }

    /**
     * Creates an array containing a subset of the original array.
     * If the {@code pLength} parameter is negative, it will be ignored.
     * If there are not {@code pLength} elements in the original array
     * after {@code pStart}, the {@code pLength} parameter will be
     * ignored.
     * If the sub array is same length as the original, the original array will
     * be returned.
     *
     * @param pArray  the original array
     * @param pStart  the start index of the original array
     * @param pLength the length of the new array
     * @return a subset of the original array, or the original array itself,
     *         if {@code pStart} is 0 and {@code pLength} is either
     *         negative, or greater or equal to {@code pArray.length}.
     *
     * @throws IllegalArgumentException if {@code pArray} is {@code null} or
     *         if {@code pArray} is not an array.
     * @throws ArrayIndexOutOfBoundsException if {@code pStart} &lt; 0
     */
    @SuppressWarnings({"SuspiciousSystemArraycopy"})
    public static Object subArray(Object pArray, int pStart, int pLength) {
        Validate.notNull(pArray, "array");

        // Get component type
        Class type;

        // Sanity check start index
        if (pStart < 0) {
            throw new ArrayIndexOutOfBoundsException(pStart + " < 0");
        }
        // Check if argument is array
        else if ((type = pArray.getClass().getComponentType()) == null) {
            // NOTE: No need to test class.isArray(), really
            throw new IllegalArgumentException("Not an array: " + pArray);
        }

        // Store original length
        int originalLength = Array.getLength(pArray);

        // Find new length, stay within bounds
        int newLength = (pLength < 0)
                ? Math.max(0, originalLength - pStart)
                : Math.min(pLength, Math.max(0, originalLength - pStart));

        // Store result
        Object result;

        if (newLength < originalLength) {
            // Create sub array & copy into
            result = Array.newInstance(type, newLength);
            System.arraycopy(pArray, pStart, result, 0, newLength);
        }
        else {
            // Just return original array
            // NOTE: This can ONLY happen if pStart == 0
            result = pArray;
        }

        // Return
        return result;
    }

    /**
     * Creates an array containing a subset of the original array.
     * If the {@code pLength} parameter is negative, it will be ignored.
     * If there are not {@code pLength} elements in the original array
     * after {@code pStart}, the {@code pLength} parameter will be
     * ignored.
     * If the sub array is same length as the original, the original array will
     * be returned.
     *
     * @param <T> the type of array
     * @param pArray  the original array
     * @param pStart  the start index of the original array
     * @param pLength the length of the new array
     * @return a subset of the original array, or the original array itself,
     *         if {@code pStart} is 0 and {@code pLength} is either
     *         negative, or greater or equal to {@code pArray.length}.
     *
     * @throws IllegalArgumentException if {@code pArray} is {@code null}
     * @throws ArrayIndexOutOfBoundsException if {@code pStart} &lt; 0
     */
    @SuppressWarnings("unchecked")
    public static <T> T[] subArray(T[] pArray, int pStart, int pLength) {
        return (T[]) subArray((Object) pArray, pStart, pLength);
    }

    public static <T> Iterator<T> iterator(final Enumeration<T> pEnum) {
        notNull(pEnum, "enumeration");
        
        return new Iterator<T>() {
            public boolean hasNext() {
                return pEnum.hasMoreElements();
            }

            public T next() {
                return pEnum.nextElement();
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

    /**
     * Adds all elements of the iterator to the collection.
     *
     * @param pCollection the collection
     * @param pIterator the elements to add
     *
     * @throws UnsupportedOperationException if {@code add} is not supported by
     *         the given collection.
     * @throws ClassCastException class of the specified element prevents it
     *         from being added to this collection.
     * @throws NullPointerException if the specified element is {@code null} and this
     *         collection does not support {@code null} elements.
     * @throws IllegalArgumentException some aspect of this element prevents
     *         it from being added to this collection.
     */
    public static <E> void addAll(Collection<E> pCollection, Iterator<? extends E> pIterator) {
        while (pIterator.hasNext()) {
            pCollection.add(pIterator.next());
        }
    }

    // Is there a use case where Arrays.asList(pArray).iterator() can't ne used?
    /**
     * Creates a thin {@link Iterator} wrapper around an array.
     *
     * @param pArray the array to iterate
     * @return a new {@link ListIterator}
     * @throws IllegalArgumentException if {@code pArray} is {@code null},
     *         {@code pStart &lt; 0}, or
     *         {@code pLength &gt; pArray.length - pStart}
     */
    public static <E> ListIterator<E> iterator(final E[] pArray) {
        return iterator(pArray, 0, notNull(pArray).length);
    }

    /**
     * Creates a thin {@link Iterator} wrapper around an array.
     *
     * @param pArray the array to iterate
     * @param pStart the offset into the array
     * @param pLength the number of elements to include in the iterator
     * @return a new {@link ListIterator}
     * @throws IllegalArgumentException if {@code pArray} is {@code null},
     *         {@code pStart &lt; 0}, or
     *         {@code pLength &gt; pArray.length - pStart}
     */
    public static <E> ListIterator<E> iterator(final E[] pArray, final int pStart, final int pLength) {
        return new ArrayIterator<E>(pArray, pStart, pLength);
    }

    /**
     * Creates an inverted mapping of the key/value pairs in the given map.
     *
     * @param pSource the source map
     * @return a new {@code Map} of same type as {@code pSource}
     * @throws IllegalArgumentException if {@code pSource == null},
     *         or if a new map can't be instantiated,
     *         or if source map contains duplicates.
     *
     * @see #invert(java.util.Map, java.util.Map, DuplicateHandler)
     */
    public static <K, V> Map<V, K> invert(Map<K, V> pSource) {
        return invert(pSource, null, null);
    }

    /**
     * Creates an inverted mapping of the key/value pairs in the given map.
     * Optionally, a duplicate handler may be specified, to resolve duplicate keys in the result map.
     *
     * @param pSource the source map
     * @param pResult the map used to contain the result, may be {@code null},
     *        in that case a new {@code Map} of same type as {@code pSource} is created.
     *        The result map <em>should</em> be empty, otherwise duplicate values will need to be resolved.
     * @param pHandler duplicate handler, may be {@code null} if source map don't contain duplicate values
     * @return {@code pResult}, or a new {@code Map} if {@code pResult == null}
     * @throws IllegalArgumentException if {@code pSource == null},
     *         or if result map is {@code null} and a new map can't be instantiated,
     *         or if source map contains duplicate values and {@code pHandler == null}.
     */
    // TODO: Create a better duplicate handler, that takes Entries as parameters and returns an Entry
    public static <K, V> Map<V, K> invert(Map<K, V> pSource, Map<V, K> pResult, DuplicateHandler<K> pHandler) {
        if (pSource == null) {
            throw new IllegalArgumentException("source == null");
        }

        Map<V, K> result = pResult;
        if (result == null) {
            try {
                //noinspection unchecked
                result = pSource.getClass().newInstance();
            }
            catch (InstantiationException e) {
                // Handled below
            }
            catch (IllegalAccessException e) {
                // Handled below
            }

            if (result == null) {
                throw new IllegalArgumentException("result == null and source class " + pSource.getClass() + " cannot be instantiated.");
            }
        }

        // Copy entries into result map, inversed
        Set<Map.Entry<K, V>> entries = pSource.entrySet();
        for (Map.Entry<K, V> entry : entries) {
            V newKey = entry.getValue();
            K newValue = entry.getKey();

            // Handle dupliates
            if (result.containsKey(newKey)) {
                if (pHandler != null) {
                    newValue = pHandler.resolve(result.get(newKey), newValue);
                }
                else {
                    throw new IllegalArgumentException("Result would include duplicate keys, but no DuplicateHandler specified.");
                }
            }

            result.put(newKey, newValue);
        }

        return result;
    }

    public static <T> Comparator<T> reverseOrder(final Comparator<T> pOriginal) {
        return new ReverseComparator<T>(pOriginal);
    }

    private static class ReverseComparator<T> implements Comparator<T> {
        private final Comparator<T> comparator;

        public ReverseComparator(final Comparator<T> pComparator) {
            comparator = notNull(pComparator);
        }


        public int compare(T pLeft, T pRight) {
            int result = comparator.compare(pLeft, pRight);

            // We can't simply return -result, as -Integer.MIN_VALUE == Integer.MIN_VALUE.
            return -(result | (result >>> 1));
        }
    }

    @SuppressWarnings({"unchecked", "UnusedDeclaration"})
    static <T extends Iterator<? super E>, E> T generify(final Iterator<?> pIterator, final Class<E> pElementType) {
        return (T) pIterator;
    }

    @SuppressWarnings({"unchecked", "UnusedDeclaration"})
    static <T extends Collection<? super E>, E> T generify(final Collection<?> pCollection, final Class<E> pElementType) {
        return (T) pCollection;
    }

    @SuppressWarnings({"unchecked", "UnusedDeclaration"})
    static <T extends Map<? super K, ? super V>, K, V> T generify(final Map<?, ?> pMap, final Class<K> pKeyType, final Class<V> pValueType) {
        return (T) pMap;
    }

    @SuppressWarnings({"unchecked"})
    static <T extends Collection<? super E>, E> T generify2(Collection<?> pCollection) {
        return (T) pCollection;
    }

    private static class ArrayIterator<E> implements ListIterator<E> {
        private int next;
        private final int start;
        private final int length;
        private final E[] array;

        public ArrayIterator(final E[] pArray, final int pStart, final int pLength) {
            array = notNull(pArray, "array");
            start = isTrue(pStart >= 0, pStart, "start < 0: %d");
            length = isTrue(pLength <= pArray.length - pStart, pLength, "length > array.length - start: %d");
            next = start;
        }

        public boolean hasNext() {
            return next < length + start;
        }

        public E next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }

            try {
                return array[next++];
            }
            catch (ArrayIndexOutOfBoundsException e) {
                NoSuchElementException nse = new NoSuchElementException(e.getMessage());
                nse.initCause(e);
                throw nse;
            }
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

        public void add(E pElement) {
            throw new UnsupportedOperationException();
        }

        public boolean hasPrevious() {
            return next > start;
        }

        public int nextIndex() {
            return next - start;
        }

        public E previous() {
            if (!hasPrevious()) {
                throw new NoSuchElementException();
            }

            try {
                return array[--next];
            }
            catch (ArrayIndexOutOfBoundsException e) {
                NoSuchElementException nse = new NoSuchElementException(e.getMessage());
                nse.initCause(e);
                throw nse;
            }
        }

        public int previousIndex() {
            return nextIndex() - 1;
        }

        public void set(E pElement) {
            array[next - 1] = pElement;
        }
    }
}