/*
* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
*  http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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 software.amazon.cloudformation.resource;

import com.amazonaws.util.IOUtils;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import org.apache.commons.codec.binary.Base64;
import software.amazon.cloudformation.proxy.aws.AWSServiceSerdeModule;

public class Serializer {

    public static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeReference<Map<String, Object>>() {
    };
    public static final String COMPRESSED = "__COMPRESSED__";
    private static final String COMPRESSION_METHOD = "__COMPRESSION_METHOD__";
    private static final String COMPRESSION_GZIP_BASE64 = "gzip_base64";
    private static final ObjectMapper OBJECT_MAPPER;
    private static final ObjectMapper STRICT_OBJECT_MAPPER;

    /**
     * Configures the specified ObjectMapper with the (de)serialization behaviours
     * we want gto enforce for strict serialization (for validation purposes)
     *
     * @param objectMapper ObjectMapper instance to configure
     */
    static {
        STRICT_OBJECT_MAPPER = JsonMapper.builder().configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
            .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build();
        STRICT_OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_ABSENT);
        STRICT_OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT);
        STRICT_OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
        STRICT_OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        STRICT_OBJECT_MAPPER.registerModule(new AWSServiceSerdeModule());
        STRICT_OBJECT_MAPPER.registerModule(new JavaTimeModule());
    }

    /**
     * Configures the specified ObjectMapper with the (de)serialization behaviours
     * we want to enforce NOTE: We intend to move towards versioned protocol between
     * caller (CloudFormation) and the various handlers. For now, loose
     * serialization at the protocol layer allows some flexibility between these
     * components.
     *
     * @param objectMapper ObjectMapper instance to configure
     */
    static {
        OBJECT_MAPPER = JsonMapper.builder().configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build();
        OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_ABSENT);
        OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT);
        OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
        OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        OBJECT_MAPPER.registerModule(new AWSServiceSerdeModule());
        OBJECT_MAPPER.registerModule(new JavaTimeModule());
    }

    public <T> String serialize(final T modelObject) throws JsonProcessingException {
        return OBJECT_MAPPER.writeValueAsString(modelObject);
    }

    public <T> String compress(final String modelInput) throws IOException {
        final Map<String, String> map = new HashMap<>();
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
            try (GZIPOutputStream gzip = new GZIPOutputStream(byteArrayOutputStream)) {
                gzip.write(modelInput.getBytes(StandardCharsets.UTF_8));
            }
            map.put(COMPRESSED, Base64.encodeBase64String(byteArrayOutputStream.toByteArray()));
            map.put(COMPRESSION_METHOD, COMPRESSION_GZIP_BASE64);
        }
        return OBJECT_MAPPER.writeValueAsString(map);
    }

    public <T> T deserialize(final String s, final TypeReference<T> reference) throws IOException {
        return OBJECT_MAPPER.readValue(s, reference);
    }

    public String decompress(final String s) throws IOException {
        final Map<String, Object> map = deserialize(s, MAP_TYPE_REFERENCE);

        if (!map.containsKey(COMPRESSED)) {
            return s;
        }

        final byte[] bytes = Base64.decodeBase64((String) map.get(COMPRESSED));
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
            GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream);) {
            return new String(IOUtils.toByteArray(gzipInputStream), StandardCharsets.UTF_8);
        }
    }

    public <T> T deserializeStrict(final String s, final TypeReference<T> reference) throws IOException {
        return STRICT_OBJECT_MAPPER.readValue(s, reference);
    }

    public <T> T convert(final Object obj, final TypeReference<T> reference) {
        return OBJECT_MAPPER.convertValue(obj, reference);
    }
}