package apidiff.internal.visitor;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import apidiff.enums.Classifier;
import apidiff.internal.analysis.comparator.ComparatorMethod;
import apidiff.internal.service.git.GitFile;
import apidiff.internal.util.UtilTools;

public class APIVersion {
	
	private ArrayList<TypeDeclaration> apiAccessibleTypes = new ArrayList<TypeDeclaration>();
	private ArrayList<TypeDeclaration> apiNonAccessibleTypes = new ArrayList<TypeDeclaration>();
	private ArrayList<EnumDeclaration> apiAccessibleEnums = new ArrayList<EnumDeclaration>();
	private ArrayList<EnumDeclaration> apiNonAccessibleEnums = new ArrayList<EnumDeclaration>();
	private Map<ChangeType, List<GitFile>> mapModifications = new HashMap<ChangeType, List<GitFile>>();
	private List<String> listFilesMofify = new ArrayList<String>();
	private Classifier classifierAPI;
	private String nameProject;
	
	private Logger logger = LoggerFactory.getLogger(APIVersion.class);
	
	private String path;

	public APIVersion(final String path, final File file, final Map<ChangeType, List<GitFile>> mapModifications, Classifier classifierAPI) {
		
		try {
			this.classifierAPI = classifierAPI;
			this.mapModifications = mapModifications;
			this.path = path;
			this.nameProject = file.getAbsolutePath().replaceAll(this.path + "/", "");
	    	String prefix = file.getAbsolutePath() + "/";
			for(ChangeType changeType : this.mapModifications.keySet()){
				for(GitFile gitFile: mapModifications.get(changeType)){
					if(gitFile.getPathOld()!= null){
						this.listFilesMofify.add(prefix + gitFile.getPathOld());
					}
					if(gitFile.getPathNew() != null && !gitFile.getPathNew().equals(gitFile.getPathOld())){
						this.listFilesMofify.add(prefix + gitFile.getPathNew());
					}
				}
			}
			this.parseFilesInDir(file, false);
		} catch (IOException e) {
			this.logger.error("Erro ao criar APIVersion", e);
		}
	}
	
	public APIVersion(final String nameProject, Classifier classifierAPI) {
		try {
			this.nameProject = nameProject;
			this.classifierAPI = classifierAPI;
			File path = new File(this.path + "/" + this.nameProject);
			this.parseFilesInDir(path, true);
		} catch (IOException e) {
			this.logger.error("Erro ao criar APIVersion", e);
		}
		
	}

	public void parseFilesInDir(File file, final Boolean ignoreTreeDiff) throws IOException {
		if (file.isFile()) {
			String simpleNameFile = UtilTools.getSimpleNameFileWithouPackageWithNameLibrary(this.path, file.getAbsolutePath(), this.nameProject);
			if (UtilTools.isJavaFile(file.getName()) && this.isFileModification(file, ignoreTreeDiff) && UtilTools.isAPIByClassifier(simpleNameFile, this.classifierAPI)) {
				this.parse(UtilTools.readFileToString(file.getAbsolutePath()), file, ignoreTreeDiff);		
			}
		} else {
			if(file.listFiles() != null){
				for (File f : file.listFiles()) {
					this.parseFilesInDir(f, ignoreTreeDiff);
				}
			}
		}
	}

	public void parse(String str, File source, final Boolean ignoreTreeDiff) throws IOException {
		
		if(this.mapModifications.size() > 0 && !this.isFileModification(source,ignoreTreeDiff)){
			return;
		}
		ASTParser parser = ASTParser.newParser(AST.JLS8);
		parser.setSource(str.toCharArray());
		parser.setKind(ASTParser.K_COMPILATION_UNIT);
		String[] classpath = java.lang.System.getProperty("java.class.path").split(";");
		String[] sources = { source.getParentFile().getAbsolutePath() };

		Hashtable<String, String> options = JavaCore.getOptions();
		options.put(JavaCore.COMPILER_SOURCE, JavaCore.VERSION_1_8);
		options.put(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, JavaCore.VERSION_1_8);
		options.put(JavaCore.COMPILER_COMPLIANCE, JavaCore.VERSION_1_8);
		parser.setUnitName(source.getAbsolutePath());

		parser.setCompilerOptions(options);
//		parser.setEnvironment(null, sources, new String[] { "UTF-8" },	true);
		parser.setResolveBindings(true);
		parser.setBindingsRecovery(true);

//		parser.setEnvironment(classpath, sources, new String[] { "UTF-8" },	true);
//		CompilationUnit compilationUnit = null;
		try {
			parser.setEnvironment(null, null, null,	true);
			CompilationUnit compilationUnit = (CompilationUnit) parser.createAST(null);
			TypeDeclarationVisitor visitorType = new TypeDeclarationVisitor();
			EnumDeclarationVisitor visitorEnum = new EnumDeclarationVisitor();
			
			compilationUnit.accept(visitorType);
			compilationUnit.accept(visitorEnum);
			
			this.configureAcessiblesAndNonAccessibleTypes(visitorType);
			this.configureAcessiblesAndNonAccessibleEnums(visitorEnum);
		} catch (Exception e) {
			this.logger.error("Erro ao criar AST sem source", e);
		}

	}
	
	private void configureAcessiblesAndNonAccessibleTypes(TypeDeclarationVisitor visitorType){
		this.apiNonAccessibleTypes.addAll(visitorType.getNonAcessibleTypes());
		this.apiAccessibleTypes.addAll(visitorType.getAcessibleTypes());
	}
	
	private void configureAcessiblesAndNonAccessibleEnums(EnumDeclarationVisitor visitorType){
		this.apiNonAccessibleEnums.addAll(visitorType.getNonAcessibleEnums());
		this.apiAccessibleEnums.addAll(visitorType.getAcessibleEnums());
	}
	
	private Boolean isFileModification(final File source, final Boolean ignoreTreeDiff){
		return (ignoreTreeDiff || this.listFilesMofify.contains(source.getAbsolutePath()))? true: false;
	}

	public ArrayList<EnumDeclaration> getApiAccessibleEnums() {
		return apiAccessibleEnums;
	}

	public ArrayList<EnumDeclaration> getApiNonAccessibleEnums() {
		return apiNonAccessibleEnums;
	}

	public ArrayList<TypeDeclaration> getApiAcessibleTypes(){
		return this.apiAccessibleTypes;
	}
	
	public ArrayList<TypeDeclaration> getApiNonAcessibleTypes(){
		return this.apiNonAccessibleTypes;
	}

	public ArrayList<AbstractTypeDeclaration> getTypesPublicAndProtected() {
		ArrayList<AbstractTypeDeclaration> list = new ArrayList<AbstractTypeDeclaration>();
		list.addAll(this.getApiAcessibleTypes());
		list.addAll(this.getApiAccessibleEnums());
		return list;
	}
	

	public ArrayList<AbstractTypeDeclaration> getTypesPrivateAndDefault() {
		ArrayList<AbstractTypeDeclaration> list = new ArrayList<AbstractTypeDeclaration>();
		list.addAll(this.getApiNonAcessibleTypes());
		list.addAll(this.getApiNonAccessibleEnums());
		return list;
	}

	public EnumDeclaration getVersionNonAccessibleEnum(EnumDeclaration enumVersrionReference){
		for (EnumDeclaration enumDeclarion : this.apiNonAccessibleEnums) {
			if(enumDeclarion.resolveBinding() != null && enumVersrionReference.resolveBinding() != null){
				if(enumDeclarion.resolveBinding().getQualifiedName().equals(enumVersrionReference.resolveBinding().getQualifiedName())){
					return enumDeclarion;
				}
			}
		}

		return null;
	}

	public EnumDeclaration getVersionAccessibleEnum(EnumDeclaration enumVersrionReference){
		for (EnumDeclaration enumDeclarion : this.apiAccessibleEnums) {
			if(enumDeclarion.resolveBinding() != null && enumVersrionReference.resolveBinding() != null){
				if(enumDeclarion.resolveBinding().getQualifiedName().equals(enumVersrionReference.resolveBinding().getQualifiedName())){
					return enumDeclarion;
				}
			}
		}

		return null;
	}

	public AbstractTypeDeclaration getVersionNonAccessibleType(AbstractTypeDeclaration typeVersrionReference){
		for (AbstractTypeDeclaration typeDeclarion : this.getTypesPrivateAndDefault()) {
			if(typeDeclarion.resolveBinding() != null && typeVersrionReference.resolveBinding() != null){
				if(typeDeclarion.resolveBinding().getQualifiedName().equals(typeVersrionReference.resolveBinding().getQualifiedName())){
					return typeDeclarion;
				}
			}
		}

		return null;
	}
	

	public AbstractTypeDeclaration getVersionAccessibleType(AbstractTypeDeclaration typeVersrionReference){
		for (AbstractTypeDeclaration typeDeclarion : this.getTypesPublicAndProtected()) {
			if(typeDeclarion.resolveBinding() != null && typeVersrionReference.resolveBinding() != null){
				if(typeDeclarion.resolveBinding().getQualifiedName().equals(typeVersrionReference.resolveBinding().getQualifiedName())){
					return typeDeclarion;
				}
			}
		}
		return null;
	}
	
	public boolean containsType(TypeDeclaration type){
		return this.containsAccessibleType(type) || this.containsNonAccessibleType(type);
	}
	
	public boolean containsAccessibleType(AbstractTypeDeclaration type){
		return this.getVersionAccessibleType(type) != null;
	}
	
	public boolean containsNonAccessibleType(AbstractTypeDeclaration type){
		return this.getVersionNonAccessibleType(type) != null;
	}
	
	public boolean containsAccessibleEnum(EnumDeclaration type){
		return this.getVersionAccessibleEnum(type) != null;
	}

	public boolean containsNonAccessibleEnum(EnumDeclaration type){
		return this.getVersionNonAccessibleEnum(type) != null;
	}

	public FieldDeclaration getVersionField(FieldDeclaration field, TypeDeclaration type){
		for (TypeDeclaration versionType : this.apiAccessibleTypes) {
			if(versionType.getName().toString().equals(type.getName().toString())){
				for (FieldDeclaration versionField : versionType.getFields()) {
					String name1 = UtilTools.getFieldName(versionField);
					String name2  = UtilTools.getFieldName(field);
					if(name1 != null && name2 != null && name1.equals(name2)){
						return versionField;
					}
				}
			}
		}
		return null;
	}

	public ArrayList<MethodDeclaration> getAllEqualMethodsByName(MethodDeclaration method, TypeDeclaration type) {
		ArrayList<MethodDeclaration> result = new ArrayList<MethodDeclaration>();
		for (TypeDeclaration versionType : this.apiAccessibleTypes) {
			if(versionType.getName().toString().equals(type.getName().toString())){
				for(MethodDeclaration versionMethod : versionType.getMethods()){
					if(versionMethod.getName().toString().equals(method.getName().toString()))
						result.add(versionMethod);
				}
			}
		}
		return result;
	}
	
	public MethodDeclaration findMethodByNameAndParametersAndReturn(MethodDeclaration method, TypeDeclaration type){
		MethodDeclaration methodVersionOld = null;
		for (TypeDeclaration versionType : this.apiAccessibleTypes) {
			if(versionType.getName().toString().equals(type.getName().toString())){
				for(MethodDeclaration versionMethod : versionType.getMethods()){
					if(!ComparatorMethod.isDiffMethodByNameAndParametersAndReturn(versionMethod, method)){
						methodVersionOld =  versionMethod;
					}
				}
			}
		}
		return methodVersionOld;
	}
	
	private MethodDeclaration findMethodByNameAndReturn(MethodDeclaration method, TypeDeclaration type){
		MethodDeclaration methodVersionOld = null;
		for (TypeDeclaration versionType : this.apiAccessibleTypes) {
			if(versionType.getName().toString().equals(type.getName().toString())){
				for(MethodDeclaration versionMethod : versionType.getMethods()){
					if(!ComparatorMethod.isDiffMethodByNameAndReturn(versionMethod, method)){
						methodVersionOld =  versionMethod;
					}
				}
			}
		}
		return methodVersionOld;
	}
	
	public MethodDeclaration findMethodByNameAndParameters(MethodDeclaration method, TypeDeclaration type){
		MethodDeclaration methodVersionOld = null;
		for (TypeDeclaration versionType : this.apiAccessibleTypes) {
			if(versionType.getName().toString().equals(type.getName().toString())){
				for(MethodDeclaration versionMethod : versionType.getMethods()){
					if(!ComparatorMethod.isDiffMethodByNameAndParameters(versionMethod, method)){
						methodVersionOld =  versionMethod;
					}
				}
			}
		}
		return methodVersionOld;
	}

	public MethodDeclaration getEqualVersionMethod(MethodDeclaration method, TypeDeclaration type){
		for(MethodDeclaration methodInThisVersion : this.getAllEqualMethodsByName(method, type)){
			if(UtilTools.isEqualMethod(method, methodInThisVersion)){
				return methodInThisVersion;
			}
		}
		return null;
	}
	
	public EnumConstantDeclaration getEqualVersionConstant(EnumConstantDeclaration constant, EnumDeclaration enumReference) {
		EnumDeclaration thisVersionEnum = this.getVersionAccessibleEnum(enumReference);
		for(Object thisVersionConstant : thisVersionEnum.enumConstants()){
			if(((EnumConstantDeclaration)thisVersionConstant).getName().toString().equals(constant.getName().toString()))
				return ((EnumConstantDeclaration)thisVersionConstant);
		}

		return null;
	}
	
	public List<AbstractTypeDeclaration> getAllTypes(){
		List<AbstractTypeDeclaration> listTypesVersion = new ArrayList<AbstractTypeDeclaration>();
		listTypesVersion.addAll(this.getTypesPublicAndProtected());
		listTypesVersion.addAll(this.getTypesPrivateAndDefault());
		return listTypesVersion;
	}
	
}