package org.bndtools.builder.validate;

import static aQute.bnd.osgi.Constants.DEFAULT_PROP_BIN_DIR;
import static aQute.bnd.osgi.Constants.DEFAULT_PROP_SRC_DIR;
import static aQute.bnd.osgi.Constants.DEFAULT_PROP_TESTBIN_DIR;
import static aQute.bnd.osgi.Constants.DEFAULT_PROP_TESTSRC_DIR;

import java.io.File;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;

import org.bndtools.api.BndtoolsConstants;
import org.bndtools.api.IProjectValidator;
import org.bndtools.api.IValidator;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;

import aQute.bnd.build.Project;
import aQute.bnd.osgi.Builder;
import aQute.bnd.osgi.Constants;
import aQute.service.reporter.Reporter.SetLocation;
import bndtools.central.Central;

/**
 * Verify that the build path setup for Eclipse matches the actual settings in bnd.
 */
public class ProjectPathsValidator implements IValidator, IProjectValidator {
    final static IPath JRE_CONTAINER = new Path("org.eclipse.jdt.launching.JRE_CONTAINER");

    /*
     * The parts of the test, needed to know what we missed
     */
    enum SetupTypes {
        testsrc,
        bndcontainer;
    }

    /*
     * Old validate method for backward compatibility (did not want to create yet another extension point for this).
     */
    @Override
    public IStatus validate(Builder builder) {
        return Status.OK_STATUS;
    }

    /**
     * Validate the project
     */
    @Override
    public void validateProject(Project model) throws Exception {
        //
        // We must have, a project and assume this is already reported
        //
        if (model == null) {
            return;
        }

        IJavaProject javaProject = Central.getJavaProject(model);
        if (javaProject == null) {
            model.error("Bndtools: The project in %s is not linked with a Java project.", model.getBase());
            return;
        }

        //
        // Verify if we have the right relation to the cnf folder ...
        //
        Project w;
        try {
            w = Central.getWorkspace()
                .getProjectFromFile(model.getBase());
        } catch (Exception e) {
            w = null;
        }
        if (w == null || w != model) {
            model.error("Bndtools: Error in setup, likely the cnf folder is not ../cnf relative to the project folder '%s'. The workspace is in '%s'.", model.getBase(), model.getWorkspace()
                .getBase());
            return;
        }

        //
        // Get the different bnd directories ...
        //
        File bin = model.getOutput();
        File testsrc = model.getTestSrc();
        File testbin = model.getTestOutput();
        Set<File> sourcePath = new HashSet<File>(model.getSourcePath());

        //
        // All the things we should find when we have traversed the build path
        //
        Set<SetupTypes> found = EnumSet.allOf(SetupTypes.class);

        for (IClasspathEntry cpe : javaProject.getRawClasspath()) {
            int kind = cpe.getEntryKind();
            switch (kind) {
                case IClasspathEntry.CPE_VARIABLE :
                    warning(model, null, null, cpe, "Bndtools: Found a variable in the eclipse build path, this variable is not available during continuous integration", cpe).file(new File(model.getBase(), ".classpath").getAbsolutePath());
                    break;

                case IClasspathEntry.CPE_LIBRARY :
                    warning(model, null, null, cpe, "Bndtools: The .classpath contains a library that will not be available during continuous integration: %s", cpe.getPath()).file(new File(model.getBase(), ".classpath").getAbsolutePath());
                    break;

                case IClasspathEntry.CPE_CONTAINER :
                    if (BndtoolsConstants.BND_CLASSPATH_ID.segment(0)
                        .equals(cpe.getPath()
                            .segment(0)))
                        found.remove(SetupTypes.bndcontainer);
                    else {
                        IPath path = cpe.getPath();
                        if (JRE_CONTAINER.segment(0)
                            .equals(path.segment(0))) {
                            if (path.segmentCount() == 1) {
                                // warning because default might vary <classpathentry kind="con"
                                // path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
                                warning(model, null, path.toString(), cpe,
                                    "Bndtools: The .classpath contains a default JRE container: %s. This makes it undefined to what version you compile and this might differ between Continuous Integration and Eclipse", path)
                                        .file(new File(model.getBase(), ".classpath").getAbsolutePath());
                            } else {
                                String segment = path.segment(1);
                                if ("org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType".equals(segment) && path.segmentCount() == 3) {
                                    // check javac version <classpathentry kind="con"
                                    // path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>

                                    String javac = model.getProperty(Constants.JAVAC_SOURCE, "1.8");
                                    if (!path.segment(2)
                                        .endsWith(javac)) {
                                        warning(model, null, path.toString(), cpe, "Bndtools: The .JRE container is set to %s but bnd is compiling against %s", path.segment(2), javac)
                                            .file(new File(model.getBase(), ".classpath").getAbsolutePath());
                                    }
                                } else {
                                    // warning because local/machine specific <classpathentry kind="con"
                                    // path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.launching.macosx.MacOSXType/Java
                                    // SE 7 [1.7.0_71]"/>
                                    warning(model, null, path.toString(), cpe,
                                        "Bndtools: The .classpath contains an non-portable JRE container: %s. This makes it undefined to what version you compile and this might differ between Continuous Integration and Eclipse", path)
                                            .file(new File(model.getBase(), ".classpath").getAbsolutePath());
                                }
                            }
                        } else {
                            warning(model, null, path.toString(), cpe, "Bndtools: The .classpath contains an unknown container: %s. This could make your build less portable.", path)
                                .file(new File(model.getBase(), ".classpath").getAbsolutePath());
                        }
                    }
                    break;

                case IClasspathEntry.CPE_SOURCE :
                    File file = toFile(cpe.getPath());
                    if (file == null) {
                        model.warning("Bndtools: Found virtual file for '%s'", cpe.getPath())
                            .details(cpe);
                    } else {
                        File output = toFile(cpe.getOutputLocation());
                        if (output == null)
                            output = toFile(javaProject.getOutputLocation());

                        if (file.equals(testsrc)) {
                            //
                            // We're talking about the test source directory
                            // This should be linked to testbin
                            //
                            found.remove(SetupTypes.testsrc);
                            if (!testbin.equals(output)) {
                                warning(model, DEFAULT_PROP_TESTBIN_DIR, testbin, cpe, "Bndtools: testsrc folder '%s' has output folder set to '%s', which does not match bnd's testbin folder '%s'", file, output, testbin);
                            }
                        } else {
                            //
                            // We must have a source directory. They must be linked to the bin
                            // folder and on the bnd source path. Since the source path has
                            // potentially multiple entries, we remove this one so we can check
                            // later if we had all of them
                            //
                            if (sourcePath.remove(file)) {
                                if (!bin.equals(output)) {
                                    warning(model, DEFAULT_PROP_BIN_DIR, bin, cpe, "Bndtools: src folder '%s' has output folder set to '%s', which does not match bnd's bin folder '%s'", file, output, bin);
                                }
                            } else {
                                warning(model, DEFAULT_PROP_SRC_DIR, null, cpe, "Bndtools: Found source folder '%s' that is not on bnd's source path '%s'", file, model.getProperty(Constants.DEFAULT_PROP_SRC_DIR));
                            }
                        }
                    }
                    break;

                default :
                    break;
            }

        }

        //
        // If we had not see all source entries, then we should
        // have something in sourcePath
        //
        for (File file : sourcePath) {
            warning(model, DEFAULT_PROP_SRC_DIR, file, null, "Bndtools: bnd's src folder '%s' is not in the Eclipse build path", file);
        }

        //
        // Check if we had all the different things we needed to check
        //
        for (SetupTypes t : found) {
            switch (t) {
                case testsrc :
                    if (testsrc.isDirectory()) // if the testsrc directory does not exist, then don't warn
                        warning(model, DEFAULT_PROP_TESTSRC_DIR, null, null, "Bndtools: bnd's testsrc folder '%s' is not in the Eclipse build path", testsrc);
                    break;

                case bndcontainer :
                    warning(model, null, null, null, "Bndtools: The build path does not refer to the bnd container '%s'", BndtoolsConstants.BND_CLASSPATH_ID.segment(0));
                    break;

                default :
                    break;
            }
        }
    }

    private SetLocation warning(Project model, String header, Object context, IClasspathEntry cpe, String format, Object... args) {
        String prefix = model.getBase()
            .getAbsolutePath();
        for (int i = 0; i < args.length; i++) {
            args[i] = relative(prefix, args[i]);
        }
        SetLocation loc = model.warning(format, args);
        if (header != null)
            loc.header(header);
        if (context != null) {
            loc.context(relative(prefix, context).toString());
        }
        if (cpe != null) {
            loc.details(cpe);
        }
        loc.file(model.getPropertiesFile()
            .getAbsolutePath());
        return loc;
    }

    private Object relative(String prefix, Object object) {
        if (!(object instanceof File))
            return object;

        String path = object.toString();
        if (!path.startsWith(prefix))
            return object;

        return path.substring(prefix.length() + 1);
    }

    private File toFile(IPath path) {
        if (path == null)
            return null;

        IFile file = ResourcesPlugin.getWorkspace()
            .getRoot()
            .getFile(path);
        if (file != null)
            return file.getLocation()
                .toFile()
                .getAbsoluteFile();
        return null;
    }
}