package querqy;

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

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;

import querqy.model.BooleanClause;
import querqy.model.BooleanQuery;
import querqy.model.BoostQuery;
import querqy.model.Clause.Occur;
import querqy.model.DisjunctionMaxClause;
import querqy.model.DisjunctionMaxQuery;
import querqy.model.Node;
import querqy.model.SubQuery;
import querqy.model.Term;

/**
 * {@link Matcher}s for {@link SubQuery}s.
 */
public class QuerqyMatchers {
    
    
    
   /**
    * Match {@link DisjunctionMaxClause} with {@link Occur#SHOULD}.
    * 
    * @param clauses
    *           Expected clauses.
    */
   @SafeVarargs
   public static DMQMatcher dmq(TypeSafeMatcher<? extends DisjunctionMaxClause>... clauses) {
      return dmq(should(), clauses);
   }

   /**
    * Match {@link DisjunctionMaxClause}.
    * 
    * @param occur
    *           Expected occur.
    * @param clauses
    *           Expected clauses.
    */
   @SafeVarargs
   public static DMQMatcher dmq(OccurMatcher occur, TypeSafeMatcher<? extends DisjunctionMaxClause>... clauses) {
      return new DMQMatcher(occur, clauses);
   }

   /**
    * Match {@link BooleanQuery} with {@link Occur#SHOULD}.
    * 
    * @param clauses
    *           Expected clauses.
    */
   @SafeVarargs
   public static BQMatcher bq(TypeSafeMatcher<? extends BooleanClause>... clauses) {
      return bq(should(), clauses);
   }

   /**
    * Match {@link BooleanQuery}.
    * 
    * @param occur
    *           Expected occur.
    * @param clauses
    *           Expected clauses.
    */
   @SafeVarargs
   public static BQMatcher bq(OccurMatcher occur, TypeSafeMatcher<? extends BooleanClause>... clauses) {
      return new BQMatcher(occur, clauses);
   }

   /**
    * Match term.
    * 
    * @param field
    *           Expected field.
    * @param value
    *           Expected value.
    */
   public static TermMatcher term(String field, String value) {
      return new TermMatcher(field, value);
   }
   
   public static TermMatcher term(String field, String value, Boolean generated) {
       return new TermMatcher(field, value, generated);
    }

   /**
    * Just match term value.
    * 
    * @param value
    *           Expected value.
    */
   public static TermMatcher term(String value) {
      return new TermMatcher(null, value);
   }
   
   public static TermMatcher term(String value, Boolean generated) {
       return new TermMatcher(null, value, generated);
    }
   
   /**
    * Match a boost query.
    * 
    * @param queryMatcher The expected query matcher
    * @param boost The expected boost factor
    * 
    * @return
    */
   public static BoostQueryMatcher boostQ(TypeSafeMatcher<? extends Node> queryMatcher, float boost) {
       return new BoostQueryMatcher(queryMatcher, boost);
   }

   /**
    * Expected {@link Occur#MUST}.
    */
   public static OccurMatcher must() {
      return new OccurMatcher(Occur.MUST);
   }

   /**
    * Expected {@link Occur#MUST_NOT}.
    */
   public static final OccurMatcher mustNot() {
      return new OccurMatcher(Occur.MUST_NOT);
   }

   /**
    * Expected {@link Occur#SHOULD}.
    */
   public static final OccurMatcher should() {
      return new OccurMatcher(Occur.SHOULD);
   }

   //
   // Inner classes for matchers.
   //

   /**
    * {@link Matcher} for {@link DisjunctionMaxQuery}s.
    */
   private static class DMQMatcher extends SubQueryMatcher<DisjunctionMaxQuery> {
      public DMQMatcher(OccurMatcher occur, TypeSafeMatcher<? extends DisjunctionMaxClause>[] clauses) {
         super(occur, clauses);
      }

      @Override
      public void describeTo(Description description) {
         description.appendText("DMQ:\n ");
         super.describeTo(description);
      }
   }

   /**
    * {@link Matcher} for {@link BooleanQuery}s.
    */
   private static class BQMatcher extends SubQueryMatcher<BooleanQuery> {
      public BQMatcher(OccurMatcher occur, TypeSafeMatcher<? extends BooleanClause>[] clauses) {
         super(occur, clauses);
      }

      @Override
      public void describeTo(Description description) {
         description.appendText("BQ:\n ");
         description.appendText("(");
         super.describeTo(description);
         description.appendText(")\n");
      }
   }

   /**
    * {@link Matcher} for {@link Term}s.
    */
   private static class TermMatcher extends TypeSafeMatcher<Term> {
      private final String expectedField;
      private final String expectedValue;
      private final Boolean expectedGenerated;

      public TermMatcher(String field, String value) {
          this(field, value, null);
      }
      
      public TermMatcher(String field, String value, Boolean expectedGenerated) {
         this.expectedField = field;
         this.expectedValue = value;
         this.expectedGenerated = expectedGenerated;
      }

      @Override
      public void describeTo(Description description) {
         description.appendText("term: " + expectedField + ":" + expectedValue + " (generated:" + expectedGenerated + ")");
      }

      @Override
      protected boolean matchesSafely(Term item) {
         return item != null &&
               // expectedField == null -> do not compare field name.
               (expectedField == null || expectedField.equals(item.getField())) &&
               // Using toString() here to avoid incompatibilities between
               // equals() of different char sequences.
               CharSequenceUtil.equals(expectedValue, item.getValue()) &&
              // expectedValue.equals(item.getValue().toString()) &&
               ((expectedGenerated == null) || (expectedGenerated.booleanValue() == item.isGenerated()))
               ;
      }
   }
   
   /**
    * {@link Matcher} for {@link BoostQuery}s.
    */
   private static class BoostQueryMatcher extends TypeSafeMatcher<BoostQuery> {
       
       final TypeSafeMatcher<? extends Node> queryMatcher;
       final float boost;
       
       public BoostQueryMatcher(TypeSafeMatcher<? extends Node> queryMatcher, float boost) {
           this.queryMatcher = queryMatcher;
           this.boost = boost;
       }

      @Override
      public void describeTo(Description description) {
         description.appendText("BoostQuery:\n ");
         description.appendText("(");
         queryMatcher.describeTo(description);
         description.appendText(", boost: ");
         description.appendValue(boost);
         description.appendText(")\n");
      }

    @Override
    protected boolean matchesSafely(BoostQuery item) {
        return queryMatcher.matches(item.getQuery()) && item.getBoost() == boost;
    }
   }


   /**
    * {@link Matcher} for {@link Occur}s.
    */
   private static class OccurMatcher extends TypeSafeMatcher<Occur> {
      private final Occur expected;

      public OccurMatcher(Occur expected) {
         this.expected = expected;
      }

      @Override
      public void describeTo(Description description) {
         description.appendText("occur: " + expected.name());
      }

      @Override
      protected boolean matchesSafely(Occur occur) {
         return expected.equals(occur);
      }
   }

   /**
    * {@link Matcher} for {@link SubQuery}s.
    */
   private static abstract class SubQueryMatcher<T extends SubQuery<?, ?>> extends TypeSafeMatcher<T> {
      private final OccurMatcher occur;
      private final TypeSafeMatcher<? extends Node>[] clauses;

      public SubQueryMatcher(OccurMatcher occur, TypeSafeMatcher<? extends Node>[] clauses) {
         this.occur = occur;
         this.clauses = clauses;
      }

      @Override
      public void describeTo(Description description) {
         occur.describeTo(description);
         description.appendList("clauses:[", ",\n", "]", Arrays.asList(clauses));
      }

      @Override
      protected boolean matchesSafely(T item) {
         if (!occur.matches(item.occur)) {
            return false;
         }

         List<? extends Node> itemClauses = item.getClauses();
         if (itemClauses == null || itemClauses.size() != clauses.length) {
            return false;
         }

         for (int i = 0; i < clauses.length; i++) {
            if (!clauses[i].matches(itemClauses.get(i))) {
               return false;
            }
         }

         return true;
      }
   }
}
