/*
 * 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.lucene.facet;

import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.BoostQuery;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryVisitor;
import org.apache.lucene.search.TermQuery;

/**
 * A {@link Query} for drill-down over facet categories. You
 * should call {@link #add(String, String...)} for every group of categories you
 * want to drill-down over.
 * <p>
 * <b>NOTE:</b> if you choose to create your own {@link Query} by calling
 * {@link #term}, it is recommended to wrap it in a {@link BoostQuery}
 * with a boost of {@code 0.0f},
 * so that it does not affect the scores of the documents.
 * 
 * @lucene.experimental
 */
public final class DrillDownQuery extends Query {

  /** Creates a drill-down term. */
  public static Term term(String field, String dim, String... path) {
    return new Term(field, FacetsConfig.pathToString(dim, path));
  }

  private final FacetsConfig config;
  private final Query baseQuery;
  private final List<BooleanQuery.Builder> dimQueries = new ArrayList<>();
  private final Map<String,Integer> drillDownDims = new LinkedHashMap<>();

  /** Used by clone() and DrillSideways */
  DrillDownQuery(FacetsConfig config, Query baseQuery, List<BooleanQuery.Builder> dimQueries, Map<String,Integer> drillDownDims) {
    this.baseQuery = baseQuery;
    this.dimQueries.addAll(dimQueries);
    this.drillDownDims.putAll(drillDownDims);
    this.config = config;
  }

  /** Used by DrillSideways */
  DrillDownQuery(FacetsConfig config, Query filter, DrillDownQuery other) {
    this.baseQuery = new BooleanQuery.Builder()
        .add(other.baseQuery == null ? new MatchAllDocsQuery() : other.baseQuery, Occur.MUST)
        .add(filter, Occur.FILTER)
        .build();
    this.dimQueries.addAll(other.dimQueries);
    this.drillDownDims.putAll(other.drillDownDims);
    this.config = config;
  }

  /** Creates a new {@code DrillDownQuery} without a base query, 
   *  to perform a pure browsing query (equivalent to using
   *  {@link MatchAllDocsQuery} as base). */
  public DrillDownQuery(FacetsConfig config) {
    this(config, null);
  }
  
  /** Creates a new {@code DrillDownQuery} over the given base query. Can be
   *  {@code null}, in which case the result {@link Query} from
   *  {@link #rewrite(IndexReader)} will be a pure browsing query, filtering on
   *  the added categories only. */
  public DrillDownQuery(FacetsConfig config, Query baseQuery) {
    this.baseQuery = baseQuery;
    this.config = config;
  }

  /** Adds one dimension of drill downs; if you pass the same
   *  dimension more than once it is OR'd with the previous
   *  constraints on that dimension, and all dimensions are
   *  AND'd against each other and the base query. */
  public void add(String dim, String... path) {
    String indexedField = config.getDimConfig(dim).indexFieldName;
    add(dim, new TermQuery(term(indexedField, dim, path)));
  }

  /** Expert: add a custom drill-down subQuery.  Use this
   *  when you have a separate way to drill-down on the
   *  dimension than the indexed facet ordinals. */
  public void add(String dim, Query subQuery) {
    assert drillDownDims.size() == dimQueries.size();
    if (drillDownDims.containsKey(dim) == false) {
      drillDownDims.put(dim, drillDownDims.size());
      BooleanQuery.Builder builder = new BooleanQuery.Builder();
      dimQueries.add(builder);
    }
    final int index = drillDownDims.get(dim);
    dimQueries.get(index).add(subQuery, Occur.SHOULD);
  }

  @Override
  public DrillDownQuery clone() {
    return new DrillDownQuery(config, baseQuery, dimQueries, drillDownDims);
  }
  
  @Override
  public int hashCode() {
    return classHash() + Objects.hash(baseQuery, dimQueries);
  }

  @Override
  public boolean equals(Object other) {
    return sameClassAs(other) &&
           equalsTo(getClass().cast(other));
  }

  private boolean equalsTo(DrillDownQuery other) {
    return Objects.equals(baseQuery, other.baseQuery) && 
           dimQueries.equals(other.dimQueries);
  }

  @Override
  public Query rewrite(IndexReader r) throws IOException {
    BooleanQuery rewritten = getBooleanQuery();
    if (rewritten.clauses().isEmpty()) {
      return new MatchAllDocsQuery();
    }
    return rewritten;
  }

  @Override
  public String toString(String field) {
    return getBooleanQuery().toString(field);
  }

  @Override
  public void visit(QueryVisitor visitor) {
    visitor.visitLeaf(this);
  }

  private BooleanQuery getBooleanQuery() {
    BooleanQuery.Builder bq = new BooleanQuery.Builder();
    if (baseQuery != null) {
      bq.add(baseQuery, Occur.MUST);
    }
    for (BooleanQuery.Builder builder : dimQueries) {
      bq.add(builder.build(), Occur.FILTER);
    }
    return bq.build();
  }

  Query getBaseQuery() {
    return baseQuery;
  }

  Query[] getDrillDownQueries() {
    Query[] dimQueries = new Query[this.dimQueries.size()];
    for (int i = 0; i < dimQueries.length; ++i) {
      dimQueries[i] = this.dimQueries.get(i).build();
    }
    return dimQueries;
  }

  Map<String,Integer> getDims() {
    return drillDownDims;
  }
}
