/*
 * 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.geode.management.internal;

import java.beans.IntrospectionException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.List;

import javax.management.JMX;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanNotificationInfo;
import javax.management.Notification;
import javax.management.NotificationBroadcaster;
import javax.management.NotificationBroadcasterSupport;
import javax.management.NotificationEmitter;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectName;

import org.apache.logging.log4j.Logger;

import org.apache.geode.SystemFailure;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.execute.FunctionService;
import org.apache.geode.cache.execute.ResultCollector;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.internal.logging.LogService;



/**
 * This class is the proxy handler for all the proxies created for federated MBeans. Its designed
 * with Java proxy mechanism. All data calls are delegated to the federation components. All method
 * calls are routed to specified members via Function service
 * 
 * 
 */

public class MBeanProxyInvocationHandler implements InvocationHandler {

  private static final Logger logger = LogService.getLogger();

  /**
   * Name of the MBean
   */
  private ObjectName objectName;

  /**
   * The monitoring region where this Object resides.
   */
  private Region<String, Object> monitoringRegion;

  /**
   * The member to which this proxy belongs
   */

  private DistributedMember member;


  /**
   * emitter is a helper class for sending notifications on behalf of the proxy
   */
  private final NotificationBroadcasterSupport emitter;

  private final ProxyInterface proxyImpl;

  private boolean isMXBean;

  private MXBeanProxyInvocationHandler mxbeanInvocationRef;



  /**
   * 
   * @param member member to which this MBean belongs
   * @param monitoringRegion corresponding MonitoringRegion
   * @param objectName ObjectName of the MBean
   * @param interfaceClass on which interface the proxy to be exposed
   * @return Object
   * @throws ClassNotFoundException
   * @throws IntrospectionException
   */
  public static Object newProxyInstance(DistributedMember member,
      Region<String, Object> monitoringRegion, ObjectName objectName, Class interfaceClass)
      throws ClassNotFoundException, IntrospectionException {
    boolean isMXBean = JMX.isMXBeanInterface(interfaceClass);
    boolean notificationBroadcaster =
        ((FederationComponent) monitoringRegion.get(objectName.toString())).isNotificationEmitter();

    InvocationHandler handler =
        new MBeanProxyInvocationHandler(member, objectName, monitoringRegion, isMXBean);

    Class[] interfaces;

    if (notificationBroadcaster) {
      interfaces =
          new Class[] {interfaceClass, ProxyInterface.class, NotificationBroadCasterProxy.class};
    } else {
      interfaces = new Class[] {interfaceClass, ProxyInterface.class};
    }

    Object proxy = Proxy.newProxyInstance(MBeanProxyInvocationHandler.class.getClassLoader(),
        interfaces, handler);

    return interfaceClass.cast(proxy);
  }

  /**
   * 
   * @param member member to which this MBean belongs
   * @param objectName ObjectName of the MBean
   * @param monitoringRegion corresponding MonitoringRegion
   * @throws IntrospectionException
   * @throws ClassNotFoundException
   */
  private MBeanProxyInvocationHandler(DistributedMember member, ObjectName objectName,
      Region<String, Object> monitoringRegion, boolean isMXBean)
      throws IntrospectionException, ClassNotFoundException {
    this.member = member;
    this.objectName = objectName;
    this.monitoringRegion = monitoringRegion;
    this.emitter = new NotificationBroadcasterSupport();
    this.proxyImpl = new ProxyInterfaceImpl();
    this.isMXBean = isMXBean;

  }

  /**
   * Inherited method from Invocation handler All object state requests are delegated to the
   * federated component.
   * 
   * All setters and operations() are delegated to the function service.
   * 
   * Notification emmitter methods are also delegated to the function service
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    if (logger.isTraceEnabled()) {
      logger.trace("Invoking Method {}", method.getName());
    }
    final Class methodClass = method.getDeclaringClass();



    if (methodClass.equals(NotificationBroadcaster.class)
        || methodClass.equals(NotificationEmitter.class))
      return invokeBroadcasterMethod(proxy, method, args);

    final String methodName = method.getName();
    final Class[] paramTypes = method.getParameterTypes();
    final Class returnType = method.getReturnType();



    final int nargs = (args == null) ? 0 : args.length;

    if (methodName.equals("setLastRefreshedTime")) {
      proxyImpl.setLastRefreshedTime((Long) args[0]);
      return null;
    }
    if (methodName.equals("getLastRefreshedTime")) {
      return proxyImpl.getLastRefreshedTime();
    }

    if (methodName.equals("sendNotification")) {
      sendNotification(args[0]);
      return null;
    }

    // local or not: equals, toString, hashCode
    if (shouldDoLocally(proxy, method)) {
      return doLocally(proxy, method, args);
    }

    // Support For MXBean open types
    if (isMXBean) {
      MXBeanProxyInvocationHandler p = findMXBeanProxy(objectName, methodClass, this);
      return p.invoke(proxy, method, args);
    }

    if (methodName.startsWith("get") && methodName.length() > 3 && nargs == 0
        && !returnType.equals(Void.TYPE)) {
      return delegateToObjectState(methodName.substring(3));
    }

    if (methodName.startsWith("is") && methodName.length() > 2 && nargs == 0
        && (returnType.equals(Boolean.TYPE) || returnType.equals(Boolean.class))) {
      return delegateToObjectState(methodName.substring(2));
    }

    final String[] signature = new String[paramTypes.length];
    for (int i = 0; i < paramTypes.length; i++)
      signature[i] = paramTypes[i].getName();

    if (methodName.startsWith("set") && methodName.length() > 3 && nargs == 1
        && returnType.equals(Void.TYPE)) {
      return delegateToFucntionService(objectName, methodName, args, signature);

    }

    return delegateToFucntionService(objectName, methodName, args, signature);

  }



  /**
   * As this proxy may behave as an notification emitter it delegates to the member
   * NotificationBroadcasterSupport object
   * 
   * @param notification
   */
  private void sendNotification(Object notification) {
    emitter.sendNotification((Notification) notification);
  }

  /**
   * This will get the data from Object state which is replicated across the hidden region
   * FederataionComponent being the carrier.
   * 
   * @param attributeName
   * @return Object
   */
  protected Object delegateToObjectState(String attributeName) throws Throwable {


    Object returnObj;
    try {
      FederationComponent fedComp =
          (FederationComponent) monitoringRegion.get(objectName.toString());
      returnObj = fedComp.getValue(attributeName);
    } catch (IllegalArgumentException e) {
      throw new MBeanException(e);
    } catch (Exception e) {
      throw new MBeanException(e);
    } catch (VirtualMachineError e) {
      SystemFailure.initiateFailure(e);
      throw e;
    } catch (Throwable th) {
      SystemFailure.checkFailure();
      throw new MBeanException(new Exception(th.getLocalizedMessage()));
    }
    return returnObj;
  }

  /**
   * It will call the Generic function to execute the method on the remote VM
   * 
   * @param objectName ObjectName of the MBean
   * @param methodName method name
   * @param args arguments to the methods
   * @param signature signature of the method
   * @return result Object
   */
  protected Object delegateToFucntionService(ObjectName objectName, String methodName,
      Object[] args, String[] signature) throws Throwable {

    Object[] functionArgs = new Object[5];
    functionArgs[0] = objectName;
    functionArgs[1] = methodName;
    functionArgs[2] = signature;
    functionArgs[3] = args;
    functionArgs[4] = member.getName();
    List<Object> result = null;
    try {

      ResultCollector rc = FunctionService.onMember(member).setArguments(functionArgs)
          .execute(ManagementConstants.MGMT_FUNCTION_ID);
      result = (List<Object>) rc.getResult();
      // Exceptions of ManagementFunctions


    } catch (Exception e) {
      if (logger.isDebugEnabled()) {
        logger.debug(" Exception while Executing Funtion {}", e.getMessage(), e);
      }
      // Only in case of Exception caused for Function framework.
      return null;
    } catch (VirtualMachineError e) {
      SystemFailure.initiateFailure(e);
      throw e;
    } catch (Throwable th) {
      SystemFailure.checkFailure();
      if (logger.isDebugEnabled()) {
        logger.debug(" Exception while Executing Funtion {}", th.getMessage(), th);
      }
      return null;
    }

    return checkErrors(result.get(ManagementConstants.RESULT_INDEX));

  }

  private Object checkErrors(Object lastResult) throws Throwable {


    if (lastResult instanceof MBeanException) {
      // Convert all MBean public API exceptions to MBeanException
      throw (Exception) lastResult;
    }

    if (lastResult instanceof Exception) {
      return null;
    }
    if (lastResult instanceof Throwable) {
      return null;
    }
    return lastResult;
  }

  /**
   * The call will delegate to Managed Node for NotificationHub to register a local listener to
   * listen for notification from the MBean
   * 
   * Moreover it will also add the client to local listener list by adding to the contained emitter.
   * 
   * @param proxy the proxy object
   * @param method method to be invoked
   * @param args method arguments
   * @return result value if any
   * @throws Exception
   */
  private Object invokeBroadcasterMethod(Object proxy, Method method, Object[] args)
      throws Throwable {
    final String methodName = method.getName();
    final int nargs = (args == null) ? 0 : args.length;

    final Class[] paramTypes = method.getParameterTypes();
    final String[] signature = new String[paramTypes.length];

    if (methodName.equals("addNotificationListener")) {

      /*
       * The various throws of IllegalArgumentException here should not happen, since we know what
       * the methods in NotificationBroadcaster and NotificationEmitter are.
       */

      if (nargs != 3) {
        final String msg = "Bad arg count to addNotificationListener: " + nargs;
        throw new IllegalArgumentException(msg);
      }
      /*
       * Other inconsistencies will produce ClassCastException below.
       */

      NotificationListener listener = (NotificationListener) args[0];
      NotificationFilter filter = (NotificationFilter) args[1];
      Object handback = args[2];
      emitter.addNotificationListener(listener, filter, handback);
      delegateToFucntionService(objectName, methodName, null, signature);
      return null;

    } else if (methodName.equals("removeNotificationListener")) {
      /*
       * NullPointerException if method with no args, but that shouldn't happen because removeNL
       * does have args.
       */
      NotificationListener listener = (NotificationListener) args[0];

      switch (nargs) {
        case 1:
          emitter.removeNotificationListener(listener);
          /**
           * No need to send listener and filter details to other members. We only need to send a
           * message saying remove the listner registered for this object on your side. Fixes Bug[
           * #47075 ]
           */
          delegateToFucntionService(objectName, methodName, null, signature);
          return null;

        case 3:
          NotificationFilter filter = (NotificationFilter) args[1];
          Object handback = args[2];
          emitter.removeNotificationListener(listener, filter, handback);

          delegateToFucntionService(objectName, methodName, null, signature);
          return null;

        default:
          final String msg = "Bad arg count to removeNotificationListener: " + nargs;
          throw new IllegalArgumentException(msg);
      }

    } else if (methodName.equals("getNotificationInfo")) {

      if (args != null) {
        throw new IllegalArgumentException("getNotificationInfo has " + "args");
      }

      if (!MBeanJMXAdapter.mbeanServer.isRegistered(objectName)) {
        return new MBeanNotificationInfo[0];
      }

      /**
       * MBean info is delegated to function service as intention is to get the info of the actual
       * mbean rather than the proxy
       */


      Object obj = delegateToFucntionService(objectName, methodName, args, signature);
      if (obj instanceof String) {
        return new MBeanNotificationInfo[0];
      }
      MBeanInfo info = (MBeanInfo) obj;
      return info.getNotifications();

    } else {
      throw new IllegalArgumentException("Bad method name: " + methodName);
    }
  }



  /**
   * Internal implementation of all the generic proxy methods
   * 
   * 
   */
  private class ProxyInterfaceImpl implements ProxyInterface {
    /**
     * last refreshed time of the proxy
     */
    private long lastRefreshedTime;

    /**
     * Constructore
     */
    public ProxyInterfaceImpl() {
      this.lastRefreshedTime = System.currentTimeMillis();
    }

    /**
     * Last refreshed time
     */
    public long getLastRefreshedTime() {
      return lastRefreshedTime;
    }

    /**
     * sets the proxy refresh time
     */
    public void setLastRefreshedTime(long lastRefreshedTime) {
      this.lastRefreshedTime = lastRefreshedTime;
    }

  }

  private boolean shouldDoLocally(Object proxy, Method method) {
    final String methodName = method.getName();
    if ((methodName.equals("hashCode") || methodName.equals("toString"))
        && method.getParameterTypes().length == 0)
      return true;
    if (methodName.equals("equals")
        && Arrays.equals(method.getParameterTypes(), new Class[] {Object.class}))
      return true;
    return false;
  }

  private Object doLocally(Object proxy, Method method, Object[] args) {
    final String methodName = method.getName();
    FederationComponent fedComp = (FederationComponent) monitoringRegion.get(objectName.toString());
    if (methodName.equals("equals")) {

      return fedComp.equals(args[0]);

    } else if (methodName.equals("toString")) {
      return fedComp.toString();
    } else if (methodName.equals("hashCode")) {
      return fedComp.hashCode();
    }

    throw new RuntimeException("Unexpected method name: " + methodName);
  }

  private MXBeanProxyInvocationHandler findMXBeanProxy(ObjectName objectName,
      Class<?> mxbeanInterface, MBeanProxyInvocationHandler handler) throws Throwable {
    MXBeanProxyInvocationHandler proxyRef = mxbeanInvocationRef;

    if (mxbeanInvocationRef == null) {
      synchronized (this) {
        try {
          mxbeanInvocationRef =
              new MXBeanProxyInvocationHandler(objectName, mxbeanInterface, handler);
        } catch (IllegalArgumentException e) {
          String msg =
              "Cannot make MXBean proxy for " + mxbeanInterface.getName() + ": " + e.getMessage();
          throw new IllegalArgumentException(msg, e.getCause());
        }

      }

    }
    return mxbeanInvocationRef;
  }

}

