/*
 * 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.solr.handler.component;

import org.apache.lucene.search.Query;
import org.apache.lucene.search.grouping.SearchGroup;
import org.apache.lucene.search.grouping.TopGroups;
import org.apache.lucene.util.BytesRef;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.util.RTimer;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrRequestInfo;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.search.CursorMark;
import org.apache.solr.search.DocListAndSet;
import org.apache.solr.search.DocSlice;
import org.apache.solr.search.QParser;
import org.apache.solr.search.QueryCommand;
import org.apache.solr.search.QueryResult;
import org.apache.solr.search.SortSpec;
import org.apache.solr.search.RankQuery;
import org.apache.solr.search.grouping.GroupingSpecification;
import org.apache.solr.search.grouping.distributed.command.QueryCommandResult;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;

/**
 * This class is experimental and will be changing in the future.
 *
 *
 * @since solr 1.3
 */
public class ResponseBuilder
{
  public SolrQueryRequest req;
  public SolrQueryResponse rsp;
  public boolean doHighlights;
  public boolean doFacets;
  public boolean doExpand;
  public boolean doStats;
  public boolean doTerms;
  public boolean doAnalytics;
  public MergeStrategy mergeFieldHandler;

  private boolean needDocList = false;
  private boolean needDocSet = false;
  private int fieldFlags = 0;
  //private boolean debug = false;
  private boolean debugTimings, debugQuery, debugResults, debugTrack;

  private QParser qparser = null;
  private String queryString = null;
  private Query query = null;
  private List<Query> filters = null;
  private SortSpec sortSpec = null;
  private GroupingSpecification groupingSpec;
  private CursorMark cursorMark;
  private CursorMark nextCursorMark;

  private List<MergeStrategy> mergeStrategies;
  private RankQuery rankQuery;


  private DocListAndSet results = null;
  private NamedList<Object> debugInfo = null;
  private RTimer timer = null;

  private Query highlightQuery = null;

  public List<SearchComponent> components;

  SolrRequestInfo requestInfo;

  public ResponseBuilder(SolrQueryRequest req, SolrQueryResponse rsp, List<SearchComponent> components)
  {
    this.req = req;
    this.rsp = rsp;
    this.components = components;
    this.requestInfo = SolrRequestInfo.getRequestInfo();
  }

  //////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////
  //// Distributed Search section
  //////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////

  public static final String FIELD_SORT_VALUES = "fsv";
  public static final String SHARDS = "shards";
  public static final String IDS = "ids";

  /**
   * public static final String NUMDOCS = "nd";
   * public static final String DOCFREQS = "tdf";
   * public static final String TERMS = "terms";
   * public static final String EXTRACT_QUERY_TERMS = "eqt";
   * public static final String LOCAL_SHARD = "local";
   * public static final String DOC_QUERY = "dq";
   * *
   */

  public static int STAGE_START = 0;
  public static int STAGE_PARSE_QUERY = 1000;
  public static int STAGE_TOP_GROUPS = 1500;
  public static int STAGE_EXECUTE_QUERY = 2000;
  public static int STAGE_GET_FIELDS = 3000;
  public static int STAGE_DONE = Integer.MAX_VALUE;

  public int stage;  // What stage is this current request at?

  //The address of the Shard
  boolean isDistrib; // is this a distributed search?
  public String[] shards;
  public String[] slices; // the optional logical ids of the shards
  public int shards_rows = -1;
  public int shards_start = -1;
  public List<ShardRequest> outgoing;  // requests to be sent
  public List<ShardRequest> finished;  // requests that have received responses from all shards
  public String shortCircuitedURL;

  /**
   * This function will return true if this was a distributed search request.
   */
  public boolean isDistributed() {
    return this.isDistrib;
  }

  public int getShardNum(String shard) {
    for (int i = 0; i < shards.length; i++) {
      if (shards[i] == shard || shards[i].equals(shard)) return i;
    }
    return -1;
  }

  public void addRequest(SearchComponent me, ShardRequest sreq) {
    outgoing.add(sreq);
    if ((sreq.purpose & ShardRequest.PURPOSE_PRIVATE) == 0) {
      // if this isn't a private request, let other components modify it.
      for (SearchComponent component : components) {
        if (component != me) {
          component.modifyRequest(this, me, sreq);
        }
      }
    }
  }

  public GlobalCollectionStat globalCollectionStat;

  public Map<Object, ShardDoc> resultIds;
  // Maps uniqueKeyValue to ShardDoc, which may be used to
  // determine order of the doc or uniqueKey in the final
  // returned sequence.
  // Only valid after STAGE_EXECUTE_QUERY has completed.

  public boolean onePassDistributedQuery;

  public FacetComponent.FacetInfo _facetInfo;
  /* private... components that don't own these shouldn't use them */
  SolrDocumentList _responseDocs;
  StatsInfo _statsInfo;
  TermsComponent.TermsHelper _termsHelper;
  SimpleOrderedMap<List<NamedList<Object>>> _pivots;
  Object _analyticsRequestManager;
  boolean _isOlapAnalytics;

  // Context fields for grouping
  public final Map<String, Collection<SearchGroup<BytesRef>>> mergedSearchGroups = new HashMap<>();
  public final Map<String, Integer> mergedGroupCounts = new HashMap<>();
  public final Map<String, Map<SearchGroup<BytesRef>, Set<String>>> searchGroupToShards = new HashMap<>();
  public final Map<String, TopGroups<BytesRef>> mergedTopGroups = new HashMap<>();
  public final Map<String, QueryCommandResult> mergedQueryCommandResults = new HashMap<>();
  public final Map<Object, SolrDocument> retrievedDocuments = new HashMap<>();
  public int totalHitCount; // Hit count used when distributed grouping is performed.
  // Used for timeAllowed parameter. First phase elapsed time is subtracted from the time allowed for the second phase.
  public int firstPhaseElapsedTime;

  /**
   * Utility function to add debugging info.  This will make sure a valid
   * debugInfo exists before adding to it.
   */
  public void addDebugInfo( String name, Object val )
  {
    if( debugInfo == null ) {
      debugInfo = new SimpleOrderedMap<>();
    }
    debugInfo.add( name, val );
  }

  public void addDebug(Object val, String... path) {
    if( debugInfo == null ) {
      debugInfo = new SimpleOrderedMap<>();
    }

    NamedList<Object> target = debugInfo;
    for (int i=0; i<path.length-1; i++) {
      String elem = path[i];
      NamedList<Object> newTarget = (NamedList<Object>)debugInfo.get(elem);
      if (newTarget == null) {
        newTarget = new SimpleOrderedMap<>();
        target.add(elem, newTarget);
      }
      target = newTarget;
    }

    target.add(path[path.length-1], val);
  }

  //-------------------------------------------------------------------------
  //-------------------------------------------------------------------------

  public boolean isDebug() {
    return debugQuery || debugTimings || debugResults || debugTrack;
  }

  /**
   *
   * @return true if all debugging options are on
   */
  public boolean isDebugAll(){
    return debugQuery && debugTimings && debugResults && debugTrack;
  }

  public void setDebug(boolean dbg){
    debugQuery = dbg;
    debugTimings = dbg;
    debugResults = dbg;
    debugTrack = dbg;
  }

  public void addMergeStrategy(MergeStrategy mergeStrategy) {
    if(mergeStrategies == null) {
      mergeStrategies = new ArrayList();
    }

    mergeStrategies.add(mergeStrategy);
  }

  public List<MergeStrategy> getMergeStrategies() {
    return this.mergeStrategies;
  }

  public RankQuery getRankQuery() {
    return rankQuery;
  }

  public void setRankQuery(RankQuery rankQuery) {
    this.rankQuery = rankQuery;
  }

  public void setResponseDocs(SolrDocumentList _responseDocs) {
    this._responseDocs = _responseDocs;
  }
  
  public SolrDocumentList getResponseDocs() {
    return this._responseDocs;
  }

  public boolean isDebugTrack() {
    return debugTrack;
  }

  public void setDebugTrack(boolean debugTrack) {
    this.debugTrack = debugTrack;
  }

  public boolean isDebugTimings() {
    return debugTimings;
  }

  public void setDebugTimings(boolean debugTimings) {
    this.debugTimings = debugTimings;
  }

  public boolean isDebugQuery() {
    return debugQuery;
  }

  public void setDebugQuery(boolean debugQuery) {
    this.debugQuery = debugQuery;
  }

  public boolean isDebugResults() {
    return debugResults;
  }

  public void setDebugResults(boolean debugResults) {
    this.debugResults = debugResults;
  }

  public NamedList<Object> getDebugInfo() {
    return debugInfo;
  }

  public void setDebugInfo(NamedList<Object> debugInfo) {
    this.debugInfo = debugInfo;
  }

  public int getFieldFlags() {
    return fieldFlags;
  }

  public void setFieldFlags(int fieldFlags) {
    this.fieldFlags = fieldFlags;
  }

  public List<Query> getFilters() {
    return filters;
  }

  public void setFilters(List<Query> filters) {
    this.filters = filters;
  }

  public Query getHighlightQuery() {
    return highlightQuery;
  }

  public void setHighlightQuery(Query highlightQuery) {
    this.highlightQuery = highlightQuery;
  }

  public boolean isNeedDocList() {
    return needDocList;
  }

  public void setNeedDocList(boolean needDocList) {
    this.needDocList = needDocList;
  }

  public boolean isNeedDocSet() {
    return needDocSet;
  }

  public void setNeedDocSet(boolean needDocSet) {
    this.needDocSet = needDocSet;
  }

  public QParser getQparser() {
    return qparser;
  }

  public void setQparser(QParser qparser) {
    this.qparser = qparser;
  }

  public String getQueryString() {
    return queryString;
  }

  public void setQueryString(String qstr) {
    this.queryString = qstr;
  }

  public Query getQuery() {
    return query;
  }

  public void setQuery(Query query) {
    this.query = query;
  }

  public DocListAndSet getResults() {
    return results;
  }

  public void setResults(DocListAndSet results) {
    this.results = results;
  }

  public SortSpec getSortSpec() {
    return sortSpec;
  }

  public void setSortSpec(SortSpec sortSpec) {
    this.sortSpec = sortSpec;
  }

  public GroupingSpecification getGroupingSpec() {
    return groupingSpec;
  }

  public void setGroupingSpec(GroupingSpecification groupingSpec) {
    this.groupingSpec = groupingSpec;
  }

  public boolean grouping() {
    return groupingSpec != null;
  }

  public RTimer getTimer() {
    return timer;
  }

  public void setTimer(RTimer timer) {
    this.timer = timer;
  }


  public static class GlobalCollectionStat {
    public final long numDocs;

    public final Map<String, Long> dfMap;

    public GlobalCollectionStat(int numDocs, Map<String, Long> dfMap) {
      this.numDocs = numDocs;
      this.dfMap = dfMap;
    }
  }

  /**
   * Creates a SolrIndexSearcher.QueryCommand from this
   * ResponseBuilder.  TimeAllowed is left unset.
   */
  public QueryCommand createQueryCommand() {
    QueryCommand cmd = new QueryCommand();
    cmd.setQuery(wrap(getQuery()))
            .setFilterList(getFilters())
            .setSort(getSortSpec().getSort())
            .setOffset(getSortSpec().getOffset())
            .setLen(getSortSpec().getCount())
            .setFlags(getFieldFlags())
            .setNeedDocSet(isNeedDocSet())
            .setCursorMark(getCursorMark());
    return cmd;
  }

  /** Calls {@link RankQuery#wrap(Query)} if there's a rank query, otherwise just returns the query. */
  public Query wrap(Query q) {
    if(this.rankQuery != null) {
      return this.rankQuery.wrap(q);
    } else {
      return q;
    }
  }

  /**
   * Sets results from a SolrIndexSearcher.QueryResult.
   */
  public void setResult(QueryResult result) {
    setResults(result.getDocListAndSet());
    if (result.isPartialResults()) {
      rsp.getResponseHeader().asShallowMap()
          .put(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY, Boolean.TRUE);
      if(getResults() != null && getResults().docList==null) {
        getResults().docList = new DocSlice(0, 0, new int[] {}, new float[] {}, 0, 0);
      }
    }
    final Boolean segmentTerminatedEarly = result.getSegmentTerminatedEarly();
    if (segmentTerminatedEarly != null) {
      rsp.getResponseHeader().add(SolrQueryResponse.RESPONSE_HEADER_SEGMENT_TERMINATED_EARLY_KEY, segmentTerminatedEarly);
    }
    if (null != cursorMark) {
      assert null != result.getNextCursorMark() : "using cursor but no next cursor set";
      this.setNextCursorMark(result.getNextCursorMark());
    }
  }
  
  public long getNumberDocumentsFound() {
    if (_responseDocs == null) {
      return 0;
    }
    return _responseDocs.getNumFound();
  }

  public CursorMark getCursorMark() {
    return cursorMark;
  }
  public void setCursorMark(CursorMark cursorMark) {
    this.cursorMark = cursorMark;
  }

  public CursorMark getNextCursorMark() {
    return nextCursorMark;
  }
  public void setNextCursorMark(CursorMark nextCursorMark) {
    this.nextCursorMark = nextCursorMark;
  }
}
