/*
 * 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 java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreMode;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.BytesRefBuilder;
import org.apache.solr.client.solrj.SolrResponse;
import org.apache.solr.cloud.CloudDescriptor;
import org.apache.solr.cloud.ZkController;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentBase;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.SolrInputField;
import org.apache.solr.common.StringUtils;
import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.DocCollection;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.ShardParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.core.SolrCore;
import org.apache.solr.request.LocalSolrQueryRequest;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.ResultContext;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.response.transform.DocTransformer;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.search.DocList;
import org.apache.solr.search.QParser;
import org.apache.solr.search.ReturnFields;
import org.apache.solr.search.SolrDocumentFetcher;
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.search.SolrReturnFields;
import org.apache.solr.search.SyntaxError;
import org.apache.solr.update.CdcrUpdateLog;
import org.apache.solr.update.DocumentBuilder;
import org.apache.solr.update.IndexFingerprint;
import org.apache.solr.update.PeerSync;
import org.apache.solr.update.PeerSyncWithLeader;
import org.apache.solr.update.UpdateLog;
import org.apache.solr.update.processor.AtomicUpdateDocumentMerger;
import org.apache.solr.util.RefCounted;
import org.apache.solr.util.TestInjection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.apache.solr.common.params.CommonParams.DISTRIB;
import static org.apache.solr.common.params.CommonParams.ID;
import static org.apache.solr.common.params.CommonParams.VERSION_FIELD;

public class RealTimeGetComponent extends SearchComponent
{
  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
  private static final Set<String> NESTED_META_FIELDS = Sets.newHashSet(IndexSchema.NEST_PATH_FIELD_NAME, IndexSchema.NEST_PARENT_FIELD_NAME);
  public static final String COMPONENT_NAME = "get";

  @Override
  public void prepare(ResponseBuilder rb) throws IOException {
    // Set field flags
    ReturnFields returnFields = new SolrReturnFields( rb.req );
    rb.rsp.setReturnFields( returnFields );
  }


  @Override
  public void process(ResponseBuilder rb) throws IOException
  {
    SolrQueryRequest req = rb.req;
    SolrQueryResponse rsp = rb.rsp;
    SolrParams params = req.getParams();
    CloudDescriptor cloudDesc = req.getCore().getCoreDescriptor().getCloudDescriptor();

    if (cloudDesc != null) {
      Replica.Type replicaType = cloudDesc.getReplicaType();
      if (replicaType != null) {
        if (replicaType == Replica.Type.PULL) {
          throw new SolrException(ErrorCode.BAD_REQUEST, 
              String.format(Locale.ROOT, "%s can't handle realtime get requests. Replicas of type %s do not support these type of requests", 
                  cloudDesc.getCoreNodeName(),
                  Replica.Type.PULL));
        } 
        // non-leader TLOG replicas should not respond to distrib /get requests, but internal requests are OK
      }
    }
    
    if (!params.getBool(COMPONENT_NAME, true)) {
      return;
    }
    
    // This seems rather kludgey, may there is better way to indicate
    // that replica can support handling version ranges
    String val = params.get("checkCanHandleVersionRanges");
    if(val != null) {
      rb.rsp.add("canHandleVersionRanges", true);
      return;
    }
    
    val = params.get("getFingerprint");
    if(val != null) {
      processGetFingeprint(rb);
      return;
    }
    
    val = params.get("getVersions");
    if (val != null) {
      processGetVersions(rb);
      return;
    }

    val = params.get("getUpdates");
    if (val != null) {
      // solrcloud_debug
      if (log.isDebugEnabled()) {
        try {
          RefCounted<SolrIndexSearcher> searchHolder = req.getCore()
              .getNewestSearcher(false);
          SolrIndexSearcher searcher = searchHolder.get();
          try {
            log.debug(req.getCore()
                .getCoreContainer().getZkController().getNodeName()
                + " min count to sync to (from most recent searcher view) "
                + searcher.count(new MatchAllDocsQuery()));
          } finally {
            searchHolder.decref();
          }
        } catch (Exception e) {
          log.debug("Error in solrcloud_debug block", e);
        }
      }
      
      processGetUpdates(rb);
      return;
    }
    
    val = params.get("getInputDocument");
    if (val != null) {
      processGetInputDocument(rb);
      return;
    }

    final IdsRequsted reqIds = IdsRequsted.parseParams(req);
    
    if (reqIds.allIds.isEmpty()) {
      return;
    }

    // parse any existing filters
    try {
      String[] fqs = req.getParams().getParams(CommonParams.FQ);
      if (fqs!=null && fqs.length!=0) {
        List<Query> filters = rb.getFilters();
        // if filters already exists, make a copy instead of modifying the original
        filters = filters == null ? new ArrayList<Query>(fqs.length) : new ArrayList<>(filters);
        for (String fq : fqs) {
          if (fq != null && fq.trim().length()!=0) {
            QParser fqp = QParser.getParser(fq, req);
            filters.add(fqp.getQuery());
          }
        }
        if (!filters.isEmpty()) {
          rb.setFilters( filters );
        }
      }
    } catch (SyntaxError e) {
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
    }

    final SolrCore core = req.getCore();
    SchemaField idField = core.getLatestSchema().getUniqueKeyField();
    FieldType fieldType = idField.getType();

    SolrDocumentList docList = new SolrDocumentList();
    UpdateLog ulog = core.getUpdateHandler().getUpdateLog();

    SearcherInfo searcherInfo =  new SearcherInfo(core);
    
    // this is initialized & set on the context *after* any searcher (re-)opening
    ResultContext resultContext = null;
    final DocTransformer transformer = rsp.getReturnFields().getTransformer();

    // true in any situation where we have to use a realtime searcher rather then returning docs
    // directly from the UpdateLog
    final boolean mustUseRealtimeSearcher =
      // if we have filters, we need to check those against the indexed form of the doc
      (rb.getFilters() != null)
      || ((null != transformer) && transformer.needsSolrIndexSearcher());

   try {


     BytesRefBuilder idBytes = new BytesRefBuilder();
     for (String idStr : reqIds.allIds) {
       fieldType.readableToIndexed(idStr, idBytes);
       if (ulog != null) {
         Object o = ulog.lookup(idBytes.get());
         if (o != null) {
           // should currently be a List<Oper,Ver,Doc/Id>
           List entry = (List)o;
           assert entry.size() >= 3;
           int oper = (Integer)entry.get(UpdateLog.FLAGS_IDX) & UpdateLog.OPERATION_MASK;
           switch (oper) {
             case UpdateLog.UPDATE_INPLACE: // fall through to ADD
             case UpdateLog.ADD:

               if (mustUseRealtimeSearcher) {
                 // close handles to current searchers & result context
                 searcherInfo.clear();
                 resultContext = null;
                 ulog.openRealtimeSearcher();  // force open a new realtime searcher
                 o = null;  // pretend we never found this record and fall through to use the searcher
                 break;
               }

               SolrDocument doc;
               if (oper == UpdateLog.ADD) {
                 doc = toSolrDoc((SolrInputDocument)entry.get(entry.size()-1), core.getLatestSchema());
               } else if (oper == UpdateLog.UPDATE_INPLACE) {
                 if (ulog instanceof CdcrUpdateLog) {
                   assert entry.size() == 6;
                 } else {
                   assert entry.size() == 5;
                 }
                 // For in-place update case, we have obtained the partial document till now. We need to
                 // resolve it to a full document to be returned to the user.
                 doc = resolveFullDocument(core, idBytes.get(), rsp.getReturnFields(), (SolrInputDocument)entry.get(entry.size()-1), entry, null);
                 if (doc == null) {
                   break; // document has been deleted as the resolve was going on
                 }
               } else {
                 throw new SolrException(ErrorCode.INVALID_STATE, "Expected ADD or UPDATE_INPLACE. Got: " + oper);
               }
               if (transformer!=null) {
                 transformer.transform(doc, -1); // unknown docID
               }
              docList.add(doc);
              break;
             case UpdateLog.DELETE:
              break;
             default:
               throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,  "Unknown Operation! " + oper);
           }
           if (o != null) continue;
         }
       }

       // didn't find it in the update log, so it should be in the newest searcher opened
       searcherInfo.init();
       // don't bother with ResultContext yet, we won't need it if doc doesn't match filters

       int docid = -1;
       long segAndId = searcherInfo.getSearcher().lookupId(idBytes.get());
       if (segAndId >= 0) {
         int segid = (int) segAndId;
         LeafReaderContext ctx = searcherInfo.getSearcher().getTopReaderContext().leaves().get((int) (segAndId >> 32));
         docid = segid + ctx.docBase;

         if (rb.getFilters() != null) {
           for (Query raw : rb.getFilters()) {
             Query q = raw.rewrite(searcherInfo.getSearcher().getIndexReader());
             Scorer scorer = searcherInfo.getSearcher().createWeight(q, ScoreMode.COMPLETE_NO_SCORES, 1f).scorer(ctx);
             if (scorer == null || segid != scorer.iterator().advance(segid)) {
               // filter doesn't match.
               docid = -1;
               break;
             }
           }
         }
       }

       if (docid < 0) continue;
       
       Document luceneDocument = searcherInfo.getSearcher().doc(docid, rsp.getReturnFields().getLuceneFieldNames());
       SolrDocument doc = toSolrDoc(luceneDocument,  core.getLatestSchema());
       SolrDocumentFetcher docFetcher = searcherInfo.getSearcher().getDocFetcher();
       docFetcher.decorateDocValueFields(doc, docid, docFetcher.getNonStoredDVs(true));
       if ( null != transformer) {
         if (null == resultContext) {
           // either first pass, or we've re-opened searcher - either way now we setContext
           resultContext = new RTGResultContext(rsp.getReturnFields(), searcherInfo.getSearcher(), req);
           transformer.setContext(resultContext);
         }
         transformer.transform(doc, docid);
       }
       docList.add(doc);
     }

   } finally {
     searcherInfo.clear();
   }

   addDocListToResponse(rb, docList);
  }
  
  /**
   * Return the requested SolrInputDocument from the tlog/index. This will
   * always be a full document, i.e. any partial in-place document will be resolved.
   */
  void processGetInputDocument(ResponseBuilder rb) throws IOException {
    SolrQueryRequest req = rb.req;
    SolrQueryResponse rsp = rb.rsp;
    SolrParams params = req.getParams();

    if (!params.getBool(COMPONENT_NAME, true)) {
      return;
    }

    String idStr = params.get("getInputDocument", null);
    if (idStr == null) return;
    AtomicLong version = new AtomicLong();
    SolrInputDocument doc = getInputDocument(req.getCore(), new BytesRef(idStr), version, null, Resolution.DOC);
    log.info("getInputDocument called for id="+idStr+", returning: "+doc);
    rb.rsp.add("inputDocument", doc);
    rb.rsp.add("version", version.get());
  }

  /**
   * A SearcherInfo provides mechanism for obtaining RT searcher, from
   * a SolrCore, and closing it, while taking care of the RefCounted references.
   */
  private static class SearcherInfo {
    private RefCounted<SolrIndexSearcher> searcherHolder = null;
    private SolrIndexSearcher searcher = null;
    final SolrCore core;
    
    public SearcherInfo(SolrCore core) {
      this.core = core;
    }
    
    void clear(){
      if (searcherHolder != null) {
        // close handles to current searchers
        searcher = null;
        searcherHolder.decref();
        searcherHolder = null;
      }
    }

    void init(){
      if (searcher == null) {
        searcherHolder = core.getRealtimeSearcher();
        searcher = searcherHolder.get();
      }
    }
    
    public SolrIndexSearcher getSearcher() {
      assert null != searcher : "init not called!";
      return searcher;
    }
  }

  /***
   * Given a partial document obtained from the transaction log (e.g. as a result of RTG), resolve to a full document
   * by populating all the partial updates that were applied on top of that last full document update.
   * 
   * @param onlyTheseFields When a non-null set of field names is passed in, the resolve process only attempts to populate
   *        the given fields in this set. When this set is null, it resolves all fields.
   * @return Returns the merged document, i.e. the resolved full document, or null if the document was not found (deleted
   *          after the resolving began)
   */
  private static SolrDocument resolveFullDocument(SolrCore core, BytesRef idBytes,
                                           ReturnFields returnFields, SolrInputDocument partialDoc, List logEntry, Set<String> onlyTheseFields) throws IOException {
    if (idBytes == null || (logEntry.size() != 5 && logEntry.size() != 6)) {
      throw new SolrException(ErrorCode.INVALID_STATE, "Either Id field not present in partial document or log entry doesn't have previous version.");
    }
    long prevPointer = (long) logEntry.get(UpdateLog.PREV_POINTER_IDX);
    long prevVersion = (long) logEntry.get(UpdateLog.PREV_VERSION_IDX);

    // get the last full document from ulog
    UpdateLog ulog = core.getUpdateHandler().getUpdateLog();
    long lastPrevPointer = ulog.applyPartialUpdates(idBytes, prevPointer, prevVersion, onlyTheseFields, partialDoc);

    if (lastPrevPointer == -1) { // full document was not found in tlog, but exists in index
      SolrDocument mergedDoc = mergePartialDocWithFullDocFromIndex(core, idBytes, returnFields, onlyTheseFields, partialDoc);
      return mergedDoc;
    } else if (lastPrevPointer > 0) {
      // We were supposed to have found the last full doc also in the tlogs, but the prevPointer links led to nowhere
      // We should reopen a new RT searcher and get the doc. This should be a rare occurrence
      Term idTerm = new Term(core.getLatestSchema().getUniqueKeyField().getName(), idBytes);
      SolrDocument mergedDoc = reopenRealtimeSearcherAndGet(core, idTerm, returnFields);
      if (mergedDoc == null) {
        return null; // the document may have been deleted as the resolving was going on.
      }
      return mergedDoc;
    } else { // i.e. lastPrevPointer==0
      assert lastPrevPointer == 0;
      // We have successfully resolved the document based off the tlogs

      // determine whether we can use the in place document, if the caller specified onlyTheseFields
      // and those fields are all supported for in-place updates
      IndexSchema schema = core.getLatestSchema();
      boolean forInPlaceUpdate = onlyTheseFields != null
          && onlyTheseFields.stream().map(schema::getField)
          .allMatch(f -> null!=f && AtomicUpdateDocumentMerger.isSupportedFieldForInPlaceUpdate(f));

      return toSolrDoc(partialDoc, schema, forInPlaceUpdate);
    }
  }

  /**
   * Re-open the RT searcher and get the document, referred to by the idTerm, from that searcher. 
   * @return Returns the document or null if not found.
   */
  private static SolrDocument reopenRealtimeSearcherAndGet(SolrCore core, Term idTerm, ReturnFields returnFields) throws IOException {
    UpdateLog ulog = core.getUpdateHandler().getUpdateLog();
    ulog.openRealtimeSearcher();
    RefCounted<SolrIndexSearcher> searcherHolder = core.getRealtimeSearcher();
    try {
      SolrIndexSearcher searcher = searcherHolder.get();

      int docid = searcher.getFirstMatch(idTerm);
      if (docid < 0) {
        return null;
      }
      Document luceneDocument = searcher.doc(docid, returnFields.getLuceneFieldNames());
      SolrDocument doc = toSolrDoc(luceneDocument, core.getLatestSchema());
      SolrDocumentFetcher docFetcher = searcher.getDocFetcher();
      docFetcher.decorateDocValueFields(doc, docid, docFetcher.getNonStoredDVs(false));

      return doc;
    } finally {
      searcherHolder.decref();
    }
  }

  /**
   * Gets a document from the index by id. If a non-null partial document (for in-place update) is passed in,
   * this method obtains the document from the tlog/index by the given id, merges the partial document on top of it and then returns
   * the resultant document.
   *
   * @param core           A SolrCore instance, useful for obtaining a realtimesearcher and the schema
   * @param idBytes        Binary representation of the value of the unique key field
   * @param returnFields   Return fields, as requested
   * @param onlyTheseFields When a non-null set of field names is passed in, the merge process only attempts to merge
   *        the given fields in this set. When this set is null, it merges all fields.
   * @param partialDoc     A partial document (containing an in-place update) used for merging against a full document
   *                       from index; this maybe be null.
   * @return If partial document is null, this returns document from the index or null if not found. 
   *         If partial document is not null, this returns a document from index merged with the partial document, or null if
   *         document doesn't exist in the index.
   */
  private static SolrDocument mergePartialDocWithFullDocFromIndex(SolrCore core, BytesRef idBytes, ReturnFields returnFields,
             Set<String> onlyTheseFields, SolrInputDocument partialDoc) throws IOException {
    RefCounted<SolrIndexSearcher> searcherHolder = core.getRealtimeSearcher(); //Searcher();
    try {
      // now fetch last document from index, and merge partialDoc on top of it
      SolrIndexSearcher searcher = searcherHolder.get();
      SchemaField idField = core.getLatestSchema().getUniqueKeyField();
      Term idTerm = new Term(idField.getName(), idBytes);

      int docid = searcher.getFirstMatch(idTerm);
      if (docid < 0) {
        // The document was not found in index! Reopen a new RT searcher (to be sure) and get again.
        // This should be because the document was deleted recently.
        SolrDocument doc = reopenRealtimeSearcherAndGet(core, idTerm, returnFields);
        if (doc == null) {
          // Unable to resolve the last full doc in tlog fully,
          // and document not found in index even after opening new rt searcher.
          // This must be a case of deleted doc
          return null;
        }
        return doc;
      }

      SolrDocument doc;
      Set<String> decorateFields = onlyTheseFields == null ? searcher.getDocFetcher().getNonStoredDVs(false): onlyTheseFields;
      Document luceneDocument = searcher.doc(docid, returnFields.getLuceneFieldNames());
      doc = toSolrDoc(luceneDocument, core.getLatestSchema());
      searcher.getDocFetcher().decorateDocValueFields(doc, docid, decorateFields);

      long docVersion = (long) doc.getFirstValue(VERSION_FIELD);
      Object partialVersionObj = partialDoc.getFieldValue(VERSION_FIELD);
      long partialDocVersion = partialVersionObj instanceof Field? ((Field) partialVersionObj).numericValue().longValue():
        partialVersionObj instanceof Number? ((Number) partialVersionObj).longValue(): Long.parseLong(partialVersionObj.toString());
      if (docVersion > partialDocVersion) {
        return doc;
      }
      for (String fieldName: partialDoc.getFieldNames()) {
        doc.setField(fieldName.toString(), partialDoc.getFieldValue(fieldName));  // since partial doc will only contain single valued fields, this is fine
      }

      return doc;
    } finally {
      if (searcherHolder != null) {
        searcherHolder.decref();
      }
    }
  }

  public static SolrInputDocument DELETED = new SolrInputDocument();

  /** returns the SolrInputDocument from the current tlog, or DELETED if it has been deleted, or
   * null if there is no record of it in the current update log.  If null is returned, it could
   * still be in the latest index.
   * @param versionReturned If a non-null AtomicLong is passed in, it is set to the version of the update returned from the TLog.
   * @param resolveFullDocument In case the document is fetched from the tlog, it could only be a partial document if the last update
   *                  was an in-place update. In that case, should this partial document be resolved to a full document (by following
   *                  back prevPointer/prevVersion)?
   */
  public static SolrInputDocument getInputDocumentFromTlog(SolrCore core, BytesRef idBytes, AtomicLong versionReturned,
      Set<String> onlyTheseNonStoredDVs, boolean resolveFullDocument) {

    UpdateLog ulog = core.getUpdateHandler().getUpdateLog();

    if (ulog != null) {
      Object o = ulog.lookup(idBytes);
      if (o != null) {
        // should currently be a List<Oper,Ver,Doc/Id>
        List entry = (List)o;
        assert entry.size() >= 3;
        int oper = (Integer)entry.get(0) & UpdateLog.OPERATION_MASK;
        if (versionReturned != null) {
          versionReturned.set((long)entry.get(UpdateLog.VERSION_IDX));
        }
        switch (oper) {
          case UpdateLog.UPDATE_INPLACE:
            if (ulog instanceof CdcrUpdateLog) {
              assert entry.size() == 6;
            } else {
              assert entry.size() == 5;
            }

            if (resolveFullDocument) {
              SolrInputDocument doc = (SolrInputDocument)entry.get(entry.size()-1);
              try {
                // For in-place update case, we have obtained the partial document till now. We need to
                // resolve it to a full document to be returned to the user.
                SolrDocument sdoc = resolveFullDocument(core, idBytes, new SolrReturnFields(), doc, entry, onlyTheseNonStoredDVs);
                if (sdoc == null) {
                  return DELETED;
                }
                doc = toSolrInputDocument(sdoc, core.getLatestSchema());
                return doc;
              } catch (IOException ex) {
                throw new SolrException(ErrorCode.SERVER_ERROR, "Error while resolving full document. ", ex);
              }
            } else {
              // fall through to ADD, so as to get only the partial document
            }
          case UpdateLog.ADD:
            return (SolrInputDocument) entry.get(entry.size()-1);
          case UpdateLog.DELETE:
            return DELETED;
          default:
            throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,  "Unknown Operation! " + oper);
        }
      }
    }

    return null;
  }

  /**
   * Obtains the latest document for a given id from the tlog or index (if not found in the tlog).
   * 
   * NOTE: This method uses the effective value for nonStoredDVs as null in the call to @see {@link RealTimeGetComponent#getInputDocument(SolrCore, BytesRef, AtomicLong, Set, Resolution)},
   * so as to retrieve all stored and non-stored DV fields from all documents.
   */

  public static SolrInputDocument getInputDocument(SolrCore core, BytesRef idBytes, Resolution lookupStrategy) throws IOException {
    return getInputDocument (core, idBytes, null, null, lookupStrategy);
  }
  
  /**
   * Obtains the latest document for a given id from the tlog or through the realtime searcher (if not found in the tlog). 
   * @param versionReturned If a non-null AtomicLong is passed in, it is set to the version of the update returned from the TLog.
   * @param onlyTheseNonStoredDVs If not-null, populate only these DV fields in the document fetched through the realtime searcher. 
   *                  If this is null, decorate all non-stored  DVs (that are not targets of copy fields) from the searcher.
   *                  When non-null, stored fields are not fetched.
   * @param resolveStrategy The strategy to resolve the the document.
   * @see Resolution
   */
  public static SolrInputDocument getInputDocument(SolrCore core, BytesRef idBytes, AtomicLong versionReturned,
      Set<String> onlyTheseNonStoredDVs, Resolution resolveStrategy) throws IOException {
    SolrInputDocument sid = null;
    RefCounted<SolrIndexSearcher> searcherHolder = null;
    try {
      SolrIndexSearcher searcher = null;
      sid = getInputDocumentFromTlog(core, idBytes, versionReturned, onlyTheseNonStoredDVs, true);
      if (sid == DELETED) {
        return null;
      }

      if (sid == null) {
        // didn't find it in the update log, so it should be in the newest searcher opened
        if (searcher == null) {
          searcherHolder = core.getRealtimeSearcher();
          searcher = searcherHolder.get();
        }

        // SolrCore.verbose("RealTimeGet using searcher ", searcher);
        final IndexSchema schema = core.getLatestSchema();
        SchemaField idField = schema.getUniqueKeyField();

        int docid = searcher.getFirstMatch(new Term(idField.getName(), idBytes));
        if (docid < 0) return null;

        SolrDocumentFetcher docFetcher = searcher.getDocFetcher();
        if (onlyTheseNonStoredDVs != null) {
          sid = new SolrInputDocument();
        } else {
          Document luceneDocument = docFetcher.doc(docid);
          sid = toSolrInputDocument(luceneDocument, schema);
        }
        final boolean isNestedRequest = resolveStrategy == Resolution.DOC_WITH_CHILDREN || resolveStrategy == Resolution.ROOT_WITH_CHILDREN;
        decorateDocValueFields(docFetcher, sid, docid, onlyTheseNonStoredDVs, isNestedRequest || schema.hasExplicitField(IndexSchema.NEST_PATH_FIELD_NAME));
        SolrInputField rootField = sid.getField(IndexSchema.ROOT_FIELD_NAME);
        if((isNestedRequest) && schema.isUsableForChildDocs() && schema.hasExplicitField(IndexSchema.NEST_PATH_FIELD_NAME) && rootField!=null) {
          // doc is part of a nested structure
          final boolean resolveRootDoc = resolveStrategy == Resolution.ROOT_WITH_CHILDREN;
          String id = resolveRootDoc? (String) rootField.getFirstValue(): (String) sid.getField(idField.getName()).getFirstValue();
          ModifiableSolrParams params = new ModifiableSolrParams()
              .set("fl", "*, _nest_path_, [child]")
              .set("limit", "-1");
          SolrQueryRequest nestedReq = new LocalSolrQueryRequest(core, params);
          final BytesRef rootIdBytes = new BytesRef(id);
          final int rootDocId = searcher.getFirstMatch(new Term(idField.getName(), rootIdBytes));
          final DocTransformer childDocTransformer = core.getTransformerFactory("child").create("child", params, nestedReq);
          final ResultContext resultContext = new RTGResultContext(new SolrReturnFields(nestedReq), searcher, nestedReq);
          childDocTransformer.setContext(resultContext);
          final SolrDocument nestedDoc;
          if(resolveRootDoc && rootIdBytes.equals(idBytes)) {
            nestedDoc = toSolrDoc(sid, schema);
          } else {
            nestedDoc = toSolrDoc(docFetcher.doc(rootDocId), schema);
            decorateDocValueFields(docFetcher, nestedDoc, rootDocId, onlyTheseNonStoredDVs, true);
          }
          childDocTransformer.transform(nestedDoc, rootDocId);
          sid = toSolrInputDocument(nestedDoc, schema);
        }
      }
    } finally {
      if (searcherHolder != null) {
        searcherHolder.decref();
      }
    }

    if (versionReturned != null) {
      if (sid.containsKey(VERSION_FIELD)) {
        versionReturned.set((long)sid.getFieldValue(VERSION_FIELD));
      }
    }
    return sid;
  }

  private static void decorateDocValueFields(SolrDocumentFetcher docFetcher, SolrDocumentBase doc, int docid, Set<String> onlyTheseNonStoredDVs, boolean resolveNestedFields) throws IOException {
    if (onlyTheseNonStoredDVs != null) {
      docFetcher.decorateDocValueFields(doc, docid, onlyTheseNonStoredDVs);
    } else {
      docFetcher.decorateDocValueFields(doc, docid, docFetcher.getNonStoredDVsWithoutCopyTargets());
    }
    if(resolveNestedFields) {
      docFetcher.decorateDocValueFields(doc, docid, NESTED_META_FIELDS);
    }
  }

  private static SolrInputDocument toSolrInputDocument(Document doc, IndexSchema schema) {
    SolrInputDocument out = new SolrInputDocument();
    for( IndexableField f : doc.getFields() ) {
      String fname = f.name();
      SchemaField sf = schema.getFieldOrNull(f.name());
      Object val = null;
      if (sf != null) {
        if ((!sf.hasDocValues() && !sf.stored()) || schema.isCopyFieldTarget(sf)) continue;
        val = sf.getType().toObject(f);   // object or external string?
      } else {
        val = f.stringValue();
        if (val == null) val = f.numericValue();
        if (val == null) val = f.binaryValue();
        if (val == null) val = f;
      }

      // todo: how to handle targets of copy fields (including polyfield sub-fields)?
      out.addField(fname, val);
    }
    return out;
  }

  private static SolrInputDocument toSolrInputDocument(SolrDocument doc, IndexSchema schema) {
    SolrInputDocument out = new SolrInputDocument();
    for( String fname : doc.getFieldNames() ) {
      boolean fieldArrayListCreated = false;
      SchemaField sf = schema.getFieldOrNull(fname);
      if (sf != null) {
        if ((!sf.hasDocValues() && !sf.stored()) || schema.isCopyFieldTarget(sf)) continue;
      }
      for (Object val: doc.getFieldValues(fname)) {
        if (val instanceof Field) {
          Field f = (Field) val;
          if (sf != null) {
            val = sf.getType().toObject(f);   // object or external string?
          } else {
            val = f.stringValue();
            if (val == null) val = f.numericValue();
            if (val == null) val = f.binaryValue();
            if (val == null) val = f;
          }
        } else if(val instanceof SolrDocument) {
          val = toSolrInputDocument((SolrDocument) val, schema);
          if(!fieldArrayListCreated && doc.getFieldValue(fname) instanceof Collection) {
            // previous value was array so we must return as an array even if was a single value array
            out.setField(fname, Lists.newArrayList(val));
            fieldArrayListCreated = true;
            continue;
          }
        }
        out.addField(fname, val);
      }
    }
    return out;
  }

  private static SolrDocument toSolrDoc(Document doc, IndexSchema schema) {
    SolrDocument out = new SolrDocument();
    for( IndexableField f : doc.getFields() ) {
      // Make sure multivalued fields are represented as lists
      Object existing = out.get(f.name());
      if (existing == null) {
        SchemaField sf = schema.getFieldOrNull(f.name());

        // don't return copyField targets
        if (sf != null && schema.isCopyFieldTarget(sf)) continue;

        if (sf != null && sf.multiValued()) {
          List<Object> vals = new ArrayList<>();
          if (f.fieldType().docValuesType() == DocValuesType.SORTED_NUMERIC) {
            // SORTED_NUMERICS store sortable bits version of the value, need to retrieve the original
            vals.add(sf.getType().toObject(f)); // (will materialize by side-effect)
          } else {
            vals.add( materialize(f) );
          }
          out.setField( f.name(), vals );
        }
        else{
          out.setField( f.name(), materialize(f) );
        }
      }
      else {
        out.addField( f.name(), materialize(f) );
      }
    }
    return out;
  }

  /**
   * Ensure we don't have {@link org.apache.lucene.document.LazyDocument.LazyField} or equivalent.
   * It can pose problems if the searcher is about to be closed and we haven't fetched a value yet.
   */
  private static IndexableField materialize(IndexableField in) {
    if (in instanceof Field) { // already materialized
      return in;
    }
    return new ClonedField(in);
  }

  private static class ClonedField extends Field { // TODO Lucene Field has no copy constructor; maybe it should?
    ClonedField(IndexableField in) {
      super(in.name(), in.fieldType());
      this.fieldsData = in.numericValue();
      if (this.fieldsData == null) {
        this.fieldsData = in.binaryValue();
        if (this.fieldsData == null) {
          this.fieldsData = in.stringValue();
          if (this.fieldsData == null) {
            // fallback:
            assert false : in; // unexpected
          }
        }
      }
    }
  }

  /**
   * Converts a SolrInputDocument to SolrDocument, using an IndexSchema instance. 
   * @lucene.experimental
   */
  public static SolrDocument toSolrDoc(SolrInputDocument sdoc, IndexSchema schema) {
    return toSolrDoc(sdoc, schema, false);
  }

  /**
   * Converts a SolrInputDocument to SolrDocument, using an IndexSchema instance.
   *
   * @param sdoc The input document.
   * @param schema The index schema.
   * @param forInPlaceUpdate Whether the document is being used for an in place update,
   *                         see {@link DocumentBuilder#toDocument(SolrInputDocument, IndexSchema, boolean, boolean)}
   */
  public static SolrDocument toSolrDoc(SolrInputDocument sdoc, IndexSchema schema, boolean forInPlaceUpdate) {
    // TODO what about child / nested docs?
    // TODO: do something more performant than this double conversion
    Document doc = DocumentBuilder.toDocument(sdoc, schema, forInPlaceUpdate, true);

    // copy the stored fields only
    Document out = new Document();
    for (IndexableField f : doc.getFields()) {
      if (f.fieldType().stored()) {
        out.add(f);
      } else if (f.fieldType().docValuesType() != DocValuesType.NONE) {
        SchemaField schemaField = schema.getFieldOrNull(f.name());
        if (schemaField != null && !schemaField.stored() && schemaField.useDocValuesAsStored()) {
          out.add(f);
        }
      } else {
        log.debug("Don't know how to handle field {}", f);
      }
    }

    SolrDocument solrDoc = toSolrDoc(out, schema);

    // add child docs
    for(SolrInputField solrInputField: sdoc) {
      if(solrInputField.getFirstValue() instanceof SolrInputDocument) {
        // is child doc
        Object val = solrInputField.getValue();
        Iterator<SolrDocument> childDocs = solrInputField.getValues().stream()
            .map(x -> toSolrDoc((SolrInputDocument) x, schema)).iterator();
        if(val instanceof Collection) {
          // add as collection even if single element collection
          solrDoc.setField(solrInputField.getName(), Lists.newArrayList(childDocs));
        } else {
          // single child doc
          solrDoc.setField(solrInputField.getName(), childDocs.next());
        }
      }
    }

    return solrDoc;
  }

  @Override
  public int distributedProcess(ResponseBuilder rb) throws IOException {
    if (rb.stage < ResponseBuilder.STAGE_GET_FIELDS)
      return ResponseBuilder.STAGE_GET_FIELDS;
    if (rb.stage == ResponseBuilder.STAGE_GET_FIELDS) {
      return createSubRequests(rb);
    }
    return ResponseBuilder.STAGE_DONE;
  }

  public int createSubRequests(ResponseBuilder rb) throws IOException {
    
    final IdsRequsted reqIds = IdsRequsted.parseParams(rb.req);
    if (reqIds.allIds.isEmpty()) {
      return ResponseBuilder.STAGE_DONE;
    }
    
    SolrParams params = rb.req.getParams();

    // TODO: handle collection=...?

    ZkController zkController = rb.req.getCore().getCoreContainer().getZkController();

    // if shards=... then use that
    if (zkController != null && params.get(ShardParams.SHARDS) == null) {
      CloudDescriptor cloudDescriptor = rb.req.getCore().getCoreDescriptor().getCloudDescriptor();

      String collection = cloudDescriptor.getCollectionName();
      ClusterState clusterState = zkController.getClusterState();
      DocCollection coll = clusterState.getCollection(collection);


      Map<String, List<String>> sliceToId = new HashMap<>();
      for (String id : reqIds.allIds) {
        Slice slice = coll.getRouter().getTargetSlice(params.get(ShardParams._ROUTE_, id), null, null, params, coll);

        List<String> idsForShard = sliceToId.get(slice.getName());
        if (idsForShard == null) {
          idsForShard = new ArrayList<>(2);
          sliceToId.put(slice.getName(), idsForShard);
        }
        idsForShard.add(id);
      }

      for (Map.Entry<String,List<String>> entry : sliceToId.entrySet()) {
        String shard = entry.getKey();

        ShardRequest sreq = createShardRequest(rb, entry.getValue());
        // sreq.shards = new String[]{shard};    // TODO: would be nice if this would work...
        sreq.shards = sliceToShards(rb, collection, shard);
        sreq.actualShards = sreq.shards;
        
        rb.addRequest(this, sreq);
      }      
    } else {
      ShardRequest sreq = createShardRequest(rb, reqIds.allIds);
      sreq.shards = null;  // ALL
      sreq.actualShards = sreq.shards;

      rb.addRequest(this, sreq);
    }

    return ResponseBuilder.STAGE_DONE;
  }

  /**
   * Helper method for creating a new ShardRequest for the specified ids, based on the params 
   * specified for the current request.  The new ShardRequest does not yet know anything about 
   * which shard/slice it will be sent to.
   */
  private ShardRequest createShardRequest(final ResponseBuilder rb, final List<String> ids) {
    final ShardRequest sreq = new ShardRequest();
    sreq.purpose = 1;
    sreq.params = new ModifiableSolrParams(rb.req.getParams());

    // TODO: how to avoid hardcoding this and hit the same handler?
    sreq.params.set(ShardParams.SHARDS_QT,"/get");      
    sreq.params.set(DISTRIB,false);

    sreq.params.remove(ShardParams.SHARDS);
    sreq.params.remove(ID);
    sreq.params.remove("ids");
    sreq.params.set("ids", StrUtils.join(ids, ','));
    
    return sreq;
  }
  
  private String[] sliceToShards(ResponseBuilder rb, String collection, String slice) {
    String lookup = collection + '_' + slice;  // seems either form may be filled in rb.slices?
    
    // We use this since the shard handler already filled in the slice to shards mapping.
    // A better approach would be to avoid filling out every slice each time, or to cache
    // the mappings.

    for (int i=0; i<rb.slices.length; i++) {
      log.info("LOOKUP_SLICE:" + rb.slices[i] + "=" + rb.shards[i]);
      if (lookup.equals(rb.slices[i]) || slice.equals(rb.slices[i])) {
        return new String[]{rb.shards[i]};
      }
    }


    throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Can't find shard '" + lookup + "'");
  }

  /***
  private void handleRegularResponses(ResponseBuilder rb, ShardRequest sreq) {
  }
  ***/

  @Override
  public void finishStage(ResponseBuilder rb) {
    if (rb.stage != ResponseBuilder.STAGE_GET_FIELDS) {
      return;
    }
    
    mergeResponses(rb);
  }
  
  private void mergeResponses(ResponseBuilder rb) {
    SolrDocumentList docList = new SolrDocumentList();
    
    for (ShardRequest sreq : rb.finished) {
      // if shards=shard1,shard2 was used, then  we query both shards for each id and
      // can get more than one response
      for (ShardResponse srsp : sreq.responses) {
        SolrResponse sr = srsp.getSolrResponse();
        NamedList nl = sr.getResponse();
        SolrDocumentList subList = (SolrDocumentList)nl.get("response");
        docList.addAll(subList);
      }
    }
    
    addDocListToResponse(rb, docList);
  }

  /**
   * Encapsulates logic for how a {@link SolrDocumentList} should be added to the response
   * based on the request params used
   */
  private void addDocListToResponse(final ResponseBuilder rb, final SolrDocumentList docList) {
    assert null != docList;
    
    final SolrQueryResponse rsp = rb.rsp;
    final IdsRequsted reqIds = IdsRequsted.parseParams(rb.req);
    
    if (reqIds.useSingleDocResponse) {
      assert docList.size() <= 1;
      // if the doc was not found, then use a value of null.
      rsp.add("doc", docList.size() > 0 ? docList.get(0) : null);
    } else {
      docList.setNumFound(docList.size());
      rsp.addResponse(docList);
    }
  }

                                                                                               

  ////////////////////////////////////////////
  ///  SolrInfoBean
  ////////////////////////////////////////////

  @Override
  public String getDescription() {
    return "query";
  }

  @Override
  public Category getCategory() {
    return Category.QUERY;
  }

  public void processGetFingeprint(ResponseBuilder rb) throws IOException {
    TestInjection.injectFailIndexFingerprintRequests();

    SolrQueryRequest req = rb.req;
    SolrParams params = req.getParams();

    long maxVersion = params.getLong("getFingerprint", Long.MAX_VALUE);
    if (TestInjection.injectWrongIndexFingerprint())  {
      maxVersion = -1;
    }
    IndexFingerprint fingerprint = IndexFingerprint.getFingerprint(req.getCore(), Math.abs(maxVersion));
    rb.rsp.add("fingerprint", fingerprint);
  }
  

  ///////////////////////////////////////////////////////////////////////////////////
  // Returns last versions added to index
  ///////////////////////////////////////////////////////////////////////////////////


  public void processGetVersions(ResponseBuilder rb) throws IOException
  {
    SolrQueryRequest req = rb.req;
    SolrQueryResponse rsp = rb.rsp;
    SolrParams params = req.getParams();

    if (!params.getBool(COMPONENT_NAME, true)) {
      return;
    }

    int nVersions = params.getInt("getVersions", -1);
    if (nVersions == -1) return;

    boolean doFingerprint = params.getBool("fingerprint", false);

    String sync = params.get("sync");
    if (sync != null) {
      processSync(rb, nVersions, sync);
      return;
    }

    UpdateLog ulog = req.getCore().getUpdateHandler().getUpdateLog();
    if (ulog == null) return;
    String syncWithLeader = params.get("syncWithLeader");
    if (syncWithLeader != null) {
      List<Long> versions;
      try (UpdateLog.RecentUpdates recentUpdates = ulog.getRecentUpdates()) {
        versions = recentUpdates.getVersions(nVersions);
      }
      processSyncWithLeader(rb, nVersions, syncWithLeader, versions);
      return;
    }

    // get fingerprint first as it will cause a soft commit
    // and would avoid mismatch if documents are being actively index especially during PeerSync
    if (doFingerprint) {
      IndexFingerprint fingerprint = IndexFingerprint.getFingerprint(req.getCore(), Long.MAX_VALUE);
      rb.rsp.add("fingerprint", fingerprint);
    }

    try (UpdateLog.RecentUpdates recentUpdates = ulog.getRecentUpdates()) {
      List<Long> versions = recentUpdates.getVersions(nVersions);
      rb.rsp.add("versions", versions);
    }
  }

  public void processSyncWithLeader(ResponseBuilder rb, int nVersions, String syncWithLeader, List<Long> versions) {
    PeerSyncWithLeader peerSync = new PeerSyncWithLeader(rb.req.getCore(), syncWithLeader, nVersions);
    boolean success = peerSync.sync(versions).isSuccess();
    rb.rsp.add("syncWithLeader", success);
  }

  
  public void processSync(ResponseBuilder rb, int nVersions, String sync) {
    
    boolean onlyIfActive = rb.req.getParams().getBool("onlyIfActive", false);
    
    if (onlyIfActive) {
      if (rb.req.getCore().getCoreDescriptor().getCloudDescriptor().getLastPublished() != Replica.State.ACTIVE) {
        log.info("Last published state was not ACTIVE, cannot sync.");
        rb.rsp.add("sync", "false");
        return;
      }
    }
    
    List<String> replicas = StrUtils.splitSmart(sync, ",", true);
    
    boolean cantReachIsSuccess = rb.req.getParams().getBool("cantReachIsSuccess", false);
    
    PeerSync peerSync = new PeerSync(rb.req.getCore(), replicas, nVersions, cantReachIsSuccess);
    boolean success = peerSync.sync().isSuccess();
    
    // TODO: more complex response?
    rb.rsp.add("sync", success);
  }
  

  public void processGetUpdates(ResponseBuilder rb) throws IOException
  {
    SolrQueryRequest req = rb.req;
    SolrQueryResponse rsp = rb.rsp;
    SolrParams params = req.getParams();

    if (!params.getBool(COMPONENT_NAME, true)) {
      return;
    }

    String versionsStr = params.get("getUpdates");
    if (versionsStr == null) return;

    UpdateLog ulog = req.getCore().getUpdateHandler().getUpdateLog();
    if (ulog == null) return;

    // handle version ranges
    List<Long> versions = null;
    if (versionsStr.indexOf("...") != -1) {
      versions = resolveVersionRanges(versionsStr, ulog);
    } else {
      versions = StrUtils.splitSmart(versionsStr, ",", true).stream().map(Long::parseLong)
          .collect(Collectors.toList());
    }

    // find fingerprint for max version for which updates are requested
    boolean doFingerprint = params.getBool("fingerprint", false);
    if (doFingerprint) {
      long maxVersionForUpdate = Collections.min(versions, PeerSync.absComparator);
      IndexFingerprint fingerprint = IndexFingerprint.getFingerprint(req.getCore(), Math.abs(maxVersionForUpdate));
      rb.rsp.add("fingerprint", fingerprint);
    }

    List<Object> updates = new ArrayList<>(versions.size());

    long minVersion = Long.MAX_VALUE;

    // TODO: get this from cache instead of rebuilding?
    try (UpdateLog.RecentUpdates recentUpdates = ulog.getRecentUpdates()) {
      for (Long version : versions) {
        try {
          Object o = recentUpdates.lookup(version);
          if (o == null) continue;

          if (version > 0) {
            minVersion = Math.min(minVersion, version);
          }

          // TODO: do any kind of validation here?
          updates.add(o);

        } catch (SolrException | ClassCastException e) {
          log.warn("Exception reading log for updates", e);
        }
      }

      // Must return all delete-by-query commands that occur after the first add requested
      // since they may apply.
      if (params.getBool("skipDbq", false)) {
        updates.addAll(recentUpdates.getDeleteByQuery(minVersion));
      }

      rb.rsp.add("updates", updates);

    }
  }
  
  
  private List<Long> resolveVersionRanges(String versionsStr, UpdateLog ulog) {
    if (StringUtils.isEmpty(versionsStr)) {
      return Collections.emptyList();
    }
    
    List<String> ranges = StrUtils.splitSmart(versionsStr, ",", true);
    
    // TODO merge ranges.
    
    // get all the versions from updatelog and sort them
    List<Long> versionAvailable = null;
    try (UpdateLog.RecentUpdates recentUpdates = ulog.getRecentUpdates()) {
      versionAvailable = recentUpdates.getVersions(ulog.getNumRecordsToKeep());
    }
    // sort versions
    Collections.sort(versionAvailable, PeerSync.absComparator);
    
    // This can be done with single pass over both ranges and versionsAvailable, that would require 
    // merging ranges. We currently use Set to ensure there are no duplicates.
    Set<Long> versionsToRet = new HashSet<>(ulog.getNumRecordsToKeep());
    for (String range : ranges) {
      String[] rangeBounds = range.split("\\.{3}");
      int indexStart = Collections.binarySearch(versionAvailable, Long.valueOf(rangeBounds[1]), PeerSync.absComparator);
      int indexEnd = Collections.binarySearch(versionAvailable, Long.valueOf(rangeBounds[0]), PeerSync.absComparator); 
      if(indexStart >=0 && indexEnd >= 0) {
        versionsToRet.addAll(versionAvailable.subList(indexStart, indexEnd + 1)); // indexEnd is exclusive
      }
    }
    // TODO do we need to sort versions using PeerSync.absComparator?
    return new ArrayList<>(versionsToRet);
  }

  /**
   *  <p>
   *    Lookup strategy for {@link #getInputDocument(SolrCore, BytesRef, AtomicLong, Set, Resolution)}.
   *  </p>
   *  <ul>
   *    <li>{@link #DOC}</li>
   *    <li>{@link #DOC_WITH_CHILDREN}</li>
   *    <li>{@link #ROOT_WITH_CHILDREN}</li>
   *  </ul>
   */
  public static enum Resolution {
    /**
     * Resolve this partial document to a full document (by following back prevPointer/prevVersion)?
     */
    DOC,
    /**
     * Check whether the document has child documents. If so, return the document including its children.
     */
    DOC_WITH_CHILDREN,
    /**
     * Check whether the document is part of a nested hierarchy. If so, return the whole hierarchy(look up root doc).
     */
    ROOT_WITH_CHILDREN
  }

  /** 
   * Simple struct for tracking what ids were requested and what response format is expected 
   * acording to the request params
   */
  private final static class IdsRequsted {
    /** An List (which may be empty but will never be null) of the uniqueKeys requested. */
    public final List<String> allIds;
    /** 
     * true if the params provided by the user indicate that a single doc response structure 
     * should be used.  
     * Value is meaninless if <code>ids</code> is empty.
     */
    public final boolean useSingleDocResponse;
    private IdsRequsted(List<String> allIds, boolean useSingleDocResponse) {
      assert null != allIds;
      this.allIds = allIds;
      this.useSingleDocResponse = useSingleDocResponse;
    }
    
    /**
     * Parsers the <code>id</code> and <code>ids</code> params attached to the specified request object, 
     * and returns an <code>IdsRequsted</code> struct to use for this request.
     * The <code>IdsRequsted</code> is cached in the {@link SolrQueryRequest#getContext} so subsequent 
     * method calls on the same request will not re-parse the params.
     */
    public static IdsRequsted parseParams(SolrQueryRequest req) {
      final String contextKey = IdsRequsted.class.toString() + "_PARSED_ID_PARAMS";
      if (req.getContext().containsKey(contextKey)) {
        return (IdsRequsted)req.getContext().get(contextKey);
      }
      final SolrParams params = req.getParams();
      final String id[] = params.getParams(ID);
      final String ids[] = params.getParams("ids");
      
      if (id == null && ids == null) {
        IdsRequsted result = new IdsRequsted(Collections.<String>emptyList(), true);
        req.getContext().put(contextKey, result);
        return result;
      }
      final List<String> allIds = new ArrayList<>((null == id ? 0 : id.length)
                                                  + (null == ids ? 0 : (2 * ids.length)));
      if (null != id) {
        for (String singleId : id) {
          allIds.add(singleId);
        }
      }
      if (null != ids) {
        for (String idList : ids) {
          allIds.addAll( StrUtils.splitSmart(idList, ",", true) );
        }
      }
      // if the client specified a single id=foo, then use "doc":{
      // otherwise use a standard doclist
      IdsRequsted result = new IdsRequsted(allIds, (ids == null && allIds.size() <= 1));
      req.getContext().put(contextKey, result);
      return result;
    }
  }

  
  /**
   * A lite weight ResultContext for use with RTG requests that can point at Realtime Searchers
   */
  private static final class RTGResultContext extends ResultContext {
    final ReturnFields returnFields;
    final SolrIndexSearcher searcher;
    final SolrQueryRequest req;
    public RTGResultContext(ReturnFields returnFields, SolrIndexSearcher searcher, SolrQueryRequest req) {
      this.returnFields = returnFields;
      this.searcher = searcher;
      this.req = req;
    }
    
    /** @returns null */
    public DocList getDocList() {
      return null;
    }
    
    public ReturnFields getReturnFields() {
      return this.returnFields;
    }
    
    public SolrIndexSearcher getSearcher() {
      return this.searcher;
    }
    
    /** @returns null */
    public Query getQuery() {
      return null;
    }
    
    public SolrQueryRequest getRequest() {
      return this.req;
    }
    
    /** @returns null */
    public Iterator<SolrDocument> getProcessedDocuments() {
      return null;
    }
  }
  
}
