package com.fasterxml.jackson.datatype.guava.deser;

import java.io.IOException;

import com.fasterxml.jackson.datatype.guava.deser.util.RangeFactory;
import com.google.common.base.Preconditions;
import com.google.common.collect.BoundType;
import com.google.common.collect.Range;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.type.TypeFactory;

/**
 * Jackson deserializer for a Guava {@link Range}.
 *<p>
 * TODO: I think it would make sense to reimplement this deserializer to
 * use Delegating Deserializer, using a POJO as an intermediate form (properties
 * could be of type {@link java.lang.Object})
 * This would also also simplify the implementation a bit.
 */
public class RangeDeserializer
    extends StdDeserializer<Range<?>>
    implements ContextualDeserializer
{
    private static final long serialVersionUID = 1L;

    protected final JavaType _rangeType;

    protected final JsonDeserializer<Object> _endpointDeserializer;

    /*
    /**********************************************************
    /* Life-cycle
    /**********************************************************
     */
    
    public RangeDeserializer(JavaType rangeType) {
        this(rangeType, null);
    }

    @SuppressWarnings("unchecked")
    public RangeDeserializer(JavaType rangeType, JsonDeserializer<?> endpointDeser)
    {
        super(rangeType);
        _rangeType = rangeType;
        _endpointDeserializer = (JsonDeserializer<Object>) endpointDeser;
    }

    @Override
    public JavaType getValueType() { return _rangeType; }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
            BeanProperty property) throws JsonMappingException
    {
        if (_endpointDeserializer == null) {
            JavaType endpointType = _rangeType.containedType(0);
            if (endpointType == null) { // should this ever occur?
                endpointType = TypeFactory.unknownType();
            }
            JsonDeserializer<Object> deser = ctxt.findContextualValueDeserializer(endpointType, property);
            return new RangeDeserializer(_rangeType, deser);
        }
        return this;
    }

    /*
    /**********************************************************
    /* Actual deserialization
    /**********************************************************
     */
    
    @Override
    public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt,
            TypeDeserializer typeDeserializer)
        throws IOException, JsonProcessingException
    {
        return typeDeserializer.deserializeTypedFromObject(jp, ctxt);
    }

    @Override
    public Range<?> deserialize(JsonParser parser, DeserializationContext context)
            throws IOException, JsonProcessingException
    {
        // NOTE: either START_OBJECT _or_ FIELD_NAME fine; latter for polymorphic cases
        JsonToken t = parser.getCurrentToken();
        if (t == JsonToken.START_OBJECT) {
            t = parser.nextToken();
        }

        Comparable<?> lowerEndpoint = null;
        Comparable<?> upperEndpoint = null;
        BoundType lowerBoundType = null;
        BoundType upperBoundType = null;

        for (; t != JsonToken.END_OBJECT; t = parser.nextToken()) {
            expect(parser, JsonToken.FIELD_NAME, t);
            String fieldName = parser.getCurrentName();
            try {
                if (fieldName.equals("lowerEndpoint")) {
                    Preconditions.checkState(lowerEndpoint == null, "'lowerEndpoint' field included multiple times.");
                    parser.nextToken();
                    lowerEndpoint = deserializeEndpoint(parser, context);
                } else if (fieldName.equals("upperEndpoint")) {
                    Preconditions.checkState(upperEndpoint == null, "'upperEndpoint' field included multiple times.");
                    parser.nextToken();
                    upperEndpoint = deserializeEndpoint(parser, context);
                } else if (fieldName.equals("lowerBoundType")) {
                    Preconditions.checkState(lowerBoundType == null, "'lowerBoundType' field included multiple times.");
                    parser.nextToken();
                    lowerBoundType = deserializeBoundType(parser);
                } else if (fieldName.equals("upperBoundType")) {
                    Preconditions.checkState(upperBoundType == null, "'upperBoundType' field included multiple times.");
                    parser.nextToken();
                    upperBoundType = deserializeBoundType(parser);
                } else {
                    throw context.mappingException("Unexpected Range field: " + fieldName);
                }
            } catch (IllegalStateException e) {
                throw new JsonMappingException(e.getMessage());
            }
        }

        try {
            if ((lowerEndpoint != null) && (upperEndpoint != null)) {
                Preconditions.checkState(lowerEndpoint.getClass() == upperEndpoint.getClass(),
                                         "Endpoint types are not the same - 'lowerEndpoint' deserialized to [%s], and 'upperEndpoint' deserialized to [%s].",
                                         lowerEndpoint.getClass().getName(),
                                         upperEndpoint.getClass().getName());
                Preconditions.checkState(lowerBoundType != null, "'lowerEndpoint' field found, but not 'lowerBoundType'");
                Preconditions.checkState(upperBoundType != null, "'upperEndpoint' field found, but not 'upperBoundType'");
                return RangeFactory.range(lowerEndpoint, lowerBoundType, upperEndpoint, upperBoundType);
            }
            if (lowerEndpoint != null) {
                Preconditions.checkState(lowerBoundType != null, "'lowerEndpoint' field found, but not 'lowerBoundType'");
                return RangeFactory.downTo(lowerEndpoint, lowerBoundType);
            }
            if (upperEndpoint != null) {
                Preconditions.checkState(upperBoundType != null, "'upperEndpoint' field found, but not 'upperBoundType'");
                return RangeFactory.upTo(upperEndpoint, upperBoundType);
            }
            return RangeFactory.all();
        } catch (IllegalStateException e) {
            throw new JsonMappingException(e.getMessage());
        }
    }

    private BoundType deserializeBoundType(JsonParser parser) throws IOException
    {
        expect(parser, JsonToken.VALUE_STRING, parser.getCurrentToken());
        String name = parser.getText();
        try {
            return BoundType.valueOf(name);
        } catch (IllegalArgumentException e) {
            throw new IllegalStateException("[" + name + "] is not a valid BoundType name.");
        }
    }

    private Comparable<?> deserializeEndpoint(JsonParser parser, DeserializationContext context) throws IOException
    {
        Object obj = _endpointDeserializer.deserialize(parser, context);
        if (!(obj instanceof Comparable)) {
            throw context.mappingException(String.format(
                                 "Field [%s] deserialized to [%s], which does not implement Comparable.",
                                 parser.getCurrentName(), obj.getClass().getName()));
        }
        return (Comparable<?>) obj;
    }

    private void expect(JsonParser jp, JsonToken expected, JsonToken actual) throws JsonMappingException
    {
        if (actual != expected) {
            throw new JsonMappingException("Expecting " + expected + ", found " + actual, jp.getCurrentLocation());
        }
    }
}
