/**
 *
 * 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.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.management.AttributeNotFoundException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.ReflectionException;

import com.yammer.metrics.stats.Snapshot;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.hbase.metrics.histogram.MetricsHistogram;
import org.apache.hadoop.metrics.util.MetricsBase;
import org.apache.hadoop.metrics.util.MetricsDynamicMBeanBase;
import org.apache.hadoop.metrics.util.MetricsRegistry;

/**
 * Extends the Hadoop MetricsDynamicMBeanBase class to provide JMX support for
 * custom HBase MetricsBase implementations.  MetricsDynamicMBeanBase ignores
 * registered MetricsBase instance that are not instances of one of the
 * org.apache.hadoop.metrics.util implementations.
 *
 */
@Deprecated
@InterfaceAudience.Private
public class MetricsMBeanBase extends MetricsDynamicMBeanBase {

  private static final Log LOG = LogFactory.getLog("org.apache.hadoop.hbase.metrics");

  protected final MetricsRegistry registry;
  protected final String description;
  protected int registryLength;
  /** HBase MetricsBase implementations that MetricsDynamicMBeanBase does
   * not understand
   */
  protected Map<String, MetricsBase> extendedAttributes =
      new ConcurrentHashMap<String, MetricsBase>();
  protected MBeanInfo extendedInfo;

  protected MetricsMBeanBase( MetricsRegistry mr, String description ) {
    super(copyMinusHBaseMetrics(mr), description);
    this.registry = mr;
    this.description = description;
    this.init();
  }

  /*
   * @param mr MetricsRegistry.
   * @return A copy of the passed MetricsRegistry minus the hbase metrics
   */
  private static MetricsRegistry copyMinusHBaseMetrics(final MetricsRegistry mr) {
    MetricsRegistry copy = new MetricsRegistry();
    for (MetricsBase metric : mr.getMetricsList()) {
      if (metric instanceof MetricsRate || metric instanceof MetricsString ||
          metric instanceof MetricsHistogram || metric instanceof ExactCounterMetric) {
        continue;
      }
      copy.add(metric.getName(), metric);
    }
    return copy;
  }

  protected void init() {
    List<MBeanAttributeInfo> attributes = new ArrayList<MBeanAttributeInfo>();
    MBeanInfo parentInfo = super.getMBeanInfo();
    List<String> parentAttributes = new ArrayList<String>();
    for (MBeanAttributeInfo attr : parentInfo.getAttributes()) {
      attributes.add(attr);
      parentAttributes.add(attr.getName());
    }

    this.registryLength = this.registry.getMetricsList().size();

    for (MetricsBase metric : this.registry.getMetricsList()) {
      if (metric.getName() == null || parentAttributes.contains(metric.getName()))
        continue;

      // add on custom HBase metric types
      if (metric instanceof MetricsRate) {
        attributes.add( new MBeanAttributeInfo(metric.getName(),
            "java.lang.Float", metric.getDescription(), true, false, false) );
        extendedAttributes.put(metric.getName(), metric);
      } else if (metric instanceof MetricsString) {
        attributes.add( new MBeanAttributeInfo(metric.getName(),
            "java.lang.String", metric.getDescription(), true, false, false) );
        extendedAttributes.put(metric.getName(), metric);
        LOG.info("MetricsString added: " + metric.getName());
      } else if (metric instanceof MetricsHistogram) {

        String metricName = metric.getName() + MetricsHistogram.NUM_OPS_METRIC_NAME;
        attributes.add(new MBeanAttributeInfo(metricName,
            "java.lang.Long", metric.getDescription(), true, false, false));
        extendedAttributes.put(metricName, metric);

        metricName = metric.getName() + MetricsHistogram.MIN_METRIC_NAME;
        attributes.add(new MBeanAttributeInfo(metricName,
            "java.lang.Long", metric.getDescription(), true, false, false));
        extendedAttributes.put(metricName, metric);

        metricName = metric.getName() + MetricsHistogram.MAX_METRIC_NAME;
        attributes.add(new MBeanAttributeInfo(metricName,
            "java.lang.Long", metric.getDescription(), true, false, false));
        extendedAttributes.put(metricName, metric);

        metricName = metric.getName() + MetricsHistogram.MEAN_METRIC_NAME;
        attributes.add(new MBeanAttributeInfo(metricName,
            "java.lang.Float", metric.getDescription(), true, false, false));
        extendedAttributes.put(metricName, metric);

        metricName = metric.getName() + MetricsHistogram.STD_DEV_METRIC_NAME;
        attributes.add(new MBeanAttributeInfo(metricName,
            "java.lang.Float", metric.getDescription(), true, false, false));
        extendedAttributes.put(metricName, metric);

        metricName = metric.getName() + MetricsHistogram.MEDIAN_METRIC_NAME;
        attributes.add(new MBeanAttributeInfo(metricName,
            "java.lang.Float", metric.getDescription(), true, false, false));
        extendedAttributes.put(metricName, metric);

        metricName = metric.getName() + MetricsHistogram.SEVENTY_FIFTH_PERCENTILE_METRIC_NAME;
        attributes.add(new MBeanAttributeInfo(metricName,
            "java.lang.Float", metric.getDescription(), true, false, false));
        extendedAttributes.put(metricName, metric);

        metricName = metric.getName() + MetricsHistogram.NINETY_FIFTH_PERCENTILE_METRIC_NAME;
        attributes.add(new MBeanAttributeInfo(metricName,
            "java.lang.Float", metric.getDescription(), true, false, false));
        extendedAttributes.put(metricName, metric);

        metricName = metric.getName() + MetricsHistogram.NINETY_NINETH_PERCENTILE_METRIC_NAME;
        attributes.add(new MBeanAttributeInfo(metricName,
            "java.lang.Float", metric.getDescription(), true, false, false));
        extendedAttributes.put(metricName, metric);
      }
      // else, its probably a hadoop metric already registered. Skip it.
    }

    LOG.info("new MBeanInfo");
    this.extendedInfo = new MBeanInfo( this.getClass().getName(),
        this.description, attributes.toArray( new MBeanAttributeInfo[0] ),
        parentInfo.getConstructors(), parentInfo.getOperations(),
        parentInfo.getNotifications() );
  }

  private void checkAndUpdateAttributes() {
    if (this.registryLength != this.registry.getMetricsList().size())
      this.init();
  }

  @Override
  public Object getAttribute( String name )
      throws AttributeNotFoundException, MBeanException,
      ReflectionException {

    if (name == null) {
      throw new IllegalArgumentException("Attribute name is NULL");
    }

    /*
     * Ugly.  Since MetricsDynamicMBeanBase implementation is private,
     * we need to first check the parent class for the attribute.
     * In case that the MetricsRegistry contents have changed, this will
     * allow the parent to update it's internal structures (which we rely on
     * to update our own.
     */
    try {
      return super.getAttribute(name);
    } catch (AttributeNotFoundException ex) {

      checkAndUpdateAttributes();

      MetricsBase metric = this.extendedAttributes.get(name);
      if (metric != null) {
        if (metric instanceof MetricsRate) {
          return ((MetricsRate) metric).getPreviousIntervalValue();
        } else if (metric instanceof MetricsString) {
          return ((MetricsString)metric).getValue();
        } else if (metric instanceof MetricsHistogram)  {
          MetricsHistogram hist = (MetricsHistogram) metric;
          if (name.endsWith(MetricsHistogram.NUM_OPS_METRIC_NAME)) {
            return hist.getCount();
          } else if (name.endsWith(MetricsHistogram.MIN_METRIC_NAME)) {
            return hist.getMin();
          } else if (name.endsWith(MetricsHistogram.MAX_METRIC_NAME)) {
            return hist.getMax();
          } else if (name.endsWith(MetricsHistogram.MEAN_METRIC_NAME)) {
            return (float) hist.getMean();
          } else if (name.endsWith(MetricsHistogram.STD_DEV_METRIC_NAME)) {
            return (float) hist.getStdDev();
          } else if (name.endsWith(MetricsHistogram.MEDIAN_METRIC_NAME)) {
            Snapshot s = hist.getSnapshot();
            return (float) s.getMedian();
          } else if (name.endsWith(MetricsHistogram.SEVENTY_FIFTH_PERCENTILE_METRIC_NAME)) {
            Snapshot s = hist.getSnapshot();
            return (float) s.get75thPercentile();
          } else if (name.endsWith(MetricsHistogram.NINETY_FIFTH_PERCENTILE_METRIC_NAME)) {
            Snapshot s = hist.getSnapshot();
            return (float) s.get95thPercentile();
          } else if (name.endsWith(MetricsHistogram.NINETY_NINETH_PERCENTILE_METRIC_NAME)) {
            Snapshot s = hist.getSnapshot();
            return (float) s.get99thPercentile();
          }

        } else {
          LOG.warn( String.format("unknown metrics type %s for attribute %s",
                        metric.getClass().getName(), name) );
        }
      }
    }

    throw new AttributeNotFoundException();
  }

  @Override
  public MBeanInfo getMBeanInfo() {
    return this.extendedInfo;
  }

}
