package com.ruoyi.platform.utils;


import com.google.gson.*;
import com.google.gson.reflect.TypeToken;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
 * @author Stone
 * @version V1.0.0
 * @date 2020/2/13
 */
public class GsonUtil {
    /**
     * 是线程安全的，详细参考文档
     */
    public static Gson gson = new Gson();


    /**
     * 将对象转为JSON字符串
     *
     * @param obj        被转的对象
     * @param dateFormat 日期格式，当传null或空串时，则被格式化为时间戳，否则返回指定的格式。例子：yyyy-MM-dd HH:mm:ss（不合法的日期格式会格式化出错）
     * @param ignoreNull 是否忽略null字段。true时，且当字段的值是null，则不输出该字段
     * @param pretty     是否格式化JSON字符串以便有更好的可读性
     * @return JSON字符串，出异常时抛出
     */
    public static String toJSONString0(Object obj,
                                       String dateFormat,
                                       boolean ignoreNull,
                                       boolean pretty) {
        try {
            return getGson(dateFormat, ignoreNull, pretty).toJson(obj);
        } catch (Exception e) {
            throw new RuntimeException("Convert object to JSON string, error[" + obj + "]", e);
        }
    }

    /**
     * 获取ObjectMapper。其本身是线程安全的，可以作为成员变量，但传入的参数不可能每次都一样，所以不使用成员变量。
     * （获取的Gson，在解析JSON字符串的时候，若字符串新增了目标类未知的字段，不报错，也就是兼容的。
     * @param dateFormat     日期格式，传null或空串则不设置
     * @param ignoreNull     是否忽略字段值是null的字段
     * @param pretty         是否格式化
    //* @param compatNewProps 是否兼容。当json字符串新增了字段，在转化为某个类，这个类没有该字段，不兼容时将抛出异常。【找不到Gson中这个开关，默认是兼容的】
     * @return
     */
    private static Gson getGson(String dateFormat,
                                boolean ignoreNull,
                                boolean pretty) {
        GsonBuilder gsonBuilder = new GsonBuilder();

        // 默认序列化的格式是 Feb 13, 2020 11:54:55 AM，如果不指定形式，改成序列化成时间戳形式
        if (dateFormat != null && dateFormat.length() > 0) {
            gsonBuilder.setDateFormat(dateFormat);
        } else {
            // 转json字符串，日期格式使用时间戳
            gsonBuilder.registerTypeAdapter(Date.class, new JsonSerializer<Date>() {
                @Override
                public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
                    return new JsonPrimitive(src.getTime());
                }
            });

            // json字符串转对象，日期格式使用时间戳的输入
            gsonBuilder.registerTypeAdapter(Date.class, new JsonDeserializer<Date>() {
                @Override
                public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
                    return new Date(json.getAsJsonPrimitive().getAsLong());
                }
            });
        }

        // 默认忽略所有null值字段
        if (!ignoreNull) {
            gsonBuilder.serializeNulls();
        }
        // 默认不格式化
        if (pretty) {
            gsonBuilder.setPrettyPrinting();
        }

        // Gson 默认不会打印出$ref的值。找了一下没找到Gson对此的配置，于是讲究不提供该配置。
        // 实际上像 Fastjson 默认会输出$ref 几乎是毫无用处的（除了节省文本长度）
        return gsonBuilder.create();
    }

    /**
     * 将对象转为JSON字符串。
     * 日期转为非时间戳的格式，不忽略null值的字段，不格式化JSON字符串
     *
     * @param obj 被转的对象
     * @return JSON字符串，出异常时抛出
     */
    public static String toJSONString(Object obj) {
        return toJSONString0(obj, "yyyy-MM-dd HH:mm:ss", false, false);
    }

    /**
     * 将对象转为JSON字符串。不抛出异常，专用于日志打印
     *
     * @param obj 被转的对象
     * @return JSON字符串，出异常时返回null
     */
    public static String toJSONStringNoThrows(Object obj) {
        try {
            return toJSONString0(obj, "yyyy-MM-dd HH:mm:ss", false, false);
        } catch (Exception e) {
            logError(e);
            return null;
        }
    }

    /**
     * 传入JSON字符串，不管是对象和列表，都能兼容的方法
     * TODO ObjectMapper 的API似乎没有Fastjson的那样，可以不事先了要转换的json字符串是对象还是列表统一一个api转。ObjectMapper似乎只能了解清楚后使用对应的api，否则会转换抛出异常
     *
     * @param jsonStr
     * @return
     */
    public static JacksonUtil.JSONEntity parseJSONStr2JSONEntity(String jsonStr) {
        throw new RuntimeException("Jackson api not support this feature!");
    }

    public static <T> T parseJSONStr2T(String jsonStr, Class<T> clazz, String dateFormat) {
        try {
            Gson gson = getGson(dateFormat, false, false);
            return gson.fromJson(jsonStr, clazz);
        } catch (Exception e) {
            throw new RuntimeException("Invalid jsonStr,parse error:" + jsonStr, e);
        }
    }

    /**
     * json 字符串转成某个类的列表
     *
     * @param jsonStr    传入的JSON字串
     * @param clazz      转为什么类型
     * @param dateFormat 日期格式，传null或空串则不设置，不设置时默认格式化时间戳。当无法格式化的时候抛出异常
     * @param <T>        类型
     * @return 转换失败则抛出异常
     */
    public static <T> List<T> parseJSONStr2TList(String jsonStr, Class<T> clazz, String dateFormat) {
        try {
            Gson gson = getGson(dateFormat, false, false);
            Type listType = TypeToken.getParameterized(ArrayList.class, clazz).getType();
            return gson.fromJson(jsonStr, listType);
        } catch (Exception e) {
            throw new RuntimeException("Invalid jsonStr,parse error:" + jsonStr, e);
        }
    }

    /**
     * 字符串转为MAP
     * （假设json字符串多了某个字段，可能是新加上去的，显然转换后会有这个字段）
     *
     * @param jsonStr    传入的JSON字串
     * //@param dateFormat 解析为map的时候，日期格式是无意义的，因为根本就不知道json字符串的字段要用什么类型去承载
     * @return 转换失败则抛出异常
     */
    public static Map<String, Object> parseJSONStr2Map(String jsonStr) {
        try {
            // 对于json字符串新增的字段，由于返回的是map，不管 compatNewProps 设置成什么值都不会抛出异常
            Gson gson = getGson(null, false, false);
            return gson.fromJson(jsonStr, Map.class);
        } catch (Exception e) {
            throw new RuntimeException("Invalid jsonStr,parse error:" + jsonStr, e);
        }
    }

    /**
     * 字符串转为MAP列表
     * （假设json字符串多了某个字段，可能是新加上去的，显然转换后会有这个字段）
     *
     * @param jsonStr    传入的JSON字串
     * //@param dateFormat 解析为map的时候，日期格式是无意义的，因为根本就不知道json字符串的字段要用什么类型去承载
     * @return 转换失败则抛出异常
     */
    public static List<Map<String, Object>> parseJSONStr2MapList(String jsonStr) {
        try {
            // 对于json字符串新增的字段，由于返回的是map，不管 compatNewProps 设置成什么值都不会抛出异常
            Gson gson = getGson(null, false, false);
            Type listType = TypeToken.getParameterized(ArrayList.class, Map.class).getType();
            return gson.fromJson(jsonStr, listType);
        } catch (Exception e) {
            throw new RuntimeException("Invalid jsonStr,parse error:" + jsonStr, e);
        }
    }

    public static class JSONEntity {
        public JSONEntity() {
        }

        public JSONEntity(boolean jsonArrayFlag, Object value) {
            this.jsonArrayFlag = jsonArrayFlag;
            this.value = value;
        }

        private boolean jsonArrayFlag;
        private Object value;

        public boolean getJsonArrayFlag() {
            return jsonArrayFlag;
        }

        public Object getValue() {
            return value;
        }

        @Override
        public String toString() {
            return "JSONEntity{" +
                    "jsonArrayFlag=" + jsonArrayFlag +
                    ", value=" + value +
                    '}';
        }
    }

    private static void logError(Exception e) {
        e.printStackTrace();
    }
}
