// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
// * with the License.  You may obtain a copy of the License at                                                              *
// *                                                                                                                         *
// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
// *                                                                                                                         *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
// * specific language governing permissions and limitations under the License.                                              *
// ***************************************************************************************************************************
package org.apache.juneau.internal;

import static org.apache.juneau.internal.StringUtils.*;
import static org.apache.juneau.internal.ObjectUtils.*;

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

import org.apache.juneau.*;
import org.apache.juneau.parser.*;

/**
 * Utility methods for collections.
 */
public final class CollectionUtils {

	/**
	 * Add a value to a list if the value is not null.
	 *
	 * @param l The list to add to.
	 * @param o The element to add.
	 * @return The same list.
	 */
	public static <T> List<T> addIfNotNull(List<T> l, T o) {
		if (o != null)
			l.add(o);
		return l;
	}

	/**
	 * Adds the contents of one list to the other in reverse order.
	 *
	 * <p>
	 * i.e. add values from 2nd list from end-to-start order to the end of the 1st list.
	 *
	 * @param list The list to append to.
	 * @param append Contains the values to append to the list.
	 * @return The same list.
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public static List<?> addReverse(List list, List append) {
		for (ListIterator i = append.listIterator(append.size()); i.hasPrevious();)
			list.add(i.previous());
		return list;
	}

	/**
	 * Adds the contents of the array to the list in reverse order.
	 *
	 * <p>
	 * i.e. add values from the array from end-to-start order to the end of the list.
	 *
	 * @param list The list to append to.
	 * @param append Contains the values to append to the list.
	 * @return The same list.
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public static List<?> addReverse(List list, Object[] append) {
		for (int i = append.length - 1; i >= 0; i--)
			list.add(append[i]);
		return list;
	}

	/**
	 * Returns an iterable over the specified collection.
	 *
	 * @param c The collection to iterate over.
	 * @param reverse If <jk>true</jk>, iterate in reverse order.
	 * @return An iterable over the collection.
	 */
	public static <T> Iterable<T> iterable(final Collection<T> c, boolean reverse) {
		if (reverse)
			return new ReverseIterable<>(c instanceof List ? (List<T>)c : new ArrayList<>(c));
		return c;
	}

	/**
	 * Returns an iterable over the specified list.
	 *
	 * @param c The collection to iterate over.
	 * @param reverse If <jk>true</jk>, iterate in reverse order.
	 * @return An iterable over the collection.
	 */
	public static <T> Iterable<T> iterable(final List<T> c, boolean reverse) {
		if (reverse)
			return new ReverseIterable<>(c);
		return c;
	}

	/**
	 * Returns a reverse iterable over the specified list.
	 *
	 * @param c The collection to iterate over.
	 * @return An iterable over the collection.
	 */
	public static <T> Iterable<T> reverseIterable(final List<T> c) {
		return iterable(c, true);
	}

	/**
	 * Returns an iterable over the specified array.
	 *
	 * @param c The collection to iterate over.
	 * @param reverse If <jk>true</jk>, iterate in reverse order.
	 * @return An iterable over the collection.
	 */
	public static <T> Iterable<T> iterable(T[] c, boolean reverse) {
		if (reverse)
			return new ReverseIterable<>(Arrays.asList(c));
		return Arrays.asList(c);
	}

	/**
	 * Returns a reverse iterable over the specified array.
	 *
	 * @param c The collection to iterate over.
	 * @return An iterable over the collection.
	 */
	public static <T> Iterable<T> reverseIterable(T[] c) {
		return iterable(c, true);
	}

	/**
	 * Returns an iterable over the specified enumeration.
	 *
	 * @param e The collection to iterate over.
	 * @return An iterable over the enumeration.
	 */
	public static <E> Iterable<E> iterable(final Enumeration<E> e) {
		if (e == null)
			return null;
		return new Iterable<E>() {
			@Override
			public Iterator<E> iterator() {
				return new Iterator<E>() {
					@Override
					public boolean hasNext() {
						return e.hasMoreElements();
					}
					@Override
					public E next() {
						return e.nextElement();
					}
					@Override
					public void remove() {
						throw new UnsupportedOperationException();
					}
				};
			}
		};
	}

	/**
	 * Creates an unmodifiable list from the specified list.
	 *
	 * @param l The collection to copy from.
	 * @return An unmodifiable view of the list, or a {@link Collections#emptyList()}
	 * 	if the list was empty or <jk>null</jk>.
	 */
	public static <T> List<T> unmodifiableList(List<T> l) {
		if (l == null || l.isEmpty())
			return Collections.emptyList();
		return Collections.unmodifiableList(l);
	}

	/**
	 * Creates an unmodifiable list from the specified array.
	 *
	 * @param l The collection to copy from.
	 * @return An unmodifiable view of the list, or a {@link Collections#emptyList()}
	 * 	if the list was empty or <jk>null</jk>.
	 */
	public static <T> List<T> unmodifiableList(T[] l) {
		if (l == null || l.length == 0)
			return Collections.emptyList();
		return Collections.unmodifiableList(Arrays.asList(l));
	}

	/**
	 * Creates an immutable list from the specified collection.
	 *
	 * @param l The collection to copy from.
	 * @return An unmodifiable {@link ArrayList} copy of the collection, or a {@link Collections#emptyList()}
	 * 	if the collection was empty or <jk>null</jk>.
	 */
	public static <T> List<T> immutableList(Collection<T> l) {
		if (l == null || l.isEmpty())
			return Collections.emptyList();
		return Collections.unmodifiableList(new ArrayList<>(l));
	}

	/**
	 * Creates an immutable list from the specified array.
	 *
	 * @param l The array to copy from.
	 * @return An unmodifiable {@link ArrayList} copy of the collection, or a {@link Collections#emptyList()}
	 * 	if the collection was empty or <jk>null</jk>.
	 */
	public static <T> List<T> immutableList(T[] l) {
		if (l == null || l.length == 0)
			return Collections.emptyList();
		return Collections.unmodifiableList(new ArrayList<>(Arrays.asList(l)));
	}

	/**
	 * Creates an immutable map from the specified map.
	 *
	 * @param m The map to copy from.
	 * @return An unmodifiable {@link LinkedHashMap} copy of the collection, or a {@link Collections#emptyMap()}
	 * 	if the collection was empty or <jk>null</jk>.
	 */
	public static <K,V> Map<K,V> immutableMap(Map<K,V> m) {
		if (m == null || m.isEmpty())
			return Collections.emptyMap();
		return Collections.unmodifiableMap(new LinkedHashMap<>(m));
	}

	/**
	 * Creates an unmodifiable map from the specified map.
	 *
	 * @param m The map to copy from.
	 * @return An unmodifiable view of the collection, or a {@link Collections#emptyMap()}
	 * 	if the collection was empty or <jk>null</jk>.
	 */
	public static <K,V> Map<K,V> unmodifiableMap(Map<K,V> m) {
		if (m == null || m.isEmpty())
			return Collections.emptyMap();
		return Collections.unmodifiableMap(m);
	}

	/**
	 * Adds a set of values to an existing list.
	 *
	 * @param appendTo
	 * 	The list to append to.
	 * 	<br>If <jk>null</jk>, a new {@link ArrayList} will be created.
	 * @param values The values to add.
	 * @param type The data type of the elements.
	 * @param args The generic type arguments of the data type.
	 * @return The converted value, or <jk>null</jk> if the input was null.
	 */
	public static <T> List<T> addToList(List<T> appendTo, Object[] values, Class<T> type, Type...args) {
		if (values == null)
			return appendTo;
		try {
			List<T> l = appendTo;
			if (appendTo == null)
				l = new ArrayList<>();
			for (Object o : values) {
				if (o != null) {
					if (isObjectList(o, false)) {
						for (Object o2 : new ObjectList(o.toString()))
							l.add(toType(o2, type, args));
					} else if (o instanceof Collection) {
						for (Object o2 : (Collection<?>)o)
							l.add(toType(o2, type, args));
					} else if (o.getClass().isArray()) {
						for (int i = 0; i < Array.getLength(o); i++)
							l.add(toType(Array.get(o, i), type, args));
					} else {
						l.add(toType(o, type, args));
					}
				}
			}
			return l.isEmpty() ? null : l;
		} catch (ParseException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Adds a set of values to an existing map.
	 *
	 * @param appendTo
	 * 	The map to append to.
	 * 	<br>If <jk>null</jk>, a new {@link LinkedHashMap} will be created.
	 * @param values The values to add.
	 * @param keyType The data type of the keys.
	 * @param valueType The data type of the values.
	 * @param valueTypeArgs The generic type arguments of the data type of the values.
	 * @return The converted value, or <jk>null</jk> if the input was null.
	 */
	@SuppressWarnings("unchecked")
	public static <K,V> Map<K,V> addToMap(Map<K,V> appendTo, Object[] values, Class<K> keyType, Class<V> valueType, Type...valueTypeArgs) {
		if (values == null)
			return appendTo;
		try {
			Map<K,V> m = appendTo;
			if (m == null)
				m = new LinkedHashMap<>();
			for (Object o : values) {
				if (o != null) {
					if (isObjectMap(o, false)) {
						for (Map.Entry<String,Object> e : new ObjectMap(o.toString()).entrySet())
							m.put(toType(e.getKey(), keyType), toType(e.getValue(), valueType, valueTypeArgs));
					} else if (o instanceof Map) {
						for (Map.Entry<Object,Object> e : ((Map<Object,Object>)o).entrySet())
							m.put(toType(e.getKey(), keyType), toType(e.getValue(), valueType, valueTypeArgs));
					} else {
						throw new FormattedRuntimeException("Invalid object type {0} passed to addToMap()", o.getClass().getName());
					}
				}
			}
			return m.isEmpty() ? null : m;
		} catch (ParseException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Creates a new list from the specified collection.
	 *
	 * @param val The value to copy from.
	 * @return A new {@link ArrayList}, or <jk>null</jk> if the input was null.
	 */
	public static <T> List<T> newList(Collection<T> val) {
		if (val == null)
			return null;
		return new ArrayList<>(val);
	}

	/**
	 * Copies the specified values into an existing list.
	 *
	 * @param l
	 * 	The list to add to.
	 * 	<br>If <jk>null</jk>, a new {@link ArrayList} will be created.
	 * @param val The values to add.
	 * @return The list with values copied into it.
	 */
	public static <T> List<T> addToList(List<T> l, Collection<T> val) {
		if (val != null) {
			if (l == null)
				l = new ArrayList<>(val);
			else
				l.addAll(val);
		}
		return l;
	}

	/**
	 * Creates a new map from the specified map.
	 *
	 * @param val The value to copy from.
	 * @return A new {@link LinkedHashMap}, or <jk>null</jk> if the input was null.
	 */
	public static <K,V> Map<K,V> newMap(Map<K,V> val) {
		if (val == null)
			return null;
		return new LinkedHashMap<>(val);
	}

	/**
	 * Copies the specified values into an existing map.
	 *
	 * @param m
	 * 	The map to add to.
	 * 	<br>If <jk>null</jk>, a new {@link LinkedHashMap} will be created.
	 * @param val The values to add.
	 * @return The list with values copied into it.
	 */
	public static <K,V> Map<K,V> addToMap(Map<K,V> m, Map<K,V> val) {
		if (val != null) {
			if (m == null)
				m = new LinkedHashMap<>(val);
			else
				m.putAll(val);
		}
		return m;
	}

	/**
	 * Adds a single entry into an existing map.
	 *
	 * @param m
	 * 	The map to add to.
	 * 	<br>If <jk>null</jk>, a new {@link LinkedHashMap} will be created.
	 * @param key The entry key.
	 * @param value The entry value.
	 * @return The list with values copied into it.
	 */
	public static <K,V> Map<K,V> addToMap(Map<K,V> m, K key, V value) {
		if (m == null)
			m = new LinkedHashMap<>();
		m.put(key, value);
		return m;
	}

	/**
	 * Creates a new map from the specified map.
	 *
	 * @param val The value to copy from.
	 * @param comparator The key comparator to use, or <jk>null</jk> to use natural ordering.
	 * @return A new {@link LinkedHashMap}, or <jk>null</jk> if the input was null.
	 */
	public static <K,V> Map<K,V> newSortedMap(Map<K,V> val, Comparator<K> comparator) {
		if (val == null)
			return null;
		Map<K,V> m = new TreeMap<>(comparator);
		m.putAll(val);
		return m;
	}

	/**
	 * Creates a case-insensitive ordered set out of the specified string values.
	 *
	 * @param values The values to populate the set with.
	 * @return A new ordered set.
	 */
	public static Set<String> newSortedCaseInsensitiveSet(String...values) {
		Set<String> s = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER) {
			private static final long serialVersionUID = 1L;
			@Override
			public boolean contains(Object v) {
				return v == null ? false : super.contains(v);
			}
		};
		for (String v : values)
			if (v != null)
				s.add(v);
		return s;
	}

	/**
	 * Creates a case-insensitive ordered set out of the specified string values.
	 *
	 * @param values
	 * 	A comma-delimited list of the values to populate the set with.
	 * @return A new ordered set.
	 */
	public static Set<String> newSortedCaseInsensitiveSet(String values) {
		return newSortedCaseInsensitiveSet(StringUtils.split(StringUtils.emptyIfNull(values)));
	}

	/**
	 * Same as {@link #newSortedCaseInsensitiveSet(String)} but makes the set unmodifiable.
	 *
	 * @param values
	 * 	A comma-delimited list of the values to populate the set with.
	 * @return A new ordered set.
	 */
	public static Set<String> newUnmodifiableSortedCaseInsensitiveSet(String values) {
		return Collections.unmodifiableSet(newSortedCaseInsensitiveSet(StringUtils.split(StringUtils.emptyIfNull(values))));
	}

	/**
	 * Copies the specified values into an existing map.
	 *
	 * @param m
	 * 	The map to add to.
	 * 	<br>If <jk>null</jk>, a new {@link LinkedHashMap} will be created.
	 * @param val The values to add.
	 * @param comparator The key comparator to use, or <jk>null</jk> to use natural ordering.
	 * @return The list with values copied into it.
	 */
	public static <K,V> Map<K,V> addToSortedMap(Map<K,V> m, Map<K,V> val, Comparator<K> comparator) {
		if (val != null) {
			if (m == null) {
				m = new TreeMap<>(comparator);
				m.putAll(val);
			} else {
				m.putAll(val);
			}
		}
		return m;
	}

	/**
	 * Adds a single entry into an existing map.
	 *
	 * @param m
	 * 	The map to add to.
	 * 	<br>If <jk>null</jk>, a new {@link LinkedHashMap} will be created.
	 * @param key The entry key.
	 * @param value The entry value.
	 * @param comparator The key comparator to use, or <jk>null</jk> to use natural ordering.
	 * @return The list with values copied into it.
	 */
	public static <K,V> Map<K,V> addToSortedMap(Map<K,V> m, K key, V value, Comparator<K> comparator) {
		if (m == null)
			m = new TreeMap<>(comparator);
		m.put(key, value);
		return m;
	}

	/**
	 * Converts the specified arguments into an unmodifiable {@link HashSet}.
	 *
	 * @param values The entries to populate the hashset with.
	 * @return A new {@link HashSet} populated with the specified arguments.
	 */
	@SafeVarargs
	public static <T> Set<T> newUnmodifiableHashSet(T...values) {
		return Collections.unmodifiableSet(newHashSet(values));
	}

	/**
	 * Converts the specified arguments into a modifiable {@link HashSet}.
	 *
	 * @param values The entries to populate the hashset with.
	 * @return A new {@link HashSet} populated with the specified arguments.
	 */
	@SafeVarargs
	public static <T> Set<T> newHashSet(T...values) {
		return new HashSet<>(Arrays.asList(values));
	}

	/**
	 * Converts the specified arguments into an unmodifiable {@link LinkedHashSet}.
	 *
	 * @param values The entries to populate the hashset with.
	 * @return A new {@link LinkedHashSet} populated with the specified arguments.
	 */
	@SafeVarargs
	public static <T> Set<T> newUnmodifiableLinkedHashSet(T...values) {
		return Collections.unmodifiableSet(newLinkedHashSet(values));
	}

	/**
	 * Converts the specified arguments into a modifiable {@link LinkedHashSet}.
	 *
	 * @param values The entries to populate the hashset with.
	 * @return A new {@link LinkedHashSet} populated with the specified arguments.
	 */
	@SafeVarargs
	public static <T> Set<T> newLinkedHashSet(T...values) {
		return new LinkedHashSet<>(Arrays.asList(values));
	}

	/**
	 * Simple passthrough to {@link Collections#emptySet()}
	 *
	 * @return A new unmodifiable empty set.
	 */
	public static <T> Set<T> emptySet() {
		return Collections.emptySet();
	}

	/**
	 * Simple passthrough to {@link Collections#emptyList()}
	 *
	 * @return A new unmodifiable empty list.
	 */
	public static <T> List<T> emptyList() {
		return Collections.emptyList();
	}

	/**
	 * Simple passthrough to {@link Collections#emptyMap()}
	 *
	 * @return A new unmodifiable empty set.
	 */
	public static <K,V> Map<K,V> emptyMap() {
		return Collections.emptyMap();
	}
}
