/*
 * 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.Map;
import java.util.Optional;
import java.util.Set;
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;

   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;
   }
}

@@ -988,60 +988,9 @@
 til.
-Map;%0Aimport java.util.Optional;%0Aimport java.util.Set
+*
 ;%0Aim
@@ -1420,16 +1420,118 @@
 lassMap;
+%0A   private static final Map%3CClass%3C?%3E, List%3CClass%3C?%3E%3E%3E classToClassHierarchyListMap = new HashMap%3C%3E();
 %0A%0A   sta
@@ -1914,16 +1914,22 @@
         
+      
 Serializ
@@ -1978,16 +1978,22 @@
         
+      
 Function
@@ -2004,16 +2004,22 @@
 ntity()%0A
+      
       ))
@@ -4461,11 +4461,675 @@
 me;%0A   %7D
+%0A%0A   /**%0A    * Returns a list consisting of the class of the object and its superclasses, excluding %7B@link Object%7D%0A    *%0A    * @param obj the object%0A    */%0A   public static List%3CClass%3C?%3E%3E getClassHierarchy(Object obj)%0A   %7B%0A      Class objClass = obj.getClass();%0A%0A      List%3CClass%3C?%3E%3E classes = classToClassHierarchyListMap.get(objClass);%0A      if (classes != null) return classes;%0A%0A      List%3CClass%3C?%3E%3E result = new LinkedList%3C%3E();%0A%0A      while (objClass != null && objClass != Object.class) %7B%0A         result.add(objClass);%0A         objClass = objClass.getSuperclass();%0A      %7D%0A%0A      classToClassHierarchyListMap.put(objClass, result);%0A%0A      return result;%0A   %7D
 %0A%7D%0A

Add SerializationHelper::getClassHierarchy