/*
 * Copyright 2014 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.sonar.runner.plugins;

import com.beust.jcommander.internal.Maps;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.gradle.api.*;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.artifacts.DependencySet;
import org.gradle.api.artifacts.dsl.DependencyHandler;
import org.gradle.api.internal.ConventionMapping;
import org.gradle.api.internal.plugins.DslObject;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.testing.Test;
import org.gradle.internal.jvm.Jvm;
import org.gradle.listener.ActionBroadcast;
import org.gradle.sonar.runner.SonarProperties;
import org.gradle.sonar.runner.SonarRunnerExtension;
import org.gradle.sonar.runner.SonarRunnerRootExtension;
import org.gradle.sonar.runner.tasks.SonarRunner;
import org.gradle.testing.jacoco.plugins.JacocoPlugin;
import org.gradle.testing.jacoco.plugins.JacocoTaskExtension;

import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import static org.gradle.util.CollectionUtils.nonEmptyOrNull;

/**
 * A plugin for analyzing projects with the <a href="http://redirect.sonarsource.com/doc/analyzing-with-sq-runner.html">SonarQube Runner</a>.
 * <p>
 * When applied to a project, both the project itself and its subprojects will be analyzed (in a single run).
 * <p>
 * Please see the “SonarQube Runner Plugin” chapter of the Gradle User Guide for more information.
 */
@Incubating
public class SonarRunnerPlugin implements Plugin<Project> {

    private static final Predicate<File> FILE_EXISTS = new Predicate<File>() {
        public boolean apply(File input) {
            return input.exists();
        }
    };
    private static final Predicate<File> IS_DIRECTORY = new Predicate<File>() {
        public boolean apply(File input) {
            return input.isDirectory();
        }
    };
    private static final Predicate<File> IS_FILE = new Predicate<File>() {
        public boolean apply(File input) {
            return input.isFile();
        }
    };
    private static final Joiner COMMA_JOINER = Joiner.on(",");

    private Project targetProject;

    public void apply(Project project) {
        targetProject = project;

        final Map<Project, ActionBroadcast<SonarProperties>> actionBroadcastMap = Maps.newHashMap();
        SonarRunner sonarRunnerTask = createTask(project, actionBroadcastMap);

        ActionBroadcast<SonarProperties> actionBroadcast = addBroadcaster(actionBroadcastMap, project);
        project.subprojects(new Action<Project>() {
            public void execute(Project project) {
                ActionBroadcast<SonarProperties> actionBroadcast = addBroadcaster(actionBroadcastMap, project);
                project.getExtensions().create(SonarRunnerExtension.SONAR_RUNNER_EXTENSION_NAME, SonarRunnerExtension.class, actionBroadcast);
            }
        });
        SonarRunnerRootExtension rootExtension = project.getExtensions().create(SonarRunnerExtension.SONAR_RUNNER_EXTENSION_NAME, SonarRunnerRootExtension.class, actionBroadcast);
        addConfiguration(project, rootExtension);
        rootExtension.setForkOptions(sonarRunnerTask.getForkOptions());
    }

    private ActionBroadcast<SonarProperties> addBroadcaster(Map<Project, ActionBroadcast<SonarProperties>> actionBroadcastMap, Project project) {
        ActionBroadcast<SonarProperties> actionBroadcast = new ActionBroadcast<SonarProperties>();
        actionBroadcastMap.put(project, actionBroadcast);
        return actionBroadcast;
    }

    private SonarRunner createTask(final Project project, final Map<Project, ActionBroadcast<SonarProperties>> actionBroadcastMap) {
        SonarRunner sonarRunnerTask = project.getTasks().create(SonarRunnerExtension.SONAR_RUNNER_TASK_NAME, SonarRunner.class);
        sonarRunnerTask.setDescription("Analyzes " + project + " and its subprojects with Sonar Runner.");

        ConventionMapping conventionMapping = new DslObject(sonarRunnerTask).getConventionMapping();
        conventionMapping.map("sonarProperties", new Callable<Object>() {
            public Object call() throws Exception {
                Map<String, Object> properties = Maps.newLinkedHashMap();
                computeSonarProperties(project, properties, actionBroadcastMap);
                return properties;
            }
        });

        sonarRunnerTask.dependsOn(new Callable<Iterable<? extends Task>>() {
            public Iterable<? extends Task> call() throws Exception {
                Iterable<Project> applicableProjects = Iterables.filter(project.getAllprojects(), new Predicate<Project>() {
                    public boolean apply(Project input) {
                        return input.getPlugins().hasPlugin(JavaPlugin.class)
                                && !input.getExtensions().getByType(SonarRunnerExtension.class).isSkipProject();
                    }
                });

                return Iterables.transform(applicableProjects, new Function<Project, Task>() {
                    @Nullable
                    public Task apply(Project input) {
                        return input.getTasks().getByName(JavaPlugin.TEST_TASK_NAME);
                    }
                });
            }
        });

        return sonarRunnerTask;
    }

    public void computeSonarProperties(Project project, Map<String, Object> properties, Map<Project, ActionBroadcast<SonarProperties>> sonarPropertiesActionBroadcastMap) {
        SonarRunnerExtension extension = project.getExtensions().getByType(SonarRunnerExtension.class);
        if (extension.isSkipProject()) {
            return;
        }

        Map<String, Object> rawProperties = Maps.newLinkedHashMap();
        addGradleDefaults(project, rawProperties);
        evaluateSonarPropertiesBlocks(sonarPropertiesActionBroadcastMap.get(project), rawProperties);
        if (project.equals(targetProject)) {
            addSystemProperties(rawProperties);
        }

        String projectPrefix = project.getPath().substring(targetProject.getPath().length()).replace(":", ".");
        if (projectPrefix.startsWith(".")) {
            projectPrefix = projectPrefix.substring(1);
        }

        convertProperties(rawProperties, projectPrefix, properties);

        List<Project> enabledChildProjects = Lists.newLinkedList(Iterables.filter(project.getChildProjects().values(), new Predicate<Project>() {
            public boolean apply(Project input) {
                return !input.getExtensions().getByType(SonarRunnerExtension.class).isSkipProject();
            }
        }));

        if (enabledChildProjects.isEmpty()) {
            return;
        }

        Collections.sort(enabledChildProjects);

        String modules = COMMA_JOINER.join(Iterables.transform(enabledChildProjects, new Function<Project, String>() {
            public String apply(Project input) {
                return input.getName();
            }
        }));

        properties.put(convertKey("sonar.modules", projectPrefix), modules);
        for (Project childProject : enabledChildProjects) {
            computeSonarProperties(childProject, properties, sonarPropertiesActionBroadcastMap);
        }
    }

    private void addGradleDefaults(final Project project, final Map<String, Object> properties) {

        // IMPORTANT: Whenever changing the properties/values here, ensure that the Gradle User Guide chapter on this is still in sync.

        properties.put("sonar.projectName", project.getName());
        properties.put("sonar.projectDescription", project.getDescription());
        properties.put("sonar.projectVersion", project.getVersion());
        properties.put("sonar.projectBaseDir", project.getProjectDir());
        properties.put("sonar.dynamicAnalysis", "reuseReports");

        if (project.equals(targetProject)) {
            // We only set project key for root project because Sonar Runner 2.0 will automatically
            // prefix subproject keys with parent key, even if subproject keys are set explicitly.
            // Therefore it's better to rely on Sonar's defaults.
            properties.put("sonar.projectKey", getProjectKey(project));
            properties.put("sonar.environment.information.key", "Gradle");
            properties.put("sonar.environment.information.version", project.getGradle().getGradleVersion());
            properties.put("sonar.working.directory", new File(project.getBuildDir(), "sonar"));
        }

        project.getPlugins().withType(JavaBasePlugin.class, new Action<JavaBasePlugin>() {
            public void execute(JavaBasePlugin javaBasePlugin) {
                JavaPluginConvention javaPluginConvention = new DslObject(project).getConvention().getPlugin(JavaPluginConvention.class);
                properties.put("sonar.java.source", javaPluginConvention.getSourceCompatibility());
                properties.put("sonar.java.target", javaPluginConvention.getTargetCompatibility());
            }
        });

        project.getPlugins().withType(JavaPlugin.class, new Action<JavaPlugin>() {
            public void execute(JavaPlugin javaPlugin) {
                JavaPluginConvention javaPluginConvention = new DslObject(project).getConvention().getPlugin(JavaPluginConvention.class);

                SourceSet main = javaPluginConvention.getSourceSets().getAt("main");
                List<File> sourceDirectories = nonEmptyOrNull(Iterables.filter(main.getAllSource().getSrcDirs(), FILE_EXISTS));
                properties.put("sonar.sources" , sourceDirectories);
                SourceSet test = javaPluginConvention.getSourceSets().getAt("test");
                List<File> testDirectories = nonEmptyOrNull(Iterables.filter(test.getAllSource().getSrcDirs(), FILE_EXISTS));
                properties.put("sonar.tests", testDirectories);

                properties.put("sonar.binaries", nonEmptyOrNull(Iterables.filter(main.getRuntimeClasspath(), IS_DIRECTORY)));
                properties.put("sonar.libraries", getLibraries(main));

                final Test testTask = (Test) project.getTasks().getByName(JavaPlugin.TEST_TASK_NAME);

                if (sourceDirectories != null || testDirectories != null) {
                    File testResultsDir = testTask.getReports().getJunitXml().getDestination();
                    // create the test results folder to prevent SonarQube from emitting
                    // a warning if a project does not contain any tests
                    testResultsDir.mkdirs();


                    properties.put("sonar.surefire.reportsPath", testResultsDir);
                    // added due to https://issues.gradle.org/browse/GRADLE-3005
                    properties.put("sonar.junit.reportsPath", testResultsDir);
                }

                project.getPlugins().withType(JacocoPlugin.class, new Action<JacocoPlugin>() {
                    public void execute(JacocoPlugin jacocoPlugin) {
                        JacocoTaskExtension jacocoTaskExtension = testTask.getExtensions().getByType(JacocoTaskExtension.class);
                        File destinationFile = jacocoTaskExtension.getDestinationFile();
                        if (destinationFile.exists()) {
                            properties.put("sonar.jacoco.reportPath", destinationFile);
                        }
                    }
                });
            }
        });

        if (properties.get("sonar.sources") == null) {
            // Should be able to remove this after upgrading to Sonar Runner 2.1 (issue is already marked as fixed),
            // if we can live with the fact that leaf projects w/o source dirs will still cause a failure.
            properties.put("sonar.sources", "");
        }
    }

    private String getProjectKey(Project project) {
        // SonarQube uses project keys in URL parameters without internally URL-encoding them.
        // According to my manual tests with sonar-runner plugin based on Sonar Runner 2.0 and Sonar 3.4.1,
        // the current defaults will only cause a problem if project.group or project.name of
        // the Gradle project to which the plugin is applied contains special characters.
        // (':' works, ' ' doesn't.) In such a case, sonar.projectKey can be overridden manually.
        String name = project.getName();
        String group = project.getGroup().toString();
        return group.isEmpty() ? name : group + ":" + name;
    }

    private void addSystemProperties(Map<String, Object> properties) {
        for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) {
            String key = entry.getKey().toString();
            if (key.startsWith("sonar.")) {
                properties.put(key, entry.getValue());
            }
        }
    }

    private Collection<File> getLibraries(SourceSet main) {
        List<File> libraries = Lists.newLinkedList(Iterables.filter(main.getRuntimeClasspath(), IS_FILE));
        File runtimeJar = Jvm.current().getRuntimeJar();
        if (runtimeJar != null) {
            libraries.add(runtimeJar);
        }

        return libraries;
    }

    private void convertProperties(Map<String, Object> rawProperties, final String projectPrefix, final Map<String, Object> properties) {
        for (Map.Entry<String, Object> entry : rawProperties.entrySet()) {
            String value = convertValue(entry.getValue());
            if (value != null) {
                properties.put(convertKey(entry.getKey(), projectPrefix), value);
            }
        }
    }

    private String convertKey(String key, final String projectPrefix) {
        return projectPrefix.isEmpty() ? key : projectPrefix + "." + key;
    }

    private String convertValue(Object value) {
        if (value == null) {
            return null;
        }
        if (value instanceof Iterable<?>) {
            Iterable<String> flattened = Iterables.transform((Iterable<?>) value, new Function<Object, String>() {
                public String apply(Object input) {
                    return convertValue(input);
                }
            });

            Iterable<String> filtered = Iterables.filter(flattened, Predicates.notNull());
            String joined = COMMA_JOINER.join(filtered);
            return joined.isEmpty() ? null : joined;
        } else {
            return value.toString();
        }
    }

    private void addConfiguration(final Project project, final SonarRunnerRootExtension rootExtension) {
        final Configuration configuration = project.getConfigurations().create(SonarRunnerExtension.SONAR_RUNNER_CONFIGURATION_NAME);
        configuration
                .setVisible(false)
                .setTransitive(false)
                .setDescription("The SonarRunner configuration to use to run analysis")
                .defaultDependencies(new Action<DependencySet>() {
                    @Override
                    public void execute(DependencySet dependencies) {
                        String toolVersion = rootExtension.getToolVersion();
                        DependencyHandler dependencyHandler = project.getDependencies();
                        Dependency dependency = dependencyHandler.create("org.codehaus.sonar.runner:sonar-runner-dist:" + toolVersion);
                        dependencies.add(dependency);
                    }
                });
    }

    private static void evaluateSonarPropertiesBlocks(ActionBroadcast<? super SonarProperties> propertiesActions, Map<String, Object> properties) {
        SonarProperties sonarProperties = new SonarProperties(properties);
        propertiesActions.execute(sonarProperties);
    }

}
