/*
 * 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.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionAttributes;
import org.apache.geode.cache.query.IndexStatistics;
import org.apache.geode.cache.query.QueryService;
import org.apache.geode.internal.cache.BucketRegion;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.internal.cache.RegionEntry;

public class CompactMapRangeIndex extends AbstractMapIndex {
  private final Map<Object, Map> entryToMapKeyIndexKeyMap;
  private IndexCreationHelper ich;

  CompactMapRangeIndex(InternalCache cache, String indexName, Region region, String fromClause,
      String indexedExpression, String projectionAttributes, String origFromClause,
      String origIndxExpr, String[] defintions, boolean isAllKeys,
      String[] multiIndexingKeysPattern, Object[] mapKeys, IndexStatistics stats) {
    super(cache, indexName, region, fromClause, indexedExpression, projectionAttributes,
        origFromClause, origIndxExpr, defintions, isAllKeys, multiIndexingKeysPattern, mapKeys,
        stats);
    RegionAttributes ra = region.getAttributes();
    this.entryToMapKeyIndexKeyMap = new java.util.concurrent.ConcurrentHashMap(
        ra.getInitialCapacity(), ra.getLoadFactor(), ra.getConcurrencyLevel());
  }

  @Override
  void instantiateEvaluator(IndexCreationHelper indexCreationHelper) {
    this.evaluator = new IMQEvaluator(indexCreationHelper);
    this.ich = indexCreationHelper;
  }

  @Override
  public boolean containsEntry(RegionEntry entry) {
    return this.entryToMapKeyIndexKeyMap.containsKey(entry);
  }

  @Override
  void recreateIndexData() throws IMQException {
    this.entryToMapKeyIndexKeyMap.clear();
    this.mapKeyToValueIndex.clear();
    this.initializeIndex(true);
  }

  protected void removeMapping(RegionEntry entry, int opCode) throws IMQException {
    // this implementation has a reverse map, so it doesn't handle
    // BEFORE_UPDATE_OP
    if (opCode == BEFORE_UPDATE_OP) {
      return;
    }

    // Object values = this.entryToMapKeysMap.remove(entry);
    Map mapKeyToIndexKey = this.entryToMapKeyIndexKeyMap.remove(entry);
    // Values in reverse coould be null if map in region value does not
    // contain any key which matches to index expression keys.
    if (mapKeyToIndexKey == null) {
      return;
    }

    Iterator<Map.Entry<?, ?>> mapKeyIterator = mapKeyToIndexKey.entrySet().iterator();
    while (mapKeyIterator.hasNext()) {
      Map.Entry<?, ?> mapEntry = mapKeyIterator.next();
      Object mapKey = mapEntry.getKey();
      Object indexKey = mapEntry.getValue();
      CompactRangeIndex ri = (CompactRangeIndex) this.mapKeyToValueIndex.get(mapKey);
      long start = System.nanoTime();
      this.internalIndexStats.incUpdatesInProgress(1);
      ri.removeMapping(indexKey, entry);
      this.internalIndexStats.incUpdatesInProgress(-1);
      long end = System.nanoTime() - start;
      this.internalIndexStats.incUpdateTime(end);
      this.internalIndexStats.incNumUpdates();
    }
  }

  @Override
  void saveMapping(Object key, Object value, RegionEntry entry) throws IMQException {
    if (key == QueryService.UNDEFINED || !(key instanceof Map)) {
      return;
    }
    if (this.isAllKeys) {
      Iterator<Map.Entry<?, ?>> entries = ((Map) key).entrySet().iterator();
      while (entries.hasNext()) {
        Map.Entry<?, ?> mapEntry = entries.next();
        Object mapKey = mapEntry.getKey();
        Object indexKey = mapEntry.getValue();
        this.saveIndexAddition(mapKey, indexKey, value, entry);
      }
      removeOldMappings(((Map) key).keySet(), entry);
    } else {
      for (Object mapKey : mapKeys) {
        Object indexKey = ((Map) key).get(mapKey);
        if (indexKey != null) {
          // Do not convert to IndexManager.NULL. We are only interested in specific keys
          this.saveIndexAddition(mapKey, indexKey, value, entry);
        }
      }
    }
  }

  private void removeOldMappings(Collection presentKeys, RegionEntry entry) throws IMQException {
    Map oldKeysAndValuesForEntry = entryToMapKeyIndexKeyMap.get(entry);
    if (oldKeysAndValuesForEntry == null) {
      oldKeysAndValuesForEntry = Collections.EMPTY_MAP;
    }
    Set<Entry> removedKeyValueEntries = oldKeysAndValuesForEntry != null
        ? oldKeysAndValuesForEntry.entrySet() : Collections.EMPTY_SET;
    Iterator<Entry> iterator = removedKeyValueEntries.iterator();
    while (iterator.hasNext()) {
      Entry keyValue = iterator.next();
      Object indexKey = keyValue.getKey() == null ? IndexManager.NULL : keyValue.getKey();
      if (!presentKeys.contains(indexKey)) {
        CompactRangeIndex rg = (CompactRangeIndex) this.mapKeyToValueIndex.get(keyValue.getKey());
        rg.removeMapping(keyValue.getValue(), entry);
        iterator.remove();
      }
    }
  }


  protected void doIndexAddition(Object mapKey, Object indexKey, Object value, RegionEntry entry)
      throws IMQException {
    if (indexKey == null) {
      indexKey = IndexManager.NULL;
    }
    if (mapKey == null) {
      mapKey = IndexManager.NULL;
    }

    boolean isPr = this.region instanceof BucketRegion;
    // Get RangeIndex for it or create it if absent
    CompactRangeIndex rg = (CompactRangeIndex) this.mapKeyToValueIndex.get(mapKey);
    if (rg == null) {
      // use previously created MapRangeIndexStatistics
      IndexStatistics stats = this.internalIndexStats;
      PartitionedIndex prIndex = null;
      if (isPr) {
        prIndex = (PartitionedIndex) this.getPRIndex();
        prIndex.incNumMapKeysStats(mapKey);
      }
      rg = new CompactRangeIndex(cache, indexName + "-" + mapKey, region, fromClause,
          indexedExpression, projectionAttributes, this.originalFromClause,
          this.originalIndexedExpression, this.canonicalizedDefinitions, stats);
      rg.instantiateEvaluator(this.ich,
          ((AbstractIndex.IMQEvaluator) this.evaluator).getIndexResultSetType());
      this.mapKeyToValueIndex.put(mapKey, rg);
      if (!isPr) {
        this.internalIndexStats.incNumMapIndexKeys(1);
      }
    }
    long start = System.nanoTime();
    rg.addMapping(indexKey, value, entry);
    // This call is skipped when addMapping is called from MapRangeIndex
    // rg.internalIndexStats.incNumUpdates();
    this.internalIndexStats.incUpdatesInProgress(-1);
    long end = System.nanoTime() - start;
    this.internalIndexStats.incUpdateTime(end);
    this.internalIndexStats.incNumUpdates();
    // add to mapkey to indexkey map
    Map mapKeyToIndexKey = this.entryToMapKeyIndexKeyMap.get(entry);
    if (mapKeyToIndexKey == null) {
      mapKeyToIndexKey = new HashMap();
      entryToMapKeyIndexKeyMap.put(entry, mapKeyToIndexKey);
    }
    mapKeyToIndexKey.put(mapKey, indexKey);
  }

  protected void saveIndexAddition(Object mapKey, Object indexKey, Object value, RegionEntry entry)
      throws IMQException {
    if (indexKey == null) {
      indexKey = IndexManager.NULL;
    }
    if (mapKey == null) {
      mapKey = IndexManager.NULL;
    }

    boolean isPr = this.region instanceof BucketRegion;
    // Get RangeIndex for it or create it if absent
    CompactRangeIndex rg = (CompactRangeIndex) this.mapKeyToValueIndex.get(mapKey);
    if (rg == null) {
      // use previously created MapRangeIndexStatistics
      IndexStatistics stats = this.internalIndexStats;
      PartitionedIndex prIndex = null;
      if (isPr) {
        prIndex = (PartitionedIndex) this.getPRIndex();
        prIndex.incNumMapKeysStats(mapKey);
      }
      rg = new CompactRangeIndex(cache, indexName + "-" + mapKey, region, fromClause,
          indexedExpression, projectionAttributes, this.originalFromClause,
          this.originalIndexedExpression, this.canonicalizedDefinitions, stats);
      rg.instantiateEvaluator(this.ich,
          ((AbstractIndex.IMQEvaluator) this.evaluator).getIndexResultSetType());
      this.mapKeyToValueIndex.put(mapKey, rg);
      if (!isPr) {
        this.internalIndexStats.incNumMapIndexKeys(1);
      }
    }
    this.internalIndexStats.incUpdatesInProgress(1);
    long start = System.nanoTime();

    // add to mapkey to indexkey map
    Map mapKeyToIndexKey = this.entryToMapKeyIndexKeyMap.get(entry);
    if (mapKeyToIndexKey == null) {
      mapKeyToIndexKey = new HashMap();
      entryToMapKeyIndexKeyMap.put(entry, mapKeyToIndexKey);
    }
    // Due to the way indexes are stored, we are actually doing an "update" here
    // and removing old keys that no longer exist for this region entry
    Object oldKey = mapKeyToIndexKey.get(mapKey);
    if (oldKey == null) {
      rg.addMapping(indexKey, value, entry);
    } else if ((oldKey != null && !oldKey.equals(indexKey))) {
      rg.addMapping(indexKey, value, entry);
      rg.removeMapping(oldKey, entry);
    }
    this.internalIndexStats.incUpdatesInProgress(-1);
    long end = System.nanoTime() - start;
    this.internalIndexStats.incUpdateTime(end);
    this.internalIndexStats.incNumUpdates();
    mapKeyToIndexKey.put(mapKey, indexKey);
  }
}
