package ru.yandex.direct.http.smart.reflection;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.Nullable;

import com.google.protobuf.Message;
import com.google.protobuf.MessageLiteOrBuilder;
import com.google.protobuf.Parser;

public final class ProtoReflectionUtils {

    private static final Map<Class<?>, Parser<?>> PARSER_MAP = new ConcurrentHashMap<>();

    public static final String GET_DEFAULT_INSTANCE_METHOD_NAME = "getDefaultInstance";
    public static final String NEW_BUILDER_METHOD = "newBuilder";

    @Nullable
    public static <T extends Message> Class<T> getProtoClass(Type type) {
        Class<T> result = null;
        Class<?> rawType = Utils.getRawType(type);
        if (Iterable.class.isAssignableFrom(rawType)) {
            var parameterType = Utils.getParameterUpperBound(0, (ParameterizedType) type);
            result = getProtoClass(parameterType);
        }
        if (MessageLiteOrBuilder.class.isAssignableFrom(rawType)) {
            //noinspection unchecked
            result = (Class<T>) rawType;
        }
        return result;
    }


    @Nullable
    public static Method getBuilderMethod(Type type) {
        try {
            return ((Class<?>) type).getMethod(NEW_BUILDER_METHOD);
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

    public static Message.Builder getBuilder(Method method) {
        try {
            return (Message.Builder) method.invoke(null);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new IllegalArgumentException("Cannot obtain new builder for type", e);
        }
    }

    public static <M extends Message> Parser<M> getParserForType(Class<M> protoClass) {
        if (PARSER_MAP.containsKey(protoClass)) {
            //noinspection unchecked
            return (Parser<M>) PARSER_MAP.get(protoClass);
        }
        Parser<M> parser;
        try {
            var method = protoClass.getDeclaredMethod(GET_DEFAULT_INSTANCE_METHOD_NAME);

            //noinspection unchecked
            var instanceOfM = (M) method.invoke(null);
            //noinspection unchecked
            parser = (Parser<M>) instanceOfM.getParserForType();
        } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
            throw new IllegalArgumentException("Cannot get parser for type " + protoClass);
        }
        PARSER_MAP.put(protoClass, parser);
        return parser;
    }
}
