package com.android.build.gradle.tasks.factory;

import static com.android.builder.core.VariantType.LIBRARY;
import static com.android.builder.core.VariantType.UNIT_TEST;
import static com.google.common.base.Preconditions.checkNotNull;

import com.android.annotations.NonNull;
import com.android.build.gradle.AndroidGradleOptions;
import com.android.build.gradle.internal.CompileOptions;
import com.android.build.gradle.internal.LoggerWrapper;
import com.android.build.gradle.internal.dsl.CoreAnnotationProcessorOptions;
import com.android.build.gradle.internal.scope.ConventionMappingHelper;
import com.android.build.gradle.internal.scope.GlobalScope;
import com.android.build.gradle.internal.scope.TaskConfigAction;
import com.android.build.gradle.internal.scope.VariantScope;
import com.android.build.gradle.internal.variant.BaseVariantData;
import com.android.builder.model.AndroidLibrary;
import com.android.builder.model.SyncIssue;
import com.android.utils.FileUtils;
import com.android.utils.ILogger;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;

import org.gradle.api.JavaVersion;
import org.gradle.api.Project;
import org.gradle.api.file.ConfigurableFileTree;
import org.gradle.api.file.FileCollection;

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

/**
 * Configuration Action for a JavaCompile task.
 */
public class JavaCompileConfigAction implements TaskConfigAction<AndroidJavaCompile> {
    private static final ILogger LOG = LoggerWrapper.getLogger(JavaCompileConfigAction.class);

    private VariantScope scope;

    public JavaCompileConfigAction(VariantScope scope) {
        this.scope = scope;
    }

    @NonNull
    @Override
    public String getName() {
        return scope.getTaskName("compile", "JavaWithJavac");
    }

    @NonNull
    @Override
    public Class<AndroidJavaCompile> getType() {
        return AndroidJavaCompile.class;
    }

    @Override
    public void execute(@NonNull final AndroidJavaCompile javacTask) {
        final BaseVariantData testedVariantData = scope.getTestedVariantData();
        scope.getVariantData().javacTask = javacTask;
        javacTask.compileSdkVersion = scope.getGlobalScope().getExtension().getCompileSdkVersion();
        javacTask.mBuildContext = scope.getInstantRunBuildContext();

        // We can't just pass the collection directly, as the instanceof check in the incremental
        // compile doesn't work recursively currently, so every ConfigurableFileTree needs to be
        // directly in the source array.
        for (ConfigurableFileTree fileTree: scope.getVariantData().getJavaSources()) {
            javacTask.source(fileTree);
        }

        // javac 1.8 may generate code that uses class not available in android.jar.  This is fine
        // if jack is used to compile code for the app and this compile task is created only for
        // unit test.  In which case, we want to keep the default bootstrap classpath.
        final boolean keepDefaultBootstrap =
                scope.getVariantConfiguration().getJackOptions().isEnabled()
                        && JavaVersion.current().isJava8Compatible();

        if (!keepDefaultBootstrap) {
            // Set boot classpath if we don't need to keep the default.  Otherwise, this is added as
            // normal classpath.
            javacTask.getOptions().setBootClasspath(
                    Joiner.on(File.pathSeparator).join(
                            scope.getGlobalScope().getAndroidBuilder()
                                    .getBootClasspathAsStrings(false)));
        }

        ConventionMappingHelper.map(javacTask, "classpath", new Callable<FileCollection>() {
            @Override
            public FileCollection call() {
                FileCollection classpath = scope.getJavaClasspath();
                Project project = scope.getGlobalScope().getProject();
                if (keepDefaultBootstrap) {
                    classpath = classpath.plus(project.files(
                            scope.getGlobalScope().getAndroidBuilder().getBootClasspath(false)));
                }

                if (testedVariantData != null) {
                    // For libraries, the classpath from androidBuilder includes the library
                    // output (bundle/classes.jar) as a normal dependency. In unit tests we
                    // don't want to package the jar at every run, so we use the *.class
                    // files instead.
                    if (!testedVariantData.getType().equals(LIBRARY)
                            || scope.getVariantData().getType().equals(UNIT_TEST)) {
                        classpath = classpath.plus(project.files(
                                        testedVariantData.getScope().getJavaClasspath(),
                                        testedVariantData.getScope().getJavaOutputDir(),
                                        testedVariantData.getScope().getJavaDependencyCache()));
                    }

                    if (scope.getVariantData().getType().equals(UNIT_TEST)
                            && testedVariantData.getType().equals(LIBRARY)) {
                        // The bundled classes.jar may exist, but it's probably old. Don't
                        // use it, we already have the *.class files in the classpath.
                        AndroidLibrary testedLibrary =
                                testedVariantData.getVariantConfiguration().getOutput();
                        if (testedLibrary != null) {
                            File jarFile = testedLibrary.getJarFile();
                            classpath = classpath.minus(project.files(jarFile));
                        }
                    }
                }
                return classpath;
            }
        });

        javacTask.setDestinationDir(scope.getJavaOutputDir());

        javacTask.setDependencyCacheDir(scope.getJavaDependencyCache());

        CompileOptions compileOptions = scope.getGlobalScope().getExtension().getCompileOptions();

        AbstractCompilesUtil.configureLanguageLevel(
                javacTask,
                compileOptions,
                scope.getGlobalScope().getExtension().getCompileSdkVersion(),
                scope.getVariantConfiguration().getJackOptions().isEnabled());

        javacTask.getOptions().setEncoding(compileOptions.getEncoding());

        GlobalScope globalScope = scope.getGlobalScope();
        Project project = scope.getGlobalScope().getProject();

        boolean incremental;

        CoreAnnotationProcessorOptions annotationProcessorOptions =
                scope.getVariantConfiguration().getJavaCompileOptions()
                        .getAnnotationProcessorOptions();


        checkNotNull(annotationProcessorOptions.getIncludeCompileClasspath());
        Collection<File> processorPath =
                Lists.newArrayList(
                        scope.getVariantData().getVariantDependency()
                                .resolveAndGetAnnotationProcessorClassPath(
                                        annotationProcessorOptions.getIncludeCompileClasspath(),
                                        scope.getGlobalScope().getAndroidBuilder().getErrorReporter()));
        if (compileOptions.getIncremental() != null) {
            incremental = compileOptions.getIncremental();
            LOG.info("Incremental flag set to %1$b in DSL", incremental);
        } else {
            if (globalScope.getExtension().getDataBinding().isEnabled()
                    || !processorPath.isEmpty()
                    || project.getPlugins().hasPlugin("com.neenbedankt.android-apt")
                    || project.getPlugins().hasPlugin("me.tatarka.retrolambda")) {
                incremental = false;
                LOG.info("Incremental Java compilation disabled in variant %1$s "
                                + "as you are using an incompatible plugin",
                        scope.getVariantConfiguration().getFullName());
            } else if (scope.getTestedVariantData() != null) {
                // Incremental javac is currently (Gradle 2.14-2.14.1) broken for invocations
                // that have directories on their classpath.
                incremental = false;
            } else {
                // For now, default to true, unless the use uses several source folders,
                // in that case, we cannot guarantee that the incremental java works fine.

                // some source folders may be configured but do not exist, in that case, don't
                // use as valid source folders to determine whether or not we should turn on
                // incremental compilation.
                List<File> sourceFolders = new ArrayList<File>();
                for (ConfigurableFileTree sourceFolder
                        : scope.getVariantData().getUserJavaSources()) {

                    if (sourceFolder.getDir().exists()) {
                        sourceFolders.add(sourceFolder.getDir());
                    }
                }
                incremental = sourceFolders.size() == 1;
                if (sourceFolders.size() > 1) {
                    LOG.info("Incremental Java compilation disabled in variant %1$s "
                            + "as you are using %2$d source folders : %3$s",
                            scope.getVariantConfiguration().getFullName(),
                            sourceFolders.size(), Joiner.on(',').join(sourceFolders));
                }
            }
        }

        if (AndroidGradleOptions.isJavaCompileIncrementalPropertySet(project)) {
            scope.getGlobalScope().getAndroidBuilder().getErrorReporter().handleSyncError(
                    null,
                    SyncIssue.TYPE_GENERIC,
                    String.format(
                            "The %s property has been replaced by a DSL property. Please add the "
                                    + "following to your build.gradle instead:\n"
                                    + "android {\n"
                                    + "  compileOptions.incremental = false\n"
                                    + "}",
                            AndroidGradleOptions.PROPERTY_INCREMENTAL_JAVA_COMPILE));
        }

        if (incremental) {
            LOG.info("Using incremental javac compilation.");
            javacTask.getOptions().setIncremental(true);
        } else {
            LOG.info("Not using incremental javac compilation.");
        }

        if (!processorPath.isEmpty()) {
            if (Boolean.TRUE.equals(annotationProcessorOptions.getIncludeCompileClasspath())) {
                processorPath.addAll(javacTask.getClasspath().getFiles());
            }
            javacTask.getOptions().getCompilerArgs().add("-processorpath");
            javacTask.getOptions().getCompilerArgs().add(FileUtils.joinFilePaths(processorPath));
        }
        if (!annotationProcessorOptions.getClassNames().isEmpty()) {
            javacTask.getOptions().getCompilerArgs().add("-processor");
            javacTask.getOptions().getCompilerArgs().add(
                    Joiner.on(',').join(annotationProcessorOptions.getClassNames()));
        }
        if (!annotationProcessorOptions.getArguments().isEmpty()) {
            for (Map.Entry<String, String> arg :
                    annotationProcessorOptions.getArguments().entrySet()) {
                javacTask.getOptions().getCompilerArgs().add(
                        "-A" + arg.getKey() + "=" + arg.getValue());
            }
        }

        // Create directory for output of annotation processor.
        javacTask.doFirst(task -> {
            FileUtils.mkdirs(scope.getAnnotationProcessorOutputDir());
        });
        javacTask.getOptions().getCompilerArgs().add("-s");
        javacTask.getOptions().getCompilerArgs().add(
                scope.getAnnotationProcessorOutputDir().getAbsolutePath());

    }
}
