/*
 * 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.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import javax.management.ObjectName;

import org.apache.logging.log4j.Logger;

import org.apache.geode.DataSerializer;
import org.apache.geode.internal.serialization.DataSerializableFixedID;
import org.apache.geode.internal.serialization.DeserializationContext;
import org.apache.geode.internal.serialization.KnownVersion;
import org.apache.geode.internal.serialization.SerializationContext;
import org.apache.geode.logging.internal.log4j.api.LogService;

/**
 * Central component for federation It consists of an Object State as well as some meta data for the
 * Object being federated.
 *
 *
 */

public class FederationComponent
    implements java.io.Serializable, DataSerializableFixedID {
  private static final Logger logger = LogService.getLogger();

  private static final String THIS_COMPONENT = FederationComponent.class.getName();
  private static final long serialVersionUID = 3123549507449088591L;

  /**
   * Name of the MBean. This name will be replicated at Managing Node
   */
  private String objectName;

  /**
   * Name if the interface class . It will determine the interface for MBean at Managing Node side
   */
  private String interfaceClassName;

  /**
   * Flag to determine if MBean emits notification or not.
   */
  private boolean notificationEmitter;

  /**
   * This Map holds the object state as property-value Every component should be serializable
   */
  private Map<String, Object> objectState = new HashMap<>();

  private transient Map<String, Method> getterMethodMap;

  private transient Object mbeanObject;

  private transient Class mbeanInterfaceClass;


  private final transient Map<String, Object> oldObjectState = new HashMap<>();

  private final transient Map<Method, OpenMethod> methodHandlerMap = OpenTypeUtil.newMap();

  private transient boolean prevRefreshChangeDetected = false;

  /**
   *
   * @param objectName ObjectName of the MBean
   * @param interfaceClass interface class of the MBean
   * @param notificationEmitter specifies whether this MBean is going to emit notifications
   */
  public FederationComponent(Object object, ObjectName objectName, Class interfaceClass,
      boolean notificationEmitter) {
    this.objectName = objectName.toString();
    interfaceClassName = interfaceClass.getCanonicalName();
    mbeanInterfaceClass = interfaceClass;
    this.notificationEmitter = notificationEmitter;
    mbeanObject = object;
    getterMethodMap = new HashMap<>();
    initGetters(interfaceClass);

  }

  public FederationComponent() {}

  // Introspect the mbeanInterface and initialize this object's maps.
  //
  private void initGetters(Class<?> mbeanInterface) {
    final Method[] methodArray = mbeanInterface.getMethods();

    for (Method m : methodArray) {
      String name = m.getName();

      String attrName = "";
      if (name.startsWith("get")) {
        attrName = name.substring(3);
      } else if (name.startsWith("is") && m.getReturnType() == boolean.class) {
        attrName = name.substring(2);
      }

      if (attrName.length() != 0 && m.getParameterTypes().length == 0
          && m.getReturnType() != void.class) { // For Getters
        m.setAccessible(true);
        getterMethodMap.put(attrName, m);
        methodHandlerMap.put(m, OpenMethod.from(m));
      }
    }
  }

  /**
   * gets the Canonical name of the MBean interface
   *
   * @return mbean interface class name
   */

  public String getMBeanInterfaceClass() {
    return interfaceClassName;
  }

  /**
   * True if this MBean is a notification emitter.
   *
   * @return whether its a notification emitter or not
   */

  public boolean isNotificationEmitter() {
    return notificationEmitter;
  }

  /**
   * This method will get called from Management Thread. This will dynamically invoke the MBeans
   * getter methods and set them in ObjectState Map.
   *
   * In Future releases we can implement the delta propagation here
   *
   * @return true if the refresh detects that the state changed. It will return false if two
   *         consecutive refresh calls results in no state change. This indicates to the
   *         LocalManager whether to send the MBean state to Manager or not.
   */

  public boolean refreshObjectState(boolean keepOldState) {
    boolean changeDetected = false;
    Object[] args = null;
    if (keepOldState) {
      oldObjectState.putAll(objectState);
    }
    for (Map.Entry<String, Method> gettorMethodEntry : getterMethodMap.entrySet()) {
      String property = gettorMethodEntry.getKey();
      Object propertyValue = null;

      try {
        Method m = gettorMethodEntry.getValue();
        propertyValue = m.invoke(mbeanObject, args);


        // To Handle open types in getter values
        OpenMethod op = methodHandlerMap.get(m);
        propertyValue = op.toOpenReturnValue(propertyValue);

      } catch (Exception e) {
        propertyValue = null;
        if (logger.isTraceEnabled()) {
          logger.trace(e.getMessage());
        }

      }

      Object oldValue = objectState.put(property, propertyValue);
      if (!changeDetected) {
        if (propertyValue != null) {
          if (!propertyValue.equals(oldValue)) {
            changeDetected = true;
          }
        } else { // new value is null
          if (oldValue != null) {
            changeDetected = true;
          }
        }
      }
    }
    boolean retVal = prevRefreshChangeDetected || changeDetected;
    prevRefreshChangeDetected = changeDetected;
    return retVal;
  }


  public boolean equals(Object anObject) {

    if (this == anObject) {
      return true;
    }
    if (anObject instanceof FederationComponent) {
      FederationComponent anotherFedComp = (FederationComponent) anObject;
      return anotherFedComp.interfaceClassName.equals(interfaceClassName)
          && anotherFedComp.notificationEmitter == notificationEmitter
          && anotherFedComp.objectState.equals(objectState)
          && anotherFedComp.objectName.equals(objectName);
    }

    return false;
  }


  public int hashCode() {
    return objectName.hashCode();
  }

  /**
   * Managing node will get Object state by calling this method
   *
   * @return value of the given property
   */
  public Object getValue(String propertyName) {
    return objectState.get(propertyName);
  }

  public String toString() {
    if (Boolean.getBoolean("debug.Management")) {
      return " ObjectName = " + objectName + ",InterfaceClassName = " + interfaceClassName
          + ", NotificationEmitter = " + notificationEmitter + ", ObjectState = "
          + objectState.toString();
    } else {
      return "ObjectName = " + objectName;
    }
  }

  public Map<String, Object> getObjectState() {
    return objectState;
  }

  public Map<String, Object> getOldState() {
    return oldObjectState;
  }


  @Override
  public void fromData(DataInput in,
      DeserializationContext context) throws IOException, ClassNotFoundException {
    notificationEmitter = DataSerializer.readPrimitiveBoolean(in);
    interfaceClassName = DataSerializer.readString(in);
    objectState = DataSerializer.readHashMap(in);
    objectName = DataSerializer.readString(in);
  }

  @Override
  public void toData(DataOutput out,
      SerializationContext context) throws IOException {

    DataSerializer.writePrimitiveBoolean(notificationEmitter, out);
    DataSerializer.writeString(interfaceClassName, out);
    DataSerializer.writeHashMap(objectState, out);
    DataSerializer.writeString(objectName, out);
  }

  @Override
  public int getDSFID() {
    return DataSerializableFixedID.MGMT_FEDERATION_COMPONENT;
  }

  public Object getMBeanObject() {
    return mbeanObject;
  }

  public Class getInterfaceClass() {
    return mbeanInterfaceClass;
  }


  @Override
  public KnownVersion[] getSerializationVersions() {
    // TODO Auto-generated method stub
    return null;
  }


}
