package apidiff.internal.analysis;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jgit.revwalk.RevCommit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import apidiff.Change;
import apidiff.enums.Category;
import apidiff.enums.ElementType;
import apidiff.internal.analysis.description.MethodDescription;
import apidiff.internal.util.UtilTools;
import apidiff.internal.visitor.APIVersion;
import refdiff.core.api.RefactoringType;
import refdiff.core.rm2.model.refactoring.SDRefactoring;

public class MethodDiff {
	
	private List<Change> listChange = new ArrayList<Change>();
	
	private Logger logger = LoggerFactory.getLogger(MethodDiff.class);
	
	private MethodDescription description = new MethodDescription();
	
	private Map<RefactoringType, List<SDRefactoring>> refactorings = new HashMap<RefactoringType, List<SDRefactoring>>();
	
	private List<String> methodsWithPathChanged = new ArrayList<String>();
	
	private RevCommit revCommit;

	public List<Change> detectChange(final APIVersion version1, final APIVersion version2, final Map<RefactoringType, List<SDRefactoring>> refactorings, final RevCommit revCommit) {
		this.logger.info("Processing Methods...");
		this.refactorings = refactorings;
		this.revCommit = revCommit;
		this.findRemoveAndRefactoringMethods(version1, version2);
		this.findChangedVisibilityMethods(version1, version2);
		this.findChangedReturnTypeMethods(version1, version2);
		this.findChangedExceptionTypeMethods(version1, version2);
		this.findChangedFinalAndStatic(version1, version2);
		this.findAddedMethods(version1, version2);
		this.findAddedDeprecatedMethods(version1, version2);
		return this.listChange;
	}

	private void addChange(final TypeDeclaration type, final MethodDeclaration method, Category category, Boolean isBreakingChange, final String description){
		Change change = new Change();
		change.setJavadoc(UtilTools.containsJavadoc(type, method));
		change.setDeprecated(this.isDeprecated(method, type));
		change.setBreakingChange(this.isDeprecated(method, type) ? false : isBreakingChange);
		change.setPath(UtilTools.getPath(type));
		change.setElement(this.getFullNameMethod(method));
		change.setCategory(category);
		change.setDescription(description);
		change.setRevCommit(this.revCommit);
		change.setElementType(method.isConstructor()? ElementType.CONSTRUCTOR: ElementType.METHOD);
		this.listChange.add(change);
	}
	
	private List<SDRefactoring> getAllRefactorOperations(){
		List<SDRefactoring> listMove = new ArrayList<SDRefactoring>();
		if(this.refactorings.containsKey(RefactoringType.PULL_UP_OPERATION)){
			listMove.addAll(this.refactorings.get(RefactoringType.PULL_UP_OPERATION));
		}
		if(this.refactorings.containsKey(RefactoringType.PUSH_DOWN_OPERATION)){
			listMove.addAll(this.refactorings.get(RefactoringType.PUSH_DOWN_OPERATION));
		}
		if(this.refactorings.containsKey(RefactoringType.MOVE_OPERATION)){
			listMove.addAll(this.refactorings.get(RefactoringType.MOVE_OPERATION));
		}
		if(this.refactorings.containsKey(RefactoringType.INLINE_OPERATION)){
			listMove.addAll(this.refactorings.get(RefactoringType.INLINE_OPERATION));
		}
		if(this.refactorings.containsKey(RefactoringType.RENAME_METHOD)){
			listMove.addAll(this.refactorings.get(RefactoringType.RENAME_METHOD));
		}
		if(this.refactorings.containsKey(RefactoringType.EXTRACT_OPERATION)){
			listMove.addAll(this.refactorings.get(RefactoringType.EXTRACT_OPERATION));
		}
		return listMove;
	}
	
	private Category getCategory(RefactoringType refactoringType){
		Category category;
		switch (refactoringType) {
			case MOVE_OPERATION:
				category = Category.METHOD_MOVE;
				break;
	
			case PUSH_DOWN_ATTRIBUTE:
				category = Category.METHOD_PUSH_DOWN;
				break;
				
			case INLINE_OPERATION:
				category = Category.METHOD_INLINE;
				break;
				
			case RENAME_METHOD:
				category = Category.METHOD_RENAME;
				break;
				
			case EXTRACT_OPERATION:
				category = Category.METHOD_EXTRACT;
				break;
				
			default:
				category = Category.METHOD_PULL_UP;
				break;
		}
		return category;
	}
	
	/**
	 * Finding refactoring operations in methods (move, pull up, push down, inline method, rename)
	 * @param method
	 * @param type
	 * @return
	 */
	private Boolean processRefactorMethod(final MethodDeclaration method, final TypeDeclaration type){
		List<SDRefactoring> listMove = this.getAllRefactorOperations();
		if(listMove != null){
			for(SDRefactoring ref : listMove){
				String fullNameAndPath = this.getFullNameMethodAndPath(method, type);
				if(fullNameAndPath.equals(ref.getEntityBefore().fullName())){
					Boolean isBreakingChange = (RefactoringType.PULL_UP_OPERATION.equals(ref.getRefactoringType()) || RefactoringType.EXTRACT_OPERATION.equals(ref.getRefactoringType()))? false:true;
					Category category = this.getCategory(ref.getRefactoringType());
					String description = this.description.refactorMethod(category, ref); 
					this.addChange(type, method, category, isBreakingChange, description);
					this.methodsWithPathChanged.add(ref.getEntityAfter().fullName());
					return true;
				}
			}
		}
		return false;
	}
	

	private Boolean processChangeListParametersMethod(final MethodDeclaration method, final TypeDeclaration type){
		List<SDRefactoring> listRename = this.refactorings.get(RefactoringType.CHANGE_METHOD_SIGNATURE);
		if(listRename != null){
			for(SDRefactoring ref : listRename){
				String fullNameAndPath = this.getFullNameMethodAndPath(method, type);
				if(fullNameAndPath.equals(ref.getEntityBefore().fullName())){
					String description = this.description.parameter(ref.getEntityAfter().simpleName(), ref.getEntityBefore().simpleName(), UtilTools.getPath(type)); 
					this.addChange(type, method, Category.METHOD_CHANGE_PARAMETER_LIST, true, description);
					this.methodsWithPathChanged.add(ref.getEntityAfter().fullName());
					return true;
				}
			}
		}
		return false;
	}
	
	private void processRemoveMethod(final MethodDeclaration method, final TypeDeclaration type){
		String description = this.description.remove(this.getSimpleNameMethod(method), UtilTools.getPath(type));
		this.addChange(type, method, Category.METHOD_REMOVE, true, description);
	}

	private Boolean checkAndProcessRefactoring(final MethodDeclaration method, final TypeDeclaration type){
		Boolean moveAndRename = this.processRefactorMethod(method, type);
		Boolean listParameters = this.processChangeListParametersMethod(method, type);
		return moveAndRename || listParameters;
	}
	
	/**
	 * @param parameterListException
	 * @param exception
	 * @return true, if the list contains the "exception"
	 */
	private boolean containsExceptionList(List<SimpleType> parameterListException, SimpleType exception){
		for(SimpleType simpleType : parameterListException){
			if(simpleType.getName().toString().equals(exception.getName().toString())){
				return true;
			}
		}
		return false;
	}
	
	/**
	 * @param methodDeclaration
	 * @return true, if is a accessible field by external systems
	 */
	private boolean isMethodAcessible(MethodDeclaration methodDeclaration){
		return methodDeclaration != null && methodDeclaration.resolveBinding() !=null && (UtilTools.isVisibilityProtected(methodDeclaration) || UtilTools.isVisibilityPublic(methodDeclaration))?true:false;
	}
	
	/**
	 * @param listExceptionsVersion1
	 * @param listExceptionsVersion2
	 * @return true, if the exception does not exist in exception list 2.
	 */
	private boolean diffListExceptions(List<SimpleType> listExceptionsVersion1, List<SimpleType> listExceptionsVersion2){
		for(SimpleType exceptionVersion1 : listExceptionsVersion1){
			if(!this.containsExceptionList(listExceptionsVersion2, exceptionVersion1)){
				return true;
			}
		}
		return false;
	}
	
	/**
	 * @param method
	 * @param type
	 * @return true, method is deprecated or type is deprecated
	 */
	private Boolean isDeprecated(MethodDeclaration method, AbstractTypeDeclaration type){
		Boolean isMethodDeprecated =  (method != null && method.resolveBinding() != null && method.resolveBinding().isDeprecated()) ? true: false;
		Boolean isTypeDeprecated = (type != null && type.resolveBinding() != null && type.resolveBinding().isDeprecated()) ? true: false;
		
		return isMethodDeprecated || isTypeDeprecated;
	}
	
	/**
	 * Finding methods with change in exception list 
	 * @param version1
	 * @param version2
	 */
	private void findChangedExceptionTypeMethods(APIVersion version1, APIVersion version2) {
		for(TypeDeclaration typeVersion1 : version1.getApiAcessibleTypes()){
			if(version2.containsAccessibleType(typeVersion1)){
				for(MethodDeclaration methodVersion1 : typeVersion1.getMethods()){
					if(this.isMethodAcessible(methodVersion1)){
						MethodDeclaration methodVersion2 = version2.getEqualVersionMethod(methodVersion1, typeVersion1);
						if(this.isMethodAcessible(methodVersion2)){
							List<SimpleType> exceptionsVersion1 = methodVersion1.thrownExceptionTypes();
							List<SimpleType> exceptionsVersion2 = methodVersion2.thrownExceptionTypes();
							if(exceptionsVersion1.size() != exceptionsVersion2.size() || (this.diffListExceptions(exceptionsVersion1, exceptionsVersion2))) {
								String nameMethod = this.getSimpleNameMethod(methodVersion1);
								String nameClass = UtilTools.getPath(typeVersion1);
								String description = this.description.exception(nameMethod, exceptionsVersion1, exceptionsVersion2, nameClass);
								this.addChange(typeVersion1, methodVersion1, Category.METHOD_CHANGE_EXCEPTION_LIST, true, description);
							}
						}
					}
				}
			}
		}
	}
	
	/**
	 * Finding methods with change in the return type
	 * @param version1
	 * @param version2
	 */
	private void findChangedReturnTypeMethods(APIVersion version1, APIVersion version2) {
		for(TypeDeclaration typeVersion1 : version1.getApiAcessibleTypes()){
			if(version2.containsAccessibleType(typeVersion1)){
				for(MethodDeclaration methodVersion1 : typeVersion1.getMethods()){
					if(this.isMethodAcessible(methodVersion1)){
						MethodDeclaration methodVersion2 = version2.findMethodByNameAndParametersAndReturn(methodVersion1, typeVersion1);
						if(methodVersion2 == null){
							methodVersion2 = version2.findMethodByNameAndParameters(methodVersion1, typeVersion1);
							if(methodVersion2 != null){
								String description = this.description.returnType(this.getSimpleNameMethod(methodVersion1), UtilTools.getPath(typeVersion1));
								this.addChange(typeVersion1, methodVersion1, Category.METHOD_CHANGE_RETURN_TYPE, true, description);
							}
						}
					}
				}
			}
		}
	}

	/**
	 * Finding methods with changed visibility
	 * @param typeVersion1
	 * @param methodVersion1
	 * @param methodVersion2
	 */
	private void checkGainOrLostVisibility(TypeDeclaration typeVersion1, MethodDeclaration methodVersion1, MethodDeclaration methodVersion2){
		if(methodVersion2 != null && methodVersion1!=null){
			String visibilityMethod1 = UtilTools.getVisibility(methodVersion1);
			String visibilityMethod2 = UtilTools.getVisibility(methodVersion2);
			if(!visibilityMethod1.equals(visibilityMethod2)){
				String description = this.description.visibility(this.getSimpleNameMethod(methodVersion2), UtilTools.getPath(typeVersion1), visibilityMethod1, visibilityMethod2);
				
				if(this.isMethodAcessible(methodVersion1) && !UtilTools.isVisibilityPublic(methodVersion2)){
					this.addChange(typeVersion1, methodVersion2, Category.METHOD_LOST_VISIBILITY, true, description);
				}
				else{
					
					Category category = UtilTools.isVisibilityDefault(methodVersion1) && UtilTools.isVisibilityPrivate(methodVersion2)? Category.METHOD_LOST_VISIBILITY: Category.METHOD_GAIN_VISIBILITY;
					this.addChange(typeVersion1, methodVersion2, category, false, description);
				}
			}
		}
	}
	
	/**
	 * Finding methods with changed visibility
	 * @param version1
	 * @param version2
	 */
	private void findChangedVisibilityMethods(APIVersion version1, APIVersion version2) {
		for(TypeDeclaration typeVersion1 : version1.getApiAcessibleTypes()){
			if(version2.containsAccessibleType(typeVersion1)){
				for(MethodDeclaration methodVersion1 : typeVersion1.getMethods()){
					MethodDeclaration methodVersion2 = version2.getEqualVersionMethod(methodVersion1, typeVersion1);
					this.checkGainOrLostVisibility(typeVersion1, methodVersion1, methodVersion2);
				}
			}
		}
	}

	/**
	 * Finding deprecated methods
	 * 
	 * @param version1
	 * @param version2
	 */
	private void findAddedDeprecatedMethods(APIVersion version1, APIVersion version2) {
		
		for(TypeDeclaration typeVersion2 : version2.getApiAcessibleTypes()){
			for(MethodDeclaration methodVersion2 : typeVersion2.getMethods()){
				
				if(this.isMethodAcessible(methodVersion2) && this.isDeprecated(methodVersion2, typeVersion2)){
					MethodDeclaration methodInVersion1 = version1.findMethodByNameAndParametersAndReturn(methodVersion2, typeVersion2);
					if(methodInVersion1 == null || !this.isDeprecated(methodInVersion1, version1.getVersionAccessibleType(typeVersion2))){
						String description = this.description.deprecate(this.getSimpleNameMethod(methodVersion2), UtilTools.getPath(typeVersion2));
						this.addChange(typeVersion2, methodVersion2, Category.METHOD_DEPRECATED, false, description);
					}
				}
			}
		}
	}

	/**
	 * Finding removed methods. If class was removed, class removal is a breaking change.
	 * @param version1
	 * @param version2
	 */
	private void findRemoveAndRefactoringMethods(APIVersion version1, APIVersion version2) {
		for (TypeDeclaration typeInVersion1 : version1.getApiAcessibleTypes()) {
			if(version2.containsAccessibleType(typeInVersion1)){
				for (MethodDeclaration methodInVersion1 : typeInVersion1.getMethods()) {
					if(this.isMethodAcessible(methodInVersion1)){
						MethodDeclaration methodInVersion2 = version2.findMethodByNameAndParameters(methodInVersion1, typeInVersion1);
						if(methodInVersion2 == null){
							Boolean refactoring = this.checkAndProcessRefactoring(methodInVersion1, typeInVersion1);
							if(!refactoring){
								this.processRemoveMethod(methodInVersion1, typeInVersion1);
							}
						}
					}
				}
			} 
		}
	}

	/**
	 * Finding added methods
	 * @param version1
	 * @param version2
	 */
	private void findAddedMethods(APIVersion version1, APIVersion version2) {
		for (TypeDeclaration typeInVersion2 : version2.getApiAcessibleTypes()) {
			if(version1.containsType(typeInVersion2)){
				for(MethodDeclaration methodInVersion2: typeInVersion2.getMethods()){
					if(this.isMethodAcessible(methodInVersion2)){
						MethodDeclaration methodInVersion1 = version1.findMethodByNameAndParameters(methodInVersion2, typeInVersion2);
						String fullNameAndPathMethodVersion2 = this.getFullNameMethodAndPath(methodInVersion2, typeInVersion2);
						if(methodInVersion1 == null && !this.methodsWithPathChanged.contains(fullNameAndPathMethodVersion2)){
							String nameMethod = this.getSimpleNameMethod(methodInVersion2);
							String nameClass = UtilTools.getPath(typeInVersion2);
							String description = this.description.addition(nameMethod, nameClass);
							this.addChange(typeInVersion2, methodInVersion2, Category.METHOD_ADD, false, description);
						}
					}
				}
			}
		}
	}
	
	/**
	 * Finding change in final modifier
	 * @param methodVersion1
	 * @param methodVersion2
	 */
	private void diffModifierFinal(TypeDeclaration typeVersion1, MethodDeclaration methodVersion1, MethodDeclaration methodVersion2){
		
		if((UtilTools.isFinal(methodVersion1) && UtilTools.isFinal(methodVersion2)) || ((!UtilTools.isFinal(methodVersion1) && !UtilTools.isFinal(methodVersion2)))){
			return;
		}
		String nameClass = UtilTools.getPath(typeVersion1);
		String nameMethod = this.getSimpleNameMethod(methodVersion2);
		
		if((!UtilTools.isFinal(methodVersion1) && UtilTools.isFinal(methodVersion2))){
			String description = this.description.modifierFinal(nameMethod, nameClass, true);
			this.addChange(typeVersion1, methodVersion1, Category.METHOD_ADD_MODIFIER_FINAL, true, description);
		}
		else{
			
			String description = this.description.modifierFinal(nameMethod, nameClass, false);
			this.addChange(typeVersion1, methodVersion1, Category.METHOD_REMOVE_MODIFIER_FINAL, false, description);
		}
	}
	
	/**
	 * Finding change in static modifier
	 * @param methodVersion1
	 * @param methodVersion2
	 */
	private void diffModifierStatic(TypeDeclaration typeVersion1, MethodDeclaration methodVersion1, MethodDeclaration methodVersion2){
		
		if((UtilTools.isStatic(methodVersion1) && UtilTools.isStatic(methodVersion2)) || ((!UtilTools.isStatic(methodVersion1) && !UtilTools.isStatic(methodVersion2)))){
			return;
		}
		String nameClass = UtilTools.getPath(typeVersion1);
		String nameMethod = this.getSimpleNameMethod(methodVersion2);
		
		if((!UtilTools.isStatic(methodVersion1) && UtilTools.isStatic(methodVersion2))){
			String description = this.description.modifierStatic(nameMethod, nameClass, true);
			this.addChange(typeVersion1, methodVersion1, Category.METHOD_ADD_MODIFIER_STATIC, false, description);
		}
		else{
			
			String description = this.description.modifierStatic(nameMethod, nameClass, false);
			this.addChange(typeVersion1, methodVersion1, Category.METHOD_REMOVE_MODIFIER_STATIC, true, description);
		}
	}
	
	/**
	 * Finding change in final/static modifiers
	 * @param version1
	 * @param version2
	 */
	private void findChangedFinalAndStatic(APIVersion version1, APIVersion version2) {
		
		for (TypeDeclaration typeVersion1 : version1.getApiAcessibleTypes()) {
			if(version2.containsType(typeVersion1)){//Se type ainda existe.
				for(MethodDeclaration methodVersion1: typeVersion1.getMethods()){
					MethodDeclaration methodVersion2 = version2.findMethodByNameAndParametersAndReturn(methodVersion1, typeVersion1);
					if(this.isMethodAcessible(methodVersion1) && (methodVersion2 != null)){
						this.diffModifierFinal(typeVersion1, methodVersion1, methodVersion2);
						this.diffModifierStatic(typeVersion1, methodVersion1, methodVersion2);
					}
				}
			}
		}
	}
	
	/**
	 * Returning method full name. [access modifier + return + name + (parameters list)]
	 * @param methodVersion
	 * @return
	 */
	private String getFullNameMethod(MethodDeclaration methodVersion){
		String nameMethod = "";
		if(methodVersion != null){
			String modifiersMethod = (methodVersion.modifiers() != null) ? (StringUtils.join(methodVersion.modifiers(), " ") + " ") : " ";
			String returnMethod = (methodVersion.getReturnType2() != null) ? (methodVersion.getReturnType2().toString() + " ") : "";
			String parametersMethod = (methodVersion.parameters() != null) ? StringUtils.join(methodVersion.parameters(), ", ") : " ";
			nameMethod = modifiersMethod + returnMethod + methodVersion.getName() + "(" + parametersMethod + ")";
		}
		return nameMethod;
	}
	
	/**
	 * Returning method name. Example: [name(parameters list)]
	 * @param methodVersion
	 * @return
	 */
	private String getSimpleNameMethod(MethodDeclaration methodVersion){
		String nameMethod = "";
		if(methodVersion != null){
			String parametersMethod = (methodVersion.parameters() != null) ? StringUtils.join(methodVersion.parameters(), ", ") : " ";
			nameMethod = methodVersion.getName() + "(" + parametersMethod + ")";
		}
		return nameMethod;
	}
	
	/**
	 * Returning type + method. Example: org.felines.Tiger#setAge(int)
	 * @param method
	 * @param type
	 * @return
	 */
	private String getFullNameMethodAndPath(final MethodDeclaration method, final TypeDeclaration type){
		String nameMethod = "";
		if(method != null){
			nameMethod = UtilTools.getPath(type) + "#" + method.getName() + "(" + this.getSignatureMethod(method) + ")";
		}
		return nameMethod;
	}
	
	private String getSignatureMethod(final MethodDeclaration method){
		List<String> signatureList = new ArrayList<String>();
		String signature = "";
		if(method != null){
			for(Object p : method.parameters()){
				String[] parameters = p.toString().split(" ");
				signatureList.add(parameters[parameters.length - 2]);
			}
			signature = StringUtils.join(signatureList, ", ");
		}
		return signature; 
	}

}