package org.apache.helix.monitoring.mbeans.dynamicMBeans;

/*
 * 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.
 */

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.AttributeNotFoundException;
import javax.management.DynamicMBean;
import javax.management.InvalidAttributeValueException;
import javax.management.JMException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanConstructorInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanOperationInfo;
import javax.management.ObjectName;
import javax.management.ReflectionException;

import org.apache.helix.monitoring.SensorNameProvider;
import org.apache.helix.monitoring.mbeans.MBeanRegistrar;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Dynamic MBean provider that reporting DynamicMetric attributes
 */
public abstract class DynamicMBeanProvider implements DynamicMBean, SensorNameProvider {
  protected final Logger _logger = LoggerFactory.getLogger(getClass());
  protected static final long DEFAULT_RESET_INTERVAL_MS = 60 * 60 * 1000; // Reset time every hour
  private static final String HELIX_MONITOR_TIME_WINDOW_LENGTH_MS =
      "helix.monitor.slidingTimeWindow.ms";

  private static String SENSOR_NAME_TAG = "SensorName";
  private static String DEFAULT_DESCRIPTION =
      "Information on the management interface of the MBean";

  // Attribute name to the DynamicMetric object mapping
  private final Map<String, DynamicMetric> _attributeMap = new HashMap<>();
  private ObjectName _objectName = null;
  private MBeanInfo _mBeanInfo;

  /**
   * Instantiates a new Dynamic MBean provider.
   * @param dynamicMetrics Dynamic Metrics that are exposed by this provider
   * @param description the MBean description
   * @param domain the MBean domain name
   * @param keyValuePairs the MBean object name components
   */
  protected synchronized boolean doRegister(Collection<DynamicMetric<?, ?>> dynamicMetrics,
      String description, String domain, String... keyValuePairs) throws JMException {
    return doRegister(dynamicMetrics, description,
        MBeanRegistrar.buildObjectName(domain, keyValuePairs));
  }

  /**
   * Instantiates a new Dynamic MBean provider.
   * @param dynamicMetrics Dynamic Metrics that are exposed by this provider
   * @param description the MBean description
   * @param objectName the proposed MBean ObjectName
   */
  protected synchronized boolean doRegister(Collection<DynamicMetric<?, ?>> dynamicMetrics,
      String description, ObjectName objectName) throws JMException {
    if (_objectName != null) {
      _logger.debug("Mbean {} has already been registered. Ignore register request.",
          objectName.getCanonicalName());
      return false;
    }
    updateAttributtInfos(dynamicMetrics, description);
    _objectName = MBeanRegistrar.register(this, objectName);
    return true;
  }

  protected synchronized boolean doRegister(Collection<DynamicMetric<?, ?>> dynamicMetrics,
      ObjectName objectName) throws JMException {
    return doRegister(dynamicMetrics, null, objectName);
  }

  /**
   * Update the Dynamic MBean provider with new metric list.
   * @param description description of the MBean
   * @param dynamicMetrics the DynamicMetrics
   */
  private void updateAttributtInfos(Collection<DynamicMetric<?, ?>> dynamicMetrics,
      String description) {
    _attributeMap.clear();

    // get all attributes that can be emit by the dynamicMetrics.
    List<MBeanAttributeInfo> attributeInfoList = new ArrayList<>();
    if (dynamicMetrics != null) {
      for (DynamicMetric dynamicMetric : dynamicMetrics) {
        Iterator<MBeanAttributeInfo> iter = dynamicMetric.getAttributeInfos().iterator();
        while (iter.hasNext()) {
          MBeanAttributeInfo attributeInfo = iter.next();
          // Info list to create MBean info
          attributeInfoList.add(attributeInfo);
          // Attribute mapping for getting attribute value when getAttribute() is called
          _attributeMap.put(attributeInfo.getName(), dynamicMetric);
        }
      }
    }

    // SensorName
    attributeInfoList.add(new MBeanAttributeInfo(SENSOR_NAME_TAG, String.class.getName(),
        "The name of the metric sensor", true, false, false));

    MBeanConstructorInfo constructorInfo = new MBeanConstructorInfo(
        String.format("Default %s Constructor", getClass().getSimpleName()),
        getClass().getConstructors()[0]);

    MBeanAttributeInfo[] attributeInfos = new MBeanAttributeInfo[attributeInfoList.size()];
    attributeInfos = attributeInfoList.toArray(attributeInfos);

    if (description == null) {
      description = DEFAULT_DESCRIPTION;
    }

    _mBeanInfo = new MBeanInfo(getClass().getName(), description, attributeInfos,
        new MBeanConstructorInfo[]{constructorInfo}, new MBeanOperationInfo[0],
        new MBeanNotificationInfo[0]);
  }

  /**
   * Call doRegister() to finish registration MBean and the attributes.
   */
  public abstract DynamicMBeanProvider register() throws JMException;

  /**
   * Unregister the MBean and clean up object name record.
   * Note that all the metric data is kept even after unregister.
   */
  public synchronized void unregister() {
    MBeanRegistrar.unregister(_objectName);
    _objectName = null;
  }

  @Override
  public Object getAttribute(String attribute)
      throws AttributeNotFoundException, MBeanException, ReflectionException {
    if (SENSOR_NAME_TAG.equals(attribute)) {
      return getSensorName();
    }

    if (!_attributeMap.containsKey(attribute)) {
      return null;
    }

    return _attributeMap.get(attribute).getAttributeValue(attribute);
  }

  @Override
  public AttributeList getAttributes(String[] attributes) {
    AttributeList attributeList = new AttributeList();
    for (String attributeName : attributes) {
      try {
        Object value = getAttribute(attributeName);
        attributeList.add(new Attribute(attributeName, value));
      } catch (AttributeNotFoundException | MBeanException | ReflectionException ex) {
        _logger.error("Failed to get attribute: " + attributeName, ex);
      }
    }
    return attributeList;
  }

  @Override
  public MBeanInfo getMBeanInfo() {
    return _mBeanInfo;
  }

  @Override
  public void setAttribute(Attribute attribute)
      throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException,
             ReflectionException {
    // All MBeans are readonly
    return;
  }

  @Override
  public AttributeList setAttributes(AttributeList attributes) {
    // All MBeans are readonly
    return null;
  }

  @Override
  public Object invoke(String actionName, Object[] params, String[] signature)
      throws MBeanException, ReflectionException {
    // No operation supported
    return null;
  }

  /**
   * NOTE: This method is not thread-safe nor atomic.
   * Increment the value of a given SimpleDynamicMetric by 1.
   */
  protected void incrementSimpleDynamicMetric(SimpleDynamicMetric<Long> metric) {
    incrementSimpleDynamicMetric(metric, 1);
  }

  /**
   * NOTE: This method is not thread-safe nor atomic.
   * Increment the value of a given SimpleDynamicMetric with input value.
   */
  protected void incrementSimpleDynamicMetric(SimpleDynamicMetric<Long> metric, long value) {
    metric.updateValue(metric.getValue() + value);
  }

  /**
   * Return the interval length for the underlying reservoir used by the MBean metric configured
   * in the system env variables. If not found, use default value.
   */
  protected Long getResetIntervalInMs() {
    return getSystemPropertyAsLong(HELIX_MONITOR_TIME_WINDOW_LENGTH_MS, DEFAULT_RESET_INTERVAL_MS);
  }

  /**
   * Get the value of system property
   * @param propertyKey
   * @param propertyDefaultValue
   * @return
   */
  private long getSystemPropertyAsLong(String propertyKey, long propertyDefaultValue) {
    String valueString = System.getProperty(propertyKey, "" + propertyDefaultValue);

    try {
      long value = Long.parseLong(valueString);
      if (value > 0) {
        return value;
      }
    } catch (NumberFormatException e) {
      _logger.warn("Exception while parsing property: " + propertyKey + ", string: " + valueString
          + ", using default value: " + propertyDefaultValue);
    }

    return propertyDefaultValue;
  }
}
