/*
 * Copyright 2017 Ivaylo Stoyanov <me@ivkos.com>
 *
 * Licensed 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 com.ivkos.gpsd4j.support;

import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.ivkos.gpsd4j.messages.GpsdMessage;
import io.vertx.core.json.DecodeException;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonObject;
import org.reflections.Reflections;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.function.Function;

import static java.lang.String.format;
import static java.util.Collections.unmodifiableMap;
import static java.util.stream.Collectors.toMap;

public class SerializationHelper
{
   private static final String JSON_CLASS_KEY = "class";
   private static final String GPSDMESSAGE_CLASS_FIELD_NAME = "CLASS";

   private static final Map<String, Class<? extends GpsdMessage>> gpsdClassNameToClassMap;
   private static final Map<Class<?>, List<Class<?>>> classToClassHierarchyListMap = new HashMap<>();

   static {
      Reflections reflections = new Reflections(GpsdMessage.class.getPackage().getName());
      Set<Class<? extends GpsdMessage>> objectClasses = reflections.getSubTypesOf(GpsdMessage.class);

      gpsdClassNameToClassMap = unmodifiableMap(objectClasses.stream()
            .filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
            .collect(toMap(
                  SerializationHelper::getGpsdClassNameByClass,
                  Function.identity()
            )));
   }

   // Support for deserialization into LocalDateTime
   static {
      JavaTimeModule javaTimeModule = new JavaTimeModule();
      Json.mapper.registerModule(javaTimeModule);
      Json.prettyMapper.registerModule(javaTimeModule);
   }

   /**
    * Deserializes a JSON string representing a message received from gpsd into a type-safe object representation,
    * subtype of {@link GpsdMessage}.
    *
    * @param json the JSON string
    *
    * @return the corresponding object, a subtype of {@link GpsdMessage}
    */
   @SuppressWarnings("unchecked")
   public static <T extends GpsdMessage> T deserialize(String json)
   {
      JsonObject obj;
      try {
         obj = new JsonObject(json);
      } catch (DecodeException e) {
         throw new GpsdParseException("Could not parse JSON", e);
      }

      String className = obj.getString(JSON_CLASS_KEY);
      if (className == null) throw new GpsdParseException("Could not parse JSON: missing '%s' key", JSON_CLASS_KEY);

      Class<T> clazz = (Class<T>) gpsdClassNameToClassMap.get(className);
      if (clazz == null) throw new GpsdParseException("Could not parse JSON: unknown class '%s'", className);

      try {
         return Json.decodeValue(json, clazz);
      } catch (DecodeException e) {
         throw new GpsdParseException("Could not parse JSON", e);
      }
   }

   /**
    * Serializes an object to a JSON string.
    *
    * @param obj the object
    *
    * @return a JSON string
    */
   public static String serialize(Object obj)
   {
      return Json.encode(obj);
   }

   /**
    * Returns the gpsd-specific class name for a given {@link GpsdMessage} type
    *
    * @param clazz the class
    *
    * @return the gpsd-specific class name
    */
   public static String getGpsdClassNameByClass(Class<? extends GpsdMessage> clazz)
   {
      String gpsdClassName;

      try {
         Field field = clazz.getDeclaredField(GPSDMESSAGE_CLASS_FIELD_NAME);

         gpsdClassName = Optional.ofNullable((String) field.get(null))
               .orElseThrow(() -> new RuntimeException("Field is null"));
      } catch (NoSuchFieldException | IllegalAccessException | RuntimeException e) {
         throw new RuntimeException(format(
               "Class %s must declare a 'public static final String %s = ...;' that is not null",
               clazz.getName(), GPSDMESSAGE_CLASS_FIELD_NAME
         ));
      }

      return gpsdClassName;
   }

   /**
    * Returns a list consisting of the class of the object and its superclasses, excluding {@link Object}
    *
    * @param obj the object
    */
   public static List<Class<?>> getClassHierarchy(Object obj)
   {
      Class objClass = obj.getClass();

      List<Class<?>> classes = classToClassHierarchyListMap.get(objClass);
      if (classes != null) return classes;

      List<Class<?>> result = new LinkedList<>();

      while (objClass != null && objClass != Object.class) {
         result.add(objClass);
         objClass = objClass.getSuperclass();
      }

      classToClassHierarchyListMap.put(objClass, result);

      return result;
   }
}

@@ -643,16 +643,124 @@
 pport;%0A%0A
+import com.fasterxml.jackson.annotation.JsonAutoDetect;%0Aimport com.fasterxml.jackson.databind.ObjectMapper;%0A
 import c
@@ -2131,24 +2131,84 @@
  )));%0A   %7D%0A%0A
+   // Configure vertx's backing ObjectMapper%0A   static %7B%0A   
    // Suppor
@@ -2248,28 +2248,16 @@
 ateTime%0A
-   static %7B%0A
       Ja
@@ -2308,27 +2308,108 @@
 dule();%0A
+%0A
       
-Json.
+for (ObjectMapper mapper : Arrays.asList(Json.mapper, Json.prettyMapper)) %7B%0A         
 mapper.r
@@ -2443,63 +2443,515 @@
 e);%0A
+%0A
       
-Json.prettyMapper.registerModule(javaTimeModule);
+   // use only fields for de/serialization%0A         mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker()%0A               .withFieldVisibility(JsonAutoDetect.Visibility.ANY)%0A               .withGetterVisibility(JsonAutoDetect.Visibility.NONE)%0A               .withSetterVisibility(JsonAutoDetect.Visibility.NONE)%0A               .withCreatorVisibility(JsonAutoDetect.Visibility.NONE)%0A               .withIsGetterVisibility(JsonAutoDetect.Visibility.NONE)%0A         );%0A      %7D
 %0A   

Configure ObjectMapper to de/serialize fields only