package org.bndtools.builder.handlers.baseline;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.bndtools.api.BndtoolsConstants;
import org.bndtools.api.ILogger;
import org.bndtools.api.Logger;
import org.bndtools.build.api.AbstractBuildErrorDetailsHandler;
import org.bndtools.build.api.MarkerData;
import org.bndtools.builder.utils.MemberValuePairLocationRetriever;
import org.bndtools.utils.jdt.ASTUtil;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.core.IAnnotation;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaModelMarker;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageDeclaration;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.JavaCore;
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.CompilationUnit;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.ui.IMarkerResolution;
import org.osgi.util.function.Predicate;

import aQute.bnd.build.Project;
import aQute.bnd.differ.Baseline.Info;
import aQute.bnd.properties.IRegion;
import aQute.bnd.properties.LineType;
import aQute.bnd.properties.PropertiesLineReader;
import aQute.bnd.service.diff.Delta;
import aQute.bnd.service.diff.Diff;
import aQute.bnd.service.diff.Tree;
import aQute.bnd.service.diff.Type;
import aQute.lib.io.IO;
import aQute.service.reporter.Report.Location;

public class BaselineErrorHandler extends AbstractBuildErrorDetailsHandler {

    private static final String PACKAGEINFO = "packageinfo";
    private static final String PACKAGEINFOJAVA = "package-info.java";
    private static final String PROP_SUGGESTED_VERSION = "suggestedVersion";

    private static final String ANNOTATION_VERSION_BND_PKG = "aQute.bnd.annotation";
    private static final String ANNOTATION_VERSION_OSGI_PKG = "org.osgi.annotation.versioning";
    private static final String ANNOTATION_VERSION_NO_PKG = "Version";
    private static final String ANNOTATION_VERSION_BND = ANNOTATION_VERSION_BND_PKG + "." + ANNOTATION_VERSION_NO_PKG;
    private static final String ANNOTATION_VERSION_OSGI = ANNOTATION_VERSION_OSGI_PKG + "." + ANNOTATION_VERSION_NO_PKG;

    private static final ILogger logger = Logger.getLogger(BaselineErrorHandler.class);

    @Override
    public List<MarkerData> generateMarkerData(IProject project, Project model, Location location) throws Exception {
        List<MarkerData> result = new LinkedList<MarkerData>();

        Info baselineInfo = (Info) location.details;
        IJavaProject javaProject = JavaCore.create(project);

        result.addAll(generatePackageInfoMarkers(baselineInfo, javaProject, location.message));
        result.addAll(generateStructuralChangeMarkers(baselineInfo, javaProject));

        return result;
    }

    List<MarkerData> generatePackageInfoMarkers(Info baselineInfo, IJavaProject javaProject, String message) throws JavaModelException {
        List<MarkerData> markers = new LinkedList<>();
        for (IClasspathEntry entry : javaProject.getRawClasspath()) {
            if (IClasspathEntry.CPE_SOURCE == entry.getEntryKind()) {
                IPath entryPath = entry.getPath();
                IPath pkgPath = entryPath.append(baselineInfo.packageName.replace('.', '/'));

                // Find in packageinfo file
                IPath pkgInfoPath = pkgPath.append(PACKAGEINFO);
                IFile pkgInfoFile = javaProject.getProject()
                    .getWorkspace()
                    .getRoot()
                    .getFile(pkgInfoPath);
                if (pkgInfoFile != null && pkgInfoFile.exists()) {
                    Map<String, Object> attribs = new HashMap<String, Object>();
                    attribs.put(IMarker.MESSAGE, message.trim());
                    attribs.put(PROP_SUGGESTED_VERSION, baselineInfo.suggestedVersion.toString());

                    LineLocation lineLoc = findVersionLocation(pkgInfoFile.getLocation()
                        .toFile());
                    if (lineLoc != null) {
                        attribs.put(IMarker.LINE_NUMBER, lineLoc.lineNum);
                        attribs.put(IMarker.CHAR_START, lineLoc.start);
                        attribs.put(IMarker.CHAR_END, lineLoc.end);
                    }

                    markers.add(new MarkerData(pkgInfoFile, attribs, true));
                }

                // Find in package-info.java
                IPackageFragment pkg = javaProject.findPackageFragment(pkgPath);
                if (pkg != null) {
                    ICompilationUnit pkgInfoJava = pkg.getCompilationUnit(PACKAGEINFOJAVA);
                    if (pkgInfoJava != null && pkgInfoJava.exists()) {
                        ISourceRange range = findPackageInfoJavaVersionLocation(baselineInfo.packageName, pkgInfoJava);

                        Map<String, Object> attribs = new HashMap<String, Object>();
                        attribs.put(IMarker.MESSAGE, message.trim());
                        attribs.put(IJavaModelMarker.ID, 8088);
                        attribs.put(PROP_SUGGESTED_VERSION, baselineInfo.suggestedVersion.toString());
                        if (range != null) {
                            attribs.put(IMarker.CHAR_START, range.getOffset());
                            attribs.put(IMarker.CHAR_END, range.getOffset() + range.getLength());
                            markers.add(new MarkerData(pkgInfoJava.getResource(), attribs, true, BndtoolsConstants.MARKER_JAVA_BASELINE));
                        }
                    }
                }
            }
        }
        return markers;
    }

    ISourceRange findPackageInfoJavaVersionLocation(String packageName, ICompilationUnit compUnit) throws JavaModelException {
        ISourceRange range = null;
        IPackageDeclaration[] pkgDecls = compUnit.getPackageDeclarations();
        if (pkgDecls != null) {
            for (IPackageDeclaration pkgDecl : pkgDecls) {
                if (packageName.equals(pkgDecl.getElementName())) {
                    IAnnotation[] annots = pkgDecl.getAnnotations();
                    for (IAnnotation annot : annots) {
                        String name = annot.getElementName();
                        if (ANNOTATION_VERSION_NO_PKG.equals(name) || ANNOTATION_VERSION_OSGI.equals(name) || ANNOTATION_VERSION_BND.equals(name)) {
                            ASTParser parser = ASTParser.newParser(AST.JLS10);
                            parser.setKind(ASTParser.K_COMPILATION_UNIT);
                            parser.setSource(compUnit);
                            parser.setResolveBindings(true);
                            CompilationUnit ast = (CompilationUnit) parser.createAST(null);
                            if (ast != null) {
                                MemberValuePairLocationRetriever mvpRetriever = new MemberValuePairLocationRetriever(annot, new Predicate<String>() {
                                    @Override
                                    public boolean test(String t) {
                                        return ANNOTATION_VERSION_BND.equals(t) || ANNOTATION_VERSION_OSGI.equals(t);
                                    }
                                }, "value");
                                ast.accept(mvpRetriever);
                                range = mvpRetriever.getMemberValuePairSourceRange();
                            }
                        }
                    }
                }
            }
        }
        return range;
    }

    List<MarkerData> generateStructuralChangeMarkers(Info baselineInfo, IJavaProject javaProject) throws JavaModelException {
        List<MarkerData> markers = new LinkedList<MarkerData>();

        Delta packageDelta = baselineInfo.packageDiff.getDelta();

        // Iterate into the package member diffs
        for (Diff pkgMemberDiff : baselineInfo.packageDiff.getChildren()) {
            // Skip deltas that have lesser significance than the overall package delta
            if (pkgMemberDiff.getDelta()
                .ordinal() < packageDelta.ordinal())
                continue;

            if (Delta.ADDED == pkgMemberDiff.getDelta()) {
                @SuppressWarnings("unused")
                Tree pkgMember = pkgMemberDiff.getNewer();
                // markers.addAll(generateAddedTypeMarker(javaProject, pkgMember.getName(), pkgMember.ifAdded()));
            } else if (Delta.REMOVED == pkgMemberDiff.getDelta()) {
            } else {
                Tree pkgMember = pkgMemberDiff.getOlder();
                if (pkgMember != null && (Type.INTERFACE == pkgMember.getType() || Type.CLASS == pkgMember.getType())) {
                    String className = pkgMember.getName();

                    // Iterate into the class member diffs
                    for (Diff classMemberDiff : pkgMemberDiff.getChildren()) {
                        // Skip deltas that have lesser significance than the overall package delta (again)
                        if (classMemberDiff.getDelta()
                            .ordinal() < packageDelta.ordinal())
                            continue;

                        if (Delta.ADDED == classMemberDiff.getDelta()) {
                            Tree classMember = classMemberDiff.getNewer();
                            if (Type.METHOD == classMember.getType())
                                markers.addAll(generateAddedMethodMarker(javaProject, className, classMember.getName(), classMember.ifAdded()));
                        } else if (Delta.REMOVED == classMemberDiff.getDelta()) {
                            Tree classMember = classMemberDiff.getOlder();
                            if (Type.METHOD == classMember.getType()) {
                                markers.addAll(generateRemovedMethodMarker(javaProject, className, classMember.getName(), classMember.ifRemoved()));
                            }
                        }
                    }
                }
            }

        }

        return markers;
    }

    /*
     * List<MarkerData> generateAddedTypeMarker(IJavaProject javaProject, String name, Delta ifAdded) { // TODO
     * Auto-generated method stub return null; }
     */

    List<MarkerData> generateAddedMethodMarker(IJavaProject javaProject, String className, final String methodName, final Delta requiresDelta) throws JavaModelException {
        final List<MarkerData> markers = new LinkedList<MarkerData>();
        final CompilationUnit ast = createAST(javaProject, className);
        if (ast != null) {
            ast.accept(new ASTVisitor() {
                @Override
                public boolean visit(MethodDeclaration methodDecl) {
                    String signature = ASTUtil.buildMethodSignature(methodDecl);
                    if (signature.equals(methodName)) {
                        // Create the marker attribs here
                        Map<String, Object> attribs = new HashMap<String, Object>();
                        attribs.put(IMarker.CHAR_START, methodDecl.getStartPosition());
                        attribs.put(IMarker.CHAR_END, methodDecl.getStartPosition() + methodDecl.getLength());

                        String message = String.format("This method was added, which requires a %s change to the package.", requiresDelta);
                        attribs.put(IMarker.MESSAGE, message);

                        markers.add(new MarkerData(ast.getJavaElement()
                            .getResource(), attribs, false));
                    }

                    return false;
                }
            });
        }
        return markers;
    }

    List<MarkerData> generateRemovedMethodMarker(IJavaProject javaProject, final String className, final String methodName, final Delta requiresDelta) throws JavaModelException {
        final List<MarkerData> markers = new LinkedList<MarkerData>();
        final CompilationUnit ast = createAST(javaProject, className);
        if (ast != null) {
            ast.accept(new ASTVisitor() {
                @Override
                public boolean visit(TypeDeclaration typeDecl) {
                    ITypeBinding typeBinding = typeDecl.resolveBinding();
                    if (typeBinding != null) {
                        if (typeBinding.getBinaryName()
                            .equals(className)) {
                            Map<String, Object> attribs = new HashMap<String, Object>();
                            SimpleName nameNode = typeDecl.getName();
                            attribs.put(IMarker.CHAR_START, nameNode.getStartPosition());
                            attribs.put(IMarker.CHAR_END, nameNode.getStartPosition() + nameNode.getLength());

                            String message = String.format("The method '%s' was removed, which requires a %s change to the package.", methodName, requiresDelta);
                            attribs.put(IMarker.MESSAGE, message);

                            markers.add(new MarkerData(ast.getJavaElement()
                                .getResource(), attribs, false));
                            return false;
                        }
                    }
                    return true;
                }
            });
        }
        return markers;
    }

    @Override
    public List<IMarkerResolution> getResolutions(IMarker marker) {
        List<IMarkerResolution> result = new LinkedList<IMarkerResolution>();

        final String suggestedVersion = marker.getAttribute(PROP_SUGGESTED_VERSION, null);
        if (suggestedVersion != null) {
            result.add(new IMarkerResolution() {
                @Override
                public void run(IMarker marker) {
                    final IFile file = (IFile) marker.getResource();
                    final IWorkspace workspace = file.getWorkspace();
                    try {
                        workspace.run(new IWorkspaceRunnable() {
                            @Override
                            public void run(IProgressMonitor monitor) throws CoreException {
                                String input = "version " + suggestedVersion;
                                ByteArrayInputStream stream = new ByteArrayInputStream(input.getBytes());
                                file.setContents(stream, false, true, monitor);
                            }
                        }, null);
                    } catch (CoreException e) {
                        logger.logError("Error applying baseline version quickfix.", e);
                    }
                }

                @Override
                public String getLabel() {
                    return "Change package version to " + suggestedVersion;
                }
            });

        }

        return result;
    }

    @Override
    public List<ICompletionProposal> getProposals(IMarker marker) {
        List<ICompletionProposal> proposals = new LinkedList<ICompletionProposal>();

        String suggestedVersion = marker.getAttribute(PROP_SUGGESTED_VERSION, null);
        int start = marker.getAttribute(IMarker.CHAR_START, 0);
        int end = marker.getAttribute(IMarker.CHAR_END, 0);
        CompletionProposal proposal = new CompletionProposal("version " + suggestedVersion, start, end - start, end, null, "Change package version to " + suggestedVersion, null, null);
        proposals.add(proposal);

        return proposals;
    }

    private LineLocation findVersionLocation(File file) {
        String content;
        try {
            content = IO.collect(file);
            PropertiesLineReader reader = new PropertiesLineReader(content);

            int lineNum = 1;
            LineType type = reader.next();
            while (type != LineType.eof) {
                if (type == LineType.entry) {
                    String key = reader.key();
                    if ("version".equals(key)) {
                        LineLocation loc = new LineLocation();
                        loc.lineNum = lineNum;
                        IRegion region = reader.region();
                        loc.start = region.getOffset();
                        loc.end = region.getOffset() + region.getLength();
                        return loc;
                    }
                }
                type = reader.next();
                lineNum++;
            }
        } catch (Exception e) {
            // ignore
        }

        return null;
    }
}
