/*
 * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.jmx;

import javax.management.*;
import javax.management.openmbean.ArrayType;
import javax.management.openmbean.OpenType;
import javax.management.openmbean.SimpleType;
import java.lang.annotation.Annotation;
import java.lang.ref.SoftReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.caucho.util.L10N;

/**
 * Resin implementation of StandardMBean.
 */
public class IntrospectionMBean implements DynamicMBean {
  private static final L10N L = new L10N(IntrospectionMBean.class);
  private static final Logger log
    = Logger.getLogger(IntrospectionMBean.class.getName());

  private static final Class[] NULL_ARG = new Class[0];

  private static final Class _descriptionAnn;
  private static final Class _nameAnn;

  private static final WeakHashMap<Class,SoftReference<MBeanInfo>> _cachedInfo
    = new WeakHashMap<Class,SoftReference<MBeanInfo>>();

  private final Object _impl;
  private final Class _mbeanInterface;
  private final boolean _isLowercaseAttributeNames;

  private final MBeanInfo _mbeanInfo;

  private final HashMap<String,OpenModelMethod> _attrGetMap
    = new HashMap<String,OpenModelMethod>();

  /**
   * Makes a DynamicMBean.
   */
  public IntrospectionMBean(Object impl, Class mbeanInterface)
    throws NotCompliantMBeanException
  {
    this(impl, mbeanInterface, false);
  }

  /**
   * Makes a DynamicMBean.
   *
   * @param isLowercaseAttributeNames true if attributes should have first
   * letter lowercased
   */
  public IntrospectionMBean(Object impl,
                            Class mbeanInterface,
                            boolean isLowercaseAttributeNames)
    throws NotCompliantMBeanException
  {
    if (impl == null)
      throw new NullPointerException();

    _mbeanInterface = mbeanInterface;
    _isLowercaseAttributeNames = isLowercaseAttributeNames;

    _mbeanInfo = introspect(impl, mbeanInterface, isLowercaseAttributeNames);
    _impl = impl;
  }

  /**
   * Returns the implementation.
   */
  public Object getImplementation()
  {
    return _impl;
  }

  /**
   * Returns an attribute value.
   */
  public Object getAttribute(String attribute)
    throws AttributeNotFoundException, MBeanException, ReflectionException
  {
    try {
      OpenModelMethod method = getGetMethod(attribute);

      if (method != null)
        return method.invoke(_impl, (Object []) null);
      else
        throw new AttributeNotFoundException(L.l("'{0}' is an unknown attribute in '{1}'",
						 attribute,
						 _mbeanInterface.getName()));
    } catch (IllegalAccessException e) {
      throw new MBeanException(e);
    } catch (InvocationTargetException e) {
      if (e.getCause() instanceof Exception)
        throw new ReflectionException((Exception) e.getCause());
      else
        throw (Error) e.getCause();
    } catch (Throwable e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Sets an attribute value.
   */
  public void setAttribute(Attribute attribute)
    throws AttributeNotFoundException, InvalidAttributeValueException,
           MBeanException, ReflectionException
  {
    try {
      Method method = getSetMethod(attribute.getName(), attribute.getValue());

      if (method != null)
        method.invoke(_impl, new Object[] { attribute.getValue() });
      else
        throw new AttributeNotFoundException(attribute.getName());
    } catch (IllegalAccessException e) {
      throw new MBeanException(e);
    } catch (InvocationTargetException e) {
      throw new MBeanException(e);
    } catch (Throwable e) {
      throw new RuntimeException(e.toString());
    }
  }

  /**
   * Returns matching attribute values.
   */
  public AttributeList getAttributes(String []attributes)
  {
    AttributeList list = new AttributeList();

    for (int i = 0; i < attributes.length; i++) {
      try {
        OpenModelMethod method = getGetMethod(attributes[i]);

        if (method != null) {
          Object value = method.invoke(_impl, (Object []) null);

          list.add(new Attribute(attributes[i], value));
        }
      } catch (Throwable e) {
        log.log(Level.WARNING, e.toString(), e);
      }
    }

    return list;
  }

  /**
   * Sets attribute values.
   */
  public AttributeList setAttributes(AttributeList attributes)
  {
    AttributeList list = new AttributeList();

    for (int i = 0; i < attributes.size(); i++) {
      try {
        Attribute attr = (Attribute) attributes.get(i);
        Method method = getSetMethod(attr.getName(), attr.getValue());

        if (method != null) {
          method.invoke(_impl, new Object[] { attr.getValue() });
          list.add(new Attribute(attr.getName(), attr.getValue()));
        }
      } catch (Throwable e) {
        log.log(Level.WARNING, e.toString(), e);
      }
    }

    return list;
  }

  /**
   * Returns the set method matching the name.
   */
  private OpenModelMethod getGetMethod(String name)
  {
    OpenModelMethod method = _attrGetMap.get(name);

    if (method != null)
      return method;

    method = createGetMethod(name);

    if (method != null)
      _attrGetMap.put(name, method);

    return method;
  }

  /**
   * Returns the get or is method matching the name.
   */
  private OpenModelMethod createGetMethod(String name)
  {
    String methodName;

    if (_isLowercaseAttributeNames)  {
      StringBuilder builder = new StringBuilder(name);
      builder.setCharAt(0, Character.toUpperCase(builder.charAt(0)));
      methodName = builder.toString();
    }
    else
      methodName = name;

    String getName = "get" + methodName;
    String isName = "is" + methodName;

    Method []methods = _mbeanInterface.getMethods();

    for (int i = 0; i < methods.length; i++) {
      if (! methods[i].getName().equals(getName) &&
          ! methods[i].getName().equals(isName))
        continue;

      Class []args = methods[i].getParameterTypes();

      if (args.length == 0 &&
	  ! methods[i].getReturnType().equals(void.class)) {
	Class retType = methods[i].getReturnType();
	
        return new OpenModelMethod(methods[i], createUnmarshall(retType));
      }
    }

    return null;
  }

  /**
v   * Returns the open mbean unmarshaller for the given return type.
   */
  private Unmarshall createUnmarshall(Class cl)
  {
    Unmarshall mbean = getMBeanObjectName(cl);

    if (mbean != null)
      return mbean;

    if (cl.isArray()) {
      Class componentType = cl.getComponentType();
      
      mbean = getMBeanObjectName(componentType);

      if (mbean != null)
	return new UnmarshallArray(ObjectName.class, mbean);
    }

    return Unmarshall.IDENTITY;
  }

  private Unmarshall getMBeanObjectName(Class cl)
  {
    try {
      Method method = cl.getMethod("getObjectName");

      if (method != null
	  && ObjectName.class.equals(method.getReturnType()))
	return new UnmarshallMBean(method);
    } catch (NoSuchMethodException e) {
    } catch (Exception e) {
      log.log(Level.FINER, e.toString(), e);
    }

    return null;
  }

  /**
   * Returns the set method matching the name.
   */
  private Method getSetMethod(String name, Object value)
  {
    String methodName;

    if (_isLowercaseAttributeNames)  {
      StringBuilder builder = new StringBuilder(name);
      builder.setCharAt(0, Character.toUpperCase(builder.charAt(0)));
      methodName = builder.toString();
    }
    else
      methodName = name;

    String setName = "set" + methodName;

    Method []methods = _mbeanInterface.getMethods();

    for (int i = 0; i < methods.length; i++) {
      if (! methods[i].getName().equals(setName))
        continue;

      Class []args = methods[i].getParameterTypes();

      if (args.length != 1)
        continue;

      /*
      if (value != null && ! args[0].isAssignableFrom(value.getClass()))
	continue;
      */

      return methods[i];
    }

    return null;
  }

  /**
   * Invokes a method on the bean.
   */
  public Object invoke(String actionName,
                       Object []params,
                       String []signature)
    throws MBeanException, ReflectionException
  {
    try {
      Method []methods = _mbeanInterface.getMethods();

      int length = 0;
      if (signature != null)
        length = signature.length;
      if (params != null)
        length = params.length;

      for (int i = 0; i < methods.length; i++) {
        if (! methods[i].getName().equals(actionName))
          continue;

        Class []args = methods[i].getParameterTypes();

        if (args.length != length)
          continue;

        boolean isMatch = true;
        for (int j = length - 1; j >= 0; j--) {
          if (signature != null && ! args[j].getName().equals(signature[j]))
            isMatch = false;
        }

        if (isMatch) {
          return methods[i].invoke(_impl, params);
	}
      }

      if (actionName.equals("hashCode")
	  && (signature == null || signature.length == 0))
        return _impl.hashCode();
      else if (actionName.equals("toString")
	       && (signature == null || signature.length == 0))
        return _impl.toString();
      else
        return null;
    } catch (IllegalAccessException e) {
      throw new MBeanException(e);
    } catch (InvocationTargetException e) {
      if (e.getCause() instanceof Exception)
        throw new ReflectionException((Exception) e.getCause());
      else
        throw (Error) e.getCause();
    }
  }

  /**
   * Returns the introspection information for the MBean.
   */
  public MBeanInfo getMBeanInfo()
  {
    return _mbeanInfo;
  }

  static MBeanInfo introspect(Object obj, Class cl,
                              boolean isLowercaseAttributeNames)
    throws NotCompliantMBeanException
  {
    try {
      SoftReference<MBeanInfo> infoRef = _cachedInfo.get(cl);
      MBeanInfo info = null;

      if (infoRef != null && (info = infoRef.get()) != null)
        return info;

      String className = cl.getName();

      HashMap<String,MBeanAttributeInfo> attributes
        = new HashMap<String,MBeanAttributeInfo>();

      ArrayList<MBeanConstructorInfo> constructors
        = new ArrayList<MBeanConstructorInfo>();

      ArrayList<MBeanOperationInfo> operations
        = new ArrayList<MBeanOperationInfo>();

      Method []methods = cl.getMethods();
      for (int i = 0; i < methods.length; i++) {
        Method method = methods[i];

        if (method.getDeclaringClass() == Object.class)
          continue;

        if (Modifier.isStatic(method.getModifiers()))
          continue;

        String methodName = method.getName();
        Class []args = method.getParameterTypes();
        Class retType = method.getReturnType();

        if (methodName.startsWith("get") && args.length == 0
            && ! retType.equals(void.class)) {
	  Method getter = method;
          String name = methodName.substring(3);

          Method setter = getSetter(methods, name, retType);

          String attributeName;

          if (isLowercaseAttributeNames)  {
            StringBuilder builder = new StringBuilder(name);
            builder.setCharAt(0, Character.toLowerCase(builder.charAt(0)));
            attributeName = builder.toString();
          }
          else
            attributeName = name;

	  Class type = method.getReturnType();

	  MBeanAttributeInfo attr;

          attr = new MBeanAttributeInfo(attributeName,
					getDescription(method),
					getter,
					setter);

	  /*
	  Descriptor descriptor = attr.getDescriptor();

	  if (descriptor != null) {
	    Object openType = getOpenType(type);

	    if (openType != null)
	      descriptor.setField("openType", openType);
	  
	    descriptor.setField("originalType", getTypeName(type));

	    attr.setDescriptor(descriptor);
	  }
	  */

	  if (attributes.get(attributeName) == null)
	    attributes.put(attributeName, attr);
        }
        else if (methodName.startsWith("is") && args.length == 0 &&
                 (retType.equals(boolean.class) ||
                  retType.equals(Boolean.class))) {
	  Method getter = method;
          String name = methodName.substring(2);

          Method setter = getSetter(methods, name, retType);

          String attributeName;

          if (isLowercaseAttributeNames)  {
            StringBuilder builder = new StringBuilder(name);
            builder.setCharAt(0, Character.toLowerCase(builder.charAt(0)));
            attributeName = builder.toString();
          }
          else
            attributeName = name;

	  if (attributes.get(attributeName) == null) {
	    attributes.put(attributeName,
			   new MBeanAttributeInfo(attributeName,
						  getDescription(method),
						  getter,
						  setter));
	  }
        }
        else if (methodName.startsWith("set") && args.length == 1) {
	  Method setter = method;
          String name = methodName.substring(3);

          Method getter = getGetter(methods, name, args[0]);

          if (getter == null) {
            String attributeName;

            if (isLowercaseAttributeNames)  {
              StringBuilder builder = new StringBuilder(name);
              builder.setCharAt(0, Character.toLowerCase(builder.charAt(0)));
              attributeName = builder.toString();
            }
            else
              attributeName = name;

	    if (attributes.get(attributeName) == null) {
	      attributes.put(attributeName,
			     new MBeanAttributeInfo(attributeName,
						    getDescription(method),
						    null,
						    setter));
	    }
	  }
	}
        else {
          operations.add(new MBeanOperationInfo(getName(method),
						getDescription(method),
						getSignature(method),
						method.getReturnType().getName(),
						MBeanOperationInfo.UNKNOWN));
        }
      }

      ArrayList<MBeanNotificationInfo> notifications
        = new ArrayList<MBeanNotificationInfo>();

      if (obj instanceof NotificationBroadcaster) {
        NotificationBroadcaster broadcaster;
        broadcaster = (NotificationBroadcaster) obj;

        MBeanNotificationInfo[] notifs = broadcaster.getNotificationInfo();

        if (notifs != null) {
          for (int i = 0; i < notifs.length; i++) {
	    MBeanNotificationInfo notif = notifs[i];

	    notifications.add((MBeanNotificationInfo) notifs[i].clone());
	  }
        }
      }

      Collections.sort(notifications, MBEAN_FEATURE_INFO_COMPARATOR);

      MBeanAttributeInfo []attrArray = new MBeanAttributeInfo[attributes.size()];
      attributes.values().toArray(attrArray);
      Arrays.sort(attrArray, MBEAN_FEATURE_INFO_COMPARATOR);
      
      MBeanConstructorInfo []conArray = new MBeanConstructorInfo[constructors.size()];
      constructors.toArray(conArray);

      MBeanOperationInfo []opArray = new MBeanOperationInfo[operations.size()];
      operations.toArray(opArray);
      Arrays.sort(opArray, MBEAN_FEATURE_INFO_COMPARATOR);
      MBeanNotificationInfo []notifArray = new MBeanNotificationInfo[notifications.size()];
      notifications.toArray(notifArray);
      Arrays.sort(notifArray, MBEAN_FEATURE_INFO_COMPARATOR);

      MBeanInfo modelInfo;

      modelInfo = new MBeanInfo(cl.getName(),
				     getDescription(cl),
				     attrArray,
				     conArray,
				     opArray,
				     notifArray);
      /*
      Descriptor descriptor = modelInfo.getMBeanDescriptor();
      if (descriptor != null) {
	descriptor.setField("mxbean", "true");
	modelInfo.setMBeanDescriptor(descriptor);
      }
      */

      info = modelInfo;

      _cachedInfo.put(cl, new SoftReference<MBeanInfo>(info));

      return info;
    } catch (Exception e) {
      NotCompliantMBeanException exn;
      exn = new NotCompliantMBeanException(String.valueOf(e));

      exn.initCause(e);

      throw exn;
    }
  }

  /**
   * Returns the matching setter.
   */
  static Method getSetter(Method []methods, String property, Class type)
  {
    String name = "set" + property;

    for (int i = 0; i < methods.length; i++) {
      if (! methods[i].getName().equals(name))
        continue;

      Class []args = methods[i].getParameterTypes();

      if (args.length != 1 || ! args[0].equals(type))
        continue;

      return methods[i];
    }

    return null;
  }

  /**
   * Returns the matching getter.
   */
  static Method getGetter(Method []methods, String property, Class type)
  {
    String getName = "get" + property;
    String isName = "is" + property;

    for (int i = 0; i < methods.length; i++) {
      if (! methods[i].getName().equals(getName) &&
          ! methods[i].getName().equals(isName))
        continue;

      Class []args = methods[i].getParameterTypes();

      if (args.length != 0)
        continue;

      Class retType = methods[i].getReturnType();

      if (! retType.equals(type))
        continue;

      return methods[i];
    }

    return null;
  }

  /**
   * Returns the class's description.
   */
  static String getDescription(Class cl)
  {
    try {
      Description desc = (Description) cl.getAnnotation(_descriptionAnn);

      if (desc != null)
        return desc.value();
      else
        return "";
    } catch (Throwable e) {
      return "";
    }
  }

  /**
   * Returns the method's description.
   */
  static String getDescription(Method method)
  {
    try {
      Description desc = (Description) method.getAnnotation(_descriptionAnn);

      if (desc != null)
        return desc.value();
      else
        return "";
    } catch (Throwable e) {
      return "";
    }
  }

  /**
   * Returns the method's name, the optional {@link Name} annotation overrides.
   */
  static String getName(Method method)
  {
    try {
      Name name = (Name) method.getAnnotation(_nameAnn);

      if (name != null)
        return name.value();
      else
        return method.getName();
    } catch (Throwable e) {
      return method.getName();
    }
  }

  private static MBeanParameterInfo[] getSignature(Method method)
  {
    Class[] params = method.getParameterTypes();
    MBeanParameterInfo[] paramInfos = new MBeanParameterInfo[params.length];

    for (int i = 0; i < params.length; i++) {
      Class  cl = params[i];

      String name = getName(method, i);
      String description = getDescription(method, i);

      paramInfos[i] = new MBeanParameterInfo(name, cl.getName(), description);
    }

    return paramInfos;
  }

  private static String getName(Method method, int i)
  {
    try {
      for (Annotation ann : method.getParameterAnnotations()[i]) {
        if (ann instanceof Name)
          return ((Name) ann).value();
      }
    } catch (Throwable e) {
      log.log(Level.FINER, e.toString(), e);
    }
    
    return "p" + i;
  }

  private static String getDescription(Method method, int i)
  {
    try {
      for (Annotation ann : method.getParameterAnnotations()[i]) {
	if (ann instanceof Description)
          return ((Description) ann).value();
      }
    } catch (Throwable e) {
      log.log(Level.FINER, e.toString(), e);
    }
    
    return "";
  }

  private static String getTypeName(Class type)
  {
    if (type.isArray())
      return getTypeName(type.getComponentType()) + "[]";
    else
      return type.getName();
  }

  private static OpenType getOpenType(Class type)
  {
    try {
      if (type.isArray()) {
	OpenType component = getOpenType(type.getComponentType());

	if (component != null)
	  return new ArrayType(1, component);
	else
	  return null;
      }
      else if (type.getName().endsWith("MXBean")
	       || type.getName().endsWith("MBean"))
	return SimpleType.OBJECTNAME;
      else if (void.class.equals(type))
	return SimpleType.VOID;
      else if (boolean.class.equals(type) || Boolean.class.equals(type))
	return SimpleType.BOOLEAN;
      else if (byte.class.equals(type) || Byte.class.equals(type))
	return SimpleType.BYTE;
      else if (short.class.equals(type) || Short.class.equals(type))
	return SimpleType.SHORT;
      else if (int.class.equals(type) || Integer.class.equals(type))
	return SimpleType.INTEGER;
      else if (long.class.equals(type) || Long.class.equals(type))
	return SimpleType.LONG;
      else if (float.class.equals(type) || Float.class.equals(type))
	return SimpleType.FLOAT;
      else if (double.class.equals(type) || Double.class.equals(type))
	return SimpleType.DOUBLE;
      else if (String.class.equals(type))
	return SimpleType.STRING;
      else if (char.class.equals(type) || Character.class.equals(type))
	return SimpleType.CHARACTER;
      else if (java.util.Date.class.equals(type))
	return SimpleType.DATE;
      else if (java.util.Calendar.class.equals(type))
	return SimpleType.DATE;
      else
	return null; // can't deal with more complex at the moment
    } catch (Exception e) {
      log.log(Level.FINER, e.toString(), e);

      return null;
    }
  }

  private static Class findClass(String name)
  {
    try {
      return Class.forName(name);
    } catch (Throwable e) {
      return null;
    }
  }

  private static final Comparator<MBeanFeatureInfo> MBEAN_FEATURE_INFO_COMPARATOR
    = new Comparator<MBeanFeatureInfo>() {

    public int compare(MBeanFeatureInfo o1, MBeanFeatureInfo o2)
    {
      return o1.getName().compareTo(o2.getName());
    }
  };

  static {
    _descriptionAnn = findClass("com.caucho.jmx.Description");
    _nameAnn = findClass("com.caucho.jmx.Name");
  }
}
