package org.implab.gradle.common.json.core;

import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;

import org.implab.gradle.common.core.lang.LazyValue;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.core.util.DefaultIndenter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

public class Json {

    private final static LazyValue<ObjectMapper> mapper = new LazyValue<>(Json::createMapper);

    private static ObjectMapper createMapper() {
        DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
        prettyPrinter.indentArraysWith(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE);

        return new ObjectMapper()
                .registerModule(new Jdk8Module())
                .registerModule(new JavaTimeModule())
                .setSerializationInclusion(Include.NON_EMPTY)
                .enable(SerializationFeature.INDENT_OUTPUT)
                .setDefaultPrettyPrinter(prettyPrinter);
    }

    public static ObjectMapper jsonMapper() {
        return mapper.get();
    }

    public static Consumer<Object> fileWriter(File file) {
        return value -> {
            try {
                jsonMapper().writeValue(file, value);
            } catch (IOException e) {
                throw new SerializationException(e);
            }
        };
    }

    public static void write(File file, Object value) {
        try {
            jsonMapper().writeValue(file, value);
        } catch (IOException e) {
            throw new SerializationException(e);
        }
    }

    public static String stringify(Object value) {
        try {
            return jsonMapper().writeValueAsString(value);
        } catch (JsonProcessingException e) {
            throw new SerializationException(e);
        }
    }

    public static <T> T parse(String json, Class<T> clazz) {
        try {
            return jsonMapper().readValue(json, clazz);
        } catch (JsonProcessingException e) {
            throw new SerializationException(e);
        }
    }

    public static <T> T parse(String json, TypeReference<T> type) {
        try {
            return jsonMapper().readValue(json, type);
        } catch (JsonProcessingException e) {
            throw new SerializationException(e);
        }
    }

    public static <V> Map<String, V> parseMap(String json, Class<V> clazz) {
        return parse(json, new TypeReference<>() {
        });
    }

    public static <V> Function<String, Map<String, V>> mapParser(Class<V> clazz) {
        return json -> parseMap(json, clazz);
    }

    public static <T> T read(File file, TypeReference<T> type) {
        try {
            return jsonMapper().readValue(file, type);
        } catch (IOException e) {
            throw new SerializationException(e);
        }
    }

    public static <V> Map<String, V> readMap(File file, Class<V> clazz) {
        return read(file, new TypeReference<>() {
        });
    }

    public static <T> T updateValue(T target, Object source) {
        try {
            return jsonMapper().updateValue(target, source);
        } catch (JsonMappingException e) {
            throw new SerializationException(e);
        }
    }

    public static <T> T convertValue(Object source, Class<T> clazz) {
        var mapper = jsonMapper();
        var buf = mapper.getSerializerProvider().bufferForValueConversion(jsonMapper());

        try {
            mapper.writeValue(buf, source);
            return mapper.readValue(buf.asParser(), clazz);
        } catch (IOException e) {
            throw new SerializationException(e);
        }
    }

    public static class SerializationException extends RuntimeException {
        public SerializationException() {
        }

        public SerializationException(String message) {
            super(message);
        }

        public SerializationException(Throwable cause) {
            super(cause);
        }

        public SerializationException(String message, Throwable cause) {
            super(message, cause);
        }
    }
}
