package org.bndtools.build.api;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.ArrayType;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.PrimitiveType.Code;
import org.eclipse.jdt.core.dom.QualifiedType;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.ui.IMarkerResolution;

import aQute.bnd.build.Project;
import aQute.bnd.osgi.Processor;
import aQute.service.reporter.Report.Location;

public abstract class AbstractBuildErrorDetailsHandler implements BuildErrorDetailsHandler {

    private static final Map<Code, String> PRIMITIVES_TO_SIGNATURES;

    static {
        Map<Code, String> tmp = new HashMap<PrimitiveType.Code, String>();

        tmp.put(PrimitiveType.VOID, "V");
        tmp.put(PrimitiveType.BOOLEAN, "Z");
        tmp.put(PrimitiveType.BYTE, "B");
        tmp.put(PrimitiveType.SHORT, "S");
        tmp.put(PrimitiveType.CHAR, "C");
        tmp.put(PrimitiveType.INT, "I");
        tmp.put(PrimitiveType.FLOAT, "F");
        tmp.put(PrimitiveType.LONG, "J");
        tmp.put(PrimitiveType.DOUBLE, "D");

        PRIMITIVES_TO_SIGNATURES = Collections.unmodifiableMap(tmp);
    }

    public static final IResource getDefaultResource(IProject project) {
        return getDefaultResource(project, Project.BNDFILE);
    }

    public static final IResource getDefaultResource(IProject project, String name) {
        if ((name == null) || name.isEmpty())
            return project;
        IResource bndFile = project.getFile(name);
        if (bndFile.exists())
            return bndFile;
        return project;
    }

    /**
     * Obtain an AST for a source file in a project
     *
     * @param javaProject
     * @param className
     * @return An AST, or null if no source file exists for that class
     * @throws JavaModelException
     */
    public static final CompilationUnit createAST(IJavaProject javaProject, String className) throws JavaModelException {
        IType type = javaProject.findType(className);
        if (type == null)
            return null;

        final ICompilationUnit cunit = type.getCompilationUnit();
        if (cunit == null)
            return null; // not a source type

        @SuppressWarnings("deprecation")
        ASTParser parser = ASTParser.newParser(AST.JLS4);
        parser.setKind(ASTParser.K_COMPILATION_UNIT);
        parser.setSource(cunit);
        parser.setResolveBindings(true);
        return (CompilationUnit) parser.createAST(null);
    }

    /**
     * Create a marker on a Java Type
     *
     * @param javaProject
     * @param className - the fully qualified class name (e.g java.lang.String)
     * @param markerAttributes
     * @param hasResolutions - true if the marker will have resolutions
     * @return Marker Data that can be used to create an {@link IMarker}, or null if no location can be found
     * @throws JavaModelException
     */
    public static final MarkerData createTypeMarkerData(IJavaProject javaProject, final String className, final Map<String, Object> markerAttributes, boolean hasResolutions) throws JavaModelException {

        final CompilationUnit ast = createAST(javaProject, className);

        if (ast == null)
            return null;

        ast.accept(new ASTVisitor() {
            @Override
            public boolean visit(TypeDeclaration typeDecl) {
                ITypeBinding typeBinding = typeDecl.resolveBinding();
                if (typeBinding != null) {
                    if (typeBinding.getBinaryName()
                        .equals(className)) {
                        SimpleName nameNode = typeDecl.getName();
                        markerAttributes.put(IMarker.CHAR_START, nameNode.getStartPosition());
                        markerAttributes.put(IMarker.CHAR_END, nameNode.getStartPosition() + nameNode.getLength());

                        return false;
                    }
                }
                return true;
            }
        });

        if (!markerAttributes.containsKey(IMarker.CHAR_START))
            return null;

        return new MarkerData(ast.getJavaElement()
            .getResource(), markerAttributes, hasResolutions);
    }

    /**
     * Create a marker on a Java Method
     *
     * @param javaProject
     * @param className - the fully qualified class name (e.g java.lang.String)
     * @param methodName
     * @param methodSignature - signatures are in "internal form" e.g. (Ljava.lang.Integer;[Ljava/lang/String;Z)V
     * @param markerAttributes - attributes that should be included in the marker, typically a message. The start and
     *            end points for the marker are added by this method.
     * @param hasResolutions - true if the marker will have resolutions
     * @return Marker Data that can be used to create an {@link IMarker}, or null if no location can be found
     * @throws JavaModelException
     */
    public static final MarkerData createMethodMarkerData(IJavaProject javaProject, final String className, final String methodName, final String methodSignature, final Map<String, Object> markerAttributes, boolean hasResolutions)
        throws JavaModelException {

        final CompilationUnit ast = createAST(javaProject, className);

        if (ast == null)
            return null;

        ast.accept(new ASTVisitor() {
            @Override
            public boolean visit(MethodDeclaration methodDecl) {
                if (matches(ast, methodDecl, methodName, methodSignature)) {
                    // Create the marker attribs here
                    markerAttributes.put(IMarker.CHAR_START, methodDecl.getStartPosition());
                    markerAttributes.put(IMarker.CHAR_END, methodDecl.getStartPosition() + methodDecl.getLength());
                }

                return false;
            }

            private boolean matches(CompilationUnit ast, MethodDeclaration methodDecl, String methodName, String signature) {
                if ("<init>".equals(methodName)) {
                    if (!methodDecl.isConstructor()) {
                        return false;
                    }
                } else if (!methodDecl.getName()
                    .getIdentifier()
                    .equals(methodName)) {
                    return false;
                }

                return getSignature(ast, methodDecl).equals(signature);
            }

            private String getSignature(CompilationUnit ast, MethodDeclaration methodDecl) {
                StringBuilder signatureBuilder = new StringBuilder("(");

                for (@SuppressWarnings("unchecked")
                Iterator<SingleVariableDeclaration> it = methodDecl.parameters()
                    .iterator(); it.hasNext();) {
                    SingleVariableDeclaration decl = it.next();
                    appendType(ast, signatureBuilder, decl.getType(), decl.getExtraDimensions());
                }

                signatureBuilder.append(")");

                appendType(ast, signatureBuilder, methodDecl.getReturnType2(), 0);
                return signatureBuilder.toString();
            }

            private void appendType(CompilationUnit ast, StringBuilder signatureBuilder, Type typeToAdd, int extraDimensions) {
                for (int i = 0; i < extraDimensions; i++) {
                    signatureBuilder.append('[');
                }
                Type rovingType = typeToAdd;
                if (rovingType == null) {
                    // A special return type for constructors, nice one Eclipse...
                    signatureBuilder.append("V");
                } else {
                    if (rovingType.isArrayType()) {
                        ArrayType type = (ArrayType) rovingType;
                        int depth = type.getDimensions();
                        for (int i = 0; i < depth; i++) {
                            signatureBuilder.append('[');
                        }
                        // We still need to add the array component type, which might be primitive or a reference
                        rovingType = type.getElementType();
                    }
                    // Type erasure means that we should ignore parameters
                    if (rovingType.isParameterizedType()) {
                        rovingType = ((ParameterizedType) rovingType).getType();
                    }

                    if (rovingType.isPrimitiveType()) {
                        PrimitiveType type = (PrimitiveType) rovingType;
                        signatureBuilder.append(PRIMITIVES_TO_SIGNATURES.get(type.getPrimitiveTypeCode()));
                    } else if (rovingType.isSimpleType()) {
                        SimpleType type = (SimpleType) rovingType;
                        String name;
                        if (type.getName()
                            .isQualifiedName()) {
                            name = type.getName()
                                .getFullyQualifiedName();
                        } else {
                            name = getFullyQualifiedNameForSimpleName(ast, type.getName());
                        }
                        name = name.replace('.', '/');
                        signatureBuilder.append("L")
                            .append(name)
                            .append(";");
                    } else if (rovingType.isQualifiedType()) {
                        QualifiedType type = (QualifiedType) rovingType;
                        String name = type.getQualifier()
                            .toString()
                            .replace('.', '/') + '/'
                            + type.getName()
                                .getFullyQualifiedName()
                                .replace('.', '/');
                        signatureBuilder.append("L")
                            .append(name)
                            .append(";");
                    } else {
                        throw new IllegalArgumentException("We hit an unknown type " + rovingType);
                    }
                }
            }

            private String getFullyQualifiedNameForSimpleName(CompilationUnit ast, Name typeName) {
                String name = typeName.getFullyQualifiedName();

                @SuppressWarnings("unchecked")
                List<ImportDeclaration> ids = ast.imports();
                for (ImportDeclaration id : ids) {
                    if (id.isStatic())
                        continue;
                    if (id.isOnDemand()) {
                        String packageName = id.getName()
                            .getFullyQualifiedName();
                        try {
                            if (ast.getJavaElement()
                                .getJavaProject()
                                .findType(packageName + "." + name) != null) {
                                name = packageName + '.' + name;
                            }
                        } catch (JavaModelException e) {}
                    } else {
                        String importName = id.getName()
                            .getFullyQualifiedName();
                        if (importName.endsWith("." + name)) {
                            name = importName;
                            break;
                        }
                    }
                }

                if (name.indexOf('.') < 0) {
                    try {
                        if (ast.getJavaElement()
                            .getJavaProject()
                            .findType(name) == null) {
                            name = "java.lang." + name;
                        }
                    } catch (JavaModelException e) {}
                }
                return name;
            }
        });

        if (!markerAttributes.containsKey(IMarker.CHAR_START))
            return null;

        return new MarkerData(ast.getJavaElement()
            .getResource(), markerAttributes, hasResolutions);
    }

    /**
     * Create a marker on a Java Method
     *
     * @param javaProject
     * @param className - the fully qualified class name (e.g java.lang.String)
     * @param methodName
     * @param methodSignature - signatures are in "internal form" e.g. (Ljava.lang.Integer;[Ljava/lang/String;Z)V
     * @param markerAttributes - attributes that should be included in the marker, typically a message. The start and
     *            end points for the marker are added by this method.
     * @param hasResolutions - true if the marker will have resolutions
     * @return Marker Data that can be used to create an {@link IMarker}, or null if no location can be found
     * @throws JavaModelException
     */
    public static final MarkerData createFieldMarkerData(IJavaProject javaProject, final String className, final String fieldName, final Map<String, Object> markerAttributes, boolean hasResolutions) throws JavaModelException {

        final CompilationUnit ast = createAST(javaProject, className);

        if (ast == null)
            return null;

        ast.accept(new ASTVisitor() {
            @Override
            public boolean visit(FieldDeclaration fieldDecl) {
                if (matches(ast, fieldDecl, fieldName)) {
                    // Create the marker attribs here
                    markerAttributes.put(IMarker.CHAR_START, fieldDecl.getStartPosition());
                    markerAttributes.put(IMarker.CHAR_END, fieldDecl.getStartPosition() + fieldDecl.getLength());
                }

                return false;
            }

            private boolean matches(@SuppressWarnings("unused") CompilationUnit ast, FieldDeclaration fieldDecl, String fieldName) {
                @SuppressWarnings("unchecked")
                List<VariableDeclarationFragment> list = (List<VariableDeclarationFragment>) fieldDecl.getStructuralProperty(FieldDeclaration.FRAGMENTS_PROPERTY);
                for (VariableDeclarationFragment vdf : list) {
                    if (fieldName.equals(vdf.getName()
                        .toString())) {
                        return true;
                    }
                }
                return false;
            }
        });

        if (!markerAttributes.containsKey(IMarker.CHAR_START))
            return null;

        return new MarkerData(ast.getJavaElement()
            .getResource(), markerAttributes, hasResolutions);
    }

    @Override
    public List<IMarkerResolution> getResolutions(IMarker marker) {
        return Collections.emptyList();
    }

    @Override
    public List<ICompletionProposal> getProposals(IMarker marker) {
        return Collections.emptyList();
    }

    /**
     * Bridge method. The actual parameter should have been Processor since we can have Builder, Workspace, Project.
     * etc. If it is a project, we defer to the old method. Otherwise we allow others to override this method.
     */
    @Override
    public List<MarkerData> generateMarkerData(IProject project, Processor model, Location location) throws Exception {
        if (model instanceof Project)
            return generateMarkerData(project, (Project) model, location);

        return Collections.emptyList();
    }

    @Override
    public List<MarkerData> generateMarkerData(IProject project, Project model, Location location) throws Exception {
        return Collections.emptyList();
    }

}
