package com.jsoniter.spi;

import com.jsoniter.any.Any;

import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;

public class TypeLiteral<T> {

    public enum NativeType {
        FLOAT,
        DOUBLE,
        BOOLEAN,
        BYTE,
        SHORT,
        INT,
        CHAR,
        LONG,
        BIG_DECIMAL,
        BIG_INTEGER,
        STRING,
        OBJECT,
        ANY,
    }

    public static Map<Type, NativeType> nativeTypes = new HashMap<Type, NativeType>() {{
        put(float.class, NativeType.FLOAT);
        put(Float.class, NativeType.FLOAT);
        put(double.class, NativeType.DOUBLE);
        put(Double.class, NativeType.DOUBLE);
        put(boolean.class, NativeType.BOOLEAN);
        put(Boolean.class, NativeType.BOOLEAN);
        put(byte.class, NativeType.BYTE);
        put(Byte.class, NativeType.BYTE);
        put(short.class, NativeType.SHORT);
        put(Short.class, NativeType.SHORT);
        put(int.class, NativeType.INT);
        put(Integer.class, NativeType.INT);
        put(char.class, NativeType.CHAR);
        put(Character.class, NativeType.CHAR);
        put(long.class, NativeType.LONG);
        put(Long.class, NativeType.LONG);
        put(BigDecimal.class, NativeType.BIG_DECIMAL);
        put(BigInteger.class, NativeType.BIG_INTEGER);
        put(String.class, NativeType.STRING);
        put(Object.class, NativeType.OBJECT);
        put(Any.class, NativeType.ANY);
    }};

    private volatile static Map<Type, TypeLiteral> typeLiteralCache = new HashMap<Type, TypeLiteral>();
    final Type type;
    final String decoderCacheKey;
    final String encoderCacheKey;
    final NativeType nativeType;

    /**
     * Constructs a new type literal. Derives represented class from type parameter.
     * Clients create an empty anonymous subclass. Doing so embeds the type parameter in the
     * anonymous class's type hierarchy so we can reconstitute it at runtime despite erasure.
     */
    @SuppressWarnings("unchecked")
    protected TypeLiteral() {
        this.type = getSuperclassTypeParameter(getClass());
        nativeType = nativeTypes.get(this.type);
        decoderCacheKey = generateDecoderCacheKey(type);
        encoderCacheKey = generateEncoderCacheKey(type);
    }

    public TypeLiteral(Type type, String decoderCacheKey, String encoderCacheKey) {
        this.type = type;
        nativeType = nativeTypes.get(this.type);
        this.decoderCacheKey = decoderCacheKey;
        this.encoderCacheKey = encoderCacheKey;
    }

    private static String generateDecoderCacheKey(Type type) {
        return generateCacheKey(type, "decoder.");
    }

    private static String generateEncoderCacheKey(Type type) {
        return generateCacheKey(type, "encoder.");
    }

    private static String generateCacheKey(Type type, String prefix) {
        StringBuilder decoderClassName = new StringBuilder(prefix);
        if (type instanceof Class) {
            Class clazz = (Class) type;
            if (clazz.isAnonymousClass()) {
                throw new JsonException("anonymous class not supported: " + clazz);
            }
            if (clazz.isArray()) {
                decoderClassName.append(clazz.getCanonicalName().replace("[]", "_array"));
            } else {
                // for nested class $
                decoderClassName.append(clazz.getName().replace("[]", "_array"));
            }
        } else if (type instanceof ParameterizedType) {
            try {
                ParameterizedType pType = (ParameterizedType) type;
                Class clazz = (Class) pType.getRawType();
                decoderClassName.append(clazz.getCanonicalName().replace("[]", "_array"));
                for (int i = 0; i < pType.getActualTypeArguments().length; i++) {
                    String typeName = formatTypeWithoutSpecialCharacter(pType.getActualTypeArguments()[i]);
                    decoderClassName.append('_');
                    decoderClassName.append(typeName);
                }
            } catch (Exception e) {
                throw new JsonException("failed to generate cache key for: " + type, e);
            }
        } else if (type instanceof GenericArrayType) {
            GenericArrayType gaType = (GenericArrayType) type;
            Type compType = gaType.getGenericComponentType();
            decoderClassName.append(formatTypeWithoutSpecialCharacter(compType));
            decoderClassName.append("_array");
        } else {
            throw new UnsupportedOperationException("do not know how to handle: " + type);
        }
        return decoderClassName.toString().replace("$", "_");
    }

    private static String formatTypeWithoutSpecialCharacter(Type type) {
        if (type instanceof Class) {
            Class clazz = (Class) type;
            return clazz.getCanonicalName();
        }
        if (type instanceof ParameterizedType) {
            ParameterizedType pType = (ParameterizedType) type;
            String typeName = formatTypeWithoutSpecialCharacter(pType.getRawType());
            for (Type typeArg : pType.getActualTypeArguments()) {
                typeName += "_";
                typeName += formatTypeWithoutSpecialCharacter(typeArg);
            }
            return typeName;
        }
        if (type instanceof GenericArrayType) {
            GenericArrayType gaType = (GenericArrayType) type;
            return formatTypeWithoutSpecialCharacter(gaType.getGenericComponentType()) + "_array";
        }
        throw new JsonException("unsupported type: " + type + ", of class " + type.getClass());
    }

    static Type getSuperclassTypeParameter(Class<?> subclass) {
        Type superclass = subclass.getGenericSuperclass();
        if (superclass instanceof Class) {
            throw new JsonException("Missing type parameter.");
        }
        ParameterizedType parameterized = (ParameterizedType) superclass;
        return parameterized.getActualTypeArguments()[0];
    }

    public static TypeLiteral create(Type valueType) {
        TypeLiteral typeLiteral = typeLiteralCache.get(valueType);
        if (typeLiteral != null) {
            return typeLiteral;
        }
        return createNew(valueType);
    }

    private synchronized static TypeLiteral createNew(Type valueType) {
        TypeLiteral typeLiteral = typeLiteralCache.get(valueType);
        if (typeLiteral != null) {
            return typeLiteral;
        }
        HashMap<Type, TypeLiteral> copy = new HashMap<Type, TypeLiteral>(typeLiteralCache);
        typeLiteral = new TypeLiteral(valueType,
                generateDecoderCacheKey(valueType),
                generateEncoderCacheKey(valueType));
        copy.put(valueType, typeLiteral);
        typeLiteralCache = copy;
        return typeLiteral;
    }

    public Type getType() {
        return type;
    }

    public String getDecoderCacheKey() {
        return decoderCacheKey;
    }

    public String getEncoderCacheKey() {
        return encoderCacheKey;
    }

    public NativeType getNativeType() {
        return nativeType;
    }

    @Override
    public String toString() {
        return "TypeLiteral{" +
                "type=" + type +
                ", decoderCacheKey='" + decoderCacheKey + '\'' +
                ", encoderCacheKey='" + encoderCacheKey + '\'' +
                '}';
    }
}