/*
 * 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.geode.cache.query.internal.index;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;

import org.apache.geode.cache.Region;
import org.apache.geode.cache.query.FunctionDomainException;
import org.apache.geode.cache.query.Index;
import org.apache.geode.cache.query.IndexStatistics;
import org.apache.geode.cache.query.NameResolutionException;
import org.apache.geode.cache.query.QueryInvocationTargetException;
import org.apache.geode.cache.query.QueryService;
import org.apache.geode.cache.query.SelectResults;
import org.apache.geode.cache.query.Struct;
import org.apache.geode.cache.query.TypeMismatchException;
import org.apache.geode.cache.query.internal.CompiledID;
import org.apache.geode.cache.query.internal.CompiledIndexOperation;
import org.apache.geode.cache.query.internal.CompiledIteratorDef;
import org.apache.geode.cache.query.internal.CompiledOperation;
import org.apache.geode.cache.query.internal.CompiledPath;
import org.apache.geode.cache.query.internal.CompiledValue;
import org.apache.geode.cache.query.internal.CqEntry;
import org.apache.geode.cache.query.internal.DefaultQuery;
import org.apache.geode.cache.query.internal.ExecutionContext;
import org.apache.geode.cache.query.internal.IndexInfo;
import org.apache.geode.cache.query.internal.QRegion;
import org.apache.geode.cache.query.internal.QueryMonitor;
import org.apache.geode.cache.query.internal.QueryUtils;
import org.apache.geode.cache.query.internal.RuntimeIterator;
import org.apache.geode.cache.query.internal.StructFields;
import org.apache.geode.cache.query.internal.StructImpl;
import org.apache.geode.cache.query.internal.Support;
import org.apache.geode.cache.query.internal.index.IndexStore.IndexStoreEntry;
import org.apache.geode.cache.query.internal.parse.OQLLexerTokenTypes;
import org.apache.geode.cache.query.internal.types.StructTypeImpl;
import org.apache.geode.cache.query.types.ObjectType;
import org.apache.geode.internal.Assert;
import org.apache.geode.internal.cache.BucketRegion;
import org.apache.geode.internal.cache.CachedDeserializable;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.internal.cache.LocalRegion;
import org.apache.geode.internal.cache.NonTXEntry;
import org.apache.geode.internal.cache.PartitionedRegion;
import org.apache.geode.internal.cache.RegionEntry;
import org.apache.geode.internal.cache.RegionEntryContext;
import org.apache.geode.internal.cache.partitioned.Bucket;
import org.apache.geode.internal.cache.persistence.query.CloseableIterator;
import org.apache.geode.internal.offheap.annotations.Retained;
import org.apache.geode.logging.internal.log4j.api.LogService;
import org.apache.geode.pdx.PdxInstance;
import org.apache.geode.pdx.internal.PdxString;

/**
 * This class implements abstract algorithms common to all indexes, such as index creation, use of a
 * path evaluator object, etc. It serves as the factory for a path evaluator object and maintains
 * the path evaluator object to use for index creation and index maintenance. It also maintains a
 * reference to the root collection on which the index is created. This class also implements the
 * abstract methods to add and remove entries to an underlying storage structure (e.g. a btree), and
 * as part of this algorithm, maintains a map of entries that map to null at the end of the index
 * path, and entries that cannot be traversed to the end of the index path (traversal is undefined).
 */
public abstract class AbstractIndex implements IndexProtocol {
  private static final Logger logger = LogService.getLogger();

  // package-private to avoid synthetic accessor
  static final AtomicIntegerFieldUpdater<RegionEntryToValuesMap> atomicUpdater =
      AtomicIntegerFieldUpdater.newUpdater(RegionEntryToValuesMap.class, "numValues");

  final InternalCache cache;

  final String indexName;

  final Region region;

  final String indexedExpression;

  final String fromClause;

  final String projectionAttributes;

  final String originalIndexedExpression;

  final String originalFromClause;

  private final String originalProjectionAttributes;

  final String[] canonicalizedDefinitions;

  private boolean isValid;

  protected IndexedExpressionEvaluator evaluator;

  InternalIndexStatistics internalIndexStats;

  /** For PartitionedIndex for now */
  protected Index prIndex;

  /**
   * Flag to indicate if index map has keys as PdxString All the keys in the index map should be
   * either Strings or PdxStrings
   */
  private Boolean isIndexedPdxKeys = false;

  /** Flag to indicate if the flag isIndexedPdxKeys is set */
  Boolean isIndexedPdxKeysFlagSet = false;

  boolean indexOnRegionKeys = false;

  boolean indexOnValues = false;

  private final ReadWriteLock removeIndexLock = new ReentrantReadWriteLock();

  /** Flag to indicate if the index is populated with data */
  volatile boolean isPopulated = false;

  AbstractIndex(InternalCache cache, String indexName, Region region, String fromClause,
      String indexedExpression, String projectionAttributes, String originalFromClause,
      String originalIndexedExpression, String[] defintions, IndexStatistics stats) {
    this.cache = cache;
    this.indexName = indexName;
    this.region = region;
    this.indexedExpression = indexedExpression;
    this.fromClause = fromClause;
    this.originalIndexedExpression = originalIndexedExpression;
    this.originalFromClause = originalFromClause;
    canonicalizedDefinitions = defintions;
    if (StringUtils.isEmpty(projectionAttributes)) {
      projectionAttributes = "*";
    }
    this.projectionAttributes = projectionAttributes;
    originalProjectionAttributes = projectionAttributes;
    if (stats != null) {
      internalIndexStats = (InternalIndexStatistics) stats;
    } else {
      internalIndexStats = createStats(indexName);
    }
  }

  /**
   * Must be implemented by all implementing classes iff they have any forward map for
   * index-key->RE.
   *
   * @return the forward map of respective index.
   */
  public Map getValueToEntriesMap() {
    return null;
  }

  /**
   * Get statistics information for this index.
   */
  @Override
  public IndexStatistics getStatistics() {
    return internalIndexStats;
  }

  @Override
  public void destroy() {
    markValid(false);
    if (internalIndexStats != null) {
      internalIndexStats.updateNumKeys(0);
      internalIndexStats.close();
    }
  }

  long updateIndexUpdateStats() {
    long result = System.nanoTime();
    internalIndexStats.incUpdatesInProgress(1);
    return result;
  }

  void updateIndexUpdateStats(long start) {
    long end = System.nanoTime();
    internalIndexStats.incUpdatesInProgress(-1);
    internalIndexStats.incUpdateTime(end - start);
  }

  long updateIndexUseStats() {
    return updateIndexUseStats(true);
  }

  long updateIndexUseStats(boolean updateStats) {
    long result = 0;
    if (updateStats) {
      internalIndexStats.incUsesInProgress(1);
      result = System.nanoTime();
    }
    return result;
  }

  void updateIndexUseEndStats(long start) {
    updateIndexUseEndStats(start, true);
  }

  void updateIndexUseEndStats(long start, boolean updateStats) {
    if (updateStats) {
      long end = System.nanoTime();
      internalIndexStats.incUsesInProgress(-1);
      internalIndexStats.incNumUses();
      internalIndexStats.incUseTime(end - start);
    }
  }

  public IndexedExpressionEvaluator getEvaluator() {
    return evaluator;
  }

  /**
   * The Region this index is on
   *
   * @return the Region for this index
   */
  @Override
  public Region getRegion() {
    return region;
  }

  /**
   * Returns the unique name of this index
   */
  @Override
  public String getName() {
    return indexName;
  }

  @Override
  public void query(Object key, int operator, Collection results, ExecutionContext context)
      throws TypeMismatchException, FunctionDomainException, NameResolutionException,
      QueryInvocationTargetException {

    // get a read lock when doing a lookup
    if (context.getBucketList() != null && region instanceof BucketRegion) {
      PartitionedRegion pr = ((Bucket) region).getPartitionedRegion();
      long start = updateIndexUseStats();
      try {
        for (Object bucketId : context.getBucketList()) {
          AbstractIndex bucketIndex =
              PartitionedIndex.getBucketIndex(pr, indexName, (Integer) bucketId);
          if (bucketIndex == null) {
            continue;
          }
          bucketIndex.lockedQuery(key, operator, results, null/* No Keys to be removed */, context);

        }
      } finally {
        updateIndexUseEndStats(start);
      }
    } else {
      long start = updateIndexUseStats();
      try {
        lockedQuery(key, operator, results, null/* No Keys to be removed */, context);
      } finally {
        updateIndexUseEndStats(start);
      }
    }
  }

  @Override
  public void query(Object key, int operator, Collection results, @Retained CompiledValue iterOp,
      RuntimeIterator indpndntItr, ExecutionContext context, List projAttrib,
      SelectResults intermediateResults, boolean isIntersection) throws TypeMismatchException,
      FunctionDomainException, NameResolutionException, QueryInvocationTargetException {

    // get a read lock when doing a lookup
    if (context.getBucketList() != null && region instanceof BucketRegion) {
      PartitionedRegion pr = ((Bucket) region).getPartitionedRegion();
      long start = updateIndexUseStats();
      try {
        for (Object bucketId : context.getBucketList()) {
          AbstractIndex bucketIndex =
              PartitionedIndex.getBucketIndex(pr, indexName, (Integer) bucketId);
          if (bucketIndex == null) {
            continue;
          }
          bucketIndex.lockedQuery(key, operator, results, iterOp, indpndntItr, context, projAttrib,
              intermediateResults, isIntersection);
        }
      } finally {
        updateIndexUseEndStats(start);
      }
    } else {
      long start = updateIndexUseStats();
      try {
        lockedQuery(key, operator, results, iterOp, indpndntItr, context, projAttrib,
            intermediateResults, isIntersection);
      } finally {
        updateIndexUseEndStats(start);
      }
    }
  }

  @Override
  public void query(Object key, int operator, Collection results, Set keysToRemove,
      ExecutionContext context) throws TypeMismatchException, FunctionDomainException,
      NameResolutionException, QueryInvocationTargetException {

    // get a read lock when doing a lookup
    if (context.getBucketList() != null && region instanceof BucketRegion) {
      PartitionedRegion pr = ((Bucket) region).getPartitionedRegion();
      long start = updateIndexUseStats();
      try {
        for (Object bucketId : context.getBucketList()) {
          AbstractIndex bucketIndex =
              PartitionedIndex.getBucketIndex(pr, indexName, (Integer) bucketId);
          if (bucketIndex == null) {
            continue;
          }
          bucketIndex.lockedQuery(key, operator, results, keysToRemove, context);
        }
      } finally {
        updateIndexUseEndStats(start);
      }
    } else {
      long start = updateIndexUseStats();
      try {
        lockedQuery(key, operator, results, keysToRemove, context);
      } finally {
        updateIndexUseEndStats(start);
      }
    }
  }

  @Override
  public void query(Collection results, Set keysToRemove, ExecutionContext context)
      throws TypeMismatchException, FunctionDomainException, NameResolutionException,
      QueryInvocationTargetException {

    Iterator iterator = keysToRemove.iterator();
    Object temp = iterator.next();
    iterator.remove();
    if (context.getBucketList() != null && region instanceof BucketRegion) {
      long start = updateIndexUseStats();
      try {
        PartitionedRegion partitionedRegion = ((Bucket) region).getPartitionedRegion();
        for (Object bucketId : context.getBucketList()) {
          AbstractIndex bucketIndex = PartitionedIndex.getBucketIndex(partitionedRegion,
              indexName, (Integer) bucketId);
          if (bucketIndex == null) {
            continue;
          }
          bucketIndex.lockedQuery(temp, OQLLexerTokenTypes.TOK_NE, results,
              iterator.hasNext() ? keysToRemove : null, context);
        }
      } finally {
        updateIndexUseEndStats(start);
      }
    } else {
      long start = updateIndexUseStats();
      try {
        lockedQuery(temp, OQLLexerTokenTypes.TOK_NE, results,
            iterator.hasNext() ? keysToRemove : null, context);
      } finally {
        updateIndexUseEndStats(start);
      }
    }
  }

  @Override
  public void query(Object lowerBoundKey, int lowerBoundOperator, Object upperBoundKey,
      int upperBoundOperator, Collection results, Set keysToRemove, ExecutionContext context)
      throws TypeMismatchException, FunctionDomainException, NameResolutionException,
      QueryInvocationTargetException {

    if (context.getBucketList() != null) {
      if (region instanceof BucketRegion) {
        PartitionedRegion partitionedRegion = ((Bucket) region).getPartitionedRegion();
        long start = updateIndexUseStats();
        try {
          for (Object bucketId : context.getBucketList()) {
            AbstractIndex bucketIndex = PartitionedIndex.getBucketIndex(partitionedRegion,
                indexName, (Integer) bucketId);
            if (bucketIndex == null) {
              continue;
            }
            bucketIndex.lockedQuery(lowerBoundKey, lowerBoundOperator, upperBoundKey,
                upperBoundOperator, results, keysToRemove, context);
          }
        } finally {
          updateIndexUseEndStats(start);
        }
      }
    } else {
      long start = updateIndexUseStats();
      try {
        lockedQuery(lowerBoundKey, lowerBoundOperator, upperBoundKey, upperBoundOperator, results,
            keysToRemove, context);
      } finally {
        updateIndexUseEndStats(start);
      }
    }
  }

  @Override
  public List queryEquijoinCondition(IndexProtocol index, ExecutionContext context)
      throws TypeMismatchException, FunctionDomainException, NameResolutionException,
      QueryInvocationTargetException {

    Support.assertionFailed(
        " This function should have never got invoked as its meaningful implementation is present only in RangeIndex class");
    return null;
  }

  /**
   * Get the projectionAttributes for this expression.
   *
   * @return the projectionAttributes, or "*" if there were none specified at index creation.
   */
  @Override
  public String getProjectionAttributes() {
    return originalProjectionAttributes;
  }

  /**
   * Get the projectionAttributes for this expression.
   *
   * @return the projectionAttributes, or "*" if there were none specified at index creation.
   */
  @Override
  public String getCanonicalizedProjectionAttributes() {
    return projectionAttributes;
  }

  /**
   * Get the Original indexedExpression for this index.
   */
  @Override
  public String getIndexedExpression() {
    return originalIndexedExpression;
  }

  /**
   * Get the Canonicalized indexedExpression for this index.
   */
  @Override
  public String getCanonicalizedIndexedExpression() {
    return indexedExpression;
  }

  /**
   * Get the original fromClause for this index.
   */
  @Override
  public String getFromClause() {
    return originalFromClause;
  }

  /**
   * Get the canonicalized fromClause for this index.
   */
  @Override
  public String getCanonicalizedFromClause() {
    return fromClause;
  }

  public boolean isMapType() {
    return false;
  }

  @Override
  public boolean addIndexMapping(RegionEntry entry) throws IMQException {
    addMapping(entry);

    // if no exception, then success
    return true;
  }

  @Override
  public boolean addAllIndexMappings(Collection<RegionEntry> c) throws IMQException {
    for (RegionEntry regionEntry : c) {
      addMapping(regionEntry);
    }
    // if no exception, then success
    return true;
  }

  /**
   * @param opCode one of OTHER_OP, BEFORE_UPDATE_OP, AFTER_UPDATE_OP.
   */
  @Override
  public boolean removeIndexMapping(RegionEntry entry, int opCode) throws IMQException {
    removeMapping(entry, opCode);
    // if no exception, then success
    return true;
  }

  @Override
  public boolean removeAllIndexMappings(Collection<RegionEntry> c) throws IMQException {
    for (RegionEntry regionEntry : c) {
      removeMapping(regionEntry, OTHER_OP);
    }
    // if no exception, then success
    return true;
  }


  @Override
  public boolean isValid() {
    return isValid;
  }

  @Override
  public void markValid(boolean b) {
    isValid = b;
  }

  @Override
  public boolean isMatchingWithIndexExpression(CompiledValue condnExpr, String condnExprStr,
      ExecutionContext context)
      throws TypeMismatchException, NameResolutionException {
    return indexedExpression.equals(condnExprStr);
  }

  // package-private to avoid synthetic accessor
  Object verifyAndGetPdxDomainObject(Object value) {
    if (value instanceof StructImpl) {
      // Doing hasPdx check first, since its cheaper.
      if (((StructImpl) value).isHasPdx()
          && !((InternalCache) region.getCache()).getPdxReadSerializedByAnyGemFireServices()) {
        // Set the pdx values for the struct object.
        StructImpl v = (StructImpl) value;
        Object[] fieldValues = v.getPdxFieldValues();
        return new StructImpl((StructTypeImpl) v.getStructType(), fieldValues);
      }
    } else if (value instanceof PdxInstance
        && !((InternalCache) region.getCache()).getPdxReadSerializedByAnyGemFireServices()) {
      return ((PdxInstance) value).getObject();
    }
    return value;
  }

  private void addToResultsWithUnionOrIntersection(Collection results,
      SelectResults intermediateResults, boolean isIntersection, Object value) {
    value = verifyAndGetPdxDomainObject(value);

    if (intermediateResults == null) {
      results.add(value);
    } else {
      if (isIntersection) {
        int numOcc = intermediateResults.occurrences(value);
        if (numOcc > 0) {
          results.add(value);
          intermediateResults.remove(value);
        }
      } else {
        results.add(value);
      }
    }
  }

  private void addToStructsWithUnionOrIntersection(Collection results,
      SelectResults intermediateResults, boolean isIntersection, Object[] values) {

    for (int i = 0; i < values.length; i++) {
      values[i] = verifyAndGetPdxDomainObject(values[i]);
    }

    if (intermediateResults == null) {
      if (results instanceof StructFields) {
        ((StructFields) results).addFieldValues(values);
      } else {
        // The results could be LinkedStructSet or SortedResultsBag or StructSet
        SelectResults selectResults = (SelectResults) results;
        StructImpl structImpl = new StructImpl(
            (StructTypeImpl) selectResults.getCollectionType().getElementType(), values);
        selectResults.add(structImpl);
      }

    } else {
      if (isIntersection) {
        if (results instanceof StructFields) {
          int occurrences = intermediateResults.occurrences(values);
          if (occurrences > 0) {
            ((StructFields) results).addFieldValues(values);
            ((StructFields) intermediateResults).removeFieldValues(values);
          }

        } else {
          // could be LinkedStructSet or SortedResultsBag
          SelectResults selectResults = (SelectResults) results;
          StructImpl structImpl = new StructImpl(
              (StructTypeImpl) selectResults.getCollectionType().getElementType(), values);
          if (intermediateResults.remove(structImpl)) {
            selectResults.add(structImpl);
          }
        }

      } else {
        if (results instanceof StructFields) {
          ((StructFields) results).addFieldValues(values);
        } else {
          // could be LinkedStructSet or SortedResultsBag
          SelectResults selectResults = (SelectResults) results;
          StructImpl structImpl = new StructImpl(
              (StructTypeImpl) selectResults.getCollectionType().getElementType(), values);
          if (intermediateResults.remove(structImpl)) {
            selectResults.add(structImpl);
          }
        }
      }
    }
  }

  void applyCqOrProjection(List projAttrib, ExecutionContext context, Collection result,
      Object iterValue, SelectResults intermediateResults, boolean isIntersection, Object key)
      throws FunctionDomainException, TypeMismatchException, NameResolutionException,
      QueryInvocationTargetException {
    if (context != null && context.isCqQueryContext()) {
      result.add(new CqEntry(key, iterValue));
    } else {
      applyProjection(projAttrib, context, result, iterValue, intermediateResults, isIntersection);
    }
  }

  void applyProjection(List projAttrib, ExecutionContext context, Collection result,
      Object iterValue, SelectResults intermediateResults, boolean isIntersection)
      throws FunctionDomainException, TypeMismatchException, NameResolutionException,
      QueryInvocationTargetException {

    if (projAttrib == null) {
      iterValue = deserializePdxForLocalDistinctQuery(context, iterValue);
      addToResultsWithUnionOrIntersection(result, intermediateResults, isIntersection,
          iterValue);

    } else {
      boolean isStruct = result instanceof SelectResults
          && ((SelectResults) result).getCollectionType().getElementType() != null
          && ((SelectResults) result).getCollectionType().getElementType().isStructType();

      if (isStruct) {
        int projCount = projAttrib.size();
        Object[] values = new Object[projCount];
        Iterator projIter = projAttrib.iterator();
        int i = 0;
        while (projIter.hasNext()) {
          Object[] projDef = (Object[]) projIter.next();
          values[i] = deserializePdxForLocalDistinctQuery(context,
              ((CompiledValue) projDef[1]).evaluate(context));
          i++;
        }
        addToStructsWithUnionOrIntersection(result, intermediateResults, isIntersection,
            values);
      } else {
        Object[] temp = (Object[]) projAttrib.get(0);
        Object val = deserializePdxForLocalDistinctQuery(context,
            ((CompiledValue) temp[1]).evaluate(context));
        addToResultsWithUnionOrIntersection(result, intermediateResults, isIntersection, val);
      }
    }
  }

  /**
   * For local queries with distinct, deserialize all PdxInstances as we do not have a way to
   * compare Pdx and non Pdx objects in case the cache has a mix of pdx and non pdx objects. We
   * still have to honor the cache level readSerialized flag in case of all Pdx objects in cache.
   * Also always convert PdxString to String before adding to resultSet for remote queries
   */
  private Object deserializePdxForLocalDistinctQuery(ExecutionContext context, Object value)
      throws QueryInvocationTargetException {

    if (!((DefaultQuery) context.getQuery()).isRemoteQuery()) {
      if (context.isDistinct() && value instanceof PdxInstance
          && !region.getCache().getPdxReadSerialized()) {
        try {
          value = ((PdxInstance) value).getObject();
        } catch (Exception ex) {
          throw new QueryInvocationTargetException(
              "Unable to retrieve domain object from PdxInstance while building the ResultSet. "
                  + ex.getMessage());
        }
      } else if (value instanceof PdxString) {
        value = value.toString();
      }
    }
    return value;
  }

  private void removeFromResultsWithUnionOrIntersection(Collection results,
      SelectResults intermediateResults, boolean isIntersection, Object value) {

    if (intermediateResults == null) {
      results.remove(value);
    } else {
      if (isIntersection) {
        int numOcc = ((SelectResults) results).occurrences(value);
        if (numOcc > 0) {
          results.remove(value);
          intermediateResults.add(value);
        }
      } else {
        results.remove(value);
      }
    }
  }

  private void removeFromStructsWithUnionOrIntersection(Collection results,
      SelectResults intermediateResults, boolean isIntersection, Object[] values) {

    if (intermediateResults == null) {
      ((StructFields) results).removeFieldValues(values);
    } else {
      if (isIntersection) {
        int numOcc = ((SelectResults) results).occurrences(values);
        if (numOcc > 0) {
          ((StructFields) results).removeFieldValues(values);
          ((StructFields) intermediateResults).addFieldValues(values);

        }
      } else {
        ((StructFields) results).removeFieldValues(values);
      }
    }
  }

  private void removeProjection(List projAttrib, ExecutionContext context, Collection result,
      Object iterValue, SelectResults intermediateResults, boolean isIntersection)
      throws FunctionDomainException, TypeMismatchException, NameResolutionException,
      QueryInvocationTargetException {

    if (projAttrib == null) {
      removeFromResultsWithUnionOrIntersection(result, intermediateResults, isIntersection,
          iterValue);
    } else {
      if (result instanceof StructFields) {
        int projCount = projAttrib.size();
        Object[] values = new Object[projCount];
        Iterator projIter = projAttrib.iterator();
        int i = 0;
        while (projIter.hasNext()) {
          Object[] projDef = (Object[]) projIter.next();
          values[i++] = ((CompiledValue) projDef[1]).evaluate(context);
        }
        removeFromStructsWithUnionOrIntersection(result, intermediateResults, isIntersection,
            values);
      } else {
        Object[] temp = (Object[]) projAttrib.get(0);
        Object val = ((CompiledValue) temp[1]).evaluate(context);
        removeFromResultsWithUnionOrIntersection(result, intermediateResults, isIntersection,
            val);
      }
    }

  }

  /**
   * This function returns the canonicalized definitions of the from clauses used in Index creation
   */
  @Override
  public String[] getCanonicalizedIteratorDefinitions() {
    return canonicalizedDefinitions;
  }

  /**
   * This implementation is for PrimaryKeyIndex. RangeIndex has its own implementation. For
   * PrimaryKeyIndex , this method should not be used
   * <p>
   * TODO: check if an Exception should be thrown if the function implementation of this class gets
   * invoked
   */
  @Override
  public boolean containsEntry(RegionEntry entry) {
    return false;
  }

  abstract void instantiateEvaluator(IndexCreationHelper indexCreationHelper);

  @Override
  public void initializeIndex(boolean loadEntries) throws IMQException {
    // implement me
  }

  @Override
  public String toString() {
    return "Index ["
        + " Name=" + getName()
        + " Type =" + getType()
        + " IdxExp=" + getIndexedExpression()
        + " From=" + getFromClause()
        + " Proj=" + getProjectionAttributes()
        + ']';
  }

  public abstract boolean isEmpty();

  protected abstract boolean isCompactRangeIndex();

  protected abstract InternalIndexStatistics createStats(String indexName);

  @Override
  public abstract ObjectType getResultSetType();

  abstract void recreateIndexData() throws IMQException;

  abstract void addMapping(RegionEntry entry) throws IMQException;

  abstract void removeMapping(RegionEntry entry, int opCode) throws IMQException;

  abstract void addMapping(Object key, Object value, RegionEntry entry) throws IMQException;

  /**
   * This is used to buffer the index entries evaluated from a RegionEntry which is getting updated
   * at present. These buffered index entries are replaced into the index later all together to
   * avoid remove-add sequence.
   */
  abstract void saveMapping(Object key, Object value, RegionEntry entry) throws IMQException;

  /** Lookup method used when appropriate lock is held */
  abstract void lockedQuery(Object key, int operator, Collection results, CompiledValue iterOps,
      RuntimeIterator indpndntItr, ExecutionContext context, List projAttrib,
      SelectResults intermediateResults, boolean isIntersection) throws TypeMismatchException,
      FunctionDomainException, NameResolutionException, QueryInvocationTargetException;

  abstract void lockedQuery(Object lowerBoundKey, int lowerBoundOperator, Object upperBoundKey,
      int upperBoundOperator, Collection results, Set keysToRemove, ExecutionContext context)
      throws TypeMismatchException, FunctionDomainException, NameResolutionException,
      QueryInvocationTargetException;

  abstract void lockedQuery(Object key, int operator, Collection results, Set keysToRemove,
      ExecutionContext context) throws TypeMismatchException, FunctionDomainException,
      NameResolutionException, QueryInvocationTargetException;

  public Index getPRIndex() {
    return prIndex;
  }

  void setPRIndex(Index parIndex) {
    prIndex = parIndex;
  }

  /**
   * Dummy implementation that subclasses can override.
   */
  protected abstract static class InternalIndexStatistics implements IndexStatistics {
    @Override
    public long getNumUpdates() {
      return 0L;
    }

    @Override
    public long getTotalUpdateTime() {
      return 0L;
    }

    @Override
    public long getTotalUses() {
      return 0L;
    }

    @Override
    public long getNumberOfKeys() {
      return 0L;
    }

    @Override
    public long getNumberOfValues() {
      return 0L;
    }

    @Override
    public long getNumberOfValues(Object key) {
      return 0L;
    }

    @Deprecated
    @Override
    public int getReadLockCount() {
      return (int) getReadLockCountLong();
    }

    @Override
    public long getReadLockCountLong() {
      return 0L;
    }

    @Override
    public long getNumberOfMapIndexKeys() {
      return 0L;
    }

    @Deprecated
    @Override
    public int getNumberOfBucketIndexes() {
      return (int) getNumberOfBucketIndexesLong();
    }

    @Override
    public long getNumberOfBucketIndexesLong() {
      return 0L;
    }

    public void close() {}

    public void incNumValues(int delta) {}

    public void incNumUpdates() {}

    public void incNumUpdates(int delta) {}

    public void incUpdatesInProgress(int delta) {}

    public void incUsesInProgress(int delta) {}

    public void updateNumKeys(long count) {}

    public void incNumKeys(long count) {}

    public void incNumMapIndexKeys(long numKeys) {}

    public void incUpdateTime(long delta) {}

    public void incNumUses() {}

    public void incUseTime(long delta) {}

    public void incReadLockCount(int delta) {}

    public void incNumBucketIndexes(int delta) {}
  }

  class IMQEvaluator implements IndexedExpressionEvaluator {
    private final InternalCache cache;

    private List fromIterators = null;

    private CompiledValue indexedExpr = null;

    private final String[] canonicalIterNames;

    private ObjectType indexResultSetType = null;

    private Map dependencyGraph = null;

    /**
     * The boolean if true indicates that the 0th iterator is on entries . If the 0th iterator is on
     * collection of Region.Entry objects, then the RegionEntry object used in Index data objects is
     * obtained directly from its corresponding Region.Entry object. However if the 0th iterator is
     * not on entries then the boolean is false. In this case the additional projection attribute
     * gives us the original value of the iterator while the Region.Entry object is obtained from
     * 0th iterator. It is possible to have index being created on a Region Entry itself , instead
     * of a Region. A Map operator( Compiled Index Operator) used with Region enables, us to create
     * such indexes. In such case the 0th iterator, even if it represents a collection of Objects
     * which are not Region.Entry objects, still the boolean remains true, as the Entry object can
     * be easily obtained from the 0th iterator. In this case, the additional projection attribute s
     * not null as it is used to evaluate the Entry object from the 0th iterator.
     */
    private boolean isFirstItrOnEntry = false;

    /** The boolean if true indicates that the 0th iterator is on keys. */
    private boolean isFirstItrOnKey = false;

    /**
     * List of modified iterators, not null only when the boolean isFirstItrOnEntry is false.
     */
    private List indexInitIterators = null;

    /**
     * The additional Projection attribute representing the value of the original 0th iterator. If
     * the isFirstItrOnEntry is false, then it is not null. However if the isFirstItrOnEntry is true
     * but & still this attribute is not null, this indicates that the 0th iterator is derived using
     * an individual entry thru Map operator on the Region.
     */
    private CompiledValue additionalProj = null;

    /** This is not null iff the boolean isFirstItrOnEntry is false. */
    private CompiledValue modifiedIndexExpr = null;

    private ObjectType addnlProjType = null;

    private int initEntriesUpdated = 0;

    private boolean hasInitOccurredOnce = false;

    private ExecutionContext initContext = null;

    private int iteratorSize = -1;

    private Region rgn = null;

    /** Creates a new instance of IMQEvaluator */
    IMQEvaluator(IndexCreationHelper helper) {
      cache = helper.getCache();
      fromIterators = helper.getIterators();
      indexedExpr = helper.getCompiledIndexedExpression();
      rgn = helper.getRegion();
      // The modified iterators for optimizing Index creation
      isFirstItrOnEntry = ((FunctionalIndexCreationHelper) helper).isFirstIteratorRegionEntry;
      isFirstItrOnKey = ((FunctionalIndexCreationHelper) helper).isFirstIteratorRegionKey;
      additionalProj = ((FunctionalIndexCreationHelper) helper).additionalProj;
      Object[] params1 = {new QRegion(rgn, false)};
      initContext = new ExecutionContext(params1, cache);
      canonicalIterNames = helper.canonicalizedIteratorNames;
      if (isFirstItrOnEntry) {
        indexInitIterators = fromIterators;
      } else {
        indexInitIterators = ((FunctionalIndexCreationHelper) helper).indexInitIterators;
        modifiedIndexExpr = ((FunctionalIndexCreationHelper) helper).modifiedIndexExpr;
        addnlProjType = ((FunctionalIndexCreationHelper) helper).addnlProjType;
      }
      iteratorSize = indexInitIterators.size();
    }

    @Override
    public String getIndexedExpression() {
      return getCanonicalizedIndexedExpression();
    }

    @Override
    public String getProjectionAttributes() {
      return getCanonicalizedProjectionAttributes();
    }

    @Override
    public String getFromClause() {
      return getCanonicalizedFromClause();
    }

    @Override
    public void expansion(List expandedResults, Object lowerBoundKey, Object upperBoundKey,
        int lowerBoundOperator, int upperBoundOperator, Object value) throws IMQException {
      // no-op
    }

    @Override
    public void evaluate(RegionEntry target, boolean add) throws IMQException {
      assert add; // ignored, but should be true here
      DummyQRegion dQRegion = new DummyQRegion(rgn);
      dQRegion.setEntry(target);
      Object[] params = {dQRegion};
      ExecutionContext context = new ExecutionContext(params, cache);
      context.newScope(IndexCreationHelper.INDEX_QUERY_SCOPE_ID);

      try {
        boolean computeDependency = true;
        if (dependencyGraph != null) {
          context.setDependencyGraph(dependencyGraph);
          computeDependency = false;
        }

        for (int i = 0; i < iteratorSize; i++) {
          CompiledIteratorDef iterDef = (CompiledIteratorDef) fromIterators.get(i);
          // Compute the dependency only once. The call to methods of this
          // class are thread safe as for update lock on Index is taken .
          if (computeDependency) {
            iterDef.computeDependencies(context);
          }
          RuntimeIterator rIter = iterDef.getRuntimeIterator(context);
          context.addToIndependentRuntimeItrMapForIndexCreation(iterDef);
          context.bindIterator(rIter);
        }

        // Save the dependency graph for future updates.
        if (dependencyGraph == null) {
          dependencyGraph = context.getDependencyGraph();
        }

        Support.Assert(indexResultSetType != null,
            "IMQEvaluator::evaluate:The StrcutType should have been initialized during index creation");

        doNestedIterations(0, context);
      } catch (IMQException imqe) {
        throw imqe;
      } catch (Exception e) {
        throw new IMQException(e);
      } finally {
        context.popScope();
      }
    }

    /**
     * This function is used for creating Index data at the start
     */
    @Override
    public void initializeIndex(boolean loadEntries) throws IMQException {
      initEntriesUpdated = 0;
      try {
        // Since an index initialization can happen multiple times for a given region, due to clear
        // operation, we are using hardcoded scope ID of 1 , as otherwise if obtained from
        // ExecutionContext object, it will get incremented on very index initialization
        initContext.newScope(1);
        for (int i = 0; i < iteratorSize; i++) {
          CompiledIteratorDef iterDef = (CompiledIteratorDef) indexInitIterators.get(i);
          RuntimeIterator rIter = null;
          if (!hasInitOccurredOnce) {
            iterDef.computeDependencies(initContext);
            rIter = iterDef.getRuntimeIterator(initContext);
            initContext.addToIndependentRuntimeItrMapForIndexCreation(iterDef);
          }
          if (rIter == null) {
            rIter = iterDef.getRuntimeIterator(initContext);
          }
          initContext.bindIterator(rIter);
        }
        hasInitOccurredOnce = true;
        if (indexResultSetType == null) {
          indexResultSetType = createIndexResultSetType();
        }
        if (loadEntries) {
          doNestedIterationsForIndexInit(0, initContext.getCurrentIterators());
        }
      } catch (IMQException imqe) {
        throw imqe;
      } catch (Exception e) {
        throw new IMQException(e);
      } finally {
        initContext.popScope();
      }
    }

    private void doNestedIterationsForIndexInit(int level, List runtimeIterators)
        throws TypeMismatchException, FunctionDomainException,
        NameResolutionException, QueryInvocationTargetException, IMQException {
      if (level == 1) {
        ++initEntriesUpdated;
      }
      if (level == iteratorSize) {
        applyProjectionForIndexInit(runtimeIterators);
      } else {
        RuntimeIterator rIter = (RuntimeIterator) runtimeIterators.get(level);
        Collection collection = rIter.evaluateCollection(initContext);
        if (collection == null) {
          return;
        }
        for (Object aCollection : collection) {
          rIter.setCurrent(aCollection);
          doNestedIterationsForIndexInit(level + 1, runtimeIterators);
        }
      }
    }

    /**
     * This function is used to obtain Index data at the time of index creation. Each element of the
     * List is an Object Array of size 3. The 0th element of Object Array stores the value of Index
     * Expression. The 1st element of ObjectArray contains the RegionEntry object ( If the boolean
     * isFirstItrOnEntry is false, then the 0th iterator will give us the Region.Entry object which
     * can be used to obtain the underlying RegionEntry object. If the boolean is true & additional
     * projection attribute is not null, then the Region.Entry object can be obtained by evaluating
     * the additional projection attribute. If the boolean isFirstItrOnEntry is true & additional
     * projection attribute is null, then the 0th iterator itself will evaluate to Region.Entry
     * Object.
     * <p>
     * The 2nd element of Object Array contains the Struct object ( tuple) created. If the boolean
     * isFirstItrOnEntry is false, then the first attribute of the Struct object is obtained by
     * evaluating the additional projection attribute.
     */
    private void applyProjectionForIndexInit(List currrentRuntimeIters)
        throws FunctionDomainException, TypeMismatchException, NameResolutionException,
        QueryInvocationTargetException, IMQException {

      if (QueryMonitor.isLowMemory()) {
        throw new IMQException(
            "Index creation canceled due to low memory");
      }

      NonTXEntry temp;

      // Evaluate NonTXEntry for index on entries or additional projections
      // on Entry or just entry value.
      if (isFirstItrOnEntry && additionalProj != null) {
        temp = (NonTXEntry) additionalProj.evaluate(initContext);
      } else {
        temp = (NonTXEntry) ((RuntimeIterator) currrentRuntimeIters.get(0))
            .evaluate(initContext);
      }

      RegionEntry re = temp.getRegionEntry();
      Object indxResultSet;

      if (iteratorSize == 1) {
        indxResultSet = isFirstItrOnEntry
            ? additionalProj == null ? temp
                : ((RuntimeIterator) currrentRuntimeIters.get(0)).evaluate(initContext)
            : additionalProj.evaluate(initContext);
      } else {
        Object[] tuple = new Object[iteratorSize];
        int i = isFirstItrOnEntry ? 0 : 1;
        for (; i < iteratorSize; i++) {
          RuntimeIterator iter = (RuntimeIterator) currrentRuntimeIters.get(i);
          tuple[i] = iter.evaluate(initContext);
        }
        if (!isFirstItrOnEntry) {
          tuple[0] = additionalProj.evaluate(initContext);
        }
        Support.Assert(indexResultSetType instanceof StructTypeImpl,
            "The Index ResultType should have been an instance of StructTypeImpl rather than ObjectTypeImpl. The indxeResultType is "
                + indexResultSetType);
        indxResultSet = new StructImpl((StructTypeImpl) indexResultSetType, tuple);
      }

      // Key must be evaluated after indexResultSet evaluation is done as Entry might be getting
      // destroyed and so if value is UNDEFINED, key will definitely will be UNDEFINED.
      Object indexKey = isFirstItrOnEntry ? indexedExpr.evaluate(initContext)
          : modifiedIndexExpr.evaluate(initContext);
      // based on the first key convert the rest to PdxString or String
      if (!isIndexedPdxKeysFlagSet) {
        setPdxStringFlag(indexKey);
      }
      indexKey = getPdxStringForIndexedPdxKeys(indexKey);
      addMapping(indexKey, indxResultSet, re);
    }

    private void doNestedIterations(int level, ExecutionContext context)
        throws TypeMismatchException, FunctionDomainException,
        NameResolutionException, QueryInvocationTargetException, IMQException {

      List iterList = context.getCurrentIterators();
      if (level == iteratorSize) {
        applyProjection(context);
      } else {
        RuntimeIterator rIter = (RuntimeIterator) iterList.get(level);
        Collection collection = rIter.evaluateCollection(context);
        if (collection == null) {
          return;
        }
        for (Object aCollection : collection) {
          rIter.setCurrent(aCollection);
          doNestedIterations(level + 1, context);
        }
      }
    }

    private void applyProjection(ExecutionContext context)
        throws FunctionDomainException, TypeMismatchException, NameResolutionException,
        QueryInvocationTargetException, IMQException {

      List currrentRuntimeIters = context.getCurrentIterators();
      Object indexKey = indexedExpr.evaluate(context);
      // based on the first key convert the rest to PdxString or String
      if (!isIndexedPdxKeysFlagSet) {
        setPdxStringFlag(indexKey);
      }
      indexKey = getPdxStringForIndexedPdxKeys(indexKey);
      Object indxResultSet;

      if (iteratorSize == 1) {
        RuntimeIterator iter = (RuntimeIterator) currrentRuntimeIters.get(0);
        indxResultSet = iter.evaluate(context);
      } else {
        Object[] tuple = new Object[iteratorSize];
        for (int i = 0; i < iteratorSize; i++) {
          RuntimeIterator iter = (RuntimeIterator) currrentRuntimeIters.get(i);
          tuple[i] = iter.evaluate(context);
        }
        Support.Assert(indexResultSetType instanceof StructTypeImpl,
            "The Index ResultType should have been an instance of StructTypeImpl rather than ObjectTypeImpl. The indxeResultType is "
                + indexResultSetType);
        indxResultSet = new StructImpl((StructTypeImpl) indexResultSetType, tuple);
      }

      // Keep Entry value in fly until all keys are evaluated
      RegionEntry entry = ((DummyQRegion) context.getBindArgument(1)).getEntry();
      saveMapping(indexKey, indxResultSet, entry);
    }

    /**
     * The struct type calculation is modified if the 0th iterator is modified to make it dependent
     * on Entry
     */
    private ObjectType createIndexResultSetType() {
      List currentIterators = initContext.getCurrentIterators();
      int len = currentIterators.size();
      ObjectType[] fieldTypes = new ObjectType[len];
      int start = isFirstItrOnEntry ? 0 : 1;
      for (; start < len; start++) {
        RuntimeIterator iter = (RuntimeIterator) currentIterators.get(start);
        fieldTypes[start] = iter.getElementType();
      }
      if (!isFirstItrOnEntry) {
        fieldTypes[0] = addnlProjType;
      }
      return len == 1 ? fieldTypes[0] : new StructTypeImpl(canonicalIterNames, fieldTypes);
    }

    @Override
    public ObjectType getIndexResultSetType() {
      return indexResultSetType;
    }

    boolean isFirstItrOnEntry() {
      return isFirstItrOnEntry;
    }

    boolean isFirstItrOnKey() {
      return isFirstItrOnKey;
    }

    @Override
    public List getAllDependentIterators() {
      return fromIterators;
    }
  }

  /**
   * Checks the limit for the resultset for distinct and non-distinct queries separately. In case of
   * non-distinct distinct elements size of result-set is matched against limit passed in as an
   * argument.
   *
   * @return true if limit is satisfied.
   */
  boolean verifyLimit(Collection result, int limit) {
    return limit > 0 && result.size() == limit;
  }

  /**
   * This will verify the consistency between RegionEntry and IndexEntry. RangeIndex has following
   * entry structure,
   *
   * IndexKey --> [RegionEntry, [Iterator1, Iterator2....., IteratorN]]
   *
   * Where Iterator1 to IteratorN are iterators defined in index from clause.
   *
   * For example: "/portfolio p, p.positions.values pos" from clause has two iterators where p is
   * independent iterator and pos is dependent iterator.
   *
   * Query iterators can be a subset, superset or exact match of index iterators. But we take query
   * iterators which are matching with index iterators to evaluate RegionEntry for new value and
   * compare it with index value which could be a plain object or a Struct.
   *
   * Note: Struct evaluated from RegionEntry can NOT have more field values than Index Value Struct
   * as we filter out iterators in query context before evaluating Struct from RegionEntry.
   *
   * @return True if Region and Index entries are consistent.
   */
  // package-private to avoid synthetic accessor
  boolean verifyEntryAndIndexValue(RegionEntry re, Object value, ExecutionContext context) {
    IMQEvaluator evaluator = (IMQEvaluator) getEvaluator();
    List valuesInRegion = null;
    Object valueInIndex = null;

    try {
      // In a RegionEntry key and Entry itself can not be modified else RegionEntry itself will
      // change. So no need to verify anything just return true.
      if (evaluator.isFirstItrOnKey()) {
        return true;
      } else if (evaluator.isFirstItrOnEntry()) {
        valuesInRegion = evaluateIndexIteratorsFromRE(re, context);
        valueInIndex = verifyAndGetPdxDomainObject(value);
      } else {
        RegionEntryContext regionEntryContext = context.getPartitionedRegion() != null
            ? context.getPartitionedRegion() : (RegionEntryContext) region;
        Object val = re.getValueInVM(regionEntryContext);
        if (val instanceof CachedDeserializable) {
          val = ((CachedDeserializable) val).getDeserializedValue(getRegion(), re);
        }
        val = verifyAndGetPdxDomainObject(val);
        valueInIndex = verifyAndGetPdxDomainObject(value);
        valuesInRegion = evaluateIndexIteratorsFromRE(val, context);
      }
    } catch (Exception e) {
      if (logger.isDebugEnabled()) {
        logger.debug(
            "Exception occurred while verifying a Region Entry value during a Query when the Region Entry is under update operation",
            e);
      }
    }

    // We could have many index keys available in one Region entry or just one.
    if (!valuesInRegion.isEmpty()) {
      for (Object valueInRegion : valuesInRegion) {
        if (compareStructWithNonStruct(valueInRegion, valueInIndex)) {
          return true;
        }
      }
      return false;
    } else {
      // Seems like value has been invalidated.
      return false;
    }
  }

  /**
   * This method compares two objects in which one could be StructType and other ObjectType. Fur
   * conditions are possible, Object1 -> Struct Object2-> Struct Object1 -> Struct Object2-> Object
   * Object1 -> Object Object2-> Struct Object1 -> Object Object2-> Object
   *
   * @return true if valueInRegion's all objects are part of valueInIndex.
   */
  private boolean compareStructWithNonStruct(Object valueInRegion, Object valueInIndex) {
    if (valueInRegion instanceof Struct && valueInIndex instanceof Struct) {
      Object[] regFields = ((StructImpl) valueInRegion).getFieldValues();
      List indFields = Arrays.asList(((StructImpl) valueInIndex).getFieldValues());
      for (Object regField : regFields) {
        if (!indFields.contains(regField)) {
          return false;
        }
      }
      return true;

    } else if (valueInRegion instanceof Struct) {
      Object[] fields = ((StructImpl) valueInRegion).getFieldValues();
      for (Object field : fields) {
        if (field.equals(valueInIndex)) {
          return true;
        }
      }

    } else if (valueInIndex instanceof Struct) {
      Object[] fields = ((StructImpl) valueInIndex).getFieldValues();
      for (Object field : fields) {
        if (field.equals(valueInRegion)) {
          return true;
        }
      }
    } else {
      return valueInRegion.equals(valueInIndex);
    }
    return false;
  }

  /**
   * Returns evaluated collection for dependent runtime iterator for this index from clause and
   * given RegionEntry.
   *
   * @param context passed here is query context.
   * @return Evaluated second level collection.
   */
  private List evaluateIndexIteratorsFromRE(Object value, ExecutionContext context)
      throws FunctionDomainException, TypeMismatchException, NameResolutionException,
      QueryInvocationTargetException {

    // We need NonTxEntry to call getValue() on it. RegionEntry does
    // NOT have public getValue() method.
    if (value instanceof RegionEntry) {
      value = new NonTXEntry((LocalRegion) getRegion(), (RegionEntry) value);
    }
    // Get all Independent and dependent iterators for this Index.
    List itrs = getAllDependentRuntimeIterators(context);

    return evaluateLastColl(value, context, itrs, 0);
  }

  private List evaluateLastColl(Object value, ExecutionContext context, List itrs, int level)
      throws FunctionDomainException, TypeMismatchException, NameResolutionException,
      QueryInvocationTargetException {

    // A tuple is a value generated from RegionEntry value which could be a StructType (Multiple
    // Dependent Iterators) or ObjectType (Single Iterator) value.
    List tuples = new ArrayList(1);

    RuntimeIterator currItrator = (RuntimeIterator) itrs.get(level);
    currItrator.setCurrent(value);

    // If its last iterator then just evaluate final struct.
    if (itrs.size() - 1 == level) {
      if (itrs.size() > 1) {
        Object[] tuple = new Object[itrs.size()];
        for (int i = 0; i < itrs.size(); i++) {
          RuntimeIterator iter = (RuntimeIterator) itrs.get(i);
          tuple[i] = iter.evaluate(context);
        }
        // Its ok to pass type as null as we are only interested in values.
        tuples.add(new StructImpl(new StructTypeImpl(), tuple));
      } else {
        tuples.add(currItrator.evaluate(context));
      }
    } else {
      // Not the last iterator.
      RuntimeIterator nextItr = (RuntimeIterator) itrs.get(level + 1);
      Collection nextLevelValues = nextItr.evaluateCollection(context);

      // If value is null or INVALID then the evaluated collection would be Null.
      if (nextLevelValues != null) {
        for (Object nextLevelValue : nextLevelValues) {
          tuples.addAll(evaluateLastColl(nextLevelValue, context, itrs, level + 1));
        }
      }
    }
    return tuples;
  }

  /**
   * Matches the Collection reference in given context for this index's from-clause in all current
   * independent collection references associated to the context. For example, if a join Query has
   * "/region1 p, region2 e" from clause context contains two region references for p and e and
   * Index could be used for any of those of both of those regions.
   *
   * Note: This Index contains its own from clause definition which corresponds to a region
   * collection reference in given context and must be contained at 0th index in
   * {@link AbstractIndex#canonicalizedDefinitions}.
   *
   * @return {@link RuntimeIterator} this should not be null ever.
   */
  RuntimeIterator getRuntimeIteratorForThisIndex(ExecutionContext context) {
    List<RuntimeIterator> indItrs = context.getCurrentIterators();
    Region rgn = getRegion();
    if (rgn instanceof BucketRegion) {
      rgn = ((Bucket) rgn).getPartitionedRegion();
    }
    String regionPath = rgn.getFullPath();
    String definition = getCanonicalizedIteratorDefinitions()[0];
    for (RuntimeIterator itr : indItrs) {
      if (itr.getDefinition().equals(regionPath) || itr.getDefinition().equals(definition)) {
        return itr;
      }
    }
    return null;
  }

  /**
   * Similar to {@link #getRuntimeIteratorForThisIndex(ExecutionContext)} except that this one also
   * matches the iterator name if present with alias used in the {@link IndexInfo}
   *
   * @return {@link RuntimeIterator}
   */
  RuntimeIterator getRuntimeIteratorForThisIndex(ExecutionContext context, IndexInfo info) {
    List<RuntimeIterator> indItrs = context.getCurrentIterators();
    Region rgn = getRegion();
    if (rgn instanceof BucketRegion) {
      rgn = ((Bucket) rgn).getPartitionedRegion();
    }
    String regionPath = rgn.getFullPath();
    String definition = getCanonicalizedIteratorDefinitions()[0];
    for (RuntimeIterator itr : indItrs) {
      if (itr.getDefinition().equals(regionPath) || itr.getDefinition().equals(definition)) {
        // if iterator has name alias must be used in the query
        if (itr.getName() != null) {
          CompiledValue path = info._path();
          // match the iterator name with alias
          String pathName = getReceiverNameFromPath(path);
          if (path.getType() == OQLLexerTokenTypes.Identifier || itr.getName().equals(pathName)) {
            return itr;
          }
        } else {
          return itr;
        }
      }
    }
    return null;
  }

  private String getReceiverNameFromPath(CompiledValue path) {
    if (path instanceof CompiledID) {
      return ((CompiledID) path).getId();
    } else if (path instanceof CompiledPath) {
      return getReceiverNameFromPath(path.getReceiver());
    } else if (path instanceof CompiledOperation) {
      return getReceiverNameFromPath(path.getReceiver());
    } else if (path instanceof CompiledIndexOperation) {
      return getReceiverNameFromPath(path.getReceiver());
    }
    return "";
  }

  /**
   * Take all independent iterators from context and remove the one which matches for this Index's
   * independent iterator. Then get all Dependent iterators from given context for this Index's
   * independent iterator.
   *
   * @param context from executing query.
   * @return List of all iterators pertaining to this Index.
   */
  private List getAllDependentRuntimeIterators(ExecutionContext context) {
    List<RuntimeIterator> indItrs = context
        .getCurrScopeDpndntItrsBasedOnSingleIndpndntItr(getRuntimeIteratorForThisIndex(context));

    List<String> definitions = Arrays.asList(getCanonicalizedIteratorDefinitions());
    // These are the common iterators between query from clause and index from clause.
    List itrs = new ArrayList();

    for (RuntimeIterator itr : indItrs) {
      if (definitions.contains(itr.getDefinition())) {
        itrs.add(itr);
      }
    }
    return itrs;
  }

  /**
   * This map is not thread-safe. We rely on the fact that every thread which is trying to update
   * this kind of map (In Indexes), must have RegionEntry lock before adding OR removing elements.
   *
   * This map does NOT provide an iterator. To iterate over its element caller has to get inside the
   * map itself through addValuesToCollection() calls.
   */
  class RegionEntryToValuesMap {
    protected Map map;
    private final boolean useList;
    volatile int numValues;

    RegionEntryToValuesMap(boolean useList) {
      map = new ConcurrentHashMap(2, 0.75f, 1);
      this.useList = useList;
    }

    RegionEntryToValuesMap(Map map, boolean useList) {
      this.map = map;
      this.useList = useList;
    }

    /**
     * We do NOT use any locks here as every add is for a RegionEntry which is locked before coming
     * here. No two threads can be entering in this method together for a RegionEntry.
     */
    public void add(RegionEntry entry, Object value) {
      assert value != null;
      // Values must NOT be null and ConcurrentHashMap does not support null values.
      if (value == null) {
        return;
      }
      Object object = map.get(entry);
      if (object == null) {
        map.put(entry, value);
      } else if (object instanceof Collection) {
        Collection coll = (Collection) object;
        // If its a list query might get ConcurrentModificationException.
        // This can only happen for Null mapped or Undefined entries in a
        // RangeIndex. So we are synchronizing on ArrayList.
        if (useList) {
          synchronized (coll) {
            coll.add(value);
          }
        } else {
          coll.add(value);
        }
      } else {
        Collection coll = useList ? new ArrayList(2) : new IndexConcurrentHashSet(2, 0.75f, 1);
        coll.add(object);
        coll.add(value);
        map.put(entry, coll);
      }
      atomicUpdater.incrementAndGet(this);
    }

    public void addAll(RegionEntry entry, Collection values) {
      Object object = map.get(entry);
      if (object == null) {
        Collection coll = useList ? new ArrayList(values.size())
            : new IndexConcurrentHashSet(values.size(), 0.75f, 1);
        coll.addAll(values);
        map.put(entry, coll);
        atomicUpdater.addAndGet(this, values.size());
      } else if (object instanceof Collection) {
        Collection coll = (Collection) object;
        // If its a list query might get ConcurrentModificationException.
        // This can only happen for Null mapped or Undefined entries in a
        // RangeIndex. So we are synchronizing on ArrayList.
        if (useList) {
          synchronized (coll) {
            coll.addAll(values);
          }
        } else {
          coll.addAll(values);
        }
      } else {
        Collection coll = useList ? new ArrayList(values.size() + 1)
            : new IndexConcurrentHashSet(values.size() + 1, 0.75f, 1);
        coll.addAll(values);
        coll.add(object);
        map.put(entry, coll);
      }
      atomicUpdater.addAndGet(this, values.size());
    }

    public Object get(RegionEntry entry) {
      return map.get(entry);
    }

    /**
     * We do NOT use any locks here as every remove is for a RegionEntry which is locked before
     * coming here. No two threads can be entering in this method together for a RegionEntry.
     */
    public void remove(RegionEntry entry, Object value) {
      Object object = map.get(entry);
      if (object == null) {
        return;
      }
      if (object instanceof Collection) {
        Collection coll = (Collection) object;
        boolean removed;
        // If its a list query might get ConcurrentModificationException.
        // This can only happen for Null mapped or Undefined entries in a
        // RangeIndex. So we are synchronizing on ArrayList.
        if (useList) {
          synchronized (coll) {
            removed = coll.remove(value);
          }
        } else {
          removed = coll.remove(value);
        }
        if (removed) {
          if (coll.size() == 0) {
            map.remove(entry);
          }
          atomicUpdater.decrementAndGet(this);
        }
      } else {
        if (object.equals(value)) {
          map.remove(entry);
        }
        atomicUpdater.decrementAndGet(this);
      }
    }

    public Object remove(RegionEntry entry) {
      Object retVal = map.remove(entry);
      if (retVal != null) {
        atomicUpdater.addAndGet(this,
            retVal instanceof Collection ? -((Collection) retVal).size() : -1);
      }
      return retVal;
    }

    int getNumValues(RegionEntry entry) {
      Object object = map.get(entry);
      if (object == null) {
        return 0;
      }
      if (object instanceof Collection) {
        Collection coll = (Collection) object;
        return coll.size();
      } else {
        return 1;
      }
    }

    public int getNumValues() {
      return atomicUpdater.get(this);
    }

    public int getNumEntries() {
      return map.keySet().size();
    }

    void addValuesToCollection(Collection result, int limit, ExecutionContext context) {
      for (final Object o : map.entrySet()) {
        // Check if query execution on this thread is canceled.
        QueryMonitor.throwExceptionIfQueryOnCurrentThreadIsCanceled();
        if (verifyLimit(result, limit, context)) {
          return;
        }
        Entry e = (Entry) o;
        Object value = e.getValue();
        assert value != null;

        RegionEntry re = (RegionEntry) e.getKey();
        boolean reUpdateInProgress = re.isUpdateInProgress();
        if (value instanceof Collection) {
          // If its a list query might get ConcurrentModificationException.
          // This can only happen for Null mapped or Undefined entries in a
          // RangeIndex. So we are synchronizing on ArrayList.
          if (useList) {
            synchronized (value) {
              for (Object val : (Iterable) value) {
                // Compare the value in index with in RegionEntry.
                if (!reUpdateInProgress || verifyEntryAndIndexValue(re, val, context)) {
                  result.add(val);
                }
                if (limit != -1) {
                  if (result.size() == limit) {
                    return;
                  }
                }
              }
            }
          } else {
            for (Object val : (Iterable) value) {
              // Compare the value in index with in RegionEntry.
              if (!reUpdateInProgress || verifyEntryAndIndexValue(re, val, context)) {
                result.add(val);
              }
              if (limit != -1) {
                if (verifyLimit(result, limit, context)) {
                  return;
                }
              }
            }
          }
        } else {
          if (!reUpdateInProgress || verifyEntryAndIndexValue(re, value, context)) {
            if (context.isCqQueryContext()) {
              result.add(new CqEntry(((RegionEntry) e.getKey()).getKey(), value));
            } else {
              result.add(verifyAndGetPdxDomainObject(value));
            }
          }
        }
      }
    }

    void addValuesToCollection(Collection result, CompiledValue iterOp, RuntimeIterator runtimeItr,
        ExecutionContext context, List projAttrib, SelectResults intermediateResults,
        boolean isIntersection, int limit) throws FunctionDomainException, TypeMismatchException,
        NameResolutionException, QueryInvocationTargetException {

      if (verifyLimit(result, limit, context)) {
        return;
      }

      for (Object o : map.entrySet()) {
        // Check if query execution on this thread is canceled.
        QueryMonitor.throwExceptionIfQueryOnCurrentThreadIsCanceled();
        Entry e = (Entry) o;
        Object value = e.getValue();
        // Key is a RegionEntry here.
        RegionEntry entry = (RegionEntry) e.getKey();
        if (value != null) {
          boolean reUpdateInProgress = entry.isUpdateInProgress();
          if (value instanceof Collection) {
            // If its a list query might get ConcurrentModificationException.
            // This can only happen for Null mapped or Undefined entries in a
            // RangeIndex. So we are synchronizing on ArrayList.
            if (useList) {
              synchronized (value) {
                for (Object o1 : ((Iterable) value)) {
                  boolean ok = true;
                  if (reUpdateInProgress) {
                    // Compare the value in index with value in RegionEntry.
                    ok = verifyEntryAndIndexValue(entry, o1, context);
                  }
                  if (ok && runtimeItr != null) {
                    runtimeItr.setCurrent(o1);
                    ok = QueryUtils.applyCondition(iterOp, context);
                  }
                  if (ok) {
                    applyProjection(projAttrib, context, result, o1, intermediateResults,
                        isIntersection);
                    if (limit != -1 && result.size() == limit) {
                      return;
                    }
                  }
                }
              }
            } else {
              for (Object o1 : ((Iterable) value)) {
                boolean ok = true;
                if (reUpdateInProgress) {
                  // Compare the value in index with value in RegionEntry.
                  ok = verifyEntryAndIndexValue(entry, o1, context);
                }
                if (ok && runtimeItr != null) {
                  runtimeItr.setCurrent(o1);
                  ok = QueryUtils.applyCondition(iterOp, context);
                }
                if (ok) {
                  applyProjection(projAttrib, context, result, o1, intermediateResults,
                      isIntersection);
                  if (verifyLimit(result, limit, context)) {
                    return;
                  }
                }
              }
            }
          } else {
            boolean ok = true;
            if (reUpdateInProgress) {
              // Compare the value in index with in RegionEntry.
              ok = verifyEntryAndIndexValue(entry, value, context);
            }
            if (ok && runtimeItr != null) {
              runtimeItr.setCurrent(value);
              ok = QueryUtils.applyCondition(iterOp, context);
            }
            if (ok) {
              if (context.isCqQueryContext()) {
                result.add(new CqEntry(((RegionEntry) e.getKey()).getKey(), value));
              } else {
                applyProjection(projAttrib, context, result, value, intermediateResults,
                    isIntersection);
              }
            }
          }
        }
      }
    }

    private boolean verifyLimit(Collection result, int limit, ExecutionContext context) {
      if (limit > 0) {
        if (!context.isDistinct()) {
          return result.size() == limit;
        } else
          return result.size() == limit;
      }
      return false;
    }

    public boolean containsEntry(RegionEntry entry) {
      return map.containsKey(entry);
    }

    public boolean containsValue(Object value) {
      throw new RuntimeException(
          "Not yet implemented");
    }

    public void clear() {
      map.clear();
      atomicUpdater.set(this, 0);
    }

    public Set entrySet() {
      return map.entrySet();
    }

    /**
     * This replaces a key's value along with updating the numValues correctly.
     */
    public void replace(RegionEntry entry, Object values) {
      int numOldValues = getNumValues(entry);
      map.put(entry, values);
      atomicUpdater.addAndGet(this,
          (values instanceof Collection ? ((Collection) values).size() : 1) - numOldValues);
    }
  }

  /**
   * This will populate resultSet from both type of indexes, {@link CompactRangeIndex} and
   * {@link RangeIndex}.
   */
  void populateListForEquiJoin(List list, Object outerEntries, Object innerEntries,
      ExecutionContext context, Object key) throws FunctionDomainException, TypeMismatchException,
      NameResolutionException, QueryInvocationTargetException {

    Assert.assertTrue(outerEntries != null && innerEntries != null,
        "OuterEntries or InnerEntries must not be null");

    Object[][] values = new Object[2][];
    Iterator itr = null;
    int j = 0;

    while (j < 2) {
      boolean isRangeIndex = false;
      if (j == 0) {
        if (outerEntries instanceof RegionEntryToValuesMap) {
          itr = ((RegionEntryToValuesMap) outerEntries).map.entrySet().iterator();
          isRangeIndex = true;
        } else if (outerEntries instanceof CloseableIterator) {
          itr = (Iterator) outerEntries;
        }
      } else {
        if (innerEntries instanceof RegionEntryToValuesMap) {
          itr = ((RegionEntryToValuesMap) innerEntries).map.entrySet().iterator();
          isRangeIndex = true;
        } else if (innerEntries instanceof CloseableIterator) {
          itr = (Iterator) innerEntries;
        }
      }

      // extract the values from the RegionEntries
      List dummy = new ArrayList();
      RegionEntry re = null;
      IndexStoreEntry ie = null;
      Object val = null;
      Object entryVal = null;

      IndexInfo[] indexInfo = (IndexInfo[]) context.cacheGet(CompiledValue.INDEX_INFO);
      IndexInfo indInfo = indexInfo[j];

      while (itr.hasNext()) {
        if (isRangeIndex) {
          Map.Entry entry = (Map.Entry) itr.next();
          val = entry.getValue();
          if (val instanceof Collection) {
            entryVal = ((Iterable) val).iterator().next();
          } else {
            entryVal = val;
          }
          re = (RegionEntry) entry.getKey();
        } else {
          ie = (IndexStoreEntry) itr.next();
        }

        // Bug#41010: We need to verify if Inner and Outer Entries
        // are consistent with index key values.
        boolean ok = true;
        if (isRangeIndex) {
          if (re.isUpdateInProgress()) {
            ok = ((RangeIndex) indInfo._getIndex()).verifyEntryAndIndexValue(re, entryVal, context);
          }
        } else if (ie.isUpdateInProgress()) {
          ok = ((CompactRangeIndex) indInfo._getIndex()).verifyInnerAndOuterEntryValues(ie, context,
              indInfo, key);
        }
        if (ok) {
          if (isRangeIndex) {
            if (val instanceof Collection) {
              dummy.addAll((Collection) val);
            } else {
              dummy.add(val);
            }
          } else {
            if (IndexManager.IS_TEST_EXPANSION) {
              dummy.addAll(((CompactRangeIndex) indInfo._getIndex()).expandValue(context, key, null,
                  OQLLexerTokenTypes.TOK_EQ, -1, ie.getDeserializedValue()));
            } else {
              dummy.add(ie.getDeserializedValue());
            }
          }
        }
      }
      Object[] newValues = new Object[dummy.size()];
      dummy.toArray(newValues);
      values[j++] = newValues;
    }
    list.add(values);
  }

  /**
   * Sets the isIndexedPdxKeys flag indicating if all the keys in the index are Strings or
   * PdxStrings. Also sets another flag isIndexedPdxKeysFlagSet that indicates isIndexedPdxKeys has
   * been set/reset to avoid frequent calculation of map size
   */
  synchronized void setPdxStringFlag(Object key) {
    // For Null and Undefined keys do not set the isIndexedPdxKeysFlagSet flag
    if (isIndexedPdxKeysFlagSet || key == null || key == IndexManager.NULL
        || key == QueryService.UNDEFINED) {
      return;
    }
    if (!isIndexedPdxKeys) {
      if (region.getAttributes().getEvictionAttributes().isNoEviction() == true
          && key instanceof PdxString && region.getAttributes().getCompressor() == null) {
        isIndexedPdxKeys = true;
      }
    }
    isIndexedPdxKeysFlagSet = true;
  }

  /**
   * Converts Strings to PdxStrings and vice-versa based on the isIndexedPdxKeys flag
   *
   * @return PdxString or String based on isIndexedPdxKeys flag
   */
  Object getPdxStringForIndexedPdxKeys(Object key) {
    if (isIndexedPdxKeys) {
      if (key instanceof String) {
        return new PdxString((String) key);
      }
    } else if (key instanceof PdxString) {
      return key.toString();
    }
    return key;
  }

  boolean acquireIndexReadLockForRemove() {
    boolean success = removeIndexLock.readLock().tryLock();
    if (success) {
      internalIndexStats.incReadLockCount(1);
      if (logger.isDebugEnabled()) {
        logger.debug("Acquired read lock on index {}", getName());
      }
    }
    return success;
  }

  public void releaseIndexReadLockForRemove() {
    removeIndexLock.readLock().unlock();
    internalIndexStats.incReadLockCount(-1);
    if (logger.isDebugEnabled()) {
      logger.debug("Released read lock on index {}", getName());
    }
  }

  /**
   * This makes current thread wait until all query threads are done using it.
   */
  public void acquireIndexWriteLockForRemove() {
    final boolean isDebugEnabled = logger.isDebugEnabled();
    if (isDebugEnabled) {
      logger.debug("Acquiring write lock on Index {}", getName());
    }
    removeIndexLock.writeLock().lock();
    if (isDebugEnabled) {
      logger.debug("Acquired write lock on index {}", getName());
    }
  }

  public void releaseIndexWriteLockForRemove() {
    final boolean isDebugEnabled = logger.isDebugEnabled();
    if (isDebugEnabled) {
      logger.debug("Releasing write lock on Index {}", getName());
    }
    removeIndexLock.writeLock().unlock();
    if (isDebugEnabled) {
      logger.debug("Released write lock on Index {}", getName());
    }
  }

  public boolean isPopulated() {
    return isPopulated;
  }

  public void setPopulated(boolean isPopulated) {
    this.isPopulated = isPopulated;
  }

  boolean isIndexOnPdxKeys() {
    return isIndexedPdxKeys;
  }
}
