/*
 * Copyright 2016-2017, Youqian Yue ([email protected]).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License 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 org.devefx.validator.http.reader.json;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.concurrent.atomic.AtomicReference;

import javax.servlet.http.HttpServletRequest;

import org.devefx.validator.http.MediaType;
import org.devefx.validator.http.reader.AbstractHttpMessageReader;
import org.devefx.validator.http.reader.HttpMessageNotReadableException;
import org.devefx.validator.util.Assert;

import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

public abstract class AbstractJackson2HttpMessageReader extends AbstractHttpMessageReader<Object> {
    
    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    
    protected ObjectMapper objectMapper;
    
    private Boolean prettyPrint;
    
    protected AbstractJackson2HttpMessageReader(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    protected AbstractJackson2HttpMessageReader(ObjectMapper objectMapper, MediaType supportedMediaType) {
        super(supportedMediaType);
        this.objectMapper = objectMapper;
    }

    protected AbstractJackson2HttpMessageReader(ObjectMapper objectMapper, MediaType... supportedMediaTypes) {
        super(supportedMediaTypes);
        this.objectMapper = objectMapper;
    }
    
    /**
     * Set the {@code ObjectMapper} for this view.
     * If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} is used.
     * <p>Setting a custom-configured {@code ObjectMapper} is one way to take further
     * control of the JSON serialization process. For example, an extended
     * {@link com.fasterxml.jackson.databind.ser.SerializerFactory}
     * can be configured that provides custom serializers for specific types.
     * The other option for refining the serialization process is to use Jackson's
     * provided annotations on the types to be serialized, in which case a
     * custom-configured ObjectMapper is unnecessary.
     */
    public void setObjectMapper(ObjectMapper objectMapper) {
        Assert.notNull(objectMapper, "ObjectMapper must not be null");
        this.objectMapper = objectMapper;
        configurePrettyPrint();
    }

    /**
     * Return the underlying {@code ObjectMapper} for this view.
     */
    public ObjectMapper getObjectMapper() {
        return this.objectMapper;
    }
    
    /**
     * Whether to use the {@link DefaultPrettyPrinter} when writing JSON.
     * This is a shortcut for setting up an {@code ObjectMapper} as follows:
     * <pre class="code">
     * ObjectMapper mapper = new ObjectMapper();
     * mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
     * converter.setObjectMapper(mapper);
     * </pre>
     */
    public void setPrettyPrint(boolean prettyPrint) {
        this.prettyPrint = prettyPrint;
        configurePrettyPrint();
    }

    private void configurePrettyPrint() {
        if (this.prettyPrint != null) {
            this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint);
        }
    }
    
    @Override
    protected boolean supports(Class<?> clazz) {
        JavaType javaType = getJavaType(clazz);
        AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
        if (this.objectMapper.canDeserialize(javaType, causeRef)) {
            return true;
        }
        Throwable cause = causeRef.get();
        if (cause != null) {
            String msg = "Failed to evaluate deserialization for type " + javaType;
            if (logger.isDebugEnabled()) {
                logger.warn(msg, cause);
            }
            else {
                logger.warn(msg + ": " + cause);
            }
        }
        return false;
    }
    
    @Override
    protected Object readInternal(Class<? extends Object> clazz, HttpServletRequest request) 
            throws IOException, HttpMessageNotReadableException {
        
        JavaType javaType = getJavaType(clazz);
        return readJavaType(javaType, request);
    }

    private Object readJavaType(JavaType javaType, HttpServletRequest request) {
        try {
            InputStream in = request.getInputStream();
            return this.objectMapper.readValue(in, javaType);
        } catch (IOException ex) {
            throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex);
        }
    }
    
    protected JavaType getJavaType(Type type) {
        return this.objectMapper.getTypeFactory().constructType(type);
    }
    
}