/*
 * Copyright 2011 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gradle.plugins.ide.eclipse;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.gradle.BuildAdapter;
import org.gradle.api.Action;
import org.gradle.api.JavaVersion;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.artifacts.PublishArtifact;
import org.gradle.api.artifacts.component.ProjectComponentIdentifier;
import org.gradle.api.internal.ConventionMapping;
import org.gradle.api.internal.IConventionAware;
import org.gradle.api.internal.artifacts.ivyservice.projectmodule.ProjectLocalComponentProvider;
import org.gradle.api.internal.artifacts.publish.DefaultPublishArtifact;
import org.gradle.api.internal.project.ProjectInternal;
import org.gradle.api.invocation.Gradle;
import org.gradle.api.plugins.GroovyBasePlugin;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.plugins.WarPlugin;
import org.gradle.api.plugins.scala.ScalaBasePlugin;
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.internal.component.local.model.LocalComponentArtifactMetadata;
import org.gradle.internal.component.local.model.PublishArtifactLocalArtifactMetadata;
import org.gradle.internal.reflect.Instantiator;
import org.gradle.plugins.ear.EarPlugin;
import org.gradle.plugins.ide.api.XmlFileContentMerger;
import org.gradle.plugins.ide.eclipse.internal.AfterEvaluateHelper;
import org.gradle.plugins.ide.eclipse.internal.LinkedResourcesCreator;
import org.gradle.plugins.ide.eclipse.model.BuildCommand;
import org.gradle.plugins.ide.eclipse.model.EclipseClasspath;
import org.gradle.plugins.ide.eclipse.model.EclipseJdt;
import org.gradle.plugins.ide.eclipse.model.EclipseModel;
import org.gradle.plugins.ide.eclipse.model.EclipseProject;
import org.gradle.plugins.ide.eclipse.model.Link;
import org.gradle.plugins.ide.internal.configurer.UniqueProjectNameProvider;
import org.gradle.plugins.ide.internal.IdePlugin;
import org.gradle.util.SingleMessageLogger;

import javax.inject.Inject;
import java.io.File;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;

import static org.gradle.internal.component.local.model.DefaultProjectComponentIdentifier.newProjectId;

/**
 * <p>A plugin which generates Eclipse files.</p>
 */
public class EclipsePlugin extends IdePlugin {

    public static final String ECLIPSE_TASK_NAME = "eclipse";
    public static final String ECLIPSE_PROJECT_TASK_NAME = "eclipseProject";
    public static final String ECLIPSE_CP_TASK_NAME = "eclipseClasspath";
    public static final String ECLIPSE_JDT_TASK_NAME = "eclipseJdt";

    private final Instantiator instantiator;
    private final UniqueProjectNameProvider uniqueProjectNameProvider;

    @Inject
    public EclipsePlugin(Instantiator instantiator, UniqueProjectNameProvider uniqueProjectNameProvider) {
        this.instantiator = instantiator;
        this.uniqueProjectNameProvider = uniqueProjectNameProvider;
    }

    @Override
    protected String getLifecycleTaskName() {
        return ECLIPSE_TASK_NAME;
    }

    @Override
    protected void onApply(Project project) {
        getLifecycleTask().setDescription("Generates all Eclipse files.");
        getCleanTask().setDescription("Cleans all Eclipse files.");

        EclipseModel model = project.getExtensions().create("eclipse", EclipseModel.class);

        configureEclipseProject(project, model);
        configureEclipseJdt(project, model);
        configureEclipseClasspath(project, model);

        registerEclipseArtifacts(project);

        applyEclipseWtpPluginOnWebProjects(project);
    }

    // No one should be calling this.
    @Deprecated
    public void performPostEvaluationActions() {
        SingleMessageLogger.nagUserOfDiscontinuedMethod("performPostEvaluationActions");
    }

    private static void registerEclipseArtifacts(Project project) {
        ProjectLocalComponentProvider projectComponentProvider = ((ProjectInternal) project).getServices().get(ProjectLocalComponentProvider.class);
        ProjectComponentIdentifier projectId = newProjectId(project);
        EclipseProject eclipseProject = project.getExtensions().getByType(EclipseModel.class).getProject();
        projectComponentProvider.registerAdditionalArtifact(projectId, createArtifact(eclipseProject, "project", projectId, project));
        projectComponentProvider.registerAdditionalArtifact(projectId, createArtifact(eclipseProject, "classpath", projectId, project));
    }

    private static LocalComponentArtifactMetadata createArtifact(EclipseProject eclipseProject, String extension, ProjectComponentIdentifier projectId, Project project) {
        Task byName = project.getTasks().getByName("eclipseProject");
        PublishArtifact publishArtifact = new EclipseArtifact(eclipseProject, project.getProjectDir(), extension, byName);
        return new PublishArtifactLocalArtifactMetadata(projectId, publishArtifact);
    }

    private void configureEclipseProject(final Project project, final EclipseModel model) {
        maybeAddTask(project, this, ECLIPSE_PROJECT_TASK_NAME, GenerateEclipseProject.class, new Action<GenerateEclipseProject>() {
            @Override
            public void execute(GenerateEclipseProject task) {
                final EclipseProject projectModel = task.getProjectModel();

                //task properties:
                task.setDescription("Generates the Eclipse project file.");
                task.setInputFile(project.file(".project"));
                task.setOutputFile(project.file(".project"));

                //model:
                model.setProject(projectModel);

                final String defaultModuleName = uniqueProjectNameProvider.getUniqueName(project);
                projectModel.setName(defaultModuleName);

                final ConventionMapping convention = ((IConventionAware) projectModel).getConventionMapping();
                convention.map("comment", new Callable<String>() {
                    @Override
                    public String call() {
                        return project.getDescription();
                    }

                });

                project.getPlugins().withType(JavaBasePlugin.class, new Action<JavaBasePlugin>() {
                    @Override
                    public void execute(JavaBasePlugin javaBasePlugin) {
                        if (!project.getPlugins().hasPlugin(EarPlugin.class)) {
                            projectModel.buildCommand("org.eclipse.jdt.core.javabuilder");
                        }

                        projectModel.natures("org.eclipse.jdt.core.javanature");
                        convention.map("linkedResources", new Callable<Set<Link>>() {
                            @Override
                            public Set<Link> call() {
                                return new LinkedResourcesCreator().links(project);
                            }

                        });
                    }

                });

                project.getPlugins().withType(GroovyBasePlugin.class, new Action<GroovyBasePlugin>() {
                    @Override
                    public void execute(GroovyBasePlugin groovyBasePlugin) {
                        projectModel.getNatures().add(projectModel.getNatures().indexOf("org.eclipse.jdt.core.javanature"), "org.eclipse.jdt.groovy.core.groovyNature");
                    }

                });

                project.getPlugins().withType(ScalaBasePlugin.class, new Action<ScalaBasePlugin>() {
                    @Override
                    public void execute(ScalaBasePlugin scalaBasePlugin) {
                        projectModel.getBuildCommands().set(Iterables.indexOf(projectModel.getBuildCommands(), new Predicate<BuildCommand>() {
                            @Override
                            public boolean apply(BuildCommand buildCommand) {
                                return buildCommand.getName().equals("org.eclipse.jdt.core.javabuilder");
                            }

                        }), new BuildCommand("org.scala-ide.sdt.core.scalabuilder"));
                        projectModel.getNatures().add(projectModel.getNatures().indexOf("org.eclipse.jdt.core.javanature"), "org.scala-ide.sdt.core.scalanature");
                    }

                });
            }

        });
    }

    private void configureEclipseClasspath(final Project project, final EclipseModel model) {
        model.setClasspath(instantiator.newInstance(EclipseClasspath.class, project));
        ((IConventionAware) model.getClasspath()).getConventionMapping().map("defaultOutputDir", new Callable<File>() {
            @Override
            public File call() {
                return new File(project.getProjectDir(), "bin");
            }

        });

        final EclipsePlugin eclipsePlugin = this;
        project.getPlugins().withType(JavaBasePlugin.class, new Action<JavaBasePlugin>() {
            @Override
            public void execute(JavaBasePlugin javaBasePlugin) {
                maybeAddTask(project, eclipsePlugin, ECLIPSE_CP_TASK_NAME, GenerateEclipseClasspath.class, new Action<GenerateEclipseClasspath>() {
                    @Override
                    public void execute(final GenerateEclipseClasspath task) {
                        //task properties:
                        task.setDescription("Generates the Eclipse classpath file.");
                        task.setInputFile(project.file(".classpath"));
                        task.setOutputFile(project.file(".classpath"));

                        //model properties:
                        task.setClasspath(model.getClasspath());
                        task.getClasspath().setFile(new XmlFileContentMerger(task.getXmlTransformer()));
                        task.getClasspath().setSourceSets(project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets());

                        AfterEvaluateHelper.afterEvaluateOrExecute(project, new Action<Project>() {
                            @Override
                            public void execute(Project p) {
                                // keep the ordering we had in earlier gradle versions
                                Set<String> containers = Sets.newLinkedHashSet();
                                containers.add("org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/" + model.getJdt().getJavaRuntimeName() + "/");
                                containers.addAll(task.getClasspath().getContainers());
                                task.getClasspath().setContainers(containers);
                            }

                        });

                        project.getPlugins().withType(JavaPlugin.class, new Action<JavaPlugin>() {
                            @Override
                            public void execute(JavaPlugin javaPlugin) {
                                configureJavaClasspath(project, task);
                            }

                        });

                        configureScalaDependencies(project, task);
                    }

                });
            }

        });
    }

    private static void configureJavaClasspath(final Project project, GenerateEclipseClasspath task) {
        task.getClasspath().setPlusConfigurations(Lists.newArrayList(project.getConfigurations().getByName("compileClasspath"), project.getConfigurations().getByName("runtimeClasspath"), project.getConfigurations().getByName("testCompileClasspath"), project.getConfigurations().getByName("testRuntimeClasspath")));
        ((IConventionAware) task.getClasspath()).getConventionMapping().map("classFolders", new Callable<List<File>>() {
            @Override
            public List<File> call() {
                SourceSetContainer sourceSets = project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets();
                return Lists.newArrayList(Iterables.concat(sourceSets.getByName("main").getOutput().getDirs(), sourceSets.getByName("test").getOutput().getDirs()));
            }

        });
        SourceSetContainer sourceSets = project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets();
        task.dependsOn(sourceSets.getByName("main").getOutput().getDirs());
        task.dependsOn(sourceSets.getByName("test").getOutput().getDirs());
    }

    private static void configureScalaDependencies(final Project project, final GenerateEclipseClasspath task) {
        project.getPlugins().withType(ScalaBasePlugin.class, new Action<ScalaBasePlugin>() {
            @Override
            public void execute(ScalaBasePlugin scalaBasePlugin) {
                task.getClasspath().containers("org.scala-ide.sdt.launching.SCALA_CONTAINER");

                // exclude the dependencies already provided by SCALA_CONTAINER; prevents problems with Eclipse Scala plugin
                project.getGradle().addBuildListener(new BuildAdapter() {
                    @Override
                    public void projectsEvaluated(Gradle gradle) {
                        final List<String> provided = Lists.newArrayList("scala-library", "scala-swing", "scala-dbc");
                        Predicate<Dependency> dependencyInProvided = new Predicate<Dependency>() {
                            @Override
                            public boolean apply(Dependency dependency) {
                                return provided.contains(dependency.getName());
                            }

                        };
                        List<Dependency> dependencies = Lists.newArrayList(Iterables.filter(Iterables.concat(Iterables.transform(task.getClasspath().getPlusConfigurations(), new Function<Configuration, Iterable<Dependency>>() {
                            @Override
                            public Iterable<Dependency> apply(Configuration config) {
                                return config.getAllDependencies();
                            }

                        })), dependencyInProvided));
                        if (!dependencies.isEmpty()) {
                            task.getClasspath().getMinusConfigurations().add(project.getConfigurations().detachedConfiguration(dependencies.toArray(new Dependency[0])));
                        }
                    }
                });
            }

        });
    }

    private void configureEclipseJdt(final Project project, final EclipseModel model) {
        final EclipsePlugin eclipsePlugin = this;
        project.getPlugins().withType(JavaBasePlugin.class, new Action<JavaBasePlugin>() {
            @Override
            public void execute(JavaBasePlugin javaBasePlugin) {
                maybeAddTask(project, eclipsePlugin, ECLIPSE_JDT_TASK_NAME, GenerateEclipseJdt.class, new Action<GenerateEclipseJdt>() {
                    @Override
                    public void execute(GenerateEclipseJdt task) {
                        //task properties:
                        task.setDescription("Generates the Eclipse JDT settings file.");
                        task.setOutputFile(project.file(".settings/org.eclipse.jdt.core.prefs"));
                        task.setInputFile(project.file(".settings/org.eclipse.jdt.core.prefs"));
                        //model properties:
                        EclipseJdt jdt = task.getJdt();
                        model.setJdt(jdt);
                        ConventionMapping conventionMapping = ((IConventionAware) jdt).getConventionMapping();
                        conventionMapping.map("sourceCompatibility", new Callable<JavaVersion>() {
                            @Override
                            public JavaVersion call() {
                                return project.getConvention().getPlugin(JavaPluginConvention.class).getSourceCompatibility();
                            }

                        });
                        conventionMapping.map("targetCompatibility", new Callable<JavaVersion>() {
                            @Override
                            public JavaVersion call() {
                                return project.getConvention().getPlugin(JavaPluginConvention.class).getTargetCompatibility();
                            }

                        });
                        conventionMapping.map("javaRuntimeName", new Callable<String>() {
                            @Override
                            public String call() {
                                return eclipseJavaRuntimeNameFor(project.getConvention().getPlugin(JavaPluginConvention.class).getTargetCompatibility());
                            }

                        });
                    }

                });
            }

        });
    }

    private static String eclipseJavaRuntimeNameFor(JavaVersion version) {
        // Default Eclipse JRE paths:
        // https://github.com/eclipse/eclipse.jdt.debug/blob/master/org.eclipse.jdt.launching/plugin.xml#L241-L303
        switch (version) {
            case VERSION_1_1:
                return "JRE-1.1";
            case VERSION_1_2:
            case VERSION_1_3:
            case VERSION_1_4:
            case VERSION_1_5:
                return "J2SE-" + version;
            case VERSION_1_6:
            case VERSION_1_7:
            case VERSION_1_8:
                return "JavaSE-" + version;
            default:
                return "JavaSE-" + version.getMajorVersion();
        }
    }

    private void applyEclipseWtpPluginOnWebProjects(Project project) {
        Action<Plugin<Project>> action = createActionApplyingEclipseWtpPlugin();
        project.getPlugins().withType(WarPlugin.class, action);
        project.getPlugins().withType(EarPlugin.class, action);
    }

    private Action<Plugin<Project>> createActionApplyingEclipseWtpPlugin() {
        return new Action<Plugin<Project>>() {
            @Override
            public void execute(Plugin<Project> plugin) {
                project.getPluginManager().apply(EclipseWtpPlugin.class);
            }

        };
    }

    private static <T extends Task> void maybeAddTask(Project project, IdePlugin plugin, String taskName, Class<T> taskType, Action<T> action) {
        TaskContainer tasks = project.getTasks();
        if (tasks.findByName(taskName) != null) {
            return;
        }

        T task = tasks.create(taskName, taskType);
        action.execute(task);
        plugin.addWorker(task);
    }

    private static final Predicate<Project> HAS_ECLIPSE_PLUGIN = new Predicate<Project>() {
        @Override
        public boolean apply(Project project) {
            return project.getPlugins().hasPlugin(EclipsePlugin.class);
        }
    };

    private static class EclipseArtifact extends DefaultPublishArtifact {
        private final EclipseProject eclipseProject;
        private final File projectDir;

        public EclipseArtifact(EclipseProject eclipseProject, File projectDir, String extension, Object... tasks) {
            super(null, extension, "eclipse." + extension, null, null, null, tasks);
            this.eclipseProject = eclipseProject;
            this.projectDir = projectDir;
        }

        @Override
        public String getName() {
            return eclipseProject.getName();
        }

        @Override
        public File getFile() {
            return new File(projectDir, "." + getExtension());
        }
    }

}
