/*
 * 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.commons.collections4;

import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;

import org.apache.commons.collections4.iterators.SingletonIterator;

/**
 * A FluentIterable provides a powerful yet simple API for manipulating
 * Iterable instances in a fluent manner.
 * <p>
 * A FluentIterable can be created either from an Iterable or from a set
 * of elements. The following types of methods are provided:
 * </p>
 * <ul>
 *   <li>fluent methods which return a new {@code FluentIterable} instance,
 *       providing a view of the original iterable (e.g. filter(Predicate));
 *   <li>conversion methods which copy the FluentIterable's contents into a
 *       new collection or array (e.g. toList());
 *   <li>utility methods which answer questions about the FluentIterable's
 *       contents (e.g. size(), anyMatch(Predicate)).
 *   <li>
 * </ul>
 * <p>
 * The following example outputs the first 3 even numbers in the range [1, 10]
 * into a list:
 * </p>
 * <pre>
 * List&lt;String&gt; result =
 *   FluentIterable
 *       .of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
 *       .filter(new Predicate&lt;Integer&gt;() {
 *                   public boolean evaluate(Integer number) {
 *                        return number % 2 == 0;
 *                   }
 *              )
 *       .transform(TransformerUtils.stringValueTransformer())
 *       .limit(3)
 *       .toList();
 * </pre>
 * The resulting list will contain the following elements:
 * <pre>[2, 4, 6]</pre>
 *
 * @param <E>  the element type
 * @since 4.1
 */
public class FluentIterable<E> implements Iterable<E> {

    /**
     * Creates a new empty FluentIterable.
     *
     * @param <T>  the element type
     * @return a new empty FluentIterable
     */
    public static <T> FluentIterable<T> empty() {
        return IterableUtils.EMPTY_ITERABLE;
    }

    /**
     * Constructs a new FluentIterable from the provided iterable. If the
     * iterable is already an instance of FluentIterable, the instance
     * will be returned instead.
     * <p>
     * The returned iterable's iterator supports {@code remove()} when the
     * corresponding input iterator supports it.
     *
     * @param <T>  the element type
     * @param iterable  the iterable to wrap into a FluentIterable, may not be null
     * @return a new FluentIterable wrapping the provided iterable
     * @throws NullPointerException if iterable is null
     */
    public static <T> FluentIterable<T> of(final Iterable<T> iterable) {
        IterableUtils.checkNotNull(iterable);
        if (iterable instanceof FluentIterable<?>) {
            return (FluentIterable<T>) iterable;
        }
        return new FluentIterable<>(iterable);
    }

    /**
     * Creates a new FluentIterable of the single provided element.
     * <p>
     * The returned iterable's iterator does not support {@code remove()}.
     *
     * @param <T>  the element type
     * @param singleton  the singleton element
     * @return a new FluentIterable containing the singleton
     */
    public static <T> FluentIterable<T> of(final T singleton) {
        return of(IteratorUtils.asIterable(new SingletonIterator<>(singleton, false)));
    }

    /**
     * Creates a new FluentIterable from the provided elements.
     * <p>
     * The returned iterable's iterator does not support {@code remove()}.
     *
     * @param <T>  the element type
     * @param elements  the elements to be contained in the FluentIterable
     * @return a new FluentIterable containing the provided elements
     */
    public static <T> FluentIterable<T> of(final T... elements) {
        return of(Arrays.asList(elements));
    }

    /** A reference to the wrapped iterable. */
    private final Iterable<E> iterable;

    /**
     * Don't allow instances.
     */
    FluentIterable() {
        this.iterable = this;
    }

    /**
     * Create a new FluentIterable by wrapping the provided iterable.
     * @param iterable  the iterable to wrap
     */
    private FluentIterable(final Iterable<E> iterable) {
        this.iterable = iterable;
    }

    /**
     * Checks if all elements contained in this iterable are matching the
     * provided predicate.
     * <p>
     * A {@code null} or empty iterable returns true.
     *
     * @param predicate  the predicate to use, may not be null
     * @return true if all elements contained in this iterable match the predicate,
     *   false otherwise
     * @throws NullPointerException if predicate is null
     */
    public boolean allMatch(final Predicate<? super E> predicate) {
        return IterableUtils.matchesAll(iterable, predicate);
    }

    /**
     * Checks if this iterable contains any element matching the provided predicate.
     * <p>
     * A {@code null} or empty iterable returns false.
     *
     * @param predicate  the predicate to use, may not be null
     * @return true if at least one element contained in this iterable matches the predicate,
     *   false otherwise
     * @throws NullPointerException if predicate is null
     */
    public boolean anyMatch(final Predicate<? super E> predicate) {
        return IterableUtils.matchesAny(iterable, predicate);
    }

    /**
     * Returns a new FluentIterable whose iterator will first traverse
     * the elements of the current iterable, followed by the provided
     * elements.
     *
     * @param elements  the elements to append to the iterable
     * @return a new iterable, combining this iterable with the elements
     */
    public FluentIterable<E> append(final E... elements) {
        return append(Arrays.asList(elements));
    }

    /**
     * Returns a new FluentIterable whose iterator will first traverse
     * the elements of the current iterable, followed by the elements
     * of the provided iterable.
     *
     * @param other  the other iterable to combine, may not be null
     * @return a new iterable, combining this iterable with other
     * @throws NullPointerException if other is null
     */
    public FluentIterable<E> append(final Iterable<? extends E> other) {
        return of(IterableUtils.chainedIterable(iterable, other));
    }

    /**
     * Returns an Enumeration that will enumerate all elements contained
     * in this iterable.
     *
     * @return an Enumeration over the elements of this iterable
     */
    public Enumeration<E> asEnumeration() {
        return IteratorUtils.asEnumeration(iterator());
    }

    /**
     * Returns a new FluentIterable whose iterator will traverse the
     * elements of the current and provided iterable in natural order.
     * <p>
     * Example: natural ordering
     * <ul>
     *   <li>this contains elements [1, 3, 5, 7]
     *   <li>other contains elements [2, 4, 6, 8]
     * </ul>
     * <p>
     * The returned iterable will traverse the elements in the following
     * order: [1, 2, 3, 4, 5, 6, 7, 8]
     *
     * @param other  the other iterable to collate, may not be null
     * @return a new iterable, collating this iterable with the other in natural order
     * @throws NullPointerException if other is null
     * @see org.apache.commons.collections4.iterators.CollatingIterator
     */
    public FluentIterable<E> collate(final Iterable<? extends E> other) {
        return of(IterableUtils.collatedIterable(iterable, other));
    }

    /**
     * Returns a new FluentIterable whose iterator will traverse the
     * elements of the current and provided iterable according to the
     * ordering defined by a comparator.
     * <p>
     * Example: descending order
     * <ul>
     *   <li>this contains elements [7, 5, 3, 1]
     *   <li>other contains elements [8, 6, 4, 2]
     * </ul>
     * <p>
     * The returned iterable will traverse the elements in the following
     * order: [8, 7, 6, 5, 4, 3, 2, 1]
     *
     * @param comparator  the comparator to define an ordering, may be null,
     *   in which case natural ordering will be used
     * @param other  the other iterable to collate, may not be null
     * @return a new iterable, collating this iterable with the other in natural order
     * @throws NullPointerException if other is null
     * @see org.apache.commons.collections4.iterators.CollatingIterator
     */
    public FluentIterable<E> collate(final Iterable<? extends E> other,
                                     final Comparator<? super E> comparator) {
        return of(IterableUtils.collatedIterable(comparator, iterable, other));
    }

    /**
     * Checks if the object is contained in this iterable.
     *
     * @param object  the object to check
     * @return true if the object is contained in this iterable, false otherwise
     */
    public boolean contains(final Object object) {
        return IterableUtils.contains(iterable, object);
    }

    /**
     * Traverses an iterator of this iterable and adds all elements
     * to the provided collection.
     *
     * @param collection  the collection to add the elements
     * @throws NullPointerException if collection is null
     */
    public void copyInto(final Collection<? super E> collection) {
        Objects.requireNonNull(collection, "collection");
        CollectionUtils.addAll(collection, iterable);
    }

    /**
     * This method fully traverses an iterator of this iterable and returns
     * a new iterable with the same contents, but without any reference
     * to the originating iterables and/or iterators.
     * <p>
     * Calling this method is equivalent to:
     * <pre>
     *   FluentIterable&lt;E&gt; someIterable = ...;
     *   FluentIterable.of(someIterable.toList());
     * </pre>
     *
     * @return a new iterable with the same contents as this iterable
     */
    public FluentIterable<E> eval() {
        return of(toList());
    }

    /**
     * Returns a new FluentIterable whose iterator will only return
     * elements from this iterable matching the provided predicate.
     *
     * @param predicate  the predicate used to filter elements
     * @return a new iterable, providing a filtered view of this iterable
     * @throws NullPointerException if predicate is null
     */
    public FluentIterable<E> filter(final Predicate<? super E> predicate) {
        return of(IterableUtils.filteredIterable(iterable, predicate));
    }

    /**
     * Applies the closure to all elements contained in this iterable.
     *
     * @param closure  the closure to apply to each element, may not be null
     * @throws NullPointerException if closure is null
     */
    public void forEach(final Closure<? super E> closure) {
        IterableUtils.forEach(iterable, closure);
    }

    /**
     * Returns the element at the provided position in this iterable.
     * In order to return the element, an iterator needs to be traversed
     * up to the requested position.
     *
     * @param position  the position of the element to return
     * @return the element
     * @throws IndexOutOfBoundsException if the provided position is outside the
     *   valid range of this iterable: [0, size)
     */
    public E get(final int position) {
        return IterableUtils.get(iterable, position);
    }

    /**
     * Checks if this iterable is empty.
     *
     * @return true if this iterable does not contain any elements, false otherwise
     */
    public boolean isEmpty() {
        return IterableUtils.isEmpty(iterable);
    }

    /** {@inheritDoc} */
    @Override
    public Iterator<E> iterator() {
        return iterable.iterator();
    }

    /**
     * Returns a new FluentIterable whose iterator will return at most
     * the provided maximum number of elements from this iterable.
     *
     * @param maxSize  the maximum number of elements
     * @return a new iterable, providing a bounded view of this iterable
     * @throws IllegalArgumentException if maxSize is negative
     */
    public FluentIterable<E> limit(final long maxSize) {
        return of(IterableUtils.boundedIterable(iterable, maxSize));
    }

    /**
     * Returns a new FluentIterable whose iterator will loop infinitely
     * over the elements from this iterable.
     *
     * @return a new iterable, providing a looping view of this iterable
     */
    public FluentIterable<E> loop() {
        return of(IterableUtils.loopingIterable(iterable));
    }

    /**
     * Returns a new FluentIterable whose iterator will traverse the
     * elements from this iterable in reverse order.
     *
     * @return a new iterable, providing a reversed view of this iterable
     */
    public FluentIterable<E> reverse() {
        return of(IterableUtils.reversedIterable(iterable));
    }

    /**
     * Returns the number of elements that are contained in this iterable.
     * In order to determine the size, an iterator needs to be traversed.
     *
     * @return the size of this iterable
     */
    public int size() {
        return IterableUtils.size(iterable);
    }

    /**
     * Returns a new FluentIterable whose iterator will skip the first
     * N elements from this iterable.
     *
     * @param elementsToSkip  the number of elements to skip
     * @return a new iterable, providing a view of this iterable by skipping
     *   the first N elements
     * @throws IllegalArgumentException if elementsToSkip is negative
     */
    public FluentIterable<E> skip(final long elementsToSkip) {
        return of(IterableUtils.skippingIterable(iterable, elementsToSkip));
    }

    /**
     * Returns an array containing all elements of this iterable by traversing
     * its iterator.
     *
     * @param arrayClass  the class of array to create
     * @return an array of the iterable contents
     * @throws ArrayStoreException if arrayClass is invalid
     */
    public E[] toArray(final Class<E> arrayClass) {
        return IteratorUtils.toArray(iterator(), arrayClass);
    }

    /**
     * Returns a mutable list containing all elements of this iterable
     * by traversing its iterator.
     * <p>
     * The returned list is guaranteed to be mutable.
     *
     * @return a list of the iterable contents
     */
    public List<E> toList() {
        return IterableUtils.toList(iterable);
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return IterableUtils.toString(iterable);
    }

    /**
     * Returns a new FluentIterable whose iterator will return all elements
     * of this iterable transformed by the provided transformer.
     *
     * @param <O>  the output element type
     * @param transformer  the transformer applied to each element
     * @return a new iterable, providing a transformed view of this iterable
     * @throws NullPointerException if transformer is null
     */
    public <O> FluentIterable<O> transform(final Transformer<? super E, ? extends O> transformer) {
        return of(IterableUtils.transformedIterable(iterable, transformer));
    }

    /**
     * Returns a new FluentIterable whose iterator will return a unique view
     * of this iterable.
     *
     * @return a new iterable, providing a unique view of this iterable
     */
    public FluentIterable<E> unique() {
        return of(IterableUtils.uniqueIterable(iterable));
    }

    /**
     * Returns a new FluentIterable whose iterator will return an unmodifiable
     * view of this iterable.
     *
     * @return a new iterable, providing an unmodifiable view of this iterable
     */
    public FluentIterable<E> unmodifiable() {
        return of(IterableUtils.unmodifiableIterable(iterable));
    }

    /**
     * Returns a new FluentIterable whose iterator will traverse
     * the elements of this iterable and the other iterable in
     * alternating order.
     *
     * @param other  the other iterable to interleave, may not be null
     * @return a new iterable, interleaving this iterable with others
     * @throws NullPointerException if other is null
     */
    public FluentIterable<E> zip(final Iterable<? extends E> other) {
        return of(IterableUtils.zippingIterable(iterable, other));
    }

    /**
     * Returns a new FluentIterable whose iterator will traverse
     * the elements of this iterable and the other iterables in
     * alternating order.
     *
     * @param others  the iterables to interleave, may not be null
     * @return a new iterable, interleaving this iterable with others
     * @throws NullPointerException if either of the provided iterables is null
     */
    public FluentIterable<E> zip(final Iterable<? extends E>... others) {
        return of(IterableUtils.zippingIterable(iterable, others));
    }

}
