package com.feenk.jdt2famix.injava;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AnnotationTypeMemberDeclaration;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.FileASTRequestor;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMemberValuePairBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.IPackageBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;

import com.feenk.jdt2famix.Famix;
import com.feenk.jdt2famix.Importer;
import com.feenk.jdt2famix.JavaFiles;
import com.feenk.jdt2famix.model.famix.Access;
import com.feenk.jdt2famix.model.famix.AnnotationInstance;
import com.feenk.jdt2famix.model.famix.AnnotationInstanceAttribute;
import com.feenk.jdt2famix.model.famix.AnnotationType;
import com.feenk.jdt2famix.model.famix.AnnotationTypeAttribute;
import com.feenk.jdt2famix.model.famix.Attribute;
import com.feenk.jdt2famix.model.famix.Class;
import com.feenk.jdt2famix.model.famix.Comment;
import com.feenk.jdt2famix.model.famix.ContainerEntity;
import com.feenk.jdt2famix.model.famix.DeclaredException;
import com.feenk.jdt2famix.model.famix.Enum;
import com.feenk.jdt2famix.model.famix.EnumValue;
import com.feenk.jdt2famix.model.famix.FAMIXModel;
import com.feenk.jdt2famix.model.famix.IndexedFileAnchor;
import com.feenk.jdt2famix.model.famix.Inheritance;
import com.feenk.jdt2famix.model.famix.Invocation;
import com.feenk.jdt2famix.model.famix.JavaSourceLanguage;
import com.feenk.jdt2famix.model.famix.LocalVariable;
import com.feenk.jdt2famix.model.famix.Method;
import com.feenk.jdt2famix.model.famix.NamedEntity;
import com.feenk.jdt2famix.model.famix.Namespace;
import com.feenk.jdt2famix.model.famix.Parameter;
import com.feenk.jdt2famix.model.famix.ParameterType;
import com.feenk.jdt2famix.model.famix.ParameterizableClass;
import com.feenk.jdt2famix.model.famix.ParameterizedType;
import com.feenk.jdt2famix.model.famix.PrimitiveType;
import com.feenk.jdt2famix.model.famix.SourcedEntity;
import com.feenk.jdt2famix.model.famix.StructuralEntity;
import com.feenk.jdt2famix.model.famix.Type;
import com.feenk.jdt2famix.model.famix.UnknownVariable;
import com.feenk.jdt2famix.model.java.JavaModel;

import ch.akuhn.fame.MetaRepository;
import ch.akuhn.fame.Repository;

/**
 * The core class that holds the logic of creating the model It looks like a god
 * class, but it is convenient to have most of the logic here
 * 
 * There are two main kinds of methods 1. ensure methods are those that always
 * return the same instance of a named entity for the same qualified name. These
 * are important for creating the graph. 2. create methods always create new
 * instances entities (both named and other types, such as associations)
 * 
 * @author girba
 */
public class InJavaImporter extends Importer {
	private static final Logger logger = LogManager.getLogger(InJavaImporter.class);

	private static final char NAME_SEPARATOR = '.';
	public static final String INITIALIZER_NAME = "<init>";
	public static final String UNKNOWN_NAME = "__UNKNOWN__";
	public static final String CONSTRUCTOR_KIND = "constructor";
	private static final String INITIALIZER_KIND = "initializer";

	private Namespace unknownNamespace;
	private Type unknownType;
	private UnknownVariable unknownVariable;

	private Repository repository;

	public Repository repository() {
		return repository;
	}

	private NamedEntityAccumulator<Namespace> namespaces;

	public NamedEntityAccumulator<Namespace> namespaces() {
		return namespaces;
	}

	private NamedEntityAccumulator<Type> types;

	public NamedEntityAccumulator<Type> types() {
		return types;
	}

	private NamedEntityAccumulator<Method> methods;

	public NamedEntityAccumulator<Method> methods() {
		return methods;
	}

	private NamedEntityAccumulator<Attribute> attributes;

	public NamedEntityAccumulator<Attribute> attributes() {
		return attributes;
	}

	private NamedEntityAccumulator<Parameter> parameters;

	private String currentFilePath;

	public String getCurrentFilePath() {
		return currentFilePath;
	}

	public void setCurrentFilePath(String currentFilePath) {
		this.currentFilePath = currentFilePath;
	}

	/**
	 * This is a structure that keeps track of the current stack of containers It is
	 * particularly useful when we deal with inner or anonymous classes
	 */
	private Deque<ContainerEntity> containerStack = new ArrayDeque<ContainerEntity>();

	public void pushOnContainerStack(ContainerEntity namespace) {
		this.containerStack.push(namespace);
	}

	public ContainerEntity popFromContainerStack() {
		return this.containerStack.pop();
	}

	public ContainerEntity topOfContainerStack() {
		return this.containerStack.peek();
	}

	@SuppressWarnings("unchecked")
	public <T> T topFromContainerStack(java.lang.Class<T> clazz) {
		for (Iterator<ContainerEntity> iterator = containerStack.iterator(); iterator.hasNext();) {
			ContainerEntity next = iterator.next();
			if (clazz.isInstance(next))
				return (T) next;
		}
		return null;
	}

	public InJavaImporter() {
		MetaRepository metaRepository = new MetaRepository();
		FAMIXModel.importInto(metaRepository);
		JavaModel.importInto(metaRepository);
		repository = new Repository(metaRepository);
		repository.add(new JavaSourceLanguage());

		namespaces = new NamedEntityAccumulator<Namespace>(repository);
		types = new NamedEntityAccumulator<Type>(repository);
		methods = new NamedEntityAccumulator<Method>(repository);
		attributes = new NamedEntityAccumulator<Attribute>(repository);
		parameters = new NamedEntityAccumulator<Parameter>(repository);
	}

	@Override
	protected FileASTRequestor getRequestor(JavaFiles allJavaFiles) {
		return new AstRequestor(this, allJavaFiles);
	}

	// NAMESPACE

	public Namespace ensureNamespaceFromPackageBinding(IPackageBinding binding) {
		return ensureNamespaceNamed(binding.getName());
	}

	Namespace ensureNamespaceNamed(String packageName) {
		if (namespaces.has(packageName))
			return namespaces.named(packageName);
		else
			return namespaces.add(packageName, createNamespaceNamed(packageName));
	}

	private Namespace createNamespaceNamed(String qualifiedName) {
		int lastIndexOfDot = qualifiedName.lastIndexOf(".");
		Namespace namespace = new Namespace();
		namespace.setIsStub(true);
		if (lastIndexOfDot <= 0)
			namespace.setName(qualifiedName);
		else {
			/*
			 * Java packages are not nested, even though they look like they are. But, in
			 * Famix, namespaces are nested. So we create nesting based on the . separator
			 */
			namespace.setName(qualifiedName.substring(lastIndexOfDot + 1));
			Namespace parentNamespace = ensureNamespaceNamed(qualifiedName.substring(0, lastIndexOfDot));
			namespace.setParentScope(parentNamespace);
		}
		return namespace;
	}

	private ContainerEntity ensureContainerEntityForTypeBinding(ITypeBinding binding) {
		if (binding.getDeclaringClass() != null)
			return ensureTypeFromTypeBinding(binding.getDeclaringClass());
		if (binding.getPackage() != null)
			return ensureNamespaceFromPackageBinding(binding.getPackage());
		return unknownNamespace();
	}

	public Namespace unknownNamespace() {
		if (unknownNamespace == null) {
			unknownNamespace = new Namespace();
			unknownNamespace.setName(UNKNOWN_NAME);
			unknownNamespace.setIsStub(true);
			namespaces.add(Famix.qualifiedNameOf(unknownNamespace), unknownNamespace);
		}
		return unknownNamespace;
	}

	// TYPE

	public Type ensureTypeFromTypeBinding(ITypeBinding binding) {
		String qualifiedName = binding.getQualifiedName();
		if (types.has(qualifiedName))
			return types.named(qualifiedName);
		Type type = createTypeFromTypeBinding(binding);
		type.setName(binding.getName());
		types.add(qualifiedName, type);
		type.setIsStub(true);
		extractBasicModifiersFromBinding(binding.getModifiers(), type);
		type.setContainer(ensureContainerEntityForTypeBinding(binding));
		if (binding.getSuperclass() != null)
			createInheritanceFromSubtypeToSuperTypeBinding(type, binding.getSuperclass());
		for (ITypeBinding interfaceBinding : binding.getInterfaces()) {
			createInheritanceFromSubtypeToSuperTypeBinding(type, interfaceBinding);
		}
		if (binding.isParameterizedType()) {
			/*
			 * This if duplicates the condition from the create method because we want to
			 * break possible infinite loops induced by the below ensure calls. This is
			 * achieved by having this condition after the addition of the type in the types
			 * map.
			 */
			ParameterizedType parameterizedType = ((ParameterizedType) type);
			if (ensureTypeFromTypeBinding(binding.getErasure()) instanceof ParameterizableClass)
				parameterizedType.setParameterizableClass(
						(ParameterizableClass) ensureTypeFromTypeBinding(binding.getErasure()));
			List<Type> arguments = Stream.of(binding.getTypeArguments()).map(arg -> ensureTypeFromTypeBinding(arg))
					.collect(Collectors.toList());
			parameterizedType.setArguments(arguments);
		}
		if (binding.isGenericType()) {
			ParameterizableClass parameterizableClass = (ParameterizableClass) type;
			Stream.of(binding.getTypeParameters())
					.forEach(p -> createParameterType(p.getName().toString(), parameterizableClass));
		}
		return type;
	}

	public AnnotationInstance createAnnotationInstanceFromAnnotationBinding(NamedEntity entity,
			IAnnotationBinding annotationInstanceBinding) {
		ITypeBinding annotationTypeBinding = annotationInstanceBinding.getAnnotationType();
		AnnotationInstance annotationInstance = new AnnotationInstance();
		annotationInstance.setAnnotatedEntity(entity);
		if (annotationInstanceBinding != null) {
			createAnnotationInstanceFromAnnotationInstanceBinding(annotationInstanceBinding, annotationTypeBinding,
					annotationInstance);
		}
		repository.add(annotationInstance);
		return annotationInstance;
	}

	private void createAnnotationInstanceFromAnnotationInstanceBinding(IAnnotationBinding annotationInstanceBinding,
			ITypeBinding annotationTypeBinding, AnnotationInstance annotationInstance) {
		AnnotationType annotationType = (AnnotationType) ensureTypeFromTypeBinding(annotationTypeBinding);
		annotationInstance.setAnnotationType(annotationType);
		IMemberValuePairBinding[] allMemberValuePairs = annotationInstanceBinding.getAllMemberValuePairs();
		for (IMemberValuePairBinding memberValueBinding : allMemberValuePairs) {
			try {
				Object value = memberValueBinding.getValue();
				AnnotationInstanceAttribute annotationInstanceAttribute = new AnnotationInstanceAttribute();

				annotationInstanceAttribute.setValue(annotationInstanceAttributeValueString(value));
				annotationInstance.addAttributes(annotationInstanceAttribute);
				repository.add(annotationInstanceAttribute);

				annotationInstanceAttribute.setAnnotationTypeAttribute(
						ensureAnnotationTypeAttribute(annotationType, memberValueBinding.getName()));
			} catch (NullPointerException npe) {
				logger.error(
						"Null pointer exception in jdt core when getting the value of annotation instance attribute "
								+ memberValueBinding.getKey());
			}
		}
	}

	private String annotationInstanceAttributeValueString(Object value) {
		if (value == null)
			/*
			 * Theoretically, this should not happen because the Java compiler prevents you
			 * from setting null to an enum constant. However, we can still get a null
			 * during import. For example, when referencing something like
			 * attribute=MyClass.class, without MyClass being available, we get a null as
			 * value.
			 */
			return "null";
		if (value instanceof ITypeBinding)
			return ((ITypeBinding) value).getName() + ".class";
		if (value instanceof Object[]) {
			Object[] array = (Object[]) value;
			StringJoiner signatureJoiner = new StringJoiner(", ", "{", "}");
			Arrays.stream(array).forEach(object -> signatureJoiner.add(annotationInstanceAttributeValueString(object)));
			return signatureJoiner.toString();
		}
		return value.toString();
	}

	private AnnotationTypeAttribute ensureAnnotationTypeAttribute(Type parentType, String name) {
		String qualifiedName = Famix.qualifiedNameOf(parentType) + NAME_SEPARATOR + name;
		if (attributes().has(qualifiedName))
			return (AnnotationTypeAttribute) attributes().named(qualifiedName);
		AnnotationTypeAttribute attribute = new AnnotationTypeAttribute();
		attribute.setName(name);
		attribute.setParentType(parentType);
		attributes.add(qualifiedName, attribute);
		return attribute;
	}

	Type createTypeFromTypeBinding(ITypeBinding binding) {
		if (binding.isPrimitive())
			return new PrimitiveType();
		if (binding.isParameterizedType())
			return new ParameterizedType();
		if (binding.isGenericType()) {
			ParameterizableClass parameterizableClass = new ParameterizableClass();
			parameterizableClass.setIsInterface(binding.isInterface());
			return parameterizableClass;
		}
		if (binding.isEnum())
			return new Enum();
		if (binding.isArray())
			return createTypeFromTypeBinding(binding.getElementType());
		if (binding.isAnnotation())
			return new AnnotationType();
		Class clazz = new Class();
		clazz.setIsInterface(binding.isInterface());
		return clazz;
	}

	private ParameterType createParameterType(String name, Type container) {
		ParameterType parameterType = new ParameterType();
		parameterType.setName(name);
		parameterType.setContainer(container);
		return parameterType;
	}

	/**
	 * All types should be ensured first via this method. We first check to see if
	 * the binding is resolvable (not null) If it is not null we ensure the type
	 * from the binding (the happy case) If the type is null we recover what we know
	 * (for example, the name of a simple type) In the worst case we return the
	 * {@link #unknownType()}
	 */
	private Type ensureTypeFromDomType(org.eclipse.jdt.core.dom.Type domType) {
		ITypeBinding binding = domType.resolveBinding();
		if (binding != null)
			return ensureTypeFromTypeBinding(binding);
		if (domType.isSimpleType())
			return ensureTypeNamedInUnknownNamespace(((SimpleType) domType).getName().toString());
		if (domType.isParameterizedType())
			return ensureTypeNamedInUnknownNamespace(
					((org.eclipse.jdt.core.dom.ParameterizedType) domType).getType().toString());
		return unknownType();
	}

	/**
	 * This is the type we used as a null object whenever we need to reference a
	 * type
	 */
	public Type unknownType() {
		if (unknownType == null) {
			unknownType = ensureTypeNamedInUnknownNamespace(UNKNOWN_NAME);
		}
		return unknownType;
	}

	public Type ensureTypeNamedInUnknownNamespace(String name) {
		Type type = createTypeNamedInUnknownNamespace(name);
		String qualifiedName = Famix.qualifiedNameOf(type);
		if (types.has(qualifiedName))
			return types.named(qualifiedName);
		else {
			types.add(Famix.qualifiedNameOf(type), type);
			return type;
		}
	}

	public Type createTypeNamedInUnknownNamespace(String name) {
		Type type = new Type();
		type.setName(name);
		type.setContainer(unknownNamespace());
		type.setIsStub(true);
		return type;
	}

	public Type ensureTypeFromAnonymousDeclaration(Type type, AnonymousClassDeclaration node) {
		type.setContainer(topOfContainerStack());
		type.setName("$" + topOfContainerStack().getTypes().size());
		if (node.getParent() instanceof ClassInstanceCreation)
			createInheritanceFromSubtypeToSuperDomType(type, ((ClassInstanceCreation) node.getParent()).getType());
		if (node.getParent() instanceof EnumConstantDeclaration)
			createInheritanceFromSubtypeToSuperType(type, topFromContainerStack(Enum.class));
		types.add(Famix.qualifiedNameOf(type), type);
		return type;
	}

	// INHERITANCE

	/**
	 * We use this one when we have the super type binding
	 */
	private Inheritance createInheritanceFromSubtypeToSuperTypeBinding(Type subType, ITypeBinding superBinding) {
		return createInheritanceFromSubtypeToSuperType(subType, ensureTypeFromTypeBinding(superBinding));
	}

	/**
	 * When we cannot resolve the binding of the superclass of a class declaration,
	 * we still want to create a {@link Type} with the best available information
	 * from {@link org.eclipse.jdt.core.dom.Type}
	 */
	public Inheritance createInheritanceFromSubtypeToSuperDomType(Type subType, org.eclipse.jdt.core.dom.Type type) {
		return createInheritanceFromSubtypeToSuperType(subType, ensureTypeFromDomType(type));
	}

	/**
	 * We use this one when we have the super type
	 */
	private Inheritance createInheritanceFromSubtypeToSuperType(Type subType, Type superType) {
		Inheritance inheritance = new Inheritance();
		inheritance.setSuperclass(superType);
		inheritance.setSubclass(subType);
		repository.add(inheritance);
		return inheritance;
	}

	// METHOD

	/**
	 * We use this one when we know that we are aiming for the top of the container
	 * stack This is important in the case of anonymous classes which have empty
	 * names in JDT
	 */
	public Method ensureMethodFromMethodBindingToCurrentContainer(IMethodBinding binding) {
		return ensureMethodFromMethodBinding(binding, (Type) topOfContainerStack());
	}

	public Method ensureMethodFromMethodBinding(IMethodBinding binding, Type parentType) {
		StringJoiner signatureJoiner = new StringJoiner(", ", "(", ")");
		Arrays.stream(binding.getParameterTypes()).forEach(p -> signatureJoiner.add((String) p.getQualifiedName()));
		String methodName = binding.getName();
		String signature = methodName + signatureJoiner.toString();
		return ensureBasicMethod(methodName, signature, parentType, m -> setUpMethodFromMethodBinding(m, binding));
	}

	private void setUpMethodFromMethodBinding(Method method, IMethodBinding binding) {
		if (binding.isConstructor())
			method.setKind(CONSTRUCTOR_KIND);
		ITypeBinding returnType = binding.getReturnType();
		if ((returnType != null) && !(returnType.isPrimitive() && returnType.getName().equals("void")))
			// we do not want to set void as a return type
			method.setDeclaredType(ensureTypeFromTypeBinding(returnType));
		extractBasicModifiersFromBinding(binding.getModifiers(), method);
		if (Modifier.isStatic(binding.getModifiers()))
			method.setHasClassScope(true);
	}

	public Method ensureMethodFromMethodDeclaration(MethodDeclaration node) {
		StringJoiner signatureJoiner = new StringJoiner(", ", "(", ")");
		Arrays.stream(node.parameters().toArray())
				.forEach(p -> signatureJoiner.add((String) ((SingleVariableDeclaration) p).getType().toString()));
		String methodName = node.getName().toString();
		String signature = methodName + signatureJoiner.toString();
		return ensureBasicMethod(methodName, signature, (Type) topOfContainerStack(),
				m -> setUpMethodFromMethodDeclaration(m, node));
	}

	private void setUpMethodFromMethodDeclaration(Method method, MethodDeclaration node) {
		if (node.getReturnType2() != null)
			method.setDeclaredType(ensureTypeFromDomType(node.getReturnType2()));
	}

	public Method ensureInitializerMethod() {
		return ensureBasicMethod(INITIALIZER_NAME, INITIALIZER_NAME, (Type) topOfContainerStack(),
				m -> setUpInitializerMethod(m));
	}

	private void setUpInitializerMethod(Method method) {
		method.setKind(INITIALIZER_KIND);
		method.setIsStub(false);
	}

	public Method ensureBasicMethod(String methodName, String signature, Type parentType, Consumer<Method> ifAbsent) {
		String qualifiedName = Famix.qualifiedNameOf(parentType) + NAME_SEPARATOR + signature;
		if (methods.has(qualifiedName))
			return methods.named(qualifiedName);
		Method method = new Method();
		method.setName(methodName);
		methods.add(qualifiedName, method);
		method.setSignature(signature);
		method.setIsStub(true);
		method.setParentType(parentType);
		ifAbsent.accept(method);
		return method;
	}

	// PARAMETER

	public Parameter ensureParameterFromSingleVariableDeclaration(SingleVariableDeclaration variableDeclaration,
			Method method) {
		String name = variableDeclaration.getName().toString();
		String qualifiedName = Famix.qualifiedNameOf(method) + NAME_SEPARATOR + name;
		if (parameters.has(qualifiedName))
			return parameters.named(qualifiedName);
		Parameter parameter = new Parameter();
		parameters.add(qualifiedName, parameter);
		parameter.setName(name);
		parameter.setParentBehaviouralEntity(method);
		parameter.setDeclaredType(ensureTypeFromDomType(variableDeclaration.getType()));
		IVariableBinding binding = variableDeclaration.resolveBinding();
		if (binding != null) {
			// We only recover the final modifier
			if (Modifier.isFinal(binding.getModifiers()))
				parameter.addModifiers("final");
		}
		return parameter;
	}

	public Parameter ensureParameterWithinCurrentMethodFromVariableBinding(IVariableBinding binding) {
		if (topOfContainerStack() instanceof Method) {
			Method method = (Method) topOfContainerStack();
			if (method != null) {
				Optional<Parameter> possibleParameter = method.getParameters().stream()
						.filter(p -> p.getName().equals(binding.getName())).findAny();
				if (possibleParameter.isPresent())
					return possibleParameter.get();
			}
		}
		return null;
	}

	// ATTRIBUTE

	/**
	 * We pass both the fragment and the field because we need the field type when
	 * the binding cannot be resolved
	 */
	public Attribute ensureAttributeForFragment(VariableDeclarationFragment fragment, FieldDeclaration field) {
		IVariableBinding binding = fragment.resolveBinding();
		Attribute attribute;
		if (binding == null)
			attribute = ensureAttributeFromFragmentIntoParentType(fragment, field,
					this.topFromContainerStack(Type.class));
		else {
			attribute = ensureAttributeForVariableBinding(binding);
			extractBasicModifiersFromBinding(binding.getModifiers(), attribute);
			if (Modifier.isStatic(binding.getModifiers()))
				attribute.setHasClassScope(true);
		}
		attribute.setIsStub(true);
		return attribute;
	}

	Attribute ensureAttributeForVariableBinding(IVariableBinding binding) {
		String name = binding.getName();
		ITypeBinding parentTypeBinding = binding.getDeclaringClass();
		Type parentType;
		if (parentTypeBinding == null)
			/*
			 * for example String[] args; args.length appears like an attribute, but the
			 * declaring class is not present
			 */
			parentType = unknownType();
		else
			parentType = ensureTypeFromTypeBinding(parentTypeBinding);
		String qualifiedName = Famix.qualifiedNameOf(parentType) + NAME_SEPARATOR + name;
		if (attributes.has(qualifiedName))
			return attributes.named(qualifiedName);
		Attribute attribute = ensureBasicAttribute(parentType, name, qualifiedName,
				ensureTypeFromTypeBinding(binding.getType()));
		return attribute;
	}

	private Attribute ensureAttributeFromFragmentIntoParentType(VariableDeclarationFragment fragment,
			FieldDeclaration field, Type parentType) {
		String name = fragment.getName().toString();
		String qualifiedName = Famix.qualifiedNameOf(parentType) + "." + name;
		if (attributes.has(qualifiedName))
			return attributes.named(qualifiedName);
		Attribute attribute = ensureBasicAttribute(parentType, name, qualifiedName,
				ensureTypeFromDomType(field.getType()));
		return attribute;
	}

	private Attribute ensureBasicAttribute(Type parentType, String name, String qualifiedName, Type declaredType) {
		Attribute attribute = new Attribute();
		attribute.setName(name);
		attribute.setParentType(parentType);
		attribute.setDeclaredType(declaredType);
		attributes.add(qualifiedName, attribute);
		return attribute;
	}

	// LOCAL VARIABLE

	/**
	 * We pass the dom type here because of the funny types of JDT
	 */
	public void ensureLocalVariableFromFragment(VariableDeclarationFragment fragment,
			org.eclipse.jdt.core.dom.Type type) {
		if (topOfContainerStack() instanceof Method) {
			LocalVariable localVariable = new LocalVariable();
			localVariable.setName(fragment.getName().toString());
			localVariable.setDeclaredType(ensureTypeFromDomType(type));
			// CHECK: We might want to recover the modifiers (e.g., final)
			localVariable.setIsStub(true);
			((Method) topOfContainerStack()).addLocalVariables(localVariable);
			repository.add(localVariable);
		}
	}

	// ENUM VALUE

	public EnumValue ensureEnumValueFromDeclaration(EnumConstantDeclaration node) {
		Enum parentEnum = topFromContainerStack(Enum.class);
		String enumValueName = node.getName().toString();
		return ensureBasicEnumValue(parentEnum, enumValueName);
	}

	public EnumValue ensureEnumValueFromVariableBinding(IVariableBinding binding) {
		Enum parentEnum = (Enum) ensureTypeFromTypeBinding(binding.getType());
		String enumValueName = binding.getName().toString();
		return ensureBasicEnumValue(parentEnum, enumValueName);
	}

	private EnumValue ensureBasicEnumValue(Enum parentEnum, String enumValueName) {
		if (parentEnum != null && parentEnum.getValues().stream().anyMatch(v -> v.getName().equals(enumValueName)))
			return parentEnum.getValues().stream().filter(v -> v.getName().equals(enumValueName)).findAny().get();
		EnumValue enumValue = new EnumValue();
		enumValue.setName(enumValueName);
		enumValue.setParentEnum(parentEnum);
		enumValue.setIsStub(true);
		repository.add(enumValue);
		return enumValue;
	}

	// ANNOTATION TYPE ATTRIBUTE

	public AnnotationTypeAttribute ensureAnnotationTypeAttributeFromDeclaration(AnnotationTypeMemberDeclaration node) {
		IMethodBinding binding = node.resolveBinding();
		if (binding != null)
			return ensureAnnotationTypeAttributeFromBinding(binding);
		return new AnnotationTypeAttribute();
	}

	private AnnotationTypeAttribute ensureAnnotationTypeAttributeFromBinding(IMethodBinding binding) {
		ITypeBinding parentTypeBinding = binding.getDeclaringClass();
		if (parentTypeBinding == null) {
			return null;
		}
		AnnotationType annotationType = (AnnotationType) ensureTypeFromTypeBinding(parentTypeBinding);
		AnnotationTypeAttribute attribute = ensureAnnotationTypeAttribute(annotationType, binding.getName());
		ITypeBinding returnType = binding.getReturnType();
		if ((returnType != null) && !(returnType.isPrimitive() && returnType.getName().equals("void"))) {
			// we do not want to set void as a return type
			attribute.setDeclaredType(ensureTypeFromTypeBinding(returnType));
		}
		return attribute;
	}

	// INVOCATION

	/**
	 * We pass the signature because we want to get it from the node, but there can
	 * be different types of nodes (funny JDT).
	 */
	public Invocation createInvocationFromMethodBinding(IMethodBinding binding, String signature) {

		Invocation invocation = new Invocation();
		if (topOfContainerStack() instanceof Method)
			invocation.setSender((Method) topOfContainerStack());
		if (binding != null && binding.getMethodDeclaration() != null) {
			IMethodBinding methodDeclarationBinding = binding.getMethodDeclaration();
			ITypeBinding declaringClass = null;
			if (methodDeclarationBinding.getDeclaringClass() != null)
				declaringClass = methodDeclarationBinding.getDeclaringClass();
			else
				declaringClass = binding.getDeclaringClass();
			Type ensureTypeFromTypeBinding = ensureTypeFromTypeBinding(declaringClass);
			invocation
					.addCandidates(ensureMethodFromMethodBinding(methodDeclarationBinding, ensureTypeFromTypeBinding));
		}
		invocation.setSignature(signature);
		repository.add(invocation);
		return invocation;
	}

	public Invocation createInvocationToMethod(Method method, String signature) {
		Invocation invocation = new Invocation();
		if (topOfContainerStack() instanceof Method)
			invocation.setSender((Method) topOfContainerStack());
		invocation.addCandidates(method);
		invocation.setSignature(signature);
		repository.add(invocation);
		return invocation;
	}

	public StructuralEntity ensureStructuralEntityFromExpression(Expression expression) {
		if (expression instanceof SimpleName) {
			IBinding simpleNameBinding = ((SimpleName) expression).resolveBinding();
			if (simpleNameBinding instanceof IVariableBinding) {
				IVariableBinding binding = ((IVariableBinding) simpleNameBinding).getVariableDeclaration();
				if (binding.isField())
					return ensureAttributeForVariableBinding(binding);
				if (binding.isParameter())
					return ensureParameterWithinCurrentMethodFromVariableBinding(binding);
				if (binding.isEnumConstant())
					return ensureEnumValueFromVariableBinding(binding);
			}
		}
		return null;
	}

	// ACCESS

	public Access createAccessFromExpression(Expression expression) {
		// is this not horrible?
		if (expression instanceof Name) {
			SimpleName simpleName;
			if (expression instanceof SimpleName)
				simpleName = (SimpleName) expression;
			else
				simpleName = ((QualifiedName) expression).getName();
			IBinding simpleNameBinding = simpleName.resolveBinding();
			if (simpleNameBinding instanceof IVariableBinding) {
				IVariableBinding variableBinding = ((IVariableBinding) simpleNameBinding).getVariableDeclaration();
				return createAccessFromVariableBinding(variableBinding, simpleName);
			}
		}
		if (expression instanceof FieldAccess) {
			FieldAccess access = (FieldAccess) expression;
			IVariableBinding variableBinding = access.resolveFieldBinding();
			return createAccessFromVariableBinding(variableBinding, access);
		}
		return new Access();
	}

	private Access createAccessFromVariableBinding(IVariableBinding binding, ASTNode node) {
		Access access = new Access();
		StructuralEntity variable = unknownVariable();
		if (binding != null) {
			/*
			 * It sometimes happen that the binding is null. Unfortunately, I was not able
			 * to isolate and reproduce the case, but we still need the guard condition.
			 */
			boolean isField = binding.isField();
			boolean isParameter = binding.isParameter();
			boolean isEnumConstant = binding.isEnumConstant();
			if (!isField && !isParameter && !isEnumConstant)
				// we only consider fields, parameters and enum constants
				return access;
			if (isField)
				variable = ensureAttributeForVariableBinding(binding);
			if (isParameter)
				variable = ensureParameterWithinCurrentMethodFromVariableBinding(binding);
			if (isEnumConstant)
				variable = ensureEnumValueFromVariableBinding(binding);
		}
		access.setVariable(variable);
		access.setIsWrite(false);
		if (topOfContainerStack() instanceof Method)
			access.setAccessor((Method) topOfContainerStack());
		if (topOfContainerStack() instanceof Type)
			/*
			 * This is ugly, but it happens when we have an access from within an annotation
			 * around a type or attribute like:
			 * 
			 * @Annotation(name="something" + AClass.DEFAULT)
			 */
			access.setAccessor(ensureInitializerMethod());
		createLightweightSourceAnchor(access, node);
		repository.add(access);
		return access;
	}

	public UnknownVariable unknownVariable() {
		if (unknownVariable == null) {
			unknownVariable = new UnknownVariable();
			repository.add(unknownVariable);
		}
		return unknownVariable;
	}

	// EXCEPTION

	public DeclaredException createDeclaredExceptionFromTypeBinding(ITypeBinding binding, Method method) {
		DeclaredException declaredException = new DeclaredException();
		method.addDeclaredExceptions(declaredException);
		declaredException.setExceptionClass((Class) ensureTypeFromTypeBinding(binding));
		repository.add(declaredException);
		return declaredException;
	}

	// SOURCE ANCHOR

	public void createLightweightSourceAnchor(SourcedEntity sourcedEntity, ASTNode node) {
		sourcedEntity.setAstStartPosition(node.getStartPosition() + 1);
		sourcedEntity.setAstStopPosition(node.getStartPosition() + node.getLength());
	}

	public void createSourceAnchor(SourcedEntity sourcedEntity, ASTNode node) {
		this.createSourceAnchor(sourcedEntity, node.getStartPosition() + 1, node.getStartPosition() + node.getLength());
	}

	public void createSourceAnchor(SourcedEntity sourcedEntity, int start, int stop) {
		IndexedFileAnchor fileAnchor = new IndexedFileAnchor();
		fileAnchor.setStartPos(start);
		fileAnchor.setEndPos(stop);
		fileAnchor.setFileName(pathWithoutIgnoredRootPath(currentFilePath));
		sourcedEntity.setAstStartPosition(start);
		sourcedEntity.setAstStopPosition(stop);
		sourcedEntity.setSourceAnchor(fileAnchor);
		repository.add(fileAnchor);
	}

	// COMMENT

	public void ensureCommentFromBodyDeclaration(SourcedEntity entity, BodyDeclaration node) {
		if (node.getJavadoc() != null)
			createBasicComment(entity, node.getJavadoc().toString());
		else {
			// if there is no javadoc, we look for single line or multi line comments before
			// the node
			CompilationUnit root = (CompilationUnit) node.getRoot();
			int firstLeadingCommentIndex = root.firstLeadingCommentIndex(node);
			if (firstLeadingCommentIndex >= 0)
				// There seems to be a problem here: JDT does not seem to provide the contents
				// of the comments.
				// Only the types (one line or multi line).
				createBasicComment(entity, root.getCommentList().get(firstLeadingCommentIndex).toString());
		}
	}

	private void createBasicComment(SourcedEntity entity, String content) {
		Comment comment = new Comment();
		comment.setContent(content);
		entity.addComments(comment);
		repository.add(comment);
	}

	// UTILS

	private void extractBasicModifiersFromBinding(int modifiers, NamedEntity entity) {
		Boolean publicModifier = Modifier.isPublic(modifiers);
		Boolean protectedModifier = Modifier.isProtected(modifiers);
		Boolean privateModifier = Modifier.isPrivate(modifiers);
		if (publicModifier)
			entity.addModifiers("public");
		if (protectedModifier)
			entity.addModifiers("protected");
		if (privateModifier)
			entity.addModifiers("private");
		if (!(publicModifier || protectedModifier || privateModifier))
			entity.addModifiers("package");
		if (Modifier.isFinal(modifiers))
			entity.addModifiers("final");
		if (Modifier.isAbstract(modifiers))
			entity.addModifiers("abstract");
		if (Modifier.isNative(modifiers))
			entity.addModifiers("native");
		if (Modifier.isSynchronized(modifiers))
			entity.addModifiers("synchronized");
		if (Modifier.isTransient(modifiers))
			entity.addModifiers("transient");
		if (Modifier.isVolatile(modifiers))
			entity.addModifiers("volatile");
		/*
		 * We do not extract the static modifier here because we want to set the
		 * hasClassScope property and we do that specifically only for attributes and
		 * methods
		 */
	}

	// EXPORT

	public void exportMSE(String fileName) {
		try {
			repository.exportMSE(Files.newBufferedWriter(Paths.get(fileName), StandardCharsets.UTF_8));
		} catch (IOException e) {
			throw new IllegalStateException(e);
		}
	}

	public void logNullBinding(String string, Object extraData, int lineNumber) {
		logger.error("unresolved " + string + " - " + extraData + " - " + currentFilePath + " - line " + lineNumber);
	}

}