package com.jsoniter.spi;

import java.lang.reflect.*;
import java.util.*;

public class JsoniterSpi {

    static List<Extension> extensions = new ArrayList<Extension>();
    static Map<Class, Class> typeImpls = new HashMap<Class, Class>();
    static volatile Map<String, Encoder> encoders = new HashMap<String, Encoder>();
    static volatile Map<String, Decoder> decoders = new HashMap<String, Decoder>();
    static volatile Map<Class, Extension> objectFactories = new HashMap<Class, Extension>();

    public static void registerExtension(Extension extension) {
        extensions.add(extension);
    }

    public static List<Extension> getExtensions() {
        return Collections.unmodifiableList(extensions);
    }

    public static void registerTypeImplementation(Class superClazz, Class implClazz) {
        typeImpls.put(superClazz, implClazz);
    }

    public static Class getTypeImplementation(Class superClazz) {
        return typeImpls.get(superClazz);
    }

    public static void registerTypeDecoder(Class clazz, Decoder decoder) {
        addNewDecoder(TypeLiteral.create(clazz).getDecoderCacheKey(), decoder);
    }

    public static void registerTypeDecoder(TypeLiteral typeLiteral, Decoder decoder) {
        addNewDecoder(typeLiteral.getDecoderCacheKey(), decoder);
    }

    public static void registerPropertyDecoder(Class clazz, String field, Decoder decoder) {
        addNewDecoder(field + "@" + TypeLiteral.create(clazz).getDecoderCacheKey(), decoder);
    }

    public static void registerPropertyDecoder(TypeLiteral typeLiteral, String field, Decoder decoder) {
        addNewDecoder(field + "@" + typeLiteral.getDecoderCacheKey(), decoder);
    }

    public static void registerTypeEncoder(Class clazz, Encoder encoder) {
        addNewEncoder(TypeLiteral.create(clazz).getEncoderCacheKey(), encoder);
    }

    public static void registerTypeEncoder(TypeLiteral typeLiteral, Encoder encoder) {
        addNewEncoder(typeLiteral.getDecoderCacheKey(), encoder);
    }

    public static void registerPropertyEncoder(Class clazz, String field, Encoder encoder) {
        addNewEncoder(field + "@" + TypeLiteral.create(clazz).getEncoderCacheKey(), encoder);
    }

    public static void registerPropertyEncoder(TypeLiteral typeLiteral, String field, Encoder encoder) {
        addNewEncoder(field + "@" + typeLiteral.getDecoderCacheKey(), encoder);
    }

    public static Decoder getDecoder(String cacheKey) {
        return decoders.get(cacheKey);
    }

    public synchronized static void addNewDecoder(String cacheKey, Decoder decoder) {
        HashMap<String, Decoder> newCache = new HashMap<String, Decoder>(decoders);
        newCache.put(cacheKey, decoder);
        decoders = newCache;
    }

    public static Encoder getEncoder(String cacheKey) {
        return encoders.get(cacheKey);
    }

    public synchronized static void addNewEncoder(String cacheKey, Encoder encoder) {
        HashMap<String, Encoder> newCache = new HashMap<String, Encoder>(encoders);
        newCache.put(cacheKey, encoder);
        encoders = newCache;
    }

    public static boolean canCreate(Class clazz) {
        if (objectFactories.containsKey(clazz)) {
            return true;
        }
        for (Extension extension : extensions) {
            if (extension.canCreate(clazz)) {
                addObjectFactory(clazz, extension);
                return true;
            }
        }
        return false;
    }

    public static Object create(Class clazz) {
        return objectFactories.get(clazz).create(clazz);
    }

    private synchronized static void addObjectFactory(Class clazz, Extension extension) {
        HashMap<Class, Extension> copy = new HashMap<Class, Extension>(objectFactories);
        copy.put(clazz, extension);
        objectFactories = copy;
    }

    public static ClassDescriptor getDecodingClassDescriptor(Class clazz, boolean includingPrivate) {
        Map<String, Type> lookup = collectTypeVariableLookup(clazz);
        ClassDescriptor desc = new ClassDescriptor();
        desc.clazz = clazz;
        desc.lookup = lookup;
        desc.ctor = getCtor(clazz);
        desc.fields = getFields(lookup, clazz, includingPrivate);
        desc.setters = getSetters(lookup, clazz, includingPrivate);
        desc.wrappers = new ArrayList<WrapperDescriptor>();
        desc.unWrappers = new ArrayList<Method>();
        for (Extension extension : extensions) {
            extension.updateClassDescriptor(desc);
        }
        for (Binding field : desc.fields) {
            if (field.valueType instanceof Class) {
                Class valueClazz = (Class) field.valueType;
                if (valueClazz.isArray()) {
                    field.valueCanReuse = false;
                    continue;
                }
            }
            field.valueCanReuse = field.valueTypeLiteral.nativeType == null;
        }
        decodingDeduplicate(desc);
        if (includingPrivate) {
            if (desc.ctor.ctor != null) {
                desc.ctor.ctor.setAccessible(true);
            }
            if (desc.ctor.staticFactory != null) {
                desc.ctor.staticFactory.setAccessible(true);
            }
            for (WrapperDescriptor setter : desc.wrappers) {
                setter.method.setAccessible(true);
            }
        }
        for (Binding binding : desc.allDecoderBindings()) {
            if (binding.fromNames == null) {
                binding.fromNames = new String[]{binding.name};
            }
            if (binding.field != null && includingPrivate) {
                binding.field.setAccessible(true);
            }
            if (binding.method != null && includingPrivate) {
                binding.method.setAccessible(true);
            }
            if (binding.decoder != null) {
                JsoniterSpi.addNewDecoder(binding.decoderCacheKey(), binding.decoder);
            }
        }
        return desc;
    }

    public static ClassDescriptor getEncodingClassDescriptor(Class clazz, boolean includingPrivate) {
        Map<String, Type> lookup = collectTypeVariableLookup(clazz);
        ClassDescriptor desc = new ClassDescriptor();
        desc.clazz = clazz;
        desc.lookup = lookup;
        desc.fields = getFields(lookup, clazz, includingPrivate);
        desc.getters = getGetters(lookup, clazz, includingPrivate);
        desc.wrappers = new ArrayList<WrapperDescriptor>();
        desc.unWrappers = new ArrayList<Method>();
        for (Extension extension : extensions) {
            extension.updateClassDescriptor(desc);
        }
        encodingDeduplicate(desc);
        for (Binding binding : desc.allEncoderBindings()) {
            if (binding.toNames == null) {
                binding.toNames = new String[]{binding.name};
            }
            if (binding.field != null && includingPrivate) {
                binding.field.setAccessible(true);
            }
            if (binding.method != null && includingPrivate) {
                binding.method.setAccessible(true);
            }
            if (binding.encoder != null) {
                JsoniterSpi.addNewEncoder(binding.encoderCacheKey(), binding.encoder);
            }
        }
        return desc;
    }

    private static void decodingDeduplicate(ClassDescriptor desc) {
        HashMap<String, Binding> byName = new HashMap<String, Binding>();
        for (Binding field : desc.fields) {
            if (byName.containsKey(field.name)) {
                throw new JsonException("field name conflict: " + field.name);
            }
            byName.put(field.name, field);
        }
        for (Binding setter : desc.setters) {
            Binding existing = byName.get(setter.name);
            if (existing == null) {
                byName.put(setter.name, setter);
                continue;
            }
            if (desc.fields.remove(existing)) {
                continue;
            }
            throw new JsonException("setter name conflict: " + setter.name);
        }
        for (WrapperDescriptor wrapper : desc.wrappers) {
            for (Binding param : wrapper.parameters) {
                Binding existing = byName.get(param.name);
                if (existing == null) {
                    byName.put(param.name, param);
                    continue;
                }
                if (desc.fields.remove(existing)) {
                    continue;
                }
                if (desc.setters.remove(existing)) {
                    continue;
                }
                throw new JsonException("wrapper parameter name conflict: " + param.name);
            }
        }
        for (Binding param : desc.ctor.parameters) {
            Binding existing = byName.get(param.name);
            if (existing == null) {
                byName.put(param.name, param);
                continue;
            }
            if (desc.fields.remove(existing)) {
                continue;
            }
            if (desc.setters.remove(existing)) {
                continue;
            }
            throw new JsonException("ctor parameter name conflict: " + param.name);
        }
    }

    private static void encodingDeduplicate(ClassDescriptor desc) {
        HashMap<String, Binding> byName = new HashMap<String, Binding>();
        for (Binding field : desc.fields) {
            if (byName.containsKey(field.name)) {
                throw new JsonException("field name conflict: " + field.name);
            }
            byName.put(field.name, field);
        }
        for (Binding getter : desc.getters) {
            Binding existing = byName.get(getter.name);
            if (existing == null) {
                byName.put(getter.name, getter);
                continue;
            }
            if (desc.fields.remove(existing)) {
                continue;
            }
            throw new JsonException("getter name conflict: " + getter.name);
        }
    }

    private static ConstructorDescriptor getCtor(Class clazz) {
        ConstructorDescriptor cctor = new ConstructorDescriptor();
        if (canCreate(clazz)) {
            cctor.objectFactory = objectFactories.get(clazz);
            return cctor;
        }
        try {
            cctor.ctor = clazz.getDeclaredConstructor();
        } catch (Exception e) {
            cctor.ctor = null;
        }
        return cctor;
    }

    private static List<Binding> getFields(Map<String, Type> lookup, Class clazz, boolean includingPrivate) {
        ArrayList<Binding> bindings = new ArrayList<Binding>();
        for (Field field : getAllFields(clazz, includingPrivate)) {
            if (Modifier.isStatic(field.getModifiers())) {
                continue;
            }
            if (Modifier.isTransient(field.getModifiers())) {
                continue;
            }
            if (!includingPrivate && !Modifier.isPublic(field.getType().getModifiers())) {
                continue;
            }
            if (includingPrivate) {
                field.setAccessible(true);
            }
            Binding binding = createBindingFromField(lookup, clazz, field);
            bindings.add(binding);
        }
        return bindings;
    }

    private static Binding createBindingFromField(Map<String, Type> lookup, Class clazz, Field field) {
        try {
            Binding binding = new Binding(clazz, lookup, field.getGenericType());
            binding.fromNames = new String[]{field.getName()};
            binding.name = field.getName();
            binding.annotations = field.getAnnotations();
            binding.field = field;
            return binding;
        } catch (Exception e) {
            throw new JsonException("failed to create binding for field: " + field, e);
        }
    }

    private static List<Field> getAllFields(Class clazz, boolean includingPrivate) {
        List<Field> allFields = Arrays.asList(clazz.getFields());
        if (includingPrivate) {
            allFields = new ArrayList<Field>();
            Class current = clazz;
            while (current != null) {
                allFields.addAll(Arrays.asList(current.getDeclaredFields()));
                current = current.getSuperclass();
            }
        }
        return allFields;
    }

    private static List<Binding> getSetters(Map<String, Type> lookup, Class clazz, boolean includingPrivate) {
        ArrayList<Binding> setters = new ArrayList<Binding>();
        for (Method method : getAllMethods(clazz, includingPrivate)) {
            if (Modifier.isStatic(method.getModifiers())) {
                continue;
            }
            String methodName = method.getName();
            if (methodName.length() < 4) {
                continue;
            }
            if (!methodName.startsWith("set")) {
                continue;
            }
            Type[] paramTypes = method.getGenericParameterTypes();
            if (paramTypes.length != 1) {
                continue;
            }
            if (!includingPrivate && !Modifier.isPublic(method.getParameterTypes()[0].getModifiers())) {
                continue;
            }
            if (includingPrivate) {
                method.setAccessible(true);
            }
            try {
                String fromName = translateSetterName(methodName);
                Binding binding = new Binding(clazz, lookup, paramTypes[0]);
                binding.fromNames = new String[]{fromName};
                binding.name = fromName;
                binding.method = method;
                binding.annotations = method.getAnnotations();
                setters.add(binding);
            } catch (Exception e) {
                throw new JsonException("failed to create binding from setter: " + method, e);
            }
        }
        return setters;
    }

    private static List<Method> getAllMethods(Class clazz, boolean includingPrivate) {
        List<Method> allMethods = Arrays.asList(clazz.getMethods());
        if (includingPrivate) {
            allMethods = new ArrayList<Method>();
            Class current = clazz;
            while (current != null) {
                allMethods.addAll(Arrays.asList(current.getDeclaredMethods()));
                current = current.getSuperclass();
            }
        }
        return allMethods;
    }

    private static String translateSetterName(String methodName) {
        if (!methodName.startsWith("set")) {
            return null;
        }
        String fromName = methodName.substring("set".length());
        char[] fromNameChars = fromName.toCharArray();
        fromNameChars[0] = Character.toLowerCase(fromNameChars[0]);
        fromName = new String(fromNameChars);
        return fromName;
    }

    private static List<Binding> getGetters(Map<String, Type> lookup, Class clazz, boolean includingPrivate) {
        ArrayList<Binding> getters = new ArrayList<Binding>();
        for (Method method : getAllMethods(clazz, includingPrivate)) {
            if (Modifier.isStatic(method.getModifiers())) {
                continue;
            }
            String methodName = method.getName();
            if ("getClass".equals(methodName)) {
                continue;
            }
            if (methodName.length() < 4) {
                continue;
            }
            if (!methodName.startsWith("get")) {
                continue;
            }
            if (method.getGenericParameterTypes().length != 0) {
                continue;
            }
            String toName = methodName.substring("get".length());
            char[] fromNameChars = toName.toCharArray();
            fromNameChars[0] = Character.toLowerCase(fromNameChars[0]);
            toName = new String(fromNameChars);
            Binding getter = new Binding(clazz, lookup, method.getGenericReturnType());
            getter.toNames = new String[]{toName};
            getter.name = toName;
            getter.method = method;
            getter.annotations = method.getAnnotations();
            getters.add(getter);
        }
        return getters;
    }

    public static void dump() {
        for (String cacheKey : decoders.keySet()) {
            System.err.println(cacheKey);
        }
        for (String cacheKey : encoders.keySet()) {
            System.err.println(cacheKey);
        }
    }

    private static Map<String, Type> collectTypeVariableLookup(Type type) {
        HashMap<String, Type> vars = new HashMap<String, Type>();
        if (null == type) {
            return vars;
        }
        if (type instanceof ParameterizedType) {
            ParameterizedType pType = (ParameterizedType) type;
            Type[] actualTypeArguments = pType.getActualTypeArguments();
            Class clazz = (Class) pType.getRawType();
            for (int i = 0; i < clazz.getTypeParameters().length; i++) {
                TypeVariable variable = clazz.getTypeParameters()[i];
                vars.put(variable.getName() + "@" + clazz.getCanonicalName(), actualTypeArguments[i]);
            }
            vars.putAll(collectTypeVariableLookup(clazz.getGenericSuperclass()));
            return vars;
        }
        if (type instanceof Class) {
            Class clazz = (Class) type;
            vars.putAll(collectTypeVariableLookup(clazz.getGenericSuperclass()));
            return vars;
        }
        throw new JsonException("unexpected type: " + type);
    }
}
