/**
 * 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.hadoop.hbase.metrics;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.hadoop.hbase.util.Pair;
import org.apache.hadoop.metrics.MetricsRecord;
import org.apache.hadoop.metrics.util.MetricsBase;
import org.apache.hadoop.metrics.util.MetricsRegistry;
import org.cliffc.high_scale_lib.Counter;

import com.google.common.collect.Lists;

@Deprecated
public class ExactCounterMetric extends MetricsBase {

  private static final int DEFAULT_TOP_N = 5;

  // only publish stats on the topN items (default to DEFAULT_TOP_N)
  private final int topN;
  private final ConcurrentMap<String, Counter> counts = new ConcurrentHashMap<String, Counter>();

  // all access to the 'counts' map should use this lock.
  // take a write lock iff you want to guarantee exclusive access
  // (the map stripes locks internally, so it's already thread safe -
  // this lock is just so you can take a consistent snapshot of data)
  private final ReadWriteLock lock;

    /**
     * Constructor to create a new counter metric
     * @param nam         the name to publish this metric under
     * @param registry    where the metrics object will be registered
     * @param description metrics description
     * @param topN        how many 'keys' to publish metrics on
     */
  public ExactCounterMetric(final String nam, final MetricsRegistry registry, 
      final String description, int topN) {
    super(nam, description);

    this.lock = new ReentrantReadWriteLock();
    this.topN = topN;

    if (registry != null) {
      registry.add(nam, this);      
    }
  }

  /**
   * Constructor creates a new ExactCounterMetric
   * @param nam       the name of the metrics to be used to publish the metric
   * @param registry  where the metrics object will be registered
   */
  public ExactCounterMetric(final String nam, MetricsRegistry registry) {
    this(nam, registry, NO_DESCRIPTION, DEFAULT_TOP_N);
  }

  /**
   * Relies on an external lock on {@link #lock} for thread safety.
   */
  private Counter getOrCreateCounter(String type){
    Counter cnt = counts.get(type);
    if (cnt == null){
      cnt = new Counter();
      counts.put(type, cnt);
    }
    return cnt;
  }

  public void update(String type) {
    this.lock.readLock().lock();
    try {
      getOrCreateCounter(type).increment();
    } finally {
      this.lock.readLock().unlock();
    }
  }
  
  public void update(String type, long count) {
    this.lock.readLock().lock();
    try {
      getOrCreateCounter(type).add(count);
    } finally {
      this.lock.readLock().unlock();
    }
  }
  
  public List<Pair<String, Long>> getTop(int n) {
    final List<Pair<String, Long>> countsSnapshot = 
        Lists.newArrayListWithCapacity(this.counts.size());
    
    // no updates are allowed while I'm holding this lock, so move fast
    this.lock.writeLock().lock();
    try {
      for(Entry<String, Counter> entry : this.counts.entrySet()) {
        countsSnapshot.add(Pair.newPair(entry.getKey(), 
            entry.getValue().get()));
      }
    } finally {
      this.lock.writeLock().unlock();
    }
    
    Collections.sort(countsSnapshot, new Comparator<Pair<String, Long>>() {
      @Override
      public int compare(Pair<String, Long> a, Pair<String, Long> b) {
        return b.getSecond().compareTo(a.getSecond());
      }      
    });
    
    return countsSnapshot.subList(0, Math.min(n, countsSnapshot.size()));
  }
  
  @Override
  public void pushMetric(MetricsRecord mr) {
    final List<Pair<String, Long>> topKeys = getTop(Integer.MAX_VALUE);
    int sum = 0;
    
    int counter = 0;
    for (Pair<String, Long> keyCount : topKeys) {
      counter++;
      // only push stats on the topN keys
      if (counter <= this.topN) {
        mr.setMetric(getName() + "_" + keyCount.getFirst(), 
            keyCount.getSecond());        
      }
      sum += keyCount.getSecond();
    }
    mr.setMetric(getName() + "_map_size", this.counts.size());
    mr.setMetric(getName() + "_total_count", sum);
  }
  
}
