package com.fasterxml.jackson.databind.module;

import java.io.File;
import java.io.IOException;
import java.util.*;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.type.TypeReference;

import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.fasterxml.jackson.databind.node.ObjectNode;

@SuppressWarnings("serial")
public class TestCustomEnumKeyDeserializer extends BaseMapTest
{
    @JsonSerialize(using = TestEnumSerializer.class, keyUsing = TestEnumKeySerializer.class)
    @JsonDeserialize(using = TestEnumDeserializer.class, keyUsing = TestEnumKeyDeserializer.class)
    public enum TestEnumMixin { }

    enum KeyEnum {
        replacements,
        rootDirectory,
        licenseString
    }

    enum TestEnum {
        RED("red"),
        GREEN("green");

        private final String code;

        TestEnum(String code) {
            this.code = code;
        }

        public static TestEnum lookup(String lower) {
            for (TestEnum item : values()) {
                if (item.code().equals(lower)) {
                    return item;
                }
            }
            throw new IllegalArgumentException("Invalid code " + lower);
        }

        public String code() {
            return code;
        }
    }

    static class TestEnumSerializer extends JsonSerializer<TestEnum> {
        @Override
        public void serialize(TestEnum languageCode, JsonGenerator g, SerializerProvider serializerProvider) throws IOException {
            g.writeString(languageCode.code());
        }

        @Override
        public Class<TestEnum> handledType() {
            return TestEnum.class;
        }
    }

    static class TestEnumKeyDeserializer extends KeyDeserializer {
        @Override
        public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException {
            try {
                return TestEnum.lookup(key);
            } catch (IllegalArgumentException e) {
                return ctxt.handleWeirdKey(TestEnum.class, key, "Unknown code");
            }
        }
    }

    static class TestEnumDeserializer extends StdDeserializer<TestEnum> {
        public TestEnumDeserializer() {
            super(TestEnum.class);
        }

        @Override
        public TestEnum deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
            String code = p.getText();
            try {
                return TestEnum.lookup(code);
            } catch (IllegalArgumentException e) {
                throw InvalidFormatException.from(p, "Undefined ISO-639 language code",
                        code, TestEnum.class);
            }
        }
    }

    static class TestEnumKeySerializer extends JsonSerializer<TestEnum> {
        @Override
        public void serialize(TestEnum test, JsonGenerator g, SerializerProvider serializerProvider) throws IOException {
            g.writeFieldName(test.code());
        }

        @Override
        public Class<TestEnum> handledType() {
            return TestEnum.class;
        }
    }

    static class Bean {
        private File rootDirectory;
        private String licenseString;
        private Map<TestEnum, Map<String, String>> replacements;

        public File getRootDirectory() {
            return rootDirectory;
        }

        public void setRootDirectory(File rootDirectory) {
            this.rootDirectory = rootDirectory;
        }

        public String getLicenseString() {
            return licenseString;
        }

        public void setLicenseString(String licenseString) {
            this.licenseString = licenseString;
        }

        public Map<TestEnum, Map<String, String>> getReplacements() {
            return replacements;
        }

        public void setReplacements(Map<TestEnum, Map<String, String>> replacements) {
            this.replacements = replacements;
        }
    }

    static class TestEnumModule extends SimpleModule {
        public TestEnumModule() {
            super(Version.unknownVersion());
        }

        @Override
        public void setupModule(SetupContext context) {
            context.setMixInAnnotations(TestEnum.class, TestEnumMixin.class);
            SimpleSerializers keySerializers = new SimpleSerializers();
            keySerializers.addSerializer(new TestEnumKeySerializer());
            context.addKeySerializers(keySerializers);
        }
    }

    // for [databind#1441]

    enum SuperTypeEnum {
        FOO;
    }

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type", defaultImpl = SuperType.class)
    static class SuperType {
        public Map<SuperTypeEnum, String> someMap;
    }

    /*
    /**********************************************************
    /* Test methods
    /**********************************************************
     */
    
    // Test passing with the fix
    public void testWithEnumKeys() throws Exception {
        ObjectMapper plainObjectMapper = new ObjectMapper();
        JsonNode tree = plainObjectMapper.readTree(aposToQuotes("{'red' : [ 'a', 'b']}"));

        ObjectMapper fancyObjectMapper = new ObjectMapper().registerModule(new TestEnumModule());

        // this line is might throw with Jackson 2.6.2.
        Map<TestEnum, Set<String>> map = fancyObjectMapper.convertValue(tree,
                new TypeReference<Map<TestEnum, Set<String>>>() { } );
        assertNotNull(map);
    }

    // and another still failing
    // NOTE: temporarily named as non-test to ignore it; JsonIgnore doesn't work for some reason
//    public void testWithTree749() throws Exception
    public void withTree749() throws Exception
    {
        ObjectMapper mapper = new ObjectMapper().registerModule(new TestEnumModule());

        Map<KeyEnum, Object> inputMap = new LinkedHashMap<KeyEnum, Object>();
        Map<TestEnum, Map<String, String>> replacements = new LinkedHashMap<TestEnum, Map<String, String>>();
        Map<String, String> reps = new LinkedHashMap<String, String>();
        reps.put("1", "one");
        replacements.put(TestEnum.GREEN, reps);
        inputMap.put(KeyEnum.replacements, replacements);

        JsonNode tree = mapper.valueToTree(inputMap);
        ObjectNode ob = (ObjectNode) tree;

        JsonNode inner = ob.get("replacements");
        String firstFieldName = inner.fieldNames().next();
        assertEquals("green", firstFieldName);
    }

    // [databind#1441]
    public void testCustomEnumKeySerializerWithPolymorphic() throws IOException
    {
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addDeserializer(SuperTypeEnum.class, new JsonDeserializer<SuperTypeEnum>() {
            @Override
            public SuperTypeEnum deserialize(JsonParser p, DeserializationContext deserializationContext)
                    throws IOException
            {
                return SuperTypeEnum.valueOf(p.getText());
            }
        });
        ObjectMapper mapper = new ObjectMapper()
                .registerModule(simpleModule);

        SuperType superType = mapper.readValue("{\"someMap\": {\"FOO\": \"bar\"}}",
                SuperType.class);
        assertEquals("Deserialized someMap.FOO should equal bar", "bar",
                superType.someMap.get(SuperTypeEnum.FOO));
    }

    // [databind#1445]
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public void testCustomEnumValueAndKeyViaModifier() throws IOException
    {
        SimpleModule module = new SimpleModule();
        module.setDeserializerModifier(new BeanDeserializerModifier() {        
            @Override
            public JsonDeserializer<Enum> modifyEnumDeserializer(DeserializationConfig config,
                    final JavaType type, BeanDescription beanDesc,
                    final JsonDeserializer<?> deserializer) {
                return new JsonDeserializer<Enum>() {
                    @Override
                    public Enum deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
                        Class<? extends Enum> rawClass = (Class<Enum<?>>) type.getRawClass();
                        final String str = p.getValueAsString().toLowerCase();
                        return KeyEnum.valueOf(rawClass, str);
                    }
                };
            }

            @Override
            public KeyDeserializer modifyKeyDeserializer(DeserializationConfig config,
                    final JavaType type, KeyDeserializer deserializer)
            {
                if (!type.isEnumType()) {
                    return deserializer;
                }
                return new KeyDeserializer() {
                    @Override
                    public Object deserializeKey(String key, DeserializationContext ctxt)
                            throws IOException
                    {
                        Class<? extends Enum> rawClass = (Class<Enum<?>>) type.getRawClass();
                        return Enum.valueOf(rawClass, key.toLowerCase());
                    }
                };
            }
        });
        ObjectMapper mapper = new ObjectMapper()
                .registerModule(module);

        // First, enum value as is
        KeyEnum key = mapper.readValue(quote(KeyEnum.replacements.name().toUpperCase()),
                KeyEnum.class);
        assertSame(KeyEnum.replacements, key);

        // and then as key
        EnumMap<KeyEnum,String> map = mapper.readValue(
                aposToQuotes("{'REPlaceMENTS':'foobar'}"),
                new TypeReference<EnumMap<KeyEnum,String>>() { });
        assertEquals(1, map.size());
        assertSame(KeyEnum.replacements, map.keySet().iterator().next());
    }
}
