/**
 * 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.hdfs.server.namenode.top.window;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;

import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hdfs.DFSConfigKeys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A class to manage the set of {@link RollingWindow}s. This class is the
 * interface of metrics system to the {@link RollingWindow}s to retrieve the
 * current top metrics.
 * <p/>
 * Thread-safety is provided by each {@link RollingWindow} being thread-safe as
 * well as {@link ConcurrentHashMap} for the collection of them.
 */
@InterfaceAudience.Private
public class RollingWindowManager {
  public static final Logger LOG = LoggerFactory.getLogger(
      RollingWindowManager.class);

  private final int windowLenMs;
  private final int bucketsPerWindow; // e.g., 10 buckets per minute
  private final int topUsersCnt; // e.g., report top 10 metrics

  static private class RollingWindowMap extends
      ConcurrentHashMap<String, RollingWindow> {
    private static final long serialVersionUID = -6785807073237052051L;
  }

  /**
   * Represents a snapshot of the rolling window. It contains one Op per 
   * operation in the window, with ranked users for each Op.
   */
  public static class TopWindow {
    private final int windowMillis;
    private final List<Op> top;

    public TopWindow(int windowMillis) {
      this.windowMillis = windowMillis;
      this.top = Lists.newArrayList();
    }

    public void addOp(Op op) {
      top.add(op);
    }

    public int getWindowLenMs() {
      return windowMillis;
    }

    public List<Op> getOps() {
      return top;
    }
  }

  /**
   * Represents an operation within a TopWindow. It contains a ranked 
   * set of the top users for the operation.
   */
  public static class Op {
    private final String opType;
    private final List<User> topUsers;
    private final long totalCount;

    public Op(String opType, long totalCount) {
      this.opType = opType;
      this.topUsers = Lists.newArrayList();
      this.totalCount = totalCount;
    }

    public void addUser(User u) {
      topUsers.add(u);
    }

    public String getOpType() {
      return opType;
    }

    public List<User> getTopUsers() {
      return topUsers;
    }

    public long getTotalCount() {
      return totalCount;
    }
  }

  /**
   * Represents a user who called an Op within a TopWindow. Specifies the 
   * user and the number of times the user called the operation.
   */
  public static class User {
    private final String user;
    private final long count;

    public User(String user, long count) {
      this.user = user;
      this.count = count;
    }

    public String getUser() {
      return user;
    }

    public long getCount() {
      return count;
    }
  }

  /**
   * A mapping from each reported metric to its {@link RollingWindowMap} that
   * maintains the set of {@link RollingWindow}s for the users that have
   * operated on that metric.
   */
  public ConcurrentHashMap<String, RollingWindowMap> metricMap =
      new ConcurrentHashMap<String, RollingWindowMap>();

  public RollingWindowManager(Configuration conf, int reportingPeriodMs) {
    
    windowLenMs = reportingPeriodMs;
    bucketsPerWindow =
        conf.getInt(DFSConfigKeys.NNTOP_BUCKETS_PER_WINDOW_KEY,
            DFSConfigKeys.NNTOP_BUCKETS_PER_WINDOW_DEFAULT);
    Preconditions.checkArgument(bucketsPerWindow > 0,
        "a window should have at least one bucket");
    Preconditions.checkArgument(bucketsPerWindow <= windowLenMs,
        "the minimum size of a bucket is 1 ms");
    //same-size buckets
    Preconditions.checkArgument(windowLenMs % bucketsPerWindow == 0,
        "window size must be a multiplication of number of buckets");
    topUsersCnt =
        conf.getInt(DFSConfigKeys.NNTOP_NUM_USERS_KEY,
            DFSConfigKeys.NNTOP_NUM_USERS_DEFAULT);
    Preconditions.checkArgument(topUsersCnt > 0,
        "the number of requested top users must be at least 1");
  }

  /**
   * Called when the metric command is changed by "delta" units at time "time"
   * via user "user"
   *
   * @param time the time of the event
   * @param command the metric that is updated, e.g., the operation name
   * @param user the user that updated the metric
   * @param delta the amount of change in the metric, e.g., +1
   */
  public void recordMetric(long time, String command, String user, long delta) {
    RollingWindow window = getRollingWindow(command, user);
    window.incAt(time, delta);
  }

  /**
   * Take a snapshot of current top users in the past period.
   *
   * @param time the current time
   * @return a TopWindow describing the top users for each metric in the 
   * window.
   */
  public TopWindow snapshot(long time) {
    TopWindow window = new TopWindow(windowLenMs);
    if (LOG.isDebugEnabled()) {
      Set<String> metricNames = metricMap.keySet();
      LOG.debug("iterating in reported metrics, size={} values={}",
          metricNames.size(), metricNames);
    }
    for (Map.Entry<String, RollingWindowMap> entry : metricMap.entrySet()) {
      String metricName = entry.getKey();
      RollingWindowMap rollingWindows = entry.getValue();
      TopN topN = getTopUsersForMetric(time, metricName, rollingWindows);
      final int size = topN.size();
      if (size == 0) {
        continue;
      }
      Op op = new Op(metricName, topN.getTotal());
      window.addOp(op);
      // Reverse the users from the TopUsers using a stack, 
      // since we'd like them sorted in descending rather than ascending order
      Stack<NameValuePair> reverse = new Stack<NameValuePair>();
      for (int i = 0; i < size; i++) {
        reverse.push(topN.poll());
      }
      for (int i = 0; i < size; i++) {
        NameValuePair userEntry = reverse.pop();
        User user = new User(userEntry.name, Long.valueOf(userEntry.value));
        op.addUser(user);
      }
    }
    return window;
  }

  /**
   * Calculates the top N users over a time interval.
   * 
   * @param time the current time
   * @param metricName Name of metric
   * @return
   */
  private TopN getTopUsersForMetric(long time, String metricName, 
      RollingWindowMap rollingWindows) {
    TopN topN = new TopN(topUsersCnt);
    Iterator<Map.Entry<String, RollingWindow>> iterator =
        rollingWindows.entrySet().iterator();
    while (iterator.hasNext()) {
      Map.Entry<String, RollingWindow> entry = iterator.next();
      String userName = entry.getKey();
      RollingWindow aWindow = entry.getValue();
      long windowSum = aWindow.getSum(time);
      // do the gc here
      if (windowSum == 0) {
        LOG.debug("gc window of metric: {} userName: {}",
            metricName, userName);
        iterator.remove();
        continue;
      }
      LOG.debug("offer window of metric: {} userName: {} sum: {}",
          metricName, userName, windowSum);
      topN.offer(new NameValuePair(userName, windowSum));
    }
    LOG.debug("topN users size for command {} is: {}", metricName, topN.size());
    return topN;
  }

  /**
   * Get the rolling window specified by metric and user.
   *
   * @param metric the updated metric
   * @param user the user that updated the metric
   * @return the rolling window
   */
  private RollingWindow getRollingWindow(String metric, String user) {
    RollingWindowMap rwMap = metricMap.get(metric);
    if (rwMap == null) {
      rwMap = new RollingWindowMap();
      RollingWindowMap prevRwMap = metricMap.putIfAbsent(metric, rwMap);
      if (prevRwMap != null) {
        rwMap = prevRwMap;
      }
    }
    RollingWindow window = rwMap.get(user);
    if (window != null) {
      return window;
    }
    window = new RollingWindow(windowLenMs, bucketsPerWindow);
    RollingWindow prevWindow = rwMap.putIfAbsent(user, window);
    if (prevWindow != null) {
      window = prevWindow;
    }
    return window;
  }

  /**
   * A pair of a name and its corresponding value. Defines a custom 
   * comparator so the TopN PriorityQueue sorts based on the count.
   */
  static private class NameValuePair implements Comparable<NameValuePair> {
    String name;
    long value;

    public NameValuePair(String metricName, long value) {
      this.name = metricName;
      this.value = value;
    }

    @Override
    public int compareTo(NameValuePair other) {
      return (int) (value - other.value);
    }

    @Override
    public boolean equals(Object other) {
      if (other instanceof NameValuePair) {
        return compareTo((NameValuePair)other) == 0;
      }
      return false;
    }

    @Override
    public int hashCode() {
      return Long.valueOf(value).hashCode();
    }
  }

  /**
   * A fixed-size priority queue, used to retrieve top-n of offered entries.
   */
  static private class TopN extends PriorityQueue<NameValuePair> {
    private static final long serialVersionUID = 5134028249611535803L;
    int n; // > 0
    private long total = 0;

    TopN(int n) {
      super(n);
      this.n = n;
    }

    @Override
    public boolean offer(NameValuePair entry) {
      updateTotal(entry.value);
      if (size() == n) {
        NameValuePair smallest = peek();
        if (smallest.value >= entry.value) {
          return false;
        }
        poll(); // remove smallest
      }
      return super.offer(entry);
    }

    private void updateTotal(long value) {
      total += value;
    }

    public long getTotal() {
      return total;
    }
  }
}
