package com.fasterxml.jackson.databind.ser.impl;

import java.io.IOException;
import java.util.Map;
import java.util.Map.Entry;

import com.fasterxml.jackson.annotation.JsonInclude;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.type.WritableTypeId;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.ContainerSerializer;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.util.ArrayBuilders;
import com.fasterxml.jackson.databind.util.BeanUtil;

/**
 * @since 2.5
 */
@SuppressWarnings("serial")
@JacksonStdImpl
public class MapEntrySerializer
    extends ContainerSerializer<Map.Entry<?,?>>
    implements ContextualSerializer
{
    /**
     * @since 2.9
     */
    public final static Object MARKER_FOR_EMPTY = JsonInclude.Include.NON_EMPTY;

    /**
     * Map-valued property being serialized with this instance
     */
    protected final BeanProperty _property;

    /**
     * Whether static types should be used for serialization of values
     * or not (if not, dynamic runtime type is used)
     */
    protected final boolean _valueTypeIsStatic;

    protected final JavaType _entryType, _keyType, _valueType;

    /*
    /**********************************************************
    /* Serializers used
    /**********************************************************
     */

    /**
     * Key serializer to use, if it can be statically determined
     */
    protected JsonSerializer<Object> _keySerializer;

    /**
     * Value serializer to use, if it can be statically determined
     */
    protected JsonSerializer<Object> _valueSerializer;

    /**
     * Type identifier serializer used for values, if any.
     */
    protected final TypeSerializer _valueTypeSerializer;

    /**
     * If value type cannot be statically determined, mapping from
     * runtime value types to serializers are stored in this object.
     */
    protected PropertySerializerMap _dynamicValueSerializers;

    /*
    /**********************************************************
    /* Config settings, filtering
    /**********************************************************
     */

    /**
     * Value that indicates suppression mechanism to use for <b>values contained</b>;
     * either "filter" (of which <code>equals()</code> is called), or marker
     * value of {@link #MARKER_FOR_EMPTY}, or null to indicate no filtering for
     * non-null values.
     * Note that inclusion value for Map instance itself is handled by caller (POJO
     * property that refers to the Map value).
     * 
     * @since 2.5
     */
    protected final Object _suppressableValue;

    /**
     * Flag that indicates what to do with `null` values, distinct from
     * handling of {@link #_suppressableValue}
     *
     * @since 2.9
     */
    protected final boolean _suppressNulls;

    /*
    /**********************************************************
    /* Construction, initialization
    /**********************************************************
     */

    public MapEntrySerializer(JavaType type, JavaType keyType, JavaType valueType,
            boolean staticTyping, TypeSerializer vts,
            BeanProperty property)
    {
        super(type);
        _entryType = type;
        _keyType = keyType;
        _valueType = valueType;
        _valueTypeIsStatic = staticTyping;
        _valueTypeSerializer = vts;
        _property = property;
        _dynamicValueSerializers = PropertySerializerMap.emptyForProperties();
        _suppressableValue = null;
        _suppressNulls = false;
    }

    @Deprecated // since 2.9
    protected MapEntrySerializer(MapEntrySerializer src, BeanProperty property,
            TypeSerializer vts,
            JsonSerializer<?> keySer, JsonSerializer<?> valueSer)
    {
        this(src, property, vts, keySer, valueSer,
                src._suppressableValue, src._suppressNulls);
    }
        
    @SuppressWarnings("unchecked")
    protected MapEntrySerializer(MapEntrySerializer src, BeanProperty property,
            TypeSerializer vts,
            JsonSerializer<?> keySer, JsonSerializer<?> valueSer,
            Object suppressableValue, boolean suppressNulls)
    {
        super(Map.class, false);
        _entryType = src._entryType;
        _keyType = src._keyType;
        _valueType = src._valueType;
        _valueTypeIsStatic = src._valueTypeIsStatic;
        _valueTypeSerializer = src._valueTypeSerializer;
        _keySerializer = (JsonSerializer<Object>) keySer;
        _valueSerializer = (JsonSerializer<Object>) valueSer;
        // [databind#2181]: may not be safe to reuse, start from empty
        _dynamicValueSerializers = PropertySerializerMap.emptyForProperties();
        _property = src._property;
        _suppressableValue = suppressableValue;
        _suppressNulls = suppressNulls;
    }

    @Override
    public ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
        return new MapEntrySerializer(this, _property, vts, _keySerializer, _valueSerializer,
                _suppressableValue, _suppressNulls);
    }

    /**
     * @since 2.9
     */
    public MapEntrySerializer withResolved(BeanProperty property,
            JsonSerializer<?> keySerializer, JsonSerializer<?> valueSerializer,
            Object suppressableValue, boolean suppressNulls) {
        return new MapEntrySerializer(this, property, _valueTypeSerializer,
                keySerializer, valueSerializer, suppressableValue, suppressNulls);
    }

    /**
     * @since 2.9
     */
    public MapEntrySerializer withContentInclusion(Object suppressableValue,
            boolean suppressNulls) {
        if ((_suppressableValue == suppressableValue)
                && (_suppressNulls == suppressNulls)) {
            return this;
        }
        return new MapEntrySerializer(this, _property, _valueTypeSerializer,
                _keySerializer, _valueSerializer, suppressableValue, suppressNulls);
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider provider,
            BeanProperty property) throws JsonMappingException
    {
        JsonSerializer<?> ser = null;
        JsonSerializer<?> keySer = null;
        final AnnotationIntrospector intr = provider.getAnnotationIntrospector();
        final AnnotatedMember propertyAcc = (property == null) ? null : property.getMember();

        // First: if we have a property, may have property-annotation overrides
        if (propertyAcc != null && intr != null) {
            Object serDef = intr.findKeySerializer(propertyAcc);
            if (serDef != null) {
                keySer = provider.serializerInstance(propertyAcc, serDef);
            }
            serDef = intr.findContentSerializer(propertyAcc);
            if (serDef != null) {
                ser = provider.serializerInstance(propertyAcc, serDef);
            }
        }
        if (ser == null) {
            ser = _valueSerializer;
        }
        // [databind#124]: May have a content converter
        ser = findContextualConvertingSerializer(provider, property, ser);
        if (ser == null) {
            // 30-Sep-2012, tatu: One more thing -- if explicit content type is annotated,
            //   we can consider it a static case as well.
            // 20-Aug-2013, tatu: Need to avoid trying to access serializer for java.lang.Object tho
            if (_valueTypeIsStatic && !_valueType.isJavaLangObject()) {
                ser = provider.findContentValueSerializer(_valueType, property);
            }
        }
        if (keySer == null) {
            keySer = _keySerializer;
        }
        if (keySer == null) {
            keySer = provider.findKeySerializer(_keyType, property);
        } else {
            keySer = provider.handleSecondaryContextualization(keySer, property);
        }

        Object valueToSuppress = _suppressableValue;
        boolean suppressNulls = _suppressNulls;
        if (property != null) {
            JsonInclude.Value inclV = property.findPropertyInclusion(provider.getConfig(), null);
            if (inclV != null) {
                JsonInclude.Include incl = inclV.getContentInclusion();
                if (incl != JsonInclude.Include.USE_DEFAULTS) {
                    switch (incl) {
                    case NON_DEFAULT:
                        valueToSuppress = BeanUtil.getDefaultValue(_valueType);
                        suppressNulls = true;
                        if (valueToSuppress != null) {
                            if (valueToSuppress.getClass().isArray()) {
                                valueToSuppress = ArrayBuilders.getArrayComparator(valueToSuppress);
                            }
                        }
                        break;
                    case NON_ABSENT:
                        suppressNulls = true;
                        valueToSuppress = _valueType.isReferenceType() ? MARKER_FOR_EMPTY : null;
                        break;
                    case NON_EMPTY:
                        suppressNulls = true;
                        valueToSuppress = MARKER_FOR_EMPTY;
                        break;
                    case CUSTOM:
                        valueToSuppress = provider.includeFilterInstance(null, inclV.getContentFilter());
                        if (valueToSuppress == null) { // is this legal?
                            suppressNulls = true;
                        } else {
                            suppressNulls = provider.includeFilterSuppressNulls(valueToSuppress);
                        }
                        break;
                    case NON_NULL:
                        valueToSuppress = null;
                        suppressNulls = true;
                        break;
                    case ALWAYS: // default
                    default:
                        valueToSuppress = null;
                        // 30-Sep-2016, tatu: Should not need to check global flags here,
                        //   if inclusion forced to be ALWAYS
                        suppressNulls = false;
                        break;
                    }
                }
            }
        }
        
        MapEntrySerializer mser = withResolved(property, keySer, ser,
                valueToSuppress, suppressNulls);
        // but note: no (full) filtering or sorting (unlike Maps)
        return mser;
    }

    /*
    /**********************************************************
    /* Accessors
    /**********************************************************
     */

    @Override
    public JavaType getContentType() {
        return _valueType;
    }

    @Override
    public JsonSerializer<?> getContentSerializer() {
        return _valueSerializer;
    }

    @Override
    public boolean hasSingleElement(Map.Entry<?,?> value) {
        return true;
    }

    @Override
    public boolean isEmpty(SerializerProvider prov, Entry<?, ?> entry)
    {
        Object value = entry.getValue();
        if (value == null) {
            return _suppressNulls;
        }
        if (_suppressableValue == null) {
            return false;
        }
        JsonSerializer<Object> valueSer = _valueSerializer;
        if (valueSer == null) {
            // Let's not worry about generic types here, actually;
            // unlikely to make any difference, but does add significant overhead
            Class<?> cc = value.getClass();
            valueSer = _dynamicValueSerializers.serializerFor(cc);
            if (valueSer == null) {
                try {
                    valueSer = _findAndAddDynamic(_dynamicValueSerializers, cc, prov);
                } catch (JsonMappingException e) { // Ugh... cannot just throw as-is, so...
                    return false;
                }
            }
        }
        if (_suppressableValue == MARKER_FOR_EMPTY) {
            return valueSer.isEmpty(prov, value);
        }
        return _suppressableValue.equals(value);
    }

    /*
    /**********************************************************
    /* Serialization methods
    /**********************************************************
     */

    @Override
    public void serialize(Map.Entry<?, ?> value, JsonGenerator gen, SerializerProvider provider)
        throws IOException
    {
        gen.writeStartObject(value);
        serializeDynamic(value, gen, provider);
        gen.writeEndObject();
    }

    @Override
    public void serializeWithType(Map.Entry<?, ?> value, JsonGenerator g,
            SerializerProvider provider, TypeSerializer typeSer) throws IOException
    {
        // [databind#631]: Assign current value, to be accessible by custom serializers
        g.setCurrentValue(value);
        WritableTypeId typeIdDef = typeSer.writeTypePrefix(g,
                typeSer.typeId(value, JsonToken.START_OBJECT));
        serializeDynamic(value, g, provider);
        typeSer.writeTypeSuffix(g, typeIdDef);
    }

    protected void serializeDynamic(Map.Entry<?, ?> value, JsonGenerator gen,
            SerializerProvider provider)
        throws IOException
    {
        final TypeSerializer vts = _valueTypeSerializer;
        final Object keyElem = value.getKey();

        JsonSerializer<Object> keySerializer;
        if (keyElem == null) {
            keySerializer = provider.findNullKeySerializer(_keyType, _property);
        } else {
            keySerializer = _keySerializer;
        }
        // or by value; nulls often suppressed
        final Object valueElem = value.getValue();
        JsonSerializer<Object> valueSer;
        // And then value
        if (valueElem == null) {
            if (_suppressNulls) {
                return;
            }
            valueSer = provider.getDefaultNullValueSerializer();
        } else {
            valueSer = _valueSerializer;
            if (valueSer == null) {
                Class<?> cc = valueElem.getClass();
                valueSer = _dynamicValueSerializers.serializerFor(cc);
                if (valueSer == null) {
                    if (_valueType.hasGenericTypes()) {
                        valueSer = _findAndAddDynamic(_dynamicValueSerializers,
                                provider.constructSpecializedType(_valueType, cc), provider);
                    } else {
                        valueSer = _findAndAddDynamic(_dynamicValueSerializers, cc, provider);
                    }
                }
            }
            // also may need to skip non-empty values:
            if (_suppressableValue != null) {
                if (_suppressableValue == MARKER_FOR_EMPTY) {
                    if (valueSer.isEmpty(provider, valueElem)) {
                        return;
                    }
                } if (_suppressableValue.equals(valueElem)) {
                    return;
                }
            }
        }
        keySerializer.serialize(keyElem, gen, provider);
        try {
            if (vts == null) {
                valueSer.serialize(valueElem, gen, provider);
            } else {
                valueSer.serializeWithType(valueElem, gen, provider, vts);
            }
        } catch (Exception e) {
            String keyDesc = ""+keyElem;
            wrapAndThrow(provider, e, value, keyDesc);
        }
    }

    /*
    /**********************************************************
    /* Internal helper methods
    /**********************************************************
     */
    
    protected final JsonSerializer<Object> _findAndAddDynamic(PropertySerializerMap map,
            Class<?> type, SerializerProvider provider) throws JsonMappingException
    {
        PropertySerializerMap.SerializerAndMapResult result = map.findAndAddSecondarySerializer(type, provider, _property);
        if (map != result.map) {
            _dynamicValueSerializers = result.map;
        }
        return result.serializer;
    }

    protected final JsonSerializer<Object> _findAndAddDynamic(PropertySerializerMap map,
            JavaType type, SerializerProvider provider) throws JsonMappingException
    {
        PropertySerializerMap.SerializerAndMapResult result = map.findAndAddSecondarySerializer(type, provider, _property);
        if (map != result.map) {
            _dynamicValueSerializers = result.map;
        }
        return result.serializer;
    }

}