package org.hamcrest.collection;

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

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

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

public class IsIterableContainingInOrder<E> extends TypeSafeDiagnosingMatcher<Iterable<E>> {
    private final List<Matcher<? super E>> matchers;

    public IsIterableContainingInOrder(List<Matcher<? super E>> matchers) {
      this.matchers = matchers;
    }
    
    @Override
    protected boolean matchesSafely(Iterable<E> iterable, Description mismatchDescription) {
      MatchSeries<E> matchSeries = new MatchSeries<E>(matchers, mismatchDescription);
      for (E item : iterable) {
        if (! matchSeries.matches(item)) {
          return false;
        }
      }
      
      return matchSeries.isFinished();
    }

    public void describeTo(Description description) {
        description.appendText("iterable over ").appendList("[", ", ", "]", matchers);
    }

    private static class MatchSeries<F> {
      public final List<Matcher<? super F>> matchers;
      private final Description mismatchDescription;
      public int nextMatchIx = 0;
      
      public MatchSeries(List<Matcher<? super F>> matchers, Description mismatchDescription) {
        this.mismatchDescription = mismatchDescription;
        if (matchers.isEmpty()) {
          throw new IllegalArgumentException("Should specify at least one expected element");
        }
        this.matchers = matchers;
      }

      public boolean matches(F item) {
        return isNotSurplus(item) && isMatched(item);
      }

      public boolean isFinished() {
        if (nextMatchIx < matchers.size()) {
          mismatchDescription.appendText("No item matched: ").appendDescriptionOf(matchers.get(nextMatchIx));
          return false;
        }
        return true;
      }

      private boolean isMatched(F item) {
        Matcher<? super F> matcher = matchers.get(nextMatchIx);
        if (!matcher.matches(item)) {
          describeMismatch(matcher, item);
          return false;
        }
        nextMatchIx++;
        return true;
      }

      private boolean isNotSurplus(F item) {
        if (matchers.size() <= nextMatchIx) {
          mismatchDescription.appendText("Not matched: ").appendValue(item);
          return false;          
        }
        return true;
      }
      
      private void describeMismatch(Matcher<? super F> matcher, F item) {
        mismatchDescription.appendText("item " + nextMatchIx + ": ");
        matcher.describeMismatch(item, mismatchDescription);
      }
    }

    @Factory
    public static <E> Matcher<Iterable<E>> contains(E... items) {
        List<Matcher<? super E>> matchers = new ArrayList<Matcher<? super E>>();
        for (E item : items) {
            matchers.add(equalTo(item));
        }
        return contains(matchers);
    }

    @Factory
    public static <E> Matcher<Iterable<E>> contains(final Matcher<E> item) {
        return contains(new ArrayList<Matcher<? super E>>() {{ add(item); }});
    }

    @Factory
    public static <E> Matcher<Iterable<E>> contains(Matcher<? super E>... items) {
        return contains(Arrays.asList(items));
    }

    @Factory
    public static <E> Matcher<Iterable<E>> contains(List<Matcher<? super E>> contents) {
        return new IsIterableContainingInOrder<E>(contents);
    }
}
