package org.bndtools.build.api;

import java.util.Collections;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

public abstract class AbstractBuildErrorDetailsHandler implements BuildErrorDetailsHandler {
	final static Logger						logger						= LoggerFactory
		.getLogger(AbstractBuildErrorDetailsHandler.class);
	private static final Map<Code, String> PRIMITIVES_TO_SIGNATURES = Maps.of(PrimitiveType.VOID, "V",
		PrimitiveType.BOOLEAN, "Z", PrimitiveType.BYTE, "B", PrimitiveType.SHORT, "S", PrimitiveType.CHAR, "C",
		PrimitiveType.INT, "I", PrimitiveType.FLOAT, "F", PrimitiveType.LONG, "J", PrimitiveType.DOUBLE, "D");

	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

		ASTParser parser = ASTParser.newParser(AST.JLS14);
		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 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.
	 */
	@SuppressWarnings("deprecation")
	@Override
	public List<MarkerData> generateMarkerData(IProject project, Processor model, Location location) throws Exception {
		if (model instanceof Project) {
			logger.warn(
				"error details handler implements a deprecated method, please replace with generateMarkerData(...Processor...)");
			return generateMarkerData(project, (Project) model, location);
		}

		logger.info("generateMarkerData: why not overridden?");
		return Collections.emptyList();
	}

}
