package org.bndtools.builder.utils;

import org.eclipse.jdt.core.IAnnotation;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.SourceRange;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.osgi.util.function.Predicate;

/**
 * Visitor that will "visit" an ASTNode and its children until it finds the expected MemberValue pair to retain its
 * location in the compilation unit source code
 */
public class MemberValuePairLocationRetriever extends ASTVisitor {

    private final IAnnotation javaAnnotation;
    private final Predicate<String> annotationNameMatch;
    private final String memberName;

    private ISourceRange locatedSourceRange = null;

    /**
     * Constructor
     */
    public MemberValuePairLocationRetriever(final IAnnotation javaAnnotation, final Predicate<String> annotationNameMatch, final String memberName) {
        this.javaAnnotation = javaAnnotation;
        this.annotationNameMatch = annotationNameMatch;
        this.memberName = memberName;
    }

    public ISourceRange getMemberValuePairSourceRange() {
        return locatedSourceRange;
    }

    /**
     * @see org.eclipse.jdt.core.dom.ASTVisitor#visit(org.eclipse.jdt.core.dom.TypeDeclaration)
     */
    @Override
    public boolean visit(AnnotationTypeDeclaration node) {
        final IJavaElement ancestor = javaAnnotation.getAncestor(IJavaElement.TYPE);
        if (ancestor != null && ancestor.exists() && ancestor.getElementName()
            .equals(node.getName()
                .getFullyQualifiedName())) {
            // keep searching
            return true;
        }
        // wrong path, stop searching from this branch of the AST
        return false;
    }

    /**
     * @see org.eclipse.jdt.core.dom.ASTVisitor#visit(org.eclipse.jdt.core.dom.TypeDeclaration)
     */
    @Override
    public boolean visit(TypeDeclaration node) {
        final IJavaElement ancestor = javaAnnotation.getAncestor(IJavaElement.TYPE);
        if (ancestor != null && ancestor.exists() && ancestor.getElementName()
            .equals(node.getName()
                .getFullyQualifiedName())) {
            // keep searching
            return true;
        }
        // wrong path, stop searching from this branch of the AST
        return false;
    }

    /**
     * @see org.eclipse.jdt.core.dom.ASTVisitor#visit(org.eclipse.jdt.core.dom.FieldDeclaration)
     */
    @Override
    public boolean visit(VariableDeclarationFragment node) {
        final IJavaElement ancestor = javaAnnotation.getAncestor(IJavaElement.FIELD);
        if (ancestor != null && ancestor.exists() && ancestor.getElementName()
            .equals(node.getName()
                .getFullyQualifiedName())) {
            // keep searching
            return true;
        }
        // wrong path, stop searching from this branch of the AST
        return false;
    }

    /**
     * @see org.eclipse.jdt.core.dom.ASTVisitor#visit(org.eclipse.jdt.core.dom.MethodInvocation)
     */
    @Override
    public boolean visit(MethodDeclaration node) {
        final IJavaElement ancestor = javaAnnotation.getAncestor(IJavaElement.METHOD);
        if (ancestor != null && ancestor.exists() && ancestor.getElementName()
            .equals(node.getName()
                .getFullyQualifiedName())) {
            // keep searching
            return true;
        }
        // wrong path, stop searching from this branch of the AST
        return false;
    }

    /**
     * @see org.eclipse.jdt.core.dom.ASTVisitor#visit(org.eclipse.jdt.core.dom.SingleMemberAnnotation)
     */
    @Override
    public boolean visit(SingleMemberAnnotation node) {
        final IAnnotationBinding annotationBinding = node.resolveAnnotationBinding();
        if (annotationBinding != null) {
            final String nodeName = annotationBinding.getAnnotationType()
                .getQualifiedName();
            boolean match;
            try {
                match = this.annotationNameMatch.test(nodeName);
            } catch (Exception e) {
                match = false;
            }
            if (match) {
                this.locatedSourceRange = new SourceRange(node.getValue()
                    .getStartPosition(),
                    node.getValue()
                        .getLength());
            }
        }
        return false;
    }

    /**
     * @see org.eclipse.jdt.core.dom.ASTVisitor#visit(org.eclipse.jdt.core.dom.NormalAnnotation)
     */
    @Override
    public boolean visit(NormalAnnotation node) {
        final IJavaElement ancestor = javaAnnotation.getAncestor(IJavaElement.ANNOTATION);
        if (ancestor != null && ancestor.exists() && ancestor.getElementName()
            .equals(node.getTypeName()
                .getFullyQualifiedName())) {
            // keep searching
            return true;
        }
        // wrong path, stop searching from this branch of the AST
        return false;
    }

    /**
     * @see org.eclipse.jdt.core.dom.ASTVisitor#visit(org.eclipse.jdt.core.dom.MemberValuePair)
     */
    @Override
    public boolean visit(MemberValuePair node) {
        if (node.getName()
            .getFullyQualifiedName()
            .equals(memberName)) {
            this.locatedSourceRange = new SourceRange(node.getStartPosition(), node.getLength());
        }
        // no need to drill down from here anyway
        return false;
    }

}
