package de.cronn.reflection.util.immutable;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import de.cronn.reflection.util.ClassUtils;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.FieldValue;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.matcher.ElementMatchers;

public final class GenericImmutableProxyForwarder {

	private static final Map<Method, Boolean> shouldProxyReturnValueCache = new ConcurrentHashMap<>();

	private GenericImmutableProxyForwarder() {
	}

	@RuntimeType
	public static Object forward(@Origin Method method,
								 @FieldValue(ImmutableProxy.DELEGATE_FIELD_NAME) Object delegate,
								 @AllArguments Object[] args) throws InvocationTargetException, IllegalAccessException {
		Object value = method.invoke(delegate, args);
		if (ImmutableProxy.isImmutable(value)) {
			return value;
		}
		if (!shouldProxyReturnValue(method)) {
			return value;
		}
		if (value instanceof Collection) {
			return createImmutableCollection(value, method);
		} else if (value instanceof Map) {
			return createImmutableMap(value, method);
		} else {
			return ImmutableProxy.create(value);
		}
	}

	@SuppressWarnings("boxing")
	private static boolean shouldProxyReturnValue(Method method) {
		return shouldProxyReturnValueCache.computeIfAbsent(method, m -> {
			if (isCloneMethod(m)) {
				return false;
			}
			ReadOnly readOnlyAnnotation = ClassUtils.findAnnotation(m, ReadOnly.class);
			return readOnlyAnnotation == null
				|| readOnlyAnnotation.proxyReturnValue();
		}).booleanValue();
	}

	private static boolean isCloneMethod(Method method) {
		return ElementMatchers.isClone().matches(new MethodDescription.ForLoadedMethod(method));
	}

	private static Object createImmutableCollection(Object value, Method method) {
		Class<?> returnType = method.getReturnType();
		if (returnType.equals(Set.class)) {
			Set<?> collection = (Set<?>) value;
			return ImmutableProxy.create(collection);
		} else if (returnType.equals(List.class)) {
			List<?> collection = (List<?>) value;
			return ImmutableProxy.create(collection);
		} else if (returnType.equals(Collection.class) || returnType.equals(Iterable.class)) {
			Collection<?> collection = (Collection<?>) value;
			return ImmutableProxy.create(collection);
		} else {
			throw new UnsupportedOperationException("Cannot create immutable collection for " + describeMethod(method) + "."
				+ " The return type is unknown or too specific: " + returnType + "."
				+ " Consider to define a more generic type: Set/List/Collection");
		}
	}

	private static Object createImmutableMap(Object value, Method method) {
		Class<?> returnType = method.getReturnType();
		if (returnType.equals(Map.class)) {
			Map<?, ?> map = (Map<?, ?>) value;
			return ImmutableProxy.create(map);
		} else {
			throw new UnsupportedOperationException("Cannot create immutable map for " + describeMethod(method) + "."
				+ " The return type is unknown or too specific: " + returnType + "."
				+ " Consider to define a more generic type: Map");
		}
	}

	private static String describeMethod(Method method) {
		return method.getDeclaringClass().getSimpleName() + "." + method.getName();
	}

}