package com.atlassian.bitbucket.util;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;

import static com.atlassian.bitbucket.util.MoreStreams.streamIterable;
import static java.util.Objects.requireNonNull;

/**
 * Allow for a more chained usage of the standard transform/filter of {@link Iterable iterables}.
 * <p>
 * Instead of writing this:
 * <pre><code>
 *     Iterables.filter(Lists.transform(asList("1", "2", "3")), greaterThan(1))
 * </code></pre>
 * Where the sequence of operations reads like a Russian Doll, one can write this:
 * <pre><code>
 *     chain(asList("1", "2", "3")).transform(TO_INT).filter(greaterThan(1))
 * </code></pre>
 */
public class Chainable<T> implements Iterable<T> {

    private final Iterable<T> iterable;

    private Chainable(Iterable<T> iterable) {
        this.iterable = iterable;
    }

    @Nonnull
    public static <T> Chainable<T> chain(@Nonnull Iterable<T> iterable) {
        return new Chainable<>(requireNonNull(iterable, "iterable"));
    }

    @Nonnull
    public static <T> Chainable<T> chain(@Nonnull T[] elements) {
        return new Chainable<>(Arrays.asList(requireNonNull(elements, "elements")));
    }

    /**
     * @param predicate to filter the elements with
     * @return a new {@link Chainable} that has the resulting elements filtered by the provided {@code predicate}
     */
    @Nonnull
    public Chainable<T> filter(@Nonnull Predicate<? super T> predicate) {
        return chain(Iterables.filter(iterable, predicate::test));
    }

    /**
     * @param function to transform the elements into a new stream, and flatten the result into a single iterable
     * @return a new {@link Chainable} that has the elements transformed by the provided {@code function}
     */
    @Nonnull
    public <R> Chainable<R> flatMap(@Nonnull Function<? super T, ? extends Iterable<? extends R>> function) {
        return chain(Iterables.concat(Iterables.transform(iterable, function::apply)));
    }

    /**
     * Get the first element in this {@code Chainable}, if it's not empty.
     * <p>
     * Note: calling this method on an empty {@code Chainable} will result in a runtime error, so clients should only
     * use it if they have established that this {@code Chainable} is {@link #isEmpty() not empty}, or they are not
     * concerned with possible runtime errors. In all other cases clients are encouraged to use
     * {@link #getFirst(Object)} instead.
     *
     * @return first element in this {@code Chainable}
     * @throws NoSuchElementException is this {@code Chainable} is {@link #isEmpty() empty}
     */
    @Nullable
    public T getFirst() {
        return iterable.iterator().next();
    }

    /**
     * Get the first element in this {@code Chainable} if it's not empty, or otherwise return {@code defaultValue}.
     *
     * @return first element in this {@code Chainable}, or {@code defaultValue}
     */
    @Nullable
    public T getFirst(@Nullable T defaultValue) {
        return Iterables.getFirst(iterable, defaultValue);
    }

    /**
     * @return whether the transformed collection is now empty
     */
    public boolean isEmpty() {
        return Iterables.isEmpty(iterable);
    }

    @Nonnull
    @Override
    public Iterator<T> iterator() {
        return iterable.iterator();
    }

    /**
     * @param size the number of elements to limit this collection by
     * @return a new {@link Chainable}, limited by {@code size}
     */
    @Nonnull
    public Chainable<T> limit(int size) {
        return chain(Iterables.limit(iterable, size));
    }

    @Nonnull
    public Chainable<List<T>> partition(int size) {
        return chain(Iterables.partition(iterable, size));
    }

    /**
     * @param size the number of elements to skip at the start of this collection
     * @return a new {@link Chainable} with a number of elements removed from the head
     */
    @Nonnull
    public Chainable<T> skip(int size) {
        return chain(Iterables.skip(iterable, size));
    }

    /**
     * Sort the underlying iterable using {@code comparator}.
     *
     * @param comparator comparator to sort
     * @return new sorted copy of the original
     */
    @Nonnull
    public Chainable<T> sort(@Nonnull Comparator<? super T> comparator) {
        Ordering<? super T> ordering = Ordering.from(requireNonNull(comparator, "comparator"));
        return chain(ordering.immutableSortedCopy(iterable));
    }

    /**
     * @return a stream over the elements in the chain
     * @since 4.8
     */
    @Nonnull
    public Stream<T> stream() {
        return streamIterable(iterable);
    }

    /**
     * @return an immutable, ordered {@link Set} created from the transformed collection
     */
    @Nonnull
    public Set<T> toSet() {
        return ImmutableSet.copyOf(iterable);
    }

    /**
     * @return an immutable {@link List} created from the transformed collection
     */
    @Nonnull
    public List<T> toList() {
        return ImmutableList.copyOf(iterable);
    }

    /**
     * @param function to transform the elements from one type to another
     * @return a new {@link Chainable} that has the elements transformed by the provided {@code function}
     */
    @Nonnull
    public <R> Chainable<R> transform(@Nonnull Function<? super T, ? extends R> function) {
        return chain(Iterables.transform(iterable, function::apply));
    }
}
