package de.tu_darmstadt.stg.mubench.utils;

import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.body.*;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.type.TypeParameter;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import com.google.common.base.Joiner;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

class MethodRetriever extends VoidVisitorAdapter<List<MethodExtractor.MethodCodeFragment>> {
    private static final String ctorId = ".ctor";

    private static final String bytecodeStaticInitializerId = "<clinit>()";
    private static final String sourcecodeStaticInitializerId = "static()";

    private String methodSignature;
    private Stack<String> currentEnclosingType;
    private List<TypeParameter> typeParameters;
    private boolean ignoreParameters;

    MethodRetriever(String methodSignature, boolean ignoreParameters) {
        this.methodSignature = normalize(methodSignature);
        this.ignoreParameters = ignoreParameters;
        this.currentEnclosingType = new Stack<>();
        this.typeParameters = new ArrayList<>();
    }

    private static String normalize(String methodSignature) {
        if (isSameSignature(methodSignature, MethodRetriever.bytecodeStaticInitializerId, false) ||
                isSameSignature(methodSignature, MethodRetriever.sourcecodeStaticInitializerId, false)) {
            return methodSignature;
        }
        return removeGenericTypeParameters(removeOuterTypeQualifiers(methodSignature));
    }

    private static String removeGenericTypeParameters(String methodSignature) {
        if (methodSignature.startsWith("<init>")) {
            methodSignature = ctorId + methodSignature.substring(6);
        }
        return methodSignature.replaceAll("<[^>]+>", "");
    }

    private static String removeOuterTypeQualifiers(String methodSignature) {
        return methodSignature.replaceAll("[^ (]+\\$", "");
    }

    @Override
    public void visit(ClassOrInterfaceDeclaration type, List<MethodExtractor.MethodCodeFragment> matchingMethodsCode) {
        currentEnclosingType.push(type.getName().asString());
        super.visit(type, matchingMethodsCode);

        if (isSameSignature(methodSignature, MethodRetriever.ctorId + "()", false) && matchingMethodsCode.isEmpty()) {
            MethodExtractor.MethodCodeFragment defaultConstructorFragment = new MethodExtractor.DefaultConstructorFragment();
            defaultConstructorFragment.declaringTypeName = type.getName().asString();
            defaultConstructorFragment.firstLineNumber = type.getBegin().get().line;
            defaultConstructorFragment.lastLineNumber = defaultConstructorFragment.firstLineNumber;
            matchingMethodsCode.add(defaultConstructorFragment);
        }

        currentEnclosingType.pop();
    }

    @Override
    public void visit(ConstructorDeclaration constructor, List<MethodExtractor.MethodCodeFragment> matchingMethodsCode) {
        String name = constructor.getName().asString();
        List<Parameter> parameters = constructor.getParameters();

        int typeNestingDepth = currentEnclosingType.size();
        do {
            String signature = getSignature(ctorId, parameters, typeParameters);
            String altSignature = getSignature(name, parameters, typeParameters);

            if (isSameSignature(methodSignature, signature, false)
                    || isSameSignature(methodSignature, altSignature, false)) {
                matchingMethodsCode.add(getCode(constructor));
                return; // stop when we have a match
            }

            // if the query method signature was extracted from bytecode and the target constructor belongs to a
            // non-static inner class, then the constructor has additional parameters of the surrounding types'
            // types.
            if (typeNestingDepth - 2 >= 0)
                parameters.add(0,
                        new Parameter(new ClassOrInterfaceType(currentEnclosingType.get(typeNestingDepth - 2)), " "));

            typeNestingDepth--;
        } while (typeNestingDepth >= 0);

        super.visit(constructor, matchingMethodsCode);
    }

    @Override
    public void visit(MethodDeclaration method, List<MethodExtractor.MethodCodeFragment> matchingMethodsCode) {
        String signature = getSignature(method.getName().asString(), method.getParameters(), typeParameters);
        if (isSameSignature(methodSignature, signature, ignoreParameters)) {
            matchingMethodsCode.add(getCode(method));
        }
        super.visit(method, matchingMethodsCode);
    }

    @Override
    public void visit(InitializerDeclaration initializer,
                      List<MethodExtractor.MethodCodeFragment> matchingMethodsCode) {
        if ((isSameSignature(methodSignature, MethodRetriever.bytecodeStaticInitializerId, false)
                || isSameSignature(methodSignature, MethodRetriever.sourcecodeStaticInitializerId, false))
                && initializer.isStatic()) {
            matchingMethodsCode.add(getCode(initializer));
        }
        super.visit(initializer, matchingMethodsCode);
    }

    @Override
    public void visit(TypeParameter typeParameter, List<MethodExtractor.MethodCodeFragment> matchingMethodsCode) {
        typeParameters.add(typeParameter);
        super.visit(typeParameter, matchingMethodsCode);
    }

    private <T extends Node> MethodExtractor.MethodCodeFragment getCode(T node) {
        MethodExtractor.MethodCodeFragment fragment = new MethodExtractor.MethodCodeFragment();
        fragment.declaringTypeName = getEnclosingTypeName();
        if (node.getComment().isPresent()) {
            fragment.firstLineNumber = node.getComment().get().getRange().get().begin.line;
        } else {
            fragment.firstLineNumber = node.getRange().get().begin.line;
        }
        fragment.lastLineNumber = node.getRange().get().end.line;
        return fragment;
    }

    private String getEnclosingTypeName() {
        return Joiner.on(".").join(currentEnclosingType);
    }

    private static String getSignature(String methodName, List<Parameter> parameters,
                                       List<TypeParameter> typeParameters) {
        StringBuilder signature = new StringBuilder(methodName).append("(");
        boolean first = true;
        for (Parameter parameter : parameters) {
            if (!first) {
                signature.append(", ");
            }
            String typeName = parameter.getType().toString();
            typeName = stripPackageQualifier(typeName);
            typeName = stripTypeParameters(typeName);
            typeName = applyTypeErasure(typeName, typeParameters);
            signature.append(typeName);
            if (parameter.isVarArgs()) {
                signature.append("[]");
            }
            first = false;
        }
        return signature.append(")").toString();
    }

    private static String stripPackageQualifier(String typeName) {
        int endOfQualifier = typeName.lastIndexOf('.');
        if (endOfQualifier > -1) {
            typeName = typeName.substring(endOfQualifier + 1);
        }
        return typeName;
    }

    private static String stripTypeParameters(String typeName) {
        int startOfTypeParameters = typeName.indexOf('<');
        int endOfTypeParameters = typeName.lastIndexOf('>');
        if (startOfTypeParameters > -1 && endOfTypeParameters > -1) {
            typeName = typeName.substring(0, startOfTypeParameters) + typeName.substring(endOfTypeParameters + 1);
        }
        return typeName;
    }

    private static String applyTypeErasure(String typeName, List<TypeParameter> typeParameters) {
        for (TypeParameter typeParameter : typeParameters) {
            String typeParameterName = typeParameter.getName().asString();
            if (typeName.startsWith(typeParameterName)) {
                typeName = typeName.replaceFirst(typeParameterName, "Object");
            }
        }
        return typeName;
    }

    private static boolean isSameSignature(String methodSignature1, String methodSignature2,
                                           boolean ignoreParameters) {
        if (ignoreParameters) {
            String methodSignature1WithoutParameters = methodSignature1.substring(0, methodSignature1.indexOf('('));
            String methodSignature2WithoutParameters = methodSignature2.substring(0, methodSignature1.indexOf('('));
            return methodSignature1WithoutParameters.equals(methodSignature2WithoutParameters);
        } else {
            return methodSignature1.equals(methodSignature2);
        }
    }
}
