/*
 * 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.cli.util;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.apache.commons.beanutils.ConvertUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import org.apache.geode.internal.ClassPathLoader;
import org.apache.geode.management.internal.cli.json.GfJsonArray;
import org.apache.geode.management.internal.cli.json.GfJsonException;
import org.apache.geode.management.internal.cli.json.GfJsonObject;
import org.apache.geode.management.internal.cli.result.CliJsonSerializable;
import org.apache.geode.management.internal.cli.result.CliJsonSerializableFactory;
import org.apache.geode.management.internal.cli.result.ResultDataException;

/**
 * This class contains utility methods for JSON (http://www.json.org/) which is used by classes used
 * for the Command Line Interface (CLI).
 *
 *
 * @since GemFire 7.0
 */
public class JsonUtil {

  /**
   * Converts given JSON String in to a Map. Refer http://www.json.org/ to construct a JSON format.
   *
   * @param jsonString jsonString to be converted in to a Map.
   * @return a Map created from
   *
   * @throws IllegalArgumentException if the specified JSON string can not be converted in to a Map
   */
  public static Map<String, String> jsonToMap(String jsonString) {
    Map<String, String> jsonMap = new TreeMap<String, String>();
    try {
      GfJsonObject jsonObject = new GfJsonObject(jsonString);
      Iterator<String> keys = jsonObject.keys();

      while (keys.hasNext()) {
        String key = keys.next();
        jsonMap.put(key, jsonObject.getString(key));
      }

    } catch (GfJsonException e) {
      throw new IllegalArgumentException(
          "Could not convert jsonString : '" + jsonString + "' to map.");
    }
    return jsonMap;
  }

  /**
   * Converts given Map in to a JSON string representing a Map. Refer http://www.json.org/ for more.
   *
   * @param properties a Map of Strings to be converted in to JSON String
   * @return a JSON string representing the specified Map.
   */
  public static String mapToJson(Map<String, String> properties) {
    return new GfJsonObject(properties).toString();
  }

  /**
   * Converts given Object in to a JSON string representing an Object. Refer http://www.json.org/
   * for more.
   *
   * @param object an Object to be converted in to JSON String
   * @return a JSON string representing the specified object.
   */
  public static String objectToJson(Object object) {
    return new GfJsonObject(object).toString();
  }

  /**
   * Converts given Object in to a JSON string representing an Object. If object contains an
   * attribute which itself is another object it will be displayed as className if its json
   * representation exceeds the length
   *
   * @param object an Object to be converted in to JSON String
   * @return a JSON string representing the specified object.
   */
  public static String objectToJsonNested(Object object, int length) {
    return objectToJsonNestedChkCDep(object, length, false);
  }

  public static String objectToJsonNestedChkCDep(Object object, int length) {
    return objectToJsonNestedChkCDep(object, length, true);
  }

  private static String objectToJsonNestedChkCDep(Object object, int length,
      boolean checkCyclicDep) {
    GfJsonObject jsonObject = new GfJsonObject(object, checkCyclicDep);
    Iterator<String> iterator = jsonObject.keys();
    while (iterator.hasNext()) {
      String key = iterator.next();
      Object value = jsonObject.get(key);
      if (value != null && !isPrimitiveOrWrapper(value.getClass())) {
        GfJsonObject jsonified = new GfJsonObject(value);
        String stringified = jsonified.toString();
        try {
          if (stringified.length() > length) {
            jsonObject.put(key, jsonified.getType());
          } else {
            jsonObject.put(key, stringified);
          }
        } catch (GfJsonException e) {
          e.printStackTrace();
        }
      }
    }
    return jsonObject.toString();
  }

  /**
   * Converts given JSON String in to a Object. Refer http://www.json.org/ to construct a JSON
   * format.
   *
   * @param jsonString jsonString to be converted in to a Map.
   * @return an object constructed from given JSON String
   *
   * @throws IllegalArgumentException if the specified JSON string can not be converted in to an
   *         Object
   */
  public static <T> T jsonToObject(String jsonString, Class<T> klass) {
    T objectFromJson = null;
    try {
      GfJsonObject jsonObject = new GfJsonObject(jsonString);
      objectFromJson = klass.newInstance();
      Method[] declaredMethods = klass.getMethods();
      Map<String, Method> methodsMap = new HashMap<String, Method>();
      for (Method method : declaredMethods) {
        methodsMap.put(method.getName(), method);
      }

      int noOfFields = jsonObject.size();
      Iterator<String> keys = jsonObject.keys();

      while (keys.hasNext()) {
        String key = keys.next();
        Method method = methodsMap.get("set" + capitalize(key));
        if (method != null) {
          Class<?>[] parameterTypes = method.getParameterTypes();
          if (parameterTypes.length == 1) {
            Class<?> parameterType = parameterTypes[0];

            Object value = jsonObject.get(key);
            if (isPrimitiveOrWrapper(parameterType)) {
              value = ConvertUtils.convert(getPrimitiveOrWrapperValue(parameterType, value),
                  parameterType);
            }
            // Bug #51175
            else if (isArray(parameterType)) {
              value = toArray(value, parameterType);
            } else if (isList(parameterType)) {
              value = toList(value, parameterType);
            } else if (isMap(parameterType)) {
              value = toMap(value, parameterType);
            } else if (isSet(parameterType)) {
              value = toSet(value, parameterType);
            } else {
              value = jsonToObject(value.toString(), parameterType);
            }
            method.invoke(objectFromJson, new Object[] {value});
            noOfFields--;
          }

        }
      }

      if (noOfFields != 0) {
        throw new IllegalArgumentException(
            "Not enough setter methods for fields in given JSON String : " + jsonString
                + " in class : " + klass);
      }

    } catch (InstantiationException e) {
      throw new IllegalArgumentException("Couldn't convert JSON to Object of type " + klass, e);
    } catch (IllegalAccessException e) {
      throw new IllegalArgumentException("Couldn't convert JSON to Object of type " + klass, e);
    } catch (GfJsonException e) {
      throw new IllegalArgumentException("Couldn't convert JSON to Object of type " + klass, e);
    } catch (IllegalArgumentException e) {
      throw new IllegalArgumentException("Couldn't convert JSON to Object of type " + klass, e);
    } catch (InvocationTargetException e) {
      throw new IllegalArgumentException("Couldn't convert JSON to Object of type " + klass, e);
    }
    return objectFromJson;
  }

  private static Object toArray(Object value, Class<?> parameterType) throws GfJsonException {
    Class arrayComponentType = parameterType.getComponentType();
    if (isPrimitiveOrWrapper(arrayComponentType)) {
      if (value instanceof JSONArray) {
        try {
          JSONArray jsonArray = (JSONArray) value;
          Object jArray = Array.newInstance(arrayComponentType, jsonArray.length());
          for (int i = 0; i < jsonArray.length(); i++) {
            Array.set(jArray, i, jsonArray.get(i));
          }
          return jArray;
        } catch (ArrayIndexOutOfBoundsException e) {
          throw new GfJsonException(e);
        } catch (IllegalArgumentException e) {
          throw new GfJsonException(e);
        } catch (JSONException e) {
          throw new GfJsonException(e);
        }
      } else {
        throw new GfJsonException("Expected JSONArray for array type");
      }
    } else
      throw new GfJsonException(
          "Array contains non-primitive element. Non-primitive elements are not supported in json array");
  }



  /**
   * This is used in Put command this method uses HashSet as default implementation
   *
   * @param value
   * @param parameterType
   * @return setValue
   * @throws GfJsonException
   */
  @SuppressWarnings({"rawtypes", "unchecked"})
  private static Object toSet(Object value, Class<?> parameterType) throws GfJsonException {
    try {
      JSONArray array = (JSONArray) value;
      Set set = new HashSet();
      for (int i = 0; i < array.length(); i++) {
        Object element = array.get(i);
        if (isPrimitiveOrWrapper(element.getClass())) {
          set.add(element);
        } else
          throw new GfJsonException(
              "Only primitive types are supported in set type for input commands");
      }
      return set;
    } catch (JSONException e) {
      throw new GfJsonException(e);
    }
  }

  private static Object toMap(Object value, Class<?> parameterType) throws GfJsonException {
    try {
      if (value instanceof JSONObject) {
        JSONObject obj = (JSONObject) value;
        Iterator iterator = obj.keys();
        Map map = new HashMap();
        while (iterator.hasNext()) {
          String key = (String) iterator.next();
          Object elem;
          elem = obj.get(key);
          if (isPrimitiveOrWrapper(elem.getClass())) {
            map.put(key, elem);
          } else
            throw new GfJsonException(
                "Only primitive types are supported in map type for input commands");
        }
        return map;
      } else
        throw new GfJsonException(
            "Expected JSONObject for Map. Retrieved type is " + value.getClass());
    } catch (JSONException e) {
      throw new GfJsonException(e);
    }
  }

  private static Object toList(Object value, Class<?> parameterType) throws GfJsonException {
    try {
      JSONArray array = (JSONArray) value;
      List list = new ArrayList();
      for (int i = 0; i < array.length(); i++) {
        Object element = array.get(i);
        if (isPrimitiveOrWrapper(element.getClass())) {
          list.add(element);
        } else
          throw new GfJsonException(
              "Only primitive types are supported in set type for input commands");
      }
      return list;
    } catch (JSONException e) {
      throw new GfJsonException(e);
    }
  }

  public static Object jsonToObject(String jsonString) {
    Object objectFromJson = null;
    try {
      GfJsonObject jsonObject = new GfJsonObject(jsonString);

      Iterator<String> keys = jsonObject.keys();

      Object[] arr = new Object[jsonObject.size()];
      int i = 0;

      while (keys.hasNext()) {
        String key = keys.next();
        Class<?> klass = ClassPathLoader.getLatest().forName(key);
        arr[i++] = jsonToObject((String) jsonObject.get(key).toString(), klass);
      }

      if (arr.length == 1) {
        objectFromJson = arr[0];
      } else {
        objectFromJson = arr;
      }
    } catch (GfJsonException e) {
      throw new IllegalArgumentException("Couldn't convert JSON to Object.", e);
    } catch (ClassNotFoundException e) {
      throw new IllegalArgumentException("Couldn't convert JSON to Object.", e);
    }

    return objectFromJson;
  }

  public static String capitalize(String str) {
    String capitalized = str;
    if (str == null || str.isEmpty()) {
      return capitalized;
    }
    capitalized = String.valueOf(str.charAt(0)).toUpperCase() + str.substring(1);

    return capitalized;
  }

  private static boolean isArray(Class<?> parameterType) {
    return parameterType.isArray();
  }

  public static boolean isList(Class<?> klass) {
    return klass.isAssignableFrom(List.class);
  }

  public static boolean isSet(Class<?> klass) {
    return klass.isAssignableFrom(Set.class);
  }

  public static boolean isMap(Class<?> klass) {
    return klass.isAssignableFrom(Map.class);
  }

  public static boolean isPrimitiveOrWrapper(Class<?> klass) {
    return klass.isAssignableFrom(Byte.class) || klass.isAssignableFrom(byte.class)
        || klass.isAssignableFrom(Short.class) || klass.isAssignableFrom(short.class)
        || klass.isAssignableFrom(Integer.class) || klass.isAssignableFrom(int.class)
        || klass.isAssignableFrom(Long.class) || klass.isAssignableFrom(long.class)
        || klass.isAssignableFrom(Float.class) || klass.isAssignableFrom(float.class)
        || klass.isAssignableFrom(Double.class) || klass.isAssignableFrom(double.class)
        || klass.isAssignableFrom(Boolean.class) || klass.isAssignableFrom(boolean.class)
        || klass.isAssignableFrom(String.class) || klass.isAssignableFrom(Character.class)
        || klass.isAssignableFrom(char.class);
  }

  public static Object getPrimitiveOrWrapperValue(Class<?> klass, Object value)
      throws IllegalArgumentException {
    if (klass.isAssignableFrom(Byte.class) || klass.isAssignableFrom(byte.class)) {
      return value;
    } else if (klass.isAssignableFrom(Short.class) || klass.isAssignableFrom(short.class)) {
      return value;
    } else if (klass.isAssignableFrom(Integer.class) || klass.isAssignableFrom(int.class)) {
      return value;
    } else if (klass.isAssignableFrom(Long.class) || klass.isAssignableFrom(long.class)) {
      return value;
    } else if (klass.isAssignableFrom(Float.class) || klass.isAssignableFrom(float.class)) {
      return value;
    } else if (klass.isAssignableFrom(Double.class) || klass.isAssignableFrom(double.class)) {
      return value;
    } else if (klass.isAssignableFrom(Boolean.class) || klass.isAssignableFrom(boolean.class)) {
      return value;
    } else if (klass.isAssignableFrom(String.class)) {
      return String.valueOf(value);
    } else if (klass.isAssignableFrom(Character.class)) {
      // Need to take care of converting between string to char values
      if (value instanceof String) {
        String str = (String) value;
        if (str.length() == 1)
          return new Character(str.charAt(0));
        else if (str.length() > 1 || str.length() == 0) {
          throw new IllegalArgumentException(
              "Expected Character value but found String with length " + str.length());
        }
      } else if (value instanceof Character) {
        return value;
      } else {
        throw new IllegalArgumentException(
            "Expected Character value but found " + value.getClass());
      }
    } else if (klass.isAssignableFrom(char.class)) {
      // Need to take care of converting between string to char values
      if (value instanceof String) {
        String str = (String) value;
        if (str.length() == 1)
          return str.charAt(0);
        else if (str.length() > 1 || str.length() == 0) {
          throw new IllegalArgumentException(
              "Expected Character value but found String with length " + str.length());
        }
      } else if (value instanceof Character) {
        return ((Character) value).charValue();
      } else {
        throw new IllegalArgumentException(
            "Expected Character value but found " + value.getClass());
      }
    } else {
      return null;
    }
    return value;
  }

  public static int getInt(GfJsonObject jsonObject, String byName) {
    return jsonObject.getInt(byName);
  }

  public static long getLong(GfJsonObject jsonObject, String byName) {
    return jsonObject.getLong(byName);
  }

  public static double getDouble(GfJsonObject jsonObject, String byName) {
    return jsonObject.getDouble(byName);
  }

  public static boolean getBoolean(GfJsonObject jsonObject, String byName) {
    return jsonObject.getBoolean(byName);
  }

  public static String getString(GfJsonObject jsonObject, String byName) {
    return jsonObject.getString(byName);
  }

  public static GfJsonObject getJSONObject(GfJsonObject jsonObject, String byName) {
    return jsonObject.getJSONObject(byName);
  }

  public static String[] getStringArray(GfJsonObject jsonObject, String byName) {
    String[] stringArray = null;
    try {
      GfJsonArray jsonArray = jsonObject.getJSONArray(byName);
      stringArray = GfJsonArray.toStringArray(jsonArray);
    } catch (GfJsonException e) {
      throw new ResultDataException(e.getMessage());
    }
    return stringArray;
  }

  public static byte[] getByteArray(GfJsonObject jsonObject, String byName) {
    byte[] byteArray = null;
    try {
      GfJsonArray jsonArray = jsonObject.getJSONArray(byName);
      byteArray = GfJsonArray.toByteArray(jsonArray);
    } catch (GfJsonException e) {
      throw new ResultDataException(e.getMessage());
    }
    return byteArray;
  }

  public static List<CliJsonSerializable> getList(GfJsonObject jsonObject, String byName) {
    List<CliJsonSerializable> cliJsonSerializables = Collections.emptyList();
    try {
      GfJsonArray cliJsonSerializableArray = jsonObject.getJSONArray(byName);
      int size = cliJsonSerializableArray.size();
      if (size > 0) {
        cliJsonSerializables = new ArrayList<CliJsonSerializable>();
      }
      for (int i = 0; i < size; i++) {
        GfJsonObject cliJsonSerializableState = cliJsonSerializableArray.getJSONObject(i);
        int jsId = cliJsonSerializableState.getInt(CliJsonSerializable.JSID);
        CliJsonSerializable cliJsonSerializable =
            CliJsonSerializableFactory.getCliJsonSerializable(jsId);
        cliJsonSerializable.fromJson(cliJsonSerializableState);
        cliJsonSerializables.add(cliJsonSerializable);
      }
    } catch (GfJsonException e) {
      throw new ResultDataException(e.getMessage());
    }
    return cliJsonSerializables;
  }

  public static Set<CliJsonSerializable> getSet(GfJsonObject jsonObject, String byName) {
    Set<CliJsonSerializable> cliJsonSerializables = Collections.emptySet();
    try {
      GfJsonArray cliJsonSerializableArray = jsonObject.getJSONArray(byName);
      int size = cliJsonSerializableArray.size();
      if (size > 0) {
        cliJsonSerializables = new HashSet<CliJsonSerializable>();
      }
      for (int i = 0; i < size; i++) {
        GfJsonObject cliJsonSerializableState = cliJsonSerializableArray.getJSONObject(i);
        int jsId = cliJsonSerializableState.getInt(CliJsonSerializable.JSID);
        CliJsonSerializable cliJsonSerializable =
            CliJsonSerializableFactory.getCliJsonSerializable(jsId);
        cliJsonSerializable.fromJson(cliJsonSerializableState);
        cliJsonSerializables.add(cliJsonSerializable);
      }
    } catch (GfJsonException e) {
      throw new ResultDataException(e.getMessage());
    }
    return cliJsonSerializables;
  }


  // For testing purpose
  public static void main(String[] args) {
    System.out.println(capitalize("key"));
    System.out.println(capitalize("Key"));

    String str =
        "{\"org.apache.geode.management.internal.cli.JsonUtil$Employee\":{\"id\":1234,\"name\":\"Foo BAR\",\"department\":{\"id\":456,\"name\":\"support\"}}}";
    Object jsonToObject = jsonToObject(str);
    System.out.println(jsonToObject);

    str = "{\"id\":1234,\"name\":\"Foo BAR\",\"department\":{\"id\":456,\"name\":\"support\"}}";
    Object jsonToObject2 = jsonToObject(str, Employee.class);
    System.out.println(jsonToObject2);
  }

  public static class Employee {
    private int id;
    private String name;
    private Department department;

    public int getId() {
      return id;
    }

    public void setId(int id) {
      this.id = id;
    }

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }

    public Department getDepartment() {
      return department;
    }

    public void setDepartment(Department department) {
      this.department = department;
    }

    @Override
    public String toString() {
      return "Employee [id=" + id + ", name=" + name + ", department=" + department + "]";
    }
  }

  public static class Department {
    private int id;
    private String name;

    public int getId() {
      return id;
    }

    public void setId(int id) {
      this.id = id;
    }

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }

    @Override
    public String toString() {
      return "Department [id=" + id + ", name=" + name + "]";
    }
  }

}
