/*
 * Copyright (c) 2008, 2015, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package org.openjdk.btrace.runtime;

/*
 This class is a simple implementation of DynamicMBean that exposes a BTrace class as a MBean. The
 static fields annotated with @Property are exposed as MBean attributes.

 @author A. Sundararajan
*/
import java.lang.management.ManagementFactory;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.AttributeNotFoundException;
import javax.management.Descriptor;
import javax.management.DynamicMBean;
import javax.management.InvalidAttributeValueException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.modelmbean.DescriptorSupport;
import javax.management.openmbean.ArrayType;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.CompositeDataSupport;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.OpenDataException;
import javax.management.openmbean.OpenType;
import javax.management.openmbean.SimpleType;
import org.openjdk.btrace.core.Profiler;
import org.openjdk.btrace.core.annotations.BTrace;
import org.openjdk.btrace.core.annotations.Property;

/**
 * This is a simple DynamicMBean implementation that exposes the static fields of BTrace class as
 * attributes. The fields exposed should be annotated as {@linkplain Property}.
 *
 * @author A. Sundararajan
 */
public class BTraceMBean implements DynamicMBean {

  private final Class<?> clazz;
  private final Map<String, Field> attributes;
  private final String beanName;
  private MBeanInfo cachedBeanInfo;

  public BTraceMBean(Class<?> clazz) {
    this.clazz = clazz;
    attributes = getJMXAttributes(clazz);
    beanName = getBeanName(clazz);
  }

  public static void registerMBean(Class<?> clazz) {
    if (isMBean(clazz)) {
      MBeanServer server = ManagementFactory.getPlatformMBeanServer();
      BTraceMBean bean = new BTraceMBean(clazz);
      try {
        ObjectName on = new ObjectName("btrace:name=" + bean.beanName);
        if (server.isRegistered(on)) {
          server.unregisterMBean(on);
        }
        server.registerMBean(bean, on);
      } catch (RuntimeException re) {
        throw re;
      } catch (Exception exp) {
        throw new RuntimeException(exp);
      }
    }
  }

  // internals only below this point
  private static String getBeanName(Class<?> clazz) {
    BTrace info = clazz.getAnnotation(BTrace.class);
    String beanName = info.name();
    if (beanName.isEmpty()) {
      beanName = clazz.getName();
    }
    return beanName;
  }

  public static boolean isMBean(Class<?> clazz) {
    // if at least one field is annotated as @Property, we create MBean
    for (Field field : clazz.getDeclaredFields()) {
      if (field.isAnnotationPresent(Property.class)) {
        return true;
      }
    }
    return false;
  }

  private static Object getFieldValue(Field field) {
    try {
      Object value = field.get(null);
      OpenType<?> ot = OpenTypeUtils.typeToOpenType(field.getGenericType());
      if (ot != null) {
        return OpenTypeUtils.convertToOpenTypeValue(ot, value);
      } else {
        // no conversion attempted!
        return value;
      }
    } catch (RuntimeException re) {
      throw re;
    } catch (Exception exp) {
      throw new RuntimeException(exp);
    }
  }

  private static Map<String, Field> getJMXAttributes(Class<?> clazz) {
    try {
      Map<String, Field> fields = new HashMap<>();
      for (Field field : clazz.getDeclaredFields()) {
        if (Modifier.isStatic(field.getModifiers()) && field.isAnnotationPresent(Property.class)) {
          Property attr = field.getAnnotation(Property.class);
          if (attr != null) {
            field.setAccessible(true);
            String attrName = attr.name();
            if (attrName.isEmpty()) {
              attrName = field.getName();
              // remove BTRACE_FIELD_PREFIX ("$") from field name
              if (attrName.startsWith("$")) {
                attrName = attrName.substring(1);
              }
            }
            fields.put(attrName, field);
          }
        }
      }
      return fields;
    } catch (RuntimeException re) {
      throw re;
    } catch (Exception exp) {
      throw new RuntimeException(exp);
    }
  }

  @Override
  public synchronized Object getAttribute(String name) throws AttributeNotFoundException {
    Field field = attributes.get(name);
    if (field == null) {
      throw new AttributeNotFoundException("No such property: " + name);
    }
    return getFieldValue(field);
  }

  @SuppressWarnings("RedundantThrows")
  @Override
  public synchronized void setAttribute(Attribute attribute)
      throws InvalidAttributeValueException, MBeanException, AttributeNotFoundException {
    throw new MBeanException(new RuntimeException("BTrace attributes are read-only"));
  }

  @Override
  public synchronized AttributeList getAttributes(String[] names) {
    AttributeList list = new AttributeList();
    for (String name : names) {
      Field field = attributes.get(name);
      Object value = null;
      if (field != null) {
        value = getFieldValue(field);
      }
      if (value != null) {
        list.add(new Attribute(name, value));
      }
    }
    return list;
  }

  @Override
  public synchronized AttributeList setAttributes(AttributeList list) {
    // we don't support attribute sets -- return an empty list.
    return new AttributeList();
  }

  @SuppressWarnings("RedundantThrows")
  @Override
  public Object invoke(String name, Object[] args, String[] sig)
      throws MBeanException, ReflectionException {
    throw new ReflectionException(new NoSuchMethodException(name));
  }

  @Override
  public synchronized MBeanInfo getMBeanInfo() {
    if (cachedBeanInfo != null) {
      return cachedBeanInfo;
    }
    SortedSet<String> names = new TreeSet<>(attributes.keySet());
    MBeanAttributeInfo[] attrs = new MBeanAttributeInfo[names.size()];
    Iterator<String> it = names.iterator();
    for (int i = 0; i < attrs.length; i++) {
      String name = it.next();
      Field field = attributes.get(name);
      Property attr = field.getAnnotation(Property.class);
      String description = attr.description();
      if (description.isEmpty()) {
        description = name;
      }
      Descriptor descriptor = new DescriptorSupport();
      OpenType<?> ot = OpenTypeUtils.typeToOpenType(field.getGenericType());
      if (ot != null) {
        descriptor.setField("openType", ot);
      }

      attrs[i] =
          new MBeanAttributeInfo(
              name,
              field.getType().getName(),
              description,
              true, // isReadable
              false, // isWritable
              false, // isIs
              descriptor);
    }

    BTrace info = clazz.getAnnotation(BTrace.class);
    String description = info.description();
    if (description.isEmpty()) {
      description = "BTrace MBean : " + beanName;
    }
    cachedBeanInfo =
        new MBeanInfo(
            beanName,
            description,
            attrs,
            null, // constructors
            null,
            null); // notifications
    return cachedBeanInfo;
  }

  private static class OpenTypeUtils {

    private static final Map<Class<?>, OpenType<?>> classToOpenTypes = new HashMap<>();

    static {
      classToOpenTypes.put(Byte.TYPE, SimpleType.BYTE);
      classToOpenTypes.put(Byte.class, SimpleType.BYTE);
      classToOpenTypes.put(Short.TYPE, SimpleType.SHORT);
      classToOpenTypes.put(Short.class, SimpleType.SHORT);
      classToOpenTypes.put(Integer.TYPE, SimpleType.INTEGER);
      classToOpenTypes.put(Integer.class, SimpleType.INTEGER);
      classToOpenTypes.put(Long.TYPE, SimpleType.LONG);
      classToOpenTypes.put(Long.class, SimpleType.LONG);
      classToOpenTypes.put(Float.TYPE, SimpleType.FLOAT);
      classToOpenTypes.put(Float.class, SimpleType.FLOAT);
      classToOpenTypes.put(Double.TYPE, SimpleType.DOUBLE);
      classToOpenTypes.put(Double.class, SimpleType.DOUBLE);
      classToOpenTypes.put(Boolean.TYPE, SimpleType.BOOLEAN);
      classToOpenTypes.put(Boolean.class, SimpleType.BOOLEAN);
      classToOpenTypes.put(Character.TYPE, SimpleType.CHARACTER);
      classToOpenTypes.put(Character.class, SimpleType.CHARACTER);
      classToOpenTypes.put(AtomicInteger.class, SimpleType.INTEGER);
      classToOpenTypes.put(AtomicLong.class, SimpleType.LONG);
      classToOpenTypes.put(BigInteger.class, SimpleType.BIGINTEGER);
      classToOpenTypes.put(BigDecimal.class, SimpleType.BIGDECIMAL);
      classToOpenTypes.put(String.class, SimpleType.STRING);
      classToOpenTypes.put(ObjectName.class, SimpleType.OBJECTNAME);
      classToOpenTypes.put(Date.class, SimpleType.DATE);
    }

    private static OpenType<?> typeToOpenType(Type t) {
      try {
        // FIXME: This is highly incomplete, revisit...
        // just enough to get Maps for now.
        if (t instanceof Class) {
          Class<?> c = (Class<?>) t;
          if (Profiler.class.isAssignableFrom(c)) {
            CompositeType record =
                new CompositeType(
                    "Record",
                    "Profiler record",
                    new String[] {
                      "block",
                      "invocations",
                      "selfTime.total",
                      "selfTime.percent",
                      "selfTime.avg",
                      "selfTime.max",
                      "selfTime.min",
                      "wallTime.total",
                      "wallTime.percent",
                      "wallTime.avg",
                      "wallTime.max",
                      "wallTime.min"
                    },
                    new String[] {
                      "block",
                      "invocations",
                      "selfTime.total",
                      "selfTime.percent",
                      "selfTime.avg",
                      "selfTime.max",
                      "selfTime.min",
                      "wallTime.total",
                      "wallTime.percent",
                      "wallTime.avg",
                      "wallTime.max",
                      "wallTime.min"
                    },
                    new OpenType[] {
                      typeToOpenType(String.class),
                      typeToOpenType(Long.class),
                      typeToOpenType(Long.class),
                      typeToOpenType(Double.class),
                      typeToOpenType(Long.class),
                      typeToOpenType(Long.class),
                      typeToOpenType(Long.class),
                      typeToOpenType(Long.class),
                      typeToOpenType(Double.class),
                      typeToOpenType(Long.class),
                      typeToOpenType(Long.class),
                      typeToOpenType(Long.class)
                    });
            CompositeType recordEntry =
                new CompositeType(
                    "Record Entry",
                    "Record map entry",
                    new String[] {"key", "value"},
                    new String[] {"key", "value"},
                    new OpenType[] {typeToOpenType(String.class), record});
            return new CompositeType(
                "Snapshot",
                "Profiler snapshot",
                new String[] {"startTime", "lastRefresh", "interval", "data"},
                new String[] {"startTime", "lastRefresh", "interval", "data"},
                new OpenType[] {
                  typeToOpenType(Long.class),
                  typeToOpenType(Long.class),
                  typeToOpenType(Long.class),
                  new ArrayType<>(1, recordEntry)
                });
          } else {
            return classToOpenTypes.get(c);
          }
        } else if (t instanceof ParameterizedType) {
          ParameterizedType pt = (ParameterizedType) t;
          Type rawType = pt.getRawType();
          if (rawType instanceof Class) {
            Class<?> rt = (Class<?>) rawType;
            Type[] argTypes = pt.getActualTypeArguments();
            if (Map.class.isAssignableFrom(rt)) {
              OpenType<?> keyType = typeToOpenType(argTypes[0]);
              OpenType<?> valueType = typeToOpenType(argTypes[1]);
              if (keyType != null && valueType != null) {
                CompositeType rowType =
                    new CompositeType(
                        "Map",
                        "Map of data",
                        new String[] {"key", "value"},
                        new String[] {"key", "value"},
                        new OpenType[] {keyType, valueType});
                return new ArrayType<>(1, rowType);
              }
            }
          }
        }
      } catch (OpenDataException ode) {
        ode.printStackTrace();
      }

      // nothing seems working...
      return null;
    }

    @SuppressWarnings("unchecked")
    private static Object convertToOpenTypeValue(OpenType<?> ot, Object value) {
      if (ot instanceof SimpleType) {
        if (value instanceof AtomicInteger) {
          return ((AtomicInteger) value).get();
        } else if (value instanceof AtomicLong) {
          return ((AtomicLong) value).get();
        } else {
          return value;
        }
      } else if (ot instanceof CompositeType) {
        //                System.err.println("!!! converting val of type " +
        // value.getClass().getName());
        //                System.err.println("!!! is profiler: " + (value instanceof Profiler));
        //                System.err.println("!!! is mbean value provider: " + (value instanceof
        // Profiler.MBeanValueProvider));
        if (value instanceof Profiler && value instanceof Profiler.MBeanValueProvider) {
          CompositeType ct = (CompositeType) ot;
          Profiler.MBeanValueProvider p = (Profiler.MBeanValueProvider) value;

          Profiler.Snapshot snapshot = p.getMBeanValue();
          //                    System.err.println("!!! snapshot == null :: " + (snapshot == null));
          if (snapshot == null) {
            //                        System.err.println("!!! NULL snapshot");
            try {
              return new CompositeDataSupport(
                  ct,
                  new String[] {"startTime", "lastRefresh", "interval", "data"},
                  new Object[] {
                    convertToOpenTypeValue(ct.getType("startTime"), ((Profiler) p).START_TIME),
                    convertToOpenTypeValue(ct.getType("lastRefresh"), -1L),
                    convertToOpenTypeValue(ct.getType("interval"), 0L),
                    new CompositeData[0]
                  });
            } catch (OpenDataException e) {
              e.printStackTrace();
              return null;
            }
          }

          //                    System.err.println("!!! Snapshot length: " + snapshot.total.length);
          CompositeData[] total = new CompositeData[snapshot.total.length];

          long divider = snapshot.timeInterval * 1000000; // converting ms to ns divider
          int index = 0;
          for (Profiler.Record r : snapshot.total) {
            try {
              //                            System.err.println("!!! adding record: " + r);
              CompositeType at =
                  (CompositeType) ((ArrayType<?>) ct.getType("data")).getElementOpenType();
              CompositeType rt = (CompositeType) at.getType("value");
              CompositeData recordData =
                  new CompositeDataSupport(
                      rt,
                      new String[] {
                        "block",
                        "invocations",
                        "selfTime.total",
                        "selfTime.percent",
                        "selfTime.avg",
                        "selfTime.max",
                        "selfTime.min",
                        "wallTime.total",
                        "wallTime.percent",
                        "wallTime.avg",
                        "wallTime.max",
                        "wallTime.min"
                      },
                      new Object[] {
                        r.blockName,
                        r.invocations,
                        r.selfTime,
                        snapshot.timeInterval > 0
                            ? ((double) r.selfTime / (double) divider) * 100
                            : 0,
                        r.selfTime / r.invocations,
                        r.selfTimeMax,
                        r.selfTimeMin == Long.MAX_VALUE ? 0 : r.selfTimeMin,
                        r.wallTime,
                        snapshot.timeInterval > 0
                            ? ((double) r.wallTime / (double) divider) * 100
                            : 0,
                        r.wallTime / r.invocations,
                        r.wallTimeMax,
                        r.wallTimeMin
                      });
              total[index] =
                  new CompositeDataSupport(
                      at, new String[] {"key", "value"}, new Object[] {r.blockName, recordData});
            } catch (Exception ode) {
              ode.printStackTrace();
            }
            index++;
          }

          CompositeData snapshotData = null;
          try {
            //                        System.err.println("!!! creating snapshot data");
            snapshotData =
                new CompositeDataSupport(
                    ct,
                    new String[] {"startTime", "lastRefresh", "interval", "data"},
                    new Object[] {
                      convertToOpenTypeValue(ct.getType("startTime"), ((Profiler) p).START_TIME),
                      convertToOpenTypeValue(ct.getType("lastRefresh"), snapshot.timeStamp),
                      convertToOpenTypeValue(ct.getType("interval"), snapshot.timeInterval),
                      total
                    });
          } catch (Exception e) {
            e.printStackTrace();
          }
          //                    System.err.println("!!! data: " + snapshot);
          return snapshotData;
        }
      } else if (ot instanceof ArrayType) {
        ArrayType<?> at = (ArrayType<?>) ot;
        OpenType<?> et = at.getElementOpenType();
        if (value instanceof Map && et instanceof CompositeType) {
          CompositeType ct = (CompositeType) et;
          Map<Object, Object> map = new HashMap<>((Map<Object, Object>) value);
          CompositeData[] array = new CompositeData[map.size()];
          OpenType<?> keyType = ct.getType("key");
          OpenType<?> valueType = ct.getType("value");
          int index = 0;
          for (Map.Entry<Object, Object> entry : map.entrySet()) {
            Map<String, Object> row = new HashMap<>();
            row.put("key", convertToOpenTypeValue(keyType, entry.getKey()));
            row.put("value", convertToOpenTypeValue(valueType, entry.getValue()));
            try {
              array[index] = new CompositeDataSupport(ct, row);
            } catch (OpenDataException ode) {
              ode.printStackTrace();
            }
            index++;
          }
          return array;
        }
      }
      return value;
    }
  }
}
