/*******************************************************************************
 * Copyright (c) 2000, 2016 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Assert;

import org.eclipse.jdt.core.BindingKey;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeParameter;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTRequestor;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;

import org.eclipse.jdt.internal.corext.dom.IASTSharedValues;

/**
 * A type environment comprises a set of {@link TType}s that stand for Java {@link ITypeBinding}s.
 * In contrast to type bindings, TTypes of the same type environment also work across project boundaries and
 * across compiler environments, i.e. a type environment can handle bindings from multiple {@link ASTParser} sessions.
 *
 * @see TType
 */
public class TypeEnvironment {

	private static class ProjectKeyPair {
		private final IJavaProject fProject;
		private final String fBindingKey;

		public ProjectKeyPair(IJavaProject project, String bindingKey) {
			fProject= project;
			fBindingKey= bindingKey;
		}

		@Override
		public boolean equals(Object other) {
			if (this == other)
				return true;
			if (! (other instanceof ProjectKeyPair))
				return false;
			ProjectKeyPair otherPair= (ProjectKeyPair) other;
			return fProject.equals(otherPair.fProject) && fBindingKey.equals(otherPair.fBindingKey);
		}

		@Override
		public int hashCode() {
			return fProject.hashCode() + fBindingKey.hashCode();
		}
	}

	/** Type code for the primitive type "int". */
	public final PrimitiveType INT= new PrimitiveType(this, PrimitiveType.INT, BindingKey.createTypeBindingKey("int")); //$NON-NLS-1$
	/** Type code for the primitive type "char". */
	public final PrimitiveType CHAR = new PrimitiveType(this, PrimitiveType.CHAR, BindingKey.createTypeBindingKey("char")); //$NON-NLS-1$
	/** Type code for the primitive type "boolean". */
	public final PrimitiveType BOOLEAN = new PrimitiveType(this, PrimitiveType.BOOLEAN, BindingKey.createTypeBindingKey("boolean")); //$NON-NLS-1$
	/** Type code for the primitive type "short". */
	public final PrimitiveType SHORT = new PrimitiveType(this, PrimitiveType.SHORT, BindingKey.createTypeBindingKey("short")); //$NON-NLS-1$
	/** Type code for the primitive type "long". */
	public final PrimitiveType LONG = new PrimitiveType(this, PrimitiveType.LONG, BindingKey.createTypeBindingKey("long")); //$NON-NLS-1$
	/** Type code for the primitive type "float". */
	public final PrimitiveType FLOAT = new PrimitiveType(this, PrimitiveType.FLOAT, BindingKey.createTypeBindingKey("float")); //$NON-NLS-1$
	/** Type code for the primitive type "double". */
	public final PrimitiveType DOUBLE = new PrimitiveType(this, PrimitiveType.DOUBLE, BindingKey.createTypeBindingKey("double")); //$NON-NLS-1$
	/** Type code for the primitive type "byte". */
	public final PrimitiveType BYTE = new PrimitiveType(this, PrimitiveType.BYTE, BindingKey.createTypeBindingKey("byte")); //$NON-NLS-1$

	/** Type code for the primitive type "null". */
	public final NullType NULL= new NullType(this);

	public final VoidType VOID= new VoidType(this);

	final PrimitiveType[] PRIMITIVE_TYPES= {INT, CHAR, BOOLEAN, SHORT, LONG, FLOAT, DOUBLE, BYTE};

	private static final String[] BOXED_PRIMITIVE_NAMES= new String[] {
		"java.lang.Integer",  //$NON-NLS-1$
		"java.lang.Character",  //$NON-NLS-1$
		"java.lang.Boolean",  //$NON-NLS-1$
		"java.lang.Short",  //$NON-NLS-1$
		"java.lang.Long",  //$NON-NLS-1$
		"java.lang.Float",  //$NON-NLS-1$
		"java.lang.Double",  //$NON-NLS-1$
		"java.lang.Byte"};  //$NON-NLS-1$

	private TType OBJECT_TYPE= null;

	private List<Map<TType, ArrayType>>      fArrayTypes= new ArrayList<>();
	private Map<IJavaElement, StandardType>  fStandardTypes= new HashMap<>();
	private Map<IJavaElement, GenericType>   fGenericTypes= new HashMap<>();
	private Map<ProjectKeyPair, ParameterizedType> fParameterizedTypes= new HashMap<>();
	private Map<IJavaElement, RawType>       fRawTypes= new HashMap<>();
	private Map<IJavaElement, TypeVariable>  fTypeVariables= new HashMap<>();
	private Map<ProjectKeyPair, CaptureType> fCaptureTypes= new HashMap<>();
	private Map<TType, ExtendsWildcardType>  fExtendsWildcardTypes= new HashMap<>();
	private Map<TType, SuperWildcardType>    fSuperWildcardTypes= new HashMap<>();
	private UnboundWildcardType fUnboundWildcardType= null;

	private static final int MAX_ENTRIES= 1024;
	private Map<TypeTuple, Boolean> fSubTypeCache= new LinkedHashMap<>(50, 0.75f, true) {
		private static final long serialVersionUID= 1L;
		@Override
		protected boolean removeEldestEntry(Map.Entry<TypeTuple, Boolean> eldest) {
			return size() > MAX_ENTRIES;
		}
	};

	/**
	 * Map from TType to its known subtypes, or <code>null</code> iff subtype
	 * information was not requested in the constructor.
	 */
	private Map<TType, ArrayList<TType>> fSubTypes;
	/**
	 * If <code>true</code>, replace all capture types by their wildcard type.
	 * @since 3.7
	 */
	private final boolean fRemoveCapures;

	public static ITypeBinding[] createTypeBindings(TType[] types, IJavaProject project) {
		final Map<String, Object> mapping= new HashMap<>();
		List<String> keys= new ArrayList<>();
		for (TType type : types) {
			String bindingKey= type.getBindingKey();
			mapping.put(bindingKey, type);
			keys.add(bindingKey);
		}
		ASTParser parser= ASTParser.newParser(IASTSharedValues.SHARED_AST_LEVEL);
		parser.setProject(project);
		parser.setResolveBindings(true);
		parser.createASTs(new ICompilationUnit[0], keys.toArray(new String[keys.size()]),
			new ASTRequestor() {
				@Override
				public void acceptBinding(String bindingKey, IBinding binding) {
					mapping.put(bindingKey, binding);
				}
			}, null);
		ITypeBinding[] result= new ITypeBinding[types.length];
		for (int i= 0; i < types.length; i++) {
			TType type= types[i];
			String bindingKey= type.getBindingKey();
			Object value= mapping.get(bindingKey);
			if (value instanceof ITypeBinding) {
				result[i]= (ITypeBinding)value;
			}
		}
		return result;
	}

	public TypeEnvironment() {
		this(false);
	}

	public TypeEnvironment(boolean rememberSubtypes) {
		this(rememberSubtypes, false);
	}

	public TypeEnvironment(boolean rememberSubtypes, boolean removeCapures) {
		if (rememberSubtypes) {
			fSubTypes= new HashMap<>();
		}
		fRemoveCapures= removeCapures;
	}

	Map<TypeTuple, Boolean> getSubTypeCache() {
		return fSubTypeCache;
	}

	public TType create(ITypeBinding binding) {
		if (binding.isPrimitive()) {
			return createPrimitiveType(binding);
		} else if (binding.isArray()) {
			return createArrayType(binding);
		} else if (binding.isRawType()) {
			return createRawType(binding);
		} else if (binding.isGenericType()) {
			return createGenericType(binding);
		} else if (binding.isParameterizedType()) {
			return createParameterizedType(binding);
		} else if (binding.isTypeVariable()) {
			return createTypeVariable(binding);
		} else if (binding.isWildcardType()) {
			if (binding.getBound() == null) {
				return createUnboundWildcardType(binding);
			} else if (binding.isUpperbound()) {
				return createExtendsWildCardType(binding);
			} else {
				return createSuperWildCardType(binding);
			}
		} else if (binding.isCapture()) {
			if (fRemoveCapures) {
				return create(binding.getWildcard());
			} else {
				return createCaptureType(binding);
			}
		}
		if ("null".equals(binding.getName())) //$NON-NLS-1$
			return NULL;
		return createStandardType(binding);
	}

	public TType[] create(ITypeBinding[] bindings) {
		TType[] result= new TType[bindings.length];
		for (int i= 0; i < bindings.length; i++) {
			result[i]= create(bindings[i]);
		}
		return result;
	}

	/**
	 * Returns the TType for java.lang.Object.
	 * <p>
	 * Warning: currently returns <code>null</code> unless this type environment
	 * has already created its first hierarchy type or it has been initialized explicitly.
	 *
	 * @return the TType for java.lang.Object
	 *
	 * @see #initializeJavaLangObject(IJavaProject)
	 */
	public TType getJavaLangObject() {
		return OBJECT_TYPE;
	}

	public void initializeJavaLangObject(IJavaProject project) {
		if (OBJECT_TYPE != null)
			return;

		TType objectType= createStandardType("java.lang.Object", project); //$NON-NLS-1$
		Assert.isTrue(objectType.isJavaLangObject());
	}

	void initializeJavaLangObject(ITypeBinding object) {
		if (OBJECT_TYPE != null)
			return;

		TType objectType= createStandardType(object);
		Assert.isTrue(objectType.isJavaLangObject());
	}

	PrimitiveType createUnBoxed(StandardType type) {
		String name= type.getPlainPrettySignature();
		for (int i= 0; i < BOXED_PRIMITIVE_NAMES.length; i++) {
			if (BOXED_PRIMITIVE_NAMES[i].equals(name))
				return PRIMITIVE_TYPES[i];
		}
		return null;
	}

	StandardType createBoxed(PrimitiveType type, IJavaProject focus) {
		String fullyQualifiedName= BOXED_PRIMITIVE_NAMES[type.getId()];
		return createStandardType(fullyQualifiedName, focus);
	}

	private StandardType createStandardType(String fullyQualifiedName, IJavaProject focus) {
		try {
			IType javaElementType= focus.findType(fullyQualifiedName);
			StandardType result= fStandardTypes.get(javaElementType);
			if (result != null)
				return result;
			ASTParser parser= ASTParser.newParser(IASTSharedValues.SHARED_AST_LEVEL);
			parser.setProject(focus);
			IBinding[] bindings= parser.createBindings(new IJavaElement[] {javaElementType} , null);
			return createStandardType((ITypeBinding)bindings[0]);
		} catch (JavaModelException e) {
			// fall through
		}
		return null;
	}

	Map<TType, ArrayList<TType>> getSubTypes() {
		return fSubTypes;
	}

	private void cacheSubType(TType supertype, TType result) {
		if (fSubTypes == null)
			return;
		if (supertype == null)
			supertype= OBJECT_TYPE;

		ArrayList<TType> subtypes= fSubTypes.get(supertype);
		if (subtypes == null) {
			subtypes= new ArrayList<>(5);
			fSubTypes.put(supertype, subtypes);
		} else {
			Assert.isTrue(! subtypes.contains(result));
		}
		subtypes.add(result);
	}

	private void cacheSubTypes(TType[] interfaces, TType result) {
		for (TType intf : interfaces) {
			cacheSubType(intf, result);
		}
	}

	private TType createPrimitiveType(ITypeBinding binding) {
		String name= binding.getName();
		String[] names= PrimitiveType.NAMES;
		for (int i= 0; i < names.length; i++) {
			if (name.equals(names[i])) {
				return PRIMITIVE_TYPES[i];
			}
		}
		Assert.isTrue(false, "Primitive type " + name + "unkown");  //$NON-NLS-1$//$NON-NLS-2$
		return null;
	}

	private ArrayType createArrayType(ITypeBinding binding) {
		int index= binding.getDimensions() - 1;
		TType elementType= create(binding.getElementType());
		Map<TType, ArrayType> arrayTypes= getArrayTypesMap(index);
		ArrayType result= arrayTypes.get(elementType);
		if (result != null)
			return result;
		result= new ArrayType(this);
		arrayTypes.put(elementType, result);
		result.initialize(binding, elementType);
		return result;
	}

	public ArrayType createArrayType(TType elementType, int dimensions) {
		Assert.isTrue(! elementType.isArrayType());
		Assert.isTrue(! elementType.isAnonymous());
		Assert.isTrue(dimensions > 0);

		int index= dimensions - 1;
		Map<TType, ArrayType> arrayTypes= getArrayTypesMap(index);
		ArrayType result= arrayTypes.get(elementType);
		if (result != null)
			return result;
		result= new ArrayType(this, BindingKey.createArrayTypeBindingKey(elementType.getBindingKey(), dimensions));
		arrayTypes.put(elementType, result);
		result.initialize(elementType, dimensions);
		return result;
	}

	private Map<TType, ArrayType> getArrayTypesMap(int index) {
		int oldLength= fArrayTypes.size();
		if (index >= oldLength) {
			fArrayTypes.addAll(Collections.<Map<TType,ArrayType>>nCopies(index + 1 - oldLength, null));
		}
		Map<TType, ArrayType> arrayTypes= fArrayTypes.get(index);
		if (arrayTypes == null) {
			arrayTypes= new HashMap<>();
			fArrayTypes.set(index, arrayTypes);
		}
		return arrayTypes;
	}

	private StandardType createStandardType(ITypeBinding binding) {
		IJavaElement javaElement= binding.getJavaElement();
		StandardType result= fStandardTypes.get(javaElement);
		if (result != null)
			return result;
		result= new StandardType(this);
		fStandardTypes.put(javaElement, result);
		result.initialize(binding, (IType)javaElement);
		if (OBJECT_TYPE == null && result.isJavaLangObject())
			OBJECT_TYPE= result;
		return result;
	}

	private GenericType createGenericType(ITypeBinding binding) {
		IJavaElement javaElement= binding.getJavaElement();
		GenericType result= fGenericTypes.get(javaElement);
		if (result != null)
			return result;
		result= new GenericType(this);
		fGenericTypes.put(javaElement, result);
		result.initialize(binding, (IType)javaElement);
		cacheSubType(result.getSuperclass(), result);
		cacheSubTypes(result.getInterfaces(), result);
		return result;
	}

	private ParameterizedType createParameterizedType(ITypeBinding binding) {
		IJavaProject javaProject= binding.getJavaElement().getJavaProject();
		String bindingKey= binding.getKey();
		ProjectKeyPair pair= new ProjectKeyPair(javaProject, bindingKey);
		ParameterizedType result= fParameterizedTypes.get(pair);
		if (result != null)
			return result;
		result= new ParameterizedType(this);
		fParameterizedTypes.put(pair, result);
		result.initialize(binding, (IType)binding.getJavaElement());
		cacheSubType(result.getSuperclass(), result);
		cacheSubTypes(result.getInterfaces(), result);
		return result;
	}

	private RawType createRawType(ITypeBinding binding) {
		IJavaElement javaElement= binding.getJavaElement();
		RawType result= fRawTypes.get(javaElement);
		if (result != null)
			return result;
		result= new RawType(this);
		fRawTypes.put(javaElement, result);
		result.initialize(binding, (IType)javaElement);
		cacheSubType(result.getSuperclass(), result);
		cacheSubTypes(result.getInterfaces(), result);
		return result;
	}

	private TType createUnboundWildcardType(ITypeBinding binding) {
		if (fUnboundWildcardType == null) {
			fUnboundWildcardType= new UnboundWildcardType(this);
			fUnboundWildcardType.initialize(binding);
		}
		return fUnboundWildcardType;
	}

	private TType createExtendsWildCardType(ITypeBinding binding) {
		TType bound= create(binding.getBound());
		ExtendsWildcardType result= fExtendsWildcardTypes.get(bound);
		if (result != null)
			return result;
		result= new ExtendsWildcardType(this);
		fExtendsWildcardTypes.put(bound, result);
		result.initialize(binding);
		return result;
	}

	private TType createSuperWildCardType(ITypeBinding binding) {
		TType bound= create(binding.getBound());
		SuperWildcardType result= fSuperWildcardTypes.get(bound);
		if (result != null)
			return result;
		result= new SuperWildcardType(this);
		fSuperWildcardTypes.put(bound, result);
		result.initialize(binding);
		return result;
	}

	private TypeVariable createTypeVariable(ITypeBinding binding) {
		IJavaElement javaElement= binding.getJavaElement();
		TypeVariable result= fTypeVariables.get(javaElement);
		if (result != null)
			return result;
		result= new TypeVariable(this);
		fTypeVariables.put(javaElement, result);
		result.initialize(binding, (ITypeParameter)javaElement);
		return result;
	}

	private CaptureType createCaptureType(ITypeBinding binding) {
		IJavaProject javaProject= binding.getDeclaringClass().getJavaElement().getJavaProject();
		String bindingKey= binding.getKey();
		ProjectKeyPair pair= new ProjectKeyPair(javaProject, bindingKey);
		CaptureType result= fCaptureTypes.get(pair);
		if (result != null)
			return result;
		result= new CaptureType(this);
		fCaptureTypes.put(pair, result);
		result.initialize(binding, javaProject);
		return result;
	}
}
