/**
 * Copyright (c) 2014 example.testdemo Systems, Inc.
 *
 * This file is part of the Java ROSBridge Client.
 *
 * The Java ROSBridge Client is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * The Java ROSBridge Client is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with the Java ROSBridge Client.  If not, see http://www.gnu.org/licenses/.
 *
 */
package com.example.testdemo.ros.rosbridge.implementation;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.example.testdemo.ros.message.Message;
import com.example.testdemo.ros.rosbridge.indication.Indication;
import com.example.testdemo.ros.rosbridge.operation.Wrapper;

import java.io.StringReader;
import java.lang.reflect.Array;
import java.lang.reflect.Field;


// The slightly crazy abstractions here are designed to isolate knowledge of
//    the JSON library and data types from the Operation details of rosbridge.
//    Why is this important?  A few reasons I can see.  First, we might want
//    to change JSON libraries and this encapsulates all use of JSON-simple.
//    Second, as much as possible I would like the semantics of the rosbridge
//    protocol to be encapsulated in the Operation and its subclasses rather
//    than in a module that is essentially about serialization.
//
//    Unfortunately the hierarchical Message abstraction is a bit broken
//    at the top level. Beginning at the actual operation (e.g., Publish), the
//    types of the fields are determined either by the fields themselves or by
//    an indicator.  However, the type of the operation itself is not determined
//    this way, because the indicator is in the object itself, which means it
//    would have to be created before its type is known. Rather than build in
//    exceptions, I elected to create a "Wrapper" operation type that simply
//    wraps the concrete operation and copies its "op" field.
//

public class JSON {

    /**
     * Translates a Message recursively into JSON. Normally the Message is also an
     * Operation, but it does not have to be. The caller constructs a complete
     * message using @Operation and @Message types. This includes situations
     * where one or more fields are marked to be turned into arrays, using @AsArray.
     * @param m  the @Message object to be recursively translated.
     * @return   the complete JSON string.
     */
    public static String toJSON(Message m) {
        JSONObject jo = convertObjectToJSONObject(m); // Object to JSON-Simple
        return jo.toJSONString();                     // JSON-Simple to string
    }

    /**
     * Translates JSON into a hierarchical Operation/Message structure.
     * This includes handling fields that are @Indicated and @AsArray. If the
     * @Class parameter is a @Wrapper, this is a special case whereby the
     * object is wrapped to create a consistent hierarchy.
     * @param json  the source JSON string
     * @param c     the top level class of the JSON. Normally @Wrapper
     * @param r     the @Registry containing topic registrations
     * @return      the fully instantiated message hierarchy represented
     *              by the JSON string.
     */
    public static Message toMessage(String json, Class c, Registry<Class> r) {
        JSONObject joUnwrapped = convertStringToJSONObject(json); // String to JSON-Simple
        JSONObject jo = joUnwrapped;
        if (Wrapper.class.isAssignableFrom(c))
            jo = wrap(joUnwrapped, c);                            // wrap: a hack to make the hierarchy homogeneous
        return convertJSONObjectToMessage(jo, c, r);              // JSON-Simple to Message
    }

    // *** Create JSON from Messages *** //

    // Translate the object into a JSON-Simple object, field-by-field,
    //   recursively via convertElementToJSON.
    //   except for the case where AsArray is indicated
    private static JSONObject convertObjectToJSONObject(Object o) {
        JSONObject result = new JSONObject();
        for (Field f : o.getClass().getFields()) {
            Object fieldObject = getFieldObject(f, o);
            if (fieldObject != null) {
                Object resultObject;
                if (Indication.isBase64Encoded(f))
                    resultObject = convertByteArrayToBase64JSONString(fieldObject);
                else if (Indication.asArray(f))
                    resultObject = convertObjectToJSONArray(fieldObject);
                else resultObject = convertElementToJSON(fieldObject);
                result.put(f.getName(), resultObject);
            }
        }
        return result;
    }

    // Convert an array type to a JSON-Simple array, element-by-element,
    //    recursively via convertElementToJSON.
    private static JSONArray convertArrayToJSONArray(Object array) {
        JSONArray result = new JSONArray();
        for (int i = 0; i < Array.getLength(array); i++) {
            Object elementObject = Array.get(array, i);
            if (elementObject != null) {
                Object resultObject = convertElementToJSON(elementObject);
                result.add(resultObject);
            }
        }
        return result;
    }

    // For AsArray objects, convert the object to a JSON-Simple array
    //     NOTE: This relies on later versions of the JDK providing
    //           the fields in order.
    private static JSONArray convertObjectToJSONArray(Object o) {
        JSONArray result = new JSONArray();
        for (Field f : o.getClass().getFields()) {
            Object fieldObject = getFieldObject(f, o);
            if (fieldObject != null) {
                Object resultObject = convertElementToJSON(fieldObject);
                result.add(resultObject);
            }
        }
        return result;
    }

    // Convert the individual field or array element items recursively
    private static Object convertElementToJSON(Object elementObject) {
        Class elementClass = elementObject.getClass();
        Object resultObject;
        if (Message.isPrimitive(elementClass))
            resultObject = elementObject;
        else if (elementClass.isArray())
            resultObject = convertArrayToJSONArray(elementObject);
        else
            resultObject = convertObjectToJSONObject(elementObject);
        return resultObject;
    }

    // Special case for Base 64-encoded fields
    private static Object convertByteArrayToBase64JSONString(Object fieldObject) {
        return Base64.encodeToString((byte[]) fieldObject, false);
    }

    // This is just to buffer the code from the exception. Better error
    //    handling needed here.
    private static Object getFieldObject(Field f, Object o) {
        Object fo = null;
        try {
            fo = f.get(o);
        }
        catch (IllegalAccessException ex) {
            ex.printStackTrace();
        }
        return fo;
    }

    // *** Create Messages from JSON *** //

    // Use the JSON-simple parser to create the JSON-Simple object
    private static JSONObject convertStringToJSONObject(String json) {
        JSONObject result = null;
        StringReader r = new StringReader(json);
        //JSONParser jp = new JSONParser();
        try {
            //result = (JSONObject) jp.parse(r);
        }
        catch (Throwable t) {
            System.out.println(t.getMessage());
        }
        r.close();
        return result;
    }

    // A bit of a hack to create a consistent hierarchy with jsonbridge operations
    // At least it does not depend on any specific field names, it just copies the
    // Indicator and Indicated fields.
    private static JSONObject wrap(JSONObject jo, Class c) {
        JSONObject result = new JSONObject();
        String indicatorName = Indication.getIndicatorName(c);
        String indicatedName = Indication.getIndicatedName(c);
        result.put(indicatorName, jo.get(indicatorName));
        result.put(indicatedName, jo);
        return result;
    }

    // Convert the JSON-Simple object to the indicated message, field-by-field
    //    recursively via convertElementToField.
    private static Message convertJSONObjectToMessage(JSONObject jo, Class c, Registry<Class> r) {
        //System.out.println("JSON.convertJSONObjectToMessage: " + jo.toJSONString());
        try {
            Message result = (Message) c.newInstance();
            for (Field f : c.getFields()) {
                Class fc = getFieldClass(result, jo, f, r);
                Object lookup = jo.get(f.getName());
                if (lookup != null) {
                    Object value = convertElementToField(lookup, fc, f, r);
                    f.set(result, value);
                }
            }
            return result;
        }
        catch (Exception ex) {
            //ex.printStackTrace();
            return null;
        }
    }

    // Convert the JSON-Simple array to the indicated message, element-by-element
    //    recursively via convertElementToField.
    private static Object convertJSONArrayToArray(JSONArray ja, Class c, Registry<Class> r) {
        Object result = Array.newInstance(c, ja.size());
        for (int i = 0; i < ja.size(); i++) {
            Object lookup = ja.get(i);
            Object value = null;
            if (lookup != null) {
                if (lookup.getClass().equals(JSONObject.class))
                    value = convertJSONObjectToMessage((JSONObject) lookup, c, r);
                else if (lookup.getClass().equals(JSONArray.class))  // this is not actually allowed in ROS
                    value = convertJSONArrayToArray((JSONArray) lookup, c.getComponentType(), r);
                else
                    value = convertJSONPrimitiveToPrimitive(lookup, c);
                Array.set(result, i, value);
            }
        }

        return result;
    }

    // Convert a JSON-Simple array to a Message, field-by-field of the Message,
    //     element-by-element of the array, recursively via convertElementToField.
    //     NOTE: This relies on later versions of the JDK providing
    //           the fields in order.
    private static Message convertJSONArrayToMessage(JSONArray ja, Class c, Registry<Class> r) {
        try {
            Message result = (Message) c.newInstance();
            int arrayIndex = 0;
            for (Field f : c.getFields()) {
                Class fc = getFieldClass(result, null, f, r);
                Object lookup = ja.get(arrayIndex++);         // yes we are assuming that the fields are delivered in order
                if (lookup != null) {
                    Object value = convertElementToField(lookup, fc, f, r);
                    f.set(result, value);
                }
            }

            return result;
        }
        catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }

    // Convert an individual array or object element to a field in the Message,
    //    recursively, and applying AsArray if needed.
    private static Object convertElementToField(Object element, Class fc, Field f, Registry<Class> r) {
        //System.out.println("JSON.convertElementToField: " + f.getName() + " " + fc.getName());
        Object value;
        if (element.getClass().equals(JSONObject.class)) {
            //System.out.println("JSON.convertElementToField: JSON Object " + ((JSONObject) element).toJSONString());
            value = convertJSONObjectToMessage((JSONObject) element, fc, r);
        }
        else if (element.getClass().equals(JSONArray.class)) {
            //System.out.println("JSON.convertElementToField: JSON Array " + ((JSONArray) element).toJSONString());
            if (Indication.asArray(f))
                value = convertJSONArrayToMessage((JSONArray) element, fc, r);
            else value = convertJSONArrayToArray((JSONArray) element, fc, r);
        }
        else {
            //System.out.println("JSON.convertElementToField: Primitive " + element);
            if (Indication.isBase64Encoded(f))
                value = convertBase64JSONStringToByteArray(element);
            else value = convertJSONPrimitiveToPrimitive(element, fc);
        }

        return value;
    }

    // Note that this is not checking ranges
    public static Object convertJSONPrimitiveToPrimitive(Object o, Class c) {
        Object result = o;
        if (c.isPrimitive() || Number.class.isAssignableFrom(c)) {
            if (c.equals(double.class) || c.equals(Double.class))
                result = new Double(((Number) o).doubleValue());
            else if (c.equals(float.class) || c.equals(Float.class))
                result = new Float(((Number) o).floatValue());
            else if (c.equals(long.class) || c.equals(Long.class))
                result = new Long(((Number) o).longValue());
            else if (int.class.equals(c) || c.equals(Integer.class))
                result = new Integer(((Number) o).intValue());
            else if (c.equals(short.class) || c.equals(Short.class))
                result = new Short(((Number) o).shortValue());
            else if (c.equals(byte.class) || c.equals(Byte.class))
                result = new Byte(((Number) o).byteValue());
        }
        return result;
    }

    public static byte[] convertBase64JSONStringToByteArray(Object element) {
        return Base64.decode((String) element);
    }

    // Determine the target class of a field in the object or array, based
    //    directly on the field's type, or using the Indicator if applicable,
    //    The Indicator field only provides the topic/service, so we have to look
    //    up the Class in the registry.
    public static Class getFieldClass(Message parent, JSONObject jo, Field f, Registry<Class> r) {
        Class fc;
        fc = f.getType();
        if (fc.isArray())
            fc = f.getType().getComponentType();
        if (Indication.isIndicated(f) && (jo != null)) {
            //fc = Indication.getIndication(parent,
            //        (String) jo.get(Indication.getIndicatorName(parent.getClass())));
            fc = r.lookup(parent.getClass(),
                    (String) jo.get(Indication.getIndicatorName(parent.getClass())));
            //System.out.println("JSON.getFieldClass: parent class " + parent.getClass().getName() +
            //        " Indicator: " + Indication.getIndicatorName(parent.getClass()) +
            //        " result: " + fc.getName());
        }
        return fc;
    }
}
