/*
 * 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.activemq.artemis.core.management.impl;

import javax.management.MBeanAttributeInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.activemq.artemis.api.core.management.Attribute;
import org.apache.activemq.artemis.api.core.management.Operation;
import org.apache.activemq.artemis.api.core.management.Parameter;

public class MBeanInfoHelper {

   private static ConcurrentMap<Class, MBeanAttributeInfo[]> attributesInfoCache = new ConcurrentHashMap<>();
   private static ConcurrentMap<Class, MBeanOperationInfo[]> operationsInfoCache = new ConcurrentHashMap<>();

   public static MBeanOperationInfo[] getMBeanOperationsInfo(final Class mbeanInterface) {
      if (operationsInfoCache.containsKey(mbeanInterface)) {
         return operationsInfoCache.get(mbeanInterface);
      } else {
         List<MBeanOperationInfo> operations = new ArrayList<>();

         for (Method method : mbeanInterface.getMethods()) {
            if (!MBeanInfoHelper.isGetterMethod(method) && !MBeanInfoHelper.isSetterMethod(method) &&
               !MBeanInfoHelper.isIsBooleanMethod(method)) {
               operations.add(MBeanInfoHelper.getOperationInfo(method));
            }
         }
         MBeanOperationInfo[] result = operations.toArray(new MBeanOperationInfo[operations.size()]);
         operationsInfoCache.put(mbeanInterface, result);
         return result;
      }
   }

   public static MBeanAttributeInfo[] getMBeanAttributesInfo(final Class mbeanInterface) {
      if (attributesInfoCache.containsKey(mbeanInterface)) {
         return attributesInfoCache.get(mbeanInterface);
      } else {
         List<MBeanAttributeInfo> tempAttributes = new ArrayList<>();
         List<MBeanAttributeInfo> finalAttributes = new ArrayList<>();
         List<String> alreadyAdded = new ArrayList<>();

         for (Method method : mbeanInterface.getMethods()) {
            if (MBeanInfoHelper.isGetterMethod(method) || MBeanInfoHelper.isSetterMethod(method) ||
               MBeanInfoHelper.isIsBooleanMethod(method)) {
               tempAttributes.add(MBeanInfoHelper.getAttributeInfo(method));
            }
         }

         // since getters and setters will each have an MBeanAttributeInfo we need to de-duplicate
         for (MBeanAttributeInfo info1 : tempAttributes) {
            MBeanAttributeInfo infoToCopy = info1;
            for (MBeanAttributeInfo info2 : tempAttributes) {
               if (info1.getName().equals(info2.getName()) && !info1.equals(info2)) {
                  infoToCopy = new MBeanAttributeInfo(info1.getName(), info1.getType().equals("void") ? info2.getType() : info1.getType(), info1.getDescription(), (info1.isReadable() || info2.isReadable()), (info1.isWritable() || info2.isWritable()), (info1.isIs() || info2.isIs()));
               }
            }
            if (!alreadyAdded.contains(infoToCopy.getName())) {
               finalAttributes.add(infoToCopy);
               alreadyAdded.add(infoToCopy.getName());
            }
         }
         MBeanAttributeInfo[] result = finalAttributes.toArray(new MBeanAttributeInfo[finalAttributes.size()]);
         attributesInfoCache.put(mbeanInterface, result);
         return result;
      }
   }

   public static int getAttributesInfoCacheSize() {
      return attributesInfoCache.size();
   }

   public static int getOperationsInfoCacheSize() {
      return operationsInfoCache.size();
   }

   public static void clearAttributesInfoCache() {
      attributesInfoCache.clear();
   }

   public static void clearOperationsInfoCache() {
      operationsInfoCache.clear();
   }

   private static boolean isGetterMethod(final Method method) {
      if (!method.getName().equals("get") && method.getName().startsWith("get") &&
         method.getParameterTypes().length == 0 &&
         !method.getReturnType().equals(void.class)) {
         return true;
      }

      return false;
   }

   private static boolean isSetterMethod(final Method method) {
      if (!method.getName().equals("set") && method.getName().startsWith("set") &&
         method.getParameterTypes().length == 1 &&
         method.getReturnType().equals(void.class)) {
         return true;
      } else {
         return false;
      }
   }

   private static boolean isIsBooleanMethod(final Method method) {
      if (!method.getName().equals("is") && method.getName().startsWith("is") &&
         method.getParameterTypes().length == 0 &&
         method.getReturnType().equals(boolean.class)) {
         return true;
      } else {
         return false;
      }
   }

   private static MBeanOperationInfo getOperationInfo(final Method operation) {
      MBeanOperationInfo info = null;
      Class<?> returnType = operation.getReturnType();

      MBeanParameterInfo[] paramsInfo = MBeanInfoHelper.getParametersInfo(operation.getParameterAnnotations(), operation.getParameterTypes());

      String description = operation.getName();
      int impact = MBeanOperationInfo.UNKNOWN;

      if (operation.getAnnotation(Operation.class) != null) {
         description = operation.getAnnotation(Operation.class).desc();
         impact = operation.getAnnotation(Operation.class).impact();
      }
      info = new MBeanOperationInfo(operation.getName(), description, paramsInfo, returnType.getName(), impact);

      return info;
   }

   private static MBeanAttributeInfo getAttributeInfo(final Method operation) {
      String description = "N/A";

      if (operation.getAnnotation(Attribute.class) != null) {
         description = operation.getAnnotation(Attribute.class).desc();
      }

      MBeanAttributeInfo info = new MBeanAttributeInfo(getAttributeName(operation), operation.getReturnType().getName(), description, (isGetterMethod(operation) || isIsBooleanMethod(operation)), isSetterMethod(operation), isIsBooleanMethod(operation));

      return info;
   }

   private static String getAttributeName(Method operation) {
      String name = operation.getName();

      if (isGetterMethod(operation) || isSetterMethod(operation))
         name = operation.getName().substring(3);
      else if (isIsBooleanMethod(operation))
         name = operation.getName().substring(2);

      return name;
   }

   private static MBeanParameterInfo[] getParametersInfo(final Annotation[][] params, final Class<?>[] paramTypes) {
      MBeanParameterInfo[] paramsInfo = new MBeanParameterInfo[params.length];

      for (int i = 0; i < params.length; i++) {
         MBeanParameterInfo paramInfo = null;
         String type = paramTypes[i].getName();
         for (Annotation anno : params[i]) {
            if (Parameter.class.isInstance(anno)) {
               String name = Parameter.class.cast(anno).name();
               String description = Parameter.class.cast(anno).desc();
               paramInfo = new MBeanParameterInfo(name, type, description);
            }
         }

         if (paramInfo == null) {
            paramInfo = new MBeanParameterInfo("p " + (i + 1), type, "parameter " + (i + 1));
         }

         paramsInfo[i] = paramInfo;
      }

      return paramsInfo;
   }

}
