// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.cosmos.implementation.directconnectivity.rntbd;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ser.PropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.DurationDeserializer;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import io.netty.handler.codec.CorruptedFrameException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ConcurrentHashMap;

import static com.azure.cosmos.implementation.guava25.base.Preconditions.checkNotNull;
import static com.azure.cosmos.implementation.guava27.Strings.lenientFormat;

public final class RntbdObjectMapper {

    private static final Logger logger = LoggerFactory.getLogger(RntbdObjectMapper.class);
    private static final SimpleFilterProvider filterProvider = new SimpleFilterProvider();

    private static final ObjectMapper objectMapper = new ObjectMapper()
        .registerModule(new SimpleModule()
            .addSerializer(Duration.class, ToStringSerializer.instance)
            .addDeserializer(Duration.class, DurationDeserializer.INSTANCE)
            .addSerializer(Instant.class, ToStringSerializer.instance))
        .setFilterProvider(filterProvider);

    private static final ObjectWriter objectWriter = objectMapper.writer();

    private static final ConcurrentHashMap<Class<?>, String> simpleClassNames = new ConcurrentHashMap<>();

    private RntbdObjectMapper() {
    }

    public static <T> T readValue(File file, Class<T> type) throws IOException {
        checkNotNull(file, "expected non-null file");
        checkNotNull(type, "expected non-null type");
        return objectMapper.readValue(file, type);
    }

    public static <T> T readValue(InputStream stream, Class<T> type) throws IOException {
        checkNotNull(stream, "expected non-null stream");
        checkNotNull(type, "expected non-null type");
        return objectMapper.readValue(stream, type);
    }

    public static <T> T readValue(String string, Class<T> type) throws IOException {
        checkNotNull(string, "expected non-null string");
        checkNotNull(type, "expected non-null type");
        return objectMapper.readValue(string, type);
    }

    public static String toJson(final Object value) {
        try {
            return objectWriter.writeValueAsString(value);
        } catch (final JsonProcessingException error) {
            logger.debug("could not convert {} value to JSON due to:", value.getClass(), error);
            try {
                return lenientFormat("{\"error\":%s}", objectWriter.writeValueAsString(error.toString()));
            } catch (final JsonProcessingException exception) {
                return "null";
            }
        }
    }

    public static String toString(final Object value) {
        final String name = simpleClassNames.computeIfAbsent(value.getClass(), Class::getSimpleName);
        return lenientFormat("%s(%s)", name, toJson(value));
    }

    public static ObjectWriter writer() {
        return objectWriter;
    }

    static ObjectNode readTree(final RntbdResponse response) {
        checkNotNull(response, "response");
        return readTree(response.getContent());
    }

    static ObjectNode readTree(final ByteBuf in) {

        checkNotNull(in, "in");
        final JsonNode node;

        try (final InputStream istream = new ByteBufInputStream(in)) {
            node = objectMapper.readTree(istream);
        } catch (final IOException error) {
            throw new CorruptedFrameException(error);
        }

        if (node.isObject()) {
            return (ObjectNode)node;
        }

        final String cause = lenientFormat("Expected %s, not %s", JsonNodeType.OBJECT, node.getNodeType());
        throw new CorruptedFrameException(cause);
    }

    @SuppressWarnings("SameParameterValue")
    static void registerPropertyFilter(final Class<?> type, final Class<? extends PropertyFilter> filter) {

        checkNotNull(type, "type");
        checkNotNull(filter, "filter");

        try {
            filterProvider.addFilter(type.getSimpleName(), filter.getDeclaredConstructor().newInstance());
        } catch (final ReflectiveOperationException error) {
            throw new IllegalStateException(error);
        }
    }
}
