package com.tngtech.archunit.testutil.syntax;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Map;

import com.google.common.collect.ImmutableMap;
import com.google.common.reflect.TypeToken;

import static com.google.common.base.Preconditions.checkState;
import static java.util.Collections.emptyMap;

class PropagatedType {
    private final Type type;
    private final Map<String, Type> typeVariables;

    PropagatedType(Type type) {
        this(type, Context.empty());
    }

    private PropagatedType(Type type, Context context) {
        this.type = type;
        typeVariables = getTypeVariables(type, context);
    }

    private Map<String, Type> getTypeVariables(Type type, Context context) {
        if (type instanceof Class<?>) {
            return resolveTypeVariables((Class<?>) type);
        } else if (type instanceof ParameterizedType) {
            return resolveTypeVariables((ParameterizedType) type, context);
        } else {
            return emptyMap();
        }
    }

    private Map<String, Type> resolveTypeVariables(Class<?> clazz) {
        ImmutableMap.Builder<String, Type> result = ImmutableMap.builder();
        for (TypeVariable<? extends Class<?>> typeParameter : clazz.getTypeParameters()) {
            result.put(typeParameter.getName(), getOnlyBound(typeParameter));
        }
        return result.build();
    }

    private Type getOnlyBound(TypeVariable<? extends Class<?>> typeParameter) {
        checkState(typeParameter.getBounds().length == 1,
                "Can't deal with multiple bounds yet -> %s<%s>", typeParameter.getGenericDeclaration(), typeParameter);
        return typeParameter.getBounds()[0];
    }

    private Map<String, Type> resolveTypeVariables(ParameterizedType parameterizedType, Context context) {
        TypeVariable<? extends Class<?>>[] typeParameters = ((Class<?>) parameterizedType.getRawType()).getTypeParameters();
        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
        checkState(typeParameters.length == actualTypeArguments.length,
                "Number of type parameters %d and actual type arguments %d do not match for %s",
                typeParameters.length, actualTypeArguments.length, parameterizedType);

        ImmutableMap.Builder<String, Type> result = ImmutableMap.builder();
        for (int i = 0; i < typeParameters.length; i++) {
            Type resolvedType = resolveMoreSpecificType(typeParameters[i], actualTypeArguments[i], context);
            result.put(typeParameters[i].getName(), resolvedType);
        }
        return result.build();
    }

    private Type resolveMoreSpecificType(TypeVariable<? extends Class<?>> typeVariable, Type actualTypeArgument, Context context) {
        Class<?> bound = TypeToken.of(getOnlyBound(typeVariable)).getRawType();
        Class<?> actual = TypeToken.of(context.resolve(actualTypeArgument)).getRawType();
        return bound.isAssignableFrom(actual) ? actual : bound;
    }

    Class<?> getRawType() {
        return TypeToken.of(type).getRawType();
    }

    PropagatedType resolveType(Type type) {
        if (type instanceof TypeVariable<?>) {
            return resolveTypeVariable((TypeVariable<?>) type);
        }
        return new PropagatedType(type, Context.forType(this));
    }

    private PropagatedType resolveTypeVariable(TypeVariable<?> typeVariable) {
        checkState(typeVariables.containsKey(typeVariable.getName()),
                "Tried to resolve unknown %s %s", TypeVariable.class.getSimpleName(), typeVariable);

        return new PropagatedType(typeVariables.get(typeVariable.getName()), Context.forType(this));
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + '[' + type + ']';
    }

    private static class Context {
        private final Map<String, Type> typeVariables;

        private Context(Map<String, Type> typeVariables) {
            this.typeVariables = typeVariables;
        }

        static Context forType(PropagatedType type) {
            return new Context(type.typeVariables);
        }

        static Context empty() {
            return new Context(emptyMap());
        }

        Type resolve(Type type) {
            if (!(type instanceof TypeVariable<?>)) {
                return type;
            }

            TypeVariable<?> typeVariable = (TypeVariable<?>) type;
            return typeVariables.getOrDefault(typeVariable.getName(), type);
        }
    }
}
