/* *******************************************************************
 * Copyright (c) 2005 Contributors.
 * All rights reserved.
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Public License v 2.0
 * which accompanies this distribution and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 * ******************************************************************/
package org.aspectj.weaver.reflect;

import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.HashMap;
import java.util.Map;

import org.aspectj.weaver.BoundedReferenceType;
import org.aspectj.weaver.ReferenceType;
import org.aspectj.weaver.ResolvedType;
import org.aspectj.weaver.TypeFactory;
import org.aspectj.weaver.TypeVariable;
import org.aspectj.weaver.TypeVariableReferenceType;
import org.aspectj.weaver.UnresolvedType;
import org.aspectj.weaver.World;

/**
 * Handles the translation of java.lang.reflect.Type objects into AspectJ UnresolvedTypes.
 *
 * @author Adrian Colyer
 */
public class JavaLangTypeToResolvedTypeConverter {

	// Used to prevent recursion - we record what we are working on and return it if asked again *whilst* working on it
	private Map<Type, TypeVariableReferenceType> typeVariablesInProgress = new HashMap<>();
	private final World world;

	public JavaLangTypeToResolvedTypeConverter(World aWorld) {
		this.world = aWorld;
	}

	private World getWorld() {
		return this.world;
	}

	public ResolvedType fromType(Type type) {
		if (type instanceof Class) {
			Class<?> clazz = (Class<?>) type;
			String name = clazz.getName();
			/**
			 * getName() can return:
			 *
			 * 1. If this class object represents a reference type that is not an
			 * array type then the binary name of the class is returned
			 * 2. If this class object represents a primitive type or void, then
			 * the name returned is a String equal to the Java language keyword
			 * corresponding to the primitive type or void.
			 * 3. If this class object represents a class of arrays, then the internal
			 * form of the name consists of the name of the element type preceded by
			 * one or more '[' characters representing the depth of the array nesting.
			 */
			if (clazz.isArray()) {
				UnresolvedType ut = UnresolvedType.forSignature(name.replace('.', '/'));
				return getWorld().resolve(ut);
			} else {
				return getWorld().resolve(name);
			}
		} else if (type instanceof ParameterizedType) {
			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=509327
			// TODO should deal with the ownerType if it set, indicating this is possibly an inner type of a parameterized type
			Type ownerType = ((ParameterizedType) type).getOwnerType();
			ParameterizedType parameterizedType = (ParameterizedType) type;
			ResolvedType baseType = fromType(parameterizedType.getRawType());
			Type[] typeArguments = parameterizedType.getActualTypeArguments();
			if (baseType.isSimpleType() && typeArguments.length == 0 && ownerType != null) {
				// 'type' is an inner type of some outer parameterized type
				// For now just return the base type - in future create the parameterized form of the outer
				// and use it with the inner. We return the base type to be compatible with what the
				// code does that accesses the info from the bytecode (unlike this code which accesses it
				// reflectively).
				return baseType;
			}
			ResolvedType[] resolvedTypeArguments = fromTypes(typeArguments);
			return TypeFactory.createParameterizedType(baseType, resolvedTypeArguments, getWorld());
		} else if (type instanceof java.lang.reflect.TypeVariable) {
			TypeVariableReferenceType inprogressVar = typeVariablesInProgress.get(type);
			if (inprogressVar != null) {
				return inprogressVar;
			}
			java.lang.reflect.TypeVariable<?> tv = (java.lang.reflect.TypeVariable<?>) type;
			TypeVariable rt_tv = new TypeVariable(tv.getName());
			TypeVariableReferenceType tvrt = new TypeVariableReferenceType(rt_tv, getWorld());
			typeVariablesInProgress.put(type, tvrt); // record what we are working on, for recursion case
			Type[] bounds = tv.getBounds();
			ResolvedType[] resBounds = fromTypes(bounds);
			ResolvedType upperBound = resBounds[0];
			ResolvedType[] additionalBounds = ResolvedType.EMPTY_RESOLVED_TYPE_ARRAY;
			if (resBounds.length > 1) {
				additionalBounds = new ResolvedType[resBounds.length - 1];
				System.arraycopy(resBounds, 1, additionalBounds, 0, additionalBounds.length);
			}
			rt_tv.setUpperBound(upperBound);
			rt_tv.setAdditionalInterfaceBounds(additionalBounds);
			typeVariablesInProgress.remove(type); // we have finished working on it
			return tvrt;
		} else if (type instanceof WildcardType) {
			WildcardType wildType = (WildcardType) type;
			Type[] lowerBounds = wildType.getLowerBounds();
			Type[] upperBounds = wildType.getUpperBounds();
			ResolvedType bound = null;
			boolean isExtends = lowerBounds.length == 0;
			if (isExtends) {
				bound = fromType(upperBounds[0]);
			} else {
				bound = fromType(lowerBounds[0]);
			}
			return new BoundedReferenceType((ReferenceType) bound, isExtends, getWorld());
		} else if (type instanceof GenericArrayType) {
			GenericArrayType genericArrayType = (GenericArrayType) type;
			Type componentType = genericArrayType.getGenericComponentType();
			return UnresolvedType.makeArray(fromType(componentType), 1).resolve(getWorld());
		}
		return ResolvedType.MISSING;
	}

	public ResolvedType[] fromTypes(Type[] types) {
		ResolvedType[] ret = new ResolvedType[types.length];
		for (int i = 0; i < ret.length; i++) {
			ret[i] = fromType(types[i]);
		}
		return ret;
	}

}
