package es.uniovi.reflection.patternmining.typedefs;

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

import javax.lang.model.element.Modifier;

import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.TypeParameterTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;

import es.uniovi.reflection.patternmining.category.ExpressionRole;
import es.uniovi.reflection.patternmining.category.Others;
import es.uniovi.reflection.patternmining.category.StatementCategory;
import es.uniovi.reflection.patternmining.category.StatementRole;
import es.uniovi.reflection.patternmining.features.ExpressionFeatures;
import es.uniovi.reflection.patternmining.features.FieldFeatures;
import es.uniovi.reflection.patternmining.features.MethodFeatures;
import es.uniovi.reflection.patternmining.features.StatementFeatures;
import es.uniovi.reflection.patternmining.features.TypeDefFeatures;
import es.uniovi.reflection.patternmining.model.ReturnFeatures;
import es.uniovi.reflection.patternmining.util.Util;
import es.uniovi.reflection.patternmining.util.Util.NodeType;

public class TypeDefVisitor extends TreeScanner<ReturnFeatures, TypeDefFeatures>{
	
	long fileId;
	
	@Override
    public ReturnFeatures visitClass(ClassTree classTree, TypeDefFeatures typeDefFeatures) {
		
		if(typeDefFeatures == null) {
			ReturnFeatures returnFeatures = new ReturnFeatures();
			returnFeatures.setMethodName("");
			return returnFeatures;
		}
        
        typeDefFeatures.setId(Util.getNextNodeId());
        
        typeDefFeatures.setHeight(typeDefFeatures.getHeight() + 1);
        
        ReturnFeatures returnFeatures = new ReturnFeatures();
       
        switch (classTree.getKind().toString()) {
	        case "INTERFACE":
	        	typeDefFeatures.setTypeDefinitionCategory(Util.NodeType.Interface);
	        	returnFeatures.setNumberOfInterfaces(returnFeatures.getNumberOfInterfaces()+1);
	            break;
	        case "ENUM":
	        	typeDefFeatures.setTypeDefinitionCategory(Util.NodeType.Enum);
	        	returnFeatures.setNumberOfEnums(returnFeatures.getNumberOfEnums()+1);
	            break;
	        default:
	        	typeDefFeatures.setTypeDefinitionCategory(Util.NodeType.Class);
	        	returnFeatures.setNumberOfClasses(returnFeatures.getNumberOfClasses()+1);
	        	break;
	    }
        
        typeDefFeatures.setSourceCode(Util.getNodeSourceCode(classTree.toString()));
        if(!classTree.getSimpleName().toString().isEmpty()) {
        	typeDefFeatures.setSourceCode(typeDefFeatures.getSourceCode().replaceFirst(classTree.getSimpleName().toString(), 
        				typeDefFeatures.getPackageName() + classTree.getSimpleName().toString()));
        }
        else {
        	JCClassDecl jccd = ((JCClassDecl)classTree);
        	typeDefFeatures.setSourceCode(typeDefFeatures.getPackageName() + "anonymous" + jccd.pos);
        }
        if("".equals(typeDefFeatures.getPackageName())) {
        	typeDefFeatures.setIsInDefaultPackage(true); 
        	returnFeatures.setNumberOfTypesInDefaultPackage(returnFeatures.getNumberOfTypesInDefaultPackage()+1);
        }
        else{
        	typeDefFeatures.setIsInDefaultPackage(false); 
        	returnFeatures.setNumberOfTypesInPackages(returnFeatures.getNumberOfTypesInPackages()+1);
        }
        
        Util.setReturnName(classTree.getSimpleName().toString(),typeDefFeatures.getTypeDefinitionCategory());
        
        //Just for classes
        if(typeDefFeatures.getTypeDefinitionCategory() ==  Util.NodeType.Class) {
        	typeDefFeatures.setFinalClass(classTree.getModifiers().toString().contains("final"));
        	typeDefFeatures.setNumberImplements(classTree.getImplementsClause().size());
        }
        
        //Just for classes or interfaces     
        if(typeDefFeatures.getTypeDefinitionCategory() ==  Util.NodeType.Class || typeDefFeatures.getTypeDefinitionCategory() ==  Util.NodeType.Interface ) {
        	if(classTree.getExtendsClause() != null) {
            	typeDefFeatures.setHasExtends(true);            	
            }
        }
        
        //Just for classes or enum     
        if(typeDefFeatures.isNestedClass()) {
        	 //Inner and nested classes
            try {
	        	if (classTree.getModifiers().getFlags().contains(Modifier.STATIC)){
	        		typeDefFeatures.setStatic(true);
	        		returnFeatures.setNumberOfStaticNestedTypes(typeDefFeatures.getNumberStaticNestedTypes() + 1);
	        	}else {
	        		returnFeatures.setNumberOfInnerTypes(typeDefFeatures.getNumberInnerTypes() + 1);
	        	}              
            }catch(Exception e) {
            	
            }
        }
        
        //Generic
        typeDefFeatures.setPublic(classTree.getModifiers().getFlags().toString().contains("public"));
        
        typeDefFeatures.setAbstract(classTree.getModifiers().getFlags().contains(Modifier.ABSTRACT));
        
        typeDefFeatures.setStrictFP(classTree.getModifiers().getFlags().contains(Modifier.STRICTFP));
        
        typeDefFeatures.setNumberAnnotations(classTree.getModifiers().getAnnotations().size());
        
        typeDefFeatures.setTypeNaming(Util.getNamingCovention(classTree.getSimpleName().toString()));
        
        if (classTree.getModifiers().getFlags().contains(Modifier.STATIC))
    		typeDefFeatures.setStatic(true);
        
        
        visitModifiers(classTree.getModifiers(), typeDefFeatures);
        
        typeDefFeatures.setInnerId(typeDefFeatures.getId());
        
        scan(classTree.getTypeParameters(),typeDefFeatures);
        
        //Method names values
       List<String> methodNames = new ArrayList<String>();
       for(Tree st: classTree.getMembers()) {
    	   ReturnFeatures auxReturnFeatures = st.accept(this, null);
    	   if(auxReturnFeatures.getMethodName() != "") {
    		   methodNames.add(auxReturnFeatures.getMethodName());
    	   }
       }
       
       Map<String, Integer> mapMethodNames = new HashMap<String, Integer>();
       for(String methodName : methodNames)
       {
           Integer value = mapMethodNames.get(methodName);
           if (value != null) {
        	   mapMethodNames.put(methodName, value + 1);
           } else {
               // No such key
        	   mapMethodNames.put(methodName, 1);
           }
       }
       
       int totalNumberMethods = 0;
       int totalOverloadedMethods = 0;
       for (Map.Entry<String, Integer> entry : mapMethodNames.entrySet()) {
    	   totalNumberMethods += entry.getValue();
    	   if(entry.getValue() > 1) totalOverloadedMethods+=entry.getValue();
    	}
       
     //total of overloaded classes
       if(totalNumberMethods > 0)
       typeDefFeatures.setPercentageOverloadedMethods((totalOverloadedMethods * 100.0f) / totalNumberMethods);
        
        for(Tree st: classTree.getMembers()) {
        		
        	TypeDefFeatures typeDefFeaturesChild = new TypeDefFeatures();
        	typeDefFeaturesChild.setParentId(typeDefFeatures.getId());
        	typeDefFeaturesChild.setNestedClass(true);
        	typeDefFeaturesChild.setFileId(0);
        	typeDefFeaturesChild.setPackageName(typeDefFeatures.getPackageName());
        	typeDefFeaturesChild.setTypeDefinitionName(classTree.getSimpleName().toString());
        	typeDefFeaturesChild.setTypeDefinitionCategory(typeDefFeatures.getTypeDefinitionCategory());
        	typeDefFeaturesChild.setInnerId(typeDefFeatures.getId());
        	typeDefFeaturesChild.setHeight(typeDefFeatures.getHeight() + 1); //actualizo la altura
        	typeDefFeaturesChild.setMapMethodNames(mapMethodNames);
            
            //nested, inner and nested static
            typeDefFeaturesChild.setNestedClass(true);
		      if (classTree.getModifiers().getFlags().contains(Modifier.STATIC))
		    	  typeDefFeaturesChild.setStatic(true);
		      else
		    	  typeDefFeaturesChild.setInnerClass(true);	   
        	
        	ReturnFeatures auxReturnFeatures = st.accept(this, typeDefFeaturesChild);
        	
        	returnFeatures.setNumberOfClasses(returnFeatures.getNumberOfClasses() + auxReturnFeatures.getNumberOfClasses());
        	returnFeatures.setNumberOfInterfaces(returnFeatures.getNumberOfInterfaces() + auxReturnFeatures.getNumberOfInterfaces());
        	returnFeatures.setNumberOfEnums(returnFeatures.getNumberOfEnums() + auxReturnFeatures.getNumberOfEnums());
        	typeDefFeatures.setNumberStaticBlocks(typeDefFeatures.getNumberStaticBlocks() + auxReturnFeatures.getNumberOfStaticBlocks());
        	
        	//method
        	if(auxReturnFeatures.getNumberOfMethods() > 0) {
        		typeDefFeatures.setNumberOfStaticMethods(typeDefFeatures.getNumberOfStaticMethods() + auxReturnFeatures.getNumberOfStaticMethods());
        		typeDefFeatures.setNumberMethods(typeDefFeatures.getNumberMethods() + auxReturnFeatures.getNumberOfMethods());
            	typeDefFeatures.setNumberConstructors(typeDefFeatures.getNumberConstructors() + auxReturnFeatures.getNumberOfConstructors());
            	
            	methodNames.add(returnFeatures.getMethodName());
        	} else {
        		typeDefFeatures.setNumberOfStaticFields(typeDefFeatures.getNumberOfStaticFields() + auxReturnFeatures.getNumberOfStaticFields());
            	typeDefFeatures.setNumberFields(typeDefFeatures.getNumberFields() + auxReturnFeatures.getNumberOfFields());
        	}
        	
        	typeDefFeatures.setNumberInnerTypes(typeDefFeatures.getNumberInnerTypes() + auxReturnFeatures.getNumberOfInnerTypes());
        	typeDefFeatures.setNumberStaticNestedTypes(typeDefFeatures.getNumberStaticNestedTypes() + auxReturnFeatures.getNumberOfStaticNestedTypes());
        	
      
        }
        
        //total number of methods
        typeDefFeatures.setNumberMethods(typeDefFeatures.getNumberMethods() - typeDefFeatures.getNumberConstructors());
        
        
        //total number of nested classes
        typeDefFeatures.setNumberNestedTypes(typeDefFeatures.getNumberStaticNestedTypes() + typeDefFeatures.getNumberInnerTypes());
        
        if(!typeDefFeatures.isNestedClass() && typeDefFeatures.getTypeDefinitionCategory() == NodeType.Class)
        	returnFeatures.setNumberOfClasses(typeDefFeatures.getNumberNestedTypes() + 1);
        
        //percentages static fields and methods
        
    	if(typeDefFeatures.getNumberOfStaticFields() > 0 && typeDefFeatures.getNumberFields() > 0) {
    		float staticFieldPercentage = (typeDefFeatures.getNumberOfStaticFields() * 100.0f) / typeDefFeatures.getNumberFields();
        	typeDefFeatures.setStaticFieldPercentage(staticFieldPercentage);
    	}
        	
        
    	if(typeDefFeatures.getNumberOfStaticMethods() > 0 && typeDefFeatures.getNumberMethods() > 0) {
    		typeDefFeatures.setStaticMethodPercentage((typeDefFeatures.getNumberOfStaticMethods() * 100.0f) / typeDefFeatures.getNumberMethods());
    	}
        
        	
        //Insertion
        try {     
        	Util.databaseService.insertTypeDefinition(typeDefFeatures);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

        return returnFeatures;
    }
	
	@Override
    public ReturnFeatures visitMethod(MethodTree methodTree, TypeDefFeatures typeDefFeatures) {
		
		if(typeDefFeatures == null) {
			ReturnFeatures returnFeatures = new ReturnFeatures();
			returnFeatures.setMethodName(methodTree.getName().toString());
			return returnFeatures;
		}
		
		//It's a method
		ReturnFeatures returnFeatures = new ReturnFeatures();
		returnFeatures.setNumberOfMethods(1);
		
		//check if the method is static
		if(methodTree.getModifiers().getFlags().contains(Modifier.STATIC))
			returnFeatures.setNumberOfStaticMethods(1);
		
        //Method call
        MethodFeatures methodFeatures = new MethodFeatures();
        methodFeatures.setIdParent(typeDefFeatures.getParentId());
        methodFeatures.setHeight(typeDefFeatures.getHeight());
        methodFeatures.setPackageName(typeDefFeatures.getPackageName());
        methodFeatures.setTypeDefinitionName(typeDefFeatures.getTypeDefinitionName() + ".");
        
        Map<String, Integer> mapMethodNames = typeDefFeatures.getMapMethodNames();
        methodFeatures.setNumberOfOverloaded(mapMethodNames.get(methodTree.getName().toString()));
        
        ReturnFeatures auxReturnFeatures = Util.visitors.getVisitorMethods().visitMethod(methodTree, methodFeatures);
        
        returnFeatures.setNumberOfConstructors(auxReturnFeatures.getNumberOfConstructors());
        returnFeatures.setNumberOfOverloaded(auxReturnFeatures.getNumberOfOverloaded());
        
    	returnFeatures.setNumberOfInnerTypes(returnFeatures.getNumberOfInnerTypes() + auxReturnFeatures.getNumberOfInnerTypes());
    	returnFeatures.setNumberOfStaticNestedTypes(returnFeatures.getNumberOfStaticNestedTypes() + auxReturnFeatures.getNumberOfStaticNestedTypes());
    	
    	returnFeatures.setMethodName(methodTree.getName().toString());
        return returnFeatures;
    }
	
	@Override
    public ReturnFeatures visitVariable(VariableTree variableTree, TypeDefFeatures typeDefFeatures) {
		
		if(typeDefFeatures == null) {
			ReturnFeatures returnFeatures = new ReturnFeatures();
			returnFeatures.setMethodName("");
			return returnFeatures;
		}
				
		FieldFeatures fieldFeatures = new FieldFeatures();		
		fieldFeatures.setIdParent(typeDefFeatures.getParentId());
		fieldFeatures.setHeight(typeDefFeatures.getHeight()); 
		fieldFeatures.setParentNode(typeDefFeatures.getTypeDefinitionCategory());
		
		ReturnFeatures returnFeatures = Util.visitors.getVisitorFields().visitVariable(variableTree, fieldFeatures);
		
		returnFeatures.setNumberOfFields(1);
		//check if the method is static
		if(variableTree.getModifiers().getFlags().contains(Modifier.STATIC))
			returnFeatures.setNumberOfStaticFields(1);
        return returnFeatures;
    }
	
	//es un statement pero lo manejamos aquí (static block)
	@Override
    public ReturnFeatures visitBlock(BlockTree blockTree, TypeDefFeatures typeDefFeatures) {
		//Static block
		if(typeDefFeatures == null) {
			ReturnFeatures returnFeatures = new ReturnFeatures();
			returnFeatures.setMethodName("");
			return returnFeatures;
		}
		
        ReturnFeatures returnFeatures = new ReturnFeatures();
		
		if (blockTree.isStatic()) {
			StatementFeatures statementFeatures = new StatementFeatures();
			statementFeatures.setId(Util.getNextNodeId());
	        statementFeatures.setParentId(typeDefFeatures.getParentId());
			statementFeatures.setParentNode(Others.Class);
			statementFeatures.setParentChild(StatementRole.StaticBlockConstructor);
	        
	        statementFeatures.setStatementCategory(StatementCategory.StaticBlock);
	        
	        statementFeatures.setHeight(statementFeatures.getHeight() + 1);
	        
	        StatementFeatures statementChildFeatures = Util.setStatementChildFeatures(statementFeatures, StatementRole.StaticBlockConstructor);
	        ReturnFeatures returnAuxFeatures = Util.visitors.getVisitorStatements().visitBlock(blockTree, statementChildFeatures);
	        Util.updateBlockStatementFeatures(statementFeatures, returnFeatures, returnAuxFeatures);

			try {
				Util.databaseService.insertStatement(statementFeatures);
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			returnFeatures.setNumberOfStaticBlocks(1);
		}
		
		
		//actualizar el numero de clases
		return returnFeatures;
    }
    
    public ReturnFeatures visitModifiers(ModifiersTree modifiersTree, TypeDefFeatures typeDefFeatures) {
    	ReturnFeatures returnFeatures = new ReturnFeatures();
    	
    	for(AnnotationTree annotation: modifiersTree.getAnnotations()) {
    		returnFeatures = scan(annotation, typeDefFeatures);
    	}

		return returnFeatures;
	}
	
	@Override
    public ReturnFeatures visitAnnotation(AnnotationTree annotationTree, TypeDefFeatures typeDefFeatures)  { //hoja
      
        ExpressionFeatures expressionFeatures = new ExpressionFeatures();
        
        expressionFeatures.setHeight(typeDefFeatures.getHeight()); 
        expressionFeatures.setParentId(typeDefFeatures.getId());
        expressionFeatures.setParentNode(Others.TypeDefinition);
        expressionFeatures.setParentChild(ExpressionRole.TypeDefinitionAnnotation);  
        
        ReturnFeatures returnFeatures = Util.visitors.getVisitorExpressions().visitAnnotation(annotationTree, expressionFeatures); 
        return returnFeatures;
    }
	
	@Override
	public ReturnFeatures visitTypeParameter(TypeParameterTree typeParameterTree, TypeDefFeatures typeDefFeatures) {
		typeDefFeatures.setNumberGenericTypes(typeDefFeatures.getNumberGenericTypes() + 1);
		ReturnFeatures returnFeatures = new ReturnFeatures();
		return returnFeatures;
	}
}
