/*
 * 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.internal.admin.remote;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.geode.InternalGemFireException;
import org.apache.geode.internal.admin.EntryValueNode;

/**
 * This class holds the metadata for a single object field in a value stored in the cache. They are
 * built during construction of {@link ObjectDetailsResponse} instances and returned to the console.
 * This class does not implement {@link org.apache.geode.DataSerializable} since that mechanism gets
 * confused by the often cyclical refrences between instances of this class.
 */
public class EntryValueNodeImpl implements EntryValueNode, Externalizable/* , DataSerializable */ {

  private Object primitiveVal;
  private String type;
  private String name;
  private boolean primitive;
  private EntryValueNodeImpl[] fields;
  private static final ThreadLocal recursionSet = new ThreadLocal();

  public static EntryValueNodeImpl createFromValueRoot(Object value, boolean logicalInspection) {
    recursionSet.set(new IdentityHashMap());
    EntryValueNodeImpl retVal = null;
    if (value != null) {
      retVal = createFromObject(constructKeyDisplay(value), value, logicalInspection);
    }
    Map map = (Map) recursionSet.get();
    map.clear();
    recursionSet.set(null);

    return retVal;
  }

  private static EntryValueNodeImpl createFromPrimitive(String fieldName, String type,
      Object primitiveWrapper) {
    EntryValueNodeImpl node = new EntryValueNodeImpl();
    node.name = fieldName;
    node.type = type;
    node.primitiveVal = primitiveWrapper;
    node.primitive = true;
    return node;
  }

  private static EntryValueNodeImpl createFromNullField(String fieldName, Class fieldType) {
    EntryValueNodeImpl node = new EntryValueNodeImpl();
    node.name = fieldName;
    if (fieldType.isArray()) {
      node.type = "array of" + fieldType.getComponentType().getName();
    } else {
      node.type = fieldType.getName();
    }
    node.primitiveVal = "null";
    node.primitive = true;
    return node;
  }

  private static EntryValueNodeImpl createFromArray(String fieldName, Object arrayObj,
      Class arrayClass) {
    EntryValueNodeImpl node = new EntryValueNodeImpl();
    Map map = (Map) recursionSet.get();
    map.put(arrayObj, node);

    node.name = fieldName;
    Class compType = arrayClass.getComponentType();
    String elType = compType.getName();
    node.type = "array of " + elType;
    node.primitiveVal = arrayObj.toString();
    node.primitive = false;
    // if (arrayObj != null) (cannot be null)
    {
      EntryValueNodeImpl[] children;
      if (arrayObj instanceof Object[]) {
        Object[] array = (Object[]) arrayObj;
        children = new EntryValueNodeImpl[array.length];
        for (int i = 0; i < array.length; i++) {
          if (array[i] != null) {
            children[i] = createFromObject("[" + i + "]", array[i], false);
          } else {
            children[i] = createFromNullField("[" + i + "]", compType);
          }
        }
        node.fields = children;
      } else if (arrayObj instanceof int[]) {
        int[] array = (int[]) arrayObj;
        children = new EntryValueNodeImpl[array.length];
        for (int i = 0; i < array.length; i++) {
          children[i] = createFromPrimitive("[" + i + "]", elType, array[i]);
        }
        node.fields = children;
      } else if (arrayObj instanceof boolean[]) {
        boolean[] array = (boolean[]) arrayObj;
        children = new EntryValueNodeImpl[array.length];
        for (int i = 0; i < array.length; i++) {
          children[i] = createFromPrimitive("[" + i + "]", elType, array[i]);
        }
        node.fields = children;
      } else if (arrayObj instanceof char[]) {
        char[] array = (char[]) arrayObj;
        children = new EntryValueNodeImpl[array.length];
        for (int i = 0; i < array.length; i++) {
          children[i] = createFromPrimitive("[" + i + "]", elType, array[i]);
        }
        node.fields = children;
      } else if (arrayObj instanceof double[]) {
        double[] array = (double[]) arrayObj;
        children = new EntryValueNodeImpl[array.length];
        for (int i = 0; i < array.length; i++) {
          children[i] = createFromPrimitive("[" + i + "]", elType, array[i]);
        }
        node.fields = children;
      } else if (arrayObj instanceof long[]) {
        long[] array = (long[]) arrayObj;
        children = new EntryValueNodeImpl[array.length];
        for (int i = 0; i < array.length; i++) {
          children[i] = createFromPrimitive("[" + i + "]", elType, array[i]);
        }
        node.fields = children;
      } else if (arrayObj instanceof float[]) {
        float[] array = (float[]) arrayObj;
        children = new EntryValueNodeImpl[array.length];
        for (int i = 0; i < array.length; i++) {
          children[i] = createFromPrimitive("[" + i + "]", elType, array[i]);
        }
        node.fields = children;
      } else if (arrayObj instanceof byte[]) {
        byte[] array = (byte[]) arrayObj;
        children = new EntryValueNodeImpl[array.length];
        for (int i = 0; i < array.length; i++) {
          children[i] = createFromPrimitive("[" + i + "]", elType, array[i]);
        }
        node.fields = children;
      } else if (arrayObj instanceof short[]) {
        short[] array = (short[]) arrayObj;
        children = new EntryValueNodeImpl[array.length];
        for (int i = 0; i < array.length; i++) {
          children[i] = createFromPrimitive("[" + i + "]", elType, array[i]);
        }
        node.fields = children;
      }
    }
    return node;
  }

  private static EntryValueNodeImpl createFromObject(String fieldName, Object obj,
      boolean logicalInspection) {
    Map map = (Map) recursionSet.get();
    EntryValueNodeImpl stored = (EntryValueNodeImpl) map.get(obj);
    if (stored != null) {
      return stored;
    }
    Class clazz = obj.getClass();
    if (clazz.isArray()) {
      return createFromArray(fieldName, obj, clazz);
    }

    EntryValueNodeImpl node = new EntryValueNodeImpl();
    map.put(obj, node);

    node.name = fieldName;
    node.type = clazz.getName();
    if (isWrapperOrString(obj)) {
      node.primitiveVal = obj;
      node.primitive = true;
      return node;
    } else {
      node.primitiveVal = obj.toString();
      node.primitive = false;
    }

    if (logicalInspection && hasLogicalView(obj)) {
      int retryCount = 0;
      boolean retry;
      List elements = new ArrayList();
      do {

        // if (cancelled) { return; }

        retry = false;
        try {
          if (obj instanceof Map) {
            Map theMap = (Map) obj;
            Set entries = theMap.entrySet();
            if (entries != null) {
              for (final Object o : entries) {
                // if (cancelled) { return; }
                Map.Entry entry = (Map.Entry) o;
                Object key = entry.getKey();
                Object value = entry.getValue();
                if (key != null) {
                  elements.add(
                      createFromObject("key->" + constructKeyDisplay(key), key, logicalInspection));
                } else {
                  elements
                      .add(createFromNullField("key->" + constructKeyDisplay(key), Object.class));
                }
                if (value != null) {
                  elements.add(createFromObject("value->" + constructKeyDisplay(value), value,
                      logicalInspection));
                } else {
                  elements.add(
                      createFromNullField("value->" + constructKeyDisplay(value), Object.class));
                }
              }
            }
          } else if (obj instanceof List) {
            java.util.List list = (List) obj;
            for (final Object element : list) {
              // if (cancelled) { return; }
              elements
                  .add(createFromObject(constructKeyDisplay(element), element, logicalInspection));
            }
          } else if (obj instanceof Collection) {
            Collection coll = (Collection) obj;
            for (final Object element : coll) {
              // if (cancelled) { return; }
              elements
                  .add(createFromObject(constructKeyDisplay(element), element, logicalInspection));
            }
          }
        } catch (ConcurrentModificationException ex) {
          elements = new ArrayList();
          retryCount++;
          if (retryCount <= 5) {
            retry = true;
          }
        }
      } while (retry);
      // if (cancelled) { return; }

      node.fields = (EntryValueNodeImpl[]) elements.toArray(new EntryValueNodeImpl[0]);

    } else { // physical inspection

      Field[] fields = clazz.getDeclaredFields();
      try {
        AccessibleObject.setAccessible(fields, true);
      } catch (SecurityException se) {
        throw new InternalGemFireException(
            "Unable to set accessibility of Field objects during cache value display construction",
            se);
      }
      List fieldList = new ArrayList();
      for (final Field field : fields) {
        int mods = field.getModifiers();
        if ((mods & Modifier.STATIC) != 0) {
          continue;
        }
        Object fieldVal = null;
        try {
          fieldVal = field.get(obj);
        } catch (Exception e) {
          throw new InternalGemFireException(
              "Unable to build cache value display",
              e);
        }
        String name = field.getName();
        if (fieldVal == null) {
          fieldList.add(createFromNullField(name, field.getType()));

        } else if (isWrapperOrString(fieldVal)) {
          fieldList.add(createFromPrimitive(name, field.getType().getName(), fieldVal));
        } else {
          fieldList.add(createFromObject(name, fieldVal, logicalInspection));
        }
      }
      node.fields = (EntryValueNodeImpl[]) fieldList.toArray(new EntryValueNodeImpl[0]);
    }
    return node;
  }

  private static boolean isWrapperOrString(Object test) {
    return (test instanceof Number || test instanceof String || test instanceof Boolean
        || test instanceof Character);

  }

  private static boolean hasLogicalView(Object obj) {
    return (obj instanceof Map || obj instanceof List || obj instanceof Collection);
  }

  @Override
  public boolean isPrimitiveOrString() {
    return primitive;
  }

  @Override
  public String getName() {
    return name;
  }

  @Override
  public String getType() {
    return type;
  }

  @Override
  public EntryValueNode[] getChildren() {
    if (fields != null) {
      return fields;
    } else {
      return new EntryValueNodeImpl[0];
    }
  }

  @Override
  public Object getPrimitiveValue() {
    return primitiveVal;
  }

  public static String constructKeyDisplay(Object toDisplay) {
    if (toDisplay == null) {
      return "null";
    } else if (toDisplay instanceof String) {
      return (String) toDisplay;
    } else if (toDisplay instanceof Number) {
      return toDisplay.toString();
    } else if (toDisplay instanceof Character) {
      return toDisplay.toString();
    } else if (toDisplay instanceof Boolean) {
      return toDisplay.toString();
    } else {
      String className = toDisplay.getClass().getName();
      className = className.substring(className.lastIndexOf(".") + 1);
      char c = className.charAt(0);
      c = Character.toLowerCase(c);
      if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
        return "an " + className;
      } else {
        return "a " + className;
      }
    }
  }

  @Override
  public void writeExternal(ObjectOutput out) throws IOException {
    out.writeObject(primitiveVal);
    out.writeObject(type);
    out.writeObject(name);
    out.writeObject(fields);
    out.writeBoolean(primitive);
  }

  @Override
  public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    primitiveVal = in.readObject();
    type = (String) in.readObject();
    name = (String) in.readObject();
    fields = (EntryValueNodeImpl[]) in.readObject();
    primitive = in.readBoolean();
  }

  // public void toData(DataOutput out) throws IOException {
  // Helper.writeObject(primitive, out);
  // Helper.writeString(type, out);
  // Helper.writeString(name, out);
  // Helper.writeObject(fields, out);
  // }

  // public void fromData(DataInput in) throws IOException, ClassNotFoundException {
  // this.primitive = Helper.readObject(in);
  // this.type = Helper.readString(in);
  // this.name = Helper.readString(in);
  // this.fields = (EntryValueNodeImpl[])Helper.readObject(in);
  // }

}
