package org.hamcrest.core;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;

import java.util.ArrayList;
import java.util.List;

import static org.hamcrest.core.AllOf.allOf;
import static org.hamcrest.core.IsEqual.equalTo;

/**
 * Tests if an iterable contains matching elements.
 * @param <T> the type of items in the iterable
 */
public class IsIterableContaining<T> extends TypeSafeDiagnosingMatcher<Iterable<? extends T>> {

    private final Matcher<? super T> elementMatcher;

    /**
     * Constructor, best called from one of the static factory methods.
     * @param elementMatcher matches the expected element
     * @see #hasItem(Object)
     * @see #hasItem(Matcher)
     * @see #hasItems(Object[])
     * @see #hasItems(Matcher[])
     */
    public IsIterableContaining(Matcher<? super T> elementMatcher) {
        this.elementMatcher = elementMatcher;
    }

    @Override
    protected boolean matchesSafely(Iterable<? extends T> collection, Description mismatchDescription) {
        if (isEmpty(collection)) {
          mismatchDescription.appendText("was empty");
          return false;
        }

        for (Object item : collection) {
            if (elementMatcher.matches(item)) {
                return true;
            }
        }

        mismatchDescription.appendText("mismatches were: [");
        boolean isPastFirst = false;
        for (Object item : collection) {
            if (isPastFirst) {
              mismatchDescription.appendText(", ");
            }
            elementMatcher.describeMismatch(item, mismatchDescription);
            isPastFirst = true;
        }
        mismatchDescription.appendText("]");
        return false;
    }

    private boolean isEmpty(Iterable<? extends T> iterable) {
      return ! iterable.iterator().hasNext();
    }

    @Override
    public void describeTo(Description description) {
        description
            .appendText("a collection containing ")
            .appendDescriptionOf(elementMatcher);
    }

    /**
     * Creates a matcher for {@link Iterable}s that only matches when a single pass over the
     * examined {@link Iterable} yields at least one item that is matched by the specified
     * <code>itemMatcher</code>.  Whilst matching, the traversal of the examined {@link Iterable}
     * will stop as soon as a matching item is found.
     * For example:
     * <pre>assertThat(Arrays.asList("foo", "bar"), hasItem(startsWith("ba")))</pre>
     *
     * @param <T>
     *     the matcher type.
     * @param itemMatcher
     *     the matcher to apply to items provided by the examined {@link Iterable}
     * @return The matcher.
     */
    public static <T> Matcher<Iterable<? extends T>> hasItem(Matcher<? super T> itemMatcher) {
        return new IsIterableContaining<>(itemMatcher);
    }

    /**
     * Creates a matcher for {@link Iterable}s that only matches when a single pass over the
     * examined {@link Iterable} yields at least one item that is equal to the specified
     * <code>item</code>.  Whilst matching, the traversal of the examined {@link Iterable}
     * will stop as soon as a matching item is found.
     * For example:
     * <pre>assertThat(Arrays.asList("foo", "bar"), hasItem("bar"))</pre>
     *
     * @param <T>
     *     the matcher type.
     * @param item
     *     the item to compare against the items provided by the examined {@link Iterable}
     * @return The matcher.
     */
    public static <T> Matcher<Iterable<? extends T>> hasItem(T item) {
        // Doesn't forward to hasItem() method so compiler can sort out generics.
        return new IsIterableContaining<>(equalTo(item));
    }

    /**
     * Creates a matcher for {@link Iterable}s that matches when consecutive passes over the
     * examined {@link Iterable} yield at least one item that is matched by the corresponding
     * matcher from the specified <code>itemMatchers</code>.  Whilst matching, each traversal of
     * the examined {@link Iterable} will stop as soon as a matching item is found.
     * For example:
     * <pre>assertThat(Arrays.asList("foo", "bar", "baz"), hasItems(endsWith("z"), endsWith("o")))</pre>
     *
     * @param <T>
     *     the matcher type.
     * @param itemMatchers
     *     the matchers to apply to items provided by the examined {@link Iterable}
     * @return The matcher.
     */
    @SafeVarargs
    public static <T> Matcher<Iterable<? extends T>> hasItems(Matcher<? super T>... itemMatchers) {
        List<Matcher<? super Iterable<? extends T>>> all = new ArrayList<>(itemMatchers.length);

        for (Matcher<? super T> elementMatcher : itemMatchers) {
          // Doesn't forward to hasItem() method so compiler can sort out generics.
          all.add(new IsIterableContaining<>(elementMatcher));
        }

        return allOf(all);
    }

    /**
     * Creates a matcher for {@link Iterable}s that matches when consecutive passes over the
     * examined {@link Iterable} yield at least one item that is equal to the corresponding
     * item from the specified <code>items</code>.  Whilst matching, each traversal of the
     * examined {@link Iterable} will stop as soon as a matching item is found.
     * For example:
     * <pre>assertThat(Arrays.asList("foo", "bar", "baz"), hasItems("baz", "foo"))</pre>
     *
     * @param <T>
     *     the matcher type.
     * @param items
     *     the items to compare against the items provided by the examined {@link Iterable}
     * @return The matcher.
     */
    @SafeVarargs
    public static <T> Matcher<Iterable<? extends T>> hasItems(T... items) {
        List<Matcher<? super Iterable<? extends T>>> all = new ArrayList<>(items.length);
        for (T item : items) {
            all.add(hasItem(item));
        }

        return allOf(all);
    }

}
