/*
 * Copyright 2012 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.api.internal.tasks.compile;

import com.google.common.base.Joiner;
import org.gradle.api.JavaVersion;
import org.gradle.api.file.FileCollection;
import org.gradle.api.tasks.compile.CompileOptions;
import org.gradle.api.tasks.compile.ForkOptions;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class JavaCompilerArgumentsBuilder {
    public static final String USE_UNSHARED_COMPILER_TABLE_OPTION = "-XDuseUnsharedTable=true";
    public static final String EMPTY_SOURCE_PATH_REF_DIR = "emptySourcePathRef";

    private final JavaCompileSpec spec;

    private boolean includeLauncherOptions;
    private boolean includeMainOptions = true;
    private boolean includeClasspath = true;
    private boolean includeSourceFiles;
    private boolean includeCustomizations = true;

    private List<String> args;

    public JavaCompilerArgumentsBuilder(JavaCompileSpec spec) {
        this.spec = spec;
    }

    public JavaCompilerArgumentsBuilder includeLauncherOptions(boolean flag) {
        includeLauncherOptions = flag;
        return this;
    }

    public JavaCompilerArgumentsBuilder includeMainOptions(boolean flag) {
        includeMainOptions = flag;
        return this;
    }

    public JavaCompilerArgumentsBuilder includeClasspath(boolean flag) {
        includeClasspath = flag;
        return this;
    }

    public JavaCompilerArgumentsBuilder includeSourceFiles(boolean flag) {
        includeSourceFiles = flag;
        return this;
    }

    public JavaCompilerArgumentsBuilder includeCustomizations(boolean flag) {
        includeCustomizations = flag;
        return this;
    }

    public List<String> build() {
        args = new ArrayList<String>();

        addLauncherOptions();
        addMainOptions();
        addClasspath();
        addSourceFiles();
        addCustomizations();

        return args;
    }

    private void addCustomizations() {
        if (includeCustomizations) {
            /*This is an internal option, it's used in com.sun.tools.javac.util.Names#createTable(Options options). The -XD backdoor switch is used to set it, as described in a comment
            in com.sun.tools.javac.main.RecognizedOptions#getAll(OptionHelper helper). This option was introduced in JDK 7 and controls if compiler's name tables should be reused.
            Without this option being set they are stored in a static list using soft references which can lead to memory pressure and performance deterioration
            when using the daemon, especially when using small heap and building a large project.
            Due to a bug (https://builds.gradle.org/viewLog.html?buildId=284033&tab=buildResultsDiv&buildTypeId=Gradle_Master_Performance_PerformanceExperimentsLinux) no instances of
            SharedNameTable are actually ever reused. It has been fixed for JDK9 and we should consider not using this option with JDK9 as not using it  will quite probably improve the
            performance of compilation.
            Using this option leads to significant performance improvements when using daemon and compiling java sources with JDK7 and JDK8.*/
            args.add(USE_UNSHARED_COMPILER_TABLE_OPTION);
        }
    }

    private void addLauncherOptions() {
        if (!includeLauncherOptions) {
            return;
        }

        ForkOptions forkOptions = spec.getCompileOptions().getForkOptions();
        if (forkOptions.getMemoryInitialSize() != null) {
            args.add("-J-Xms" + forkOptions.getMemoryInitialSize().trim());
        }
        if (forkOptions.getMemoryMaximumSize() != null) {
            args.add("-J-Xmx" + forkOptions.getMemoryMaximumSize().trim());
        }
        if (forkOptions.getJvmArgs() != null) {
            args.addAll(forkOptions.getJvmArgs());
        }
    }

    private void addMainOptions() {
        if (!includeMainOptions) {
            return;
        }

        String sourceCompatibility = spec.getSourceCompatibility();
        if (sourceCompatibility != null && !JavaVersion.current().equals(JavaVersion.toVersion(sourceCompatibility))) {
            args.add("-source");
            args.add(sourceCompatibility);
        }
        String targetCompatibility = spec.getTargetCompatibility();
        if (targetCompatibility != null && !JavaVersion.current().equals(JavaVersion.toVersion(targetCompatibility))) {
            args.add("-target");
            args.add(targetCompatibility);
        }
        File destinationDir = spec.getDestinationDir();
        if (destinationDir != null) {
            args.add("-d");
            args.add(destinationDir.getPath());
        }
        CompileOptions compileOptions = spec.getCompileOptions();
        if (compileOptions.isVerbose()) {
            args.add("-verbose");
        }
        if (compileOptions.isDeprecation()) {
            args.add("-deprecation");
        }
        if (!compileOptions.isWarnings()) {
            args.add("-nowarn");
        }
        if (compileOptions.isDebug()) {
            if (compileOptions.getDebugOptions().getDebugLevel() != null) {
                args.add("-g:" + compileOptions.getDebugOptions().getDebugLevel().trim());
            } else {
                args.add("-g");
            }
        } else {
            args.add("-g:none");
        }
        if (compileOptions.getEncoding() != null) {
            args.add("-encoding");
            args.add(compileOptions.getEncoding());
        }
        if (compileOptions.getBootClasspath() != null) { //TODO: move bootclasspath to platform
            args.add("-bootclasspath");
            args.add(compileOptions.getBootClasspath());
        }
        if (compileOptions.getExtensionDirs() != null) {
            args.add("-extdirs");
            args.add(compileOptions.getExtensionDirs());
        }
        FileCollection sourcepath = compileOptions.getSourcepath();
        Iterable<File> classpath = spec.getClasspath();
        if ((sourcepath != null && !sourcepath.isEmpty()) || (includeClasspath && (classpath != null && classpath.iterator().hasNext()))) {
            args.add("-sourcepath");
            args.add(sourcepath == null ? emptyFolder(spec.getTempDir()) : sourcepath.getAsPath());
        }
        if (compileOptions.getCompilerArgs() != null) {
            args.addAll(compileOptions.getCompilerArgs());
        }
    }

    private String emptyFolder(File parent) {
        File emptySourcePath = new File(parent, EMPTY_SOURCE_PATH_REF_DIR);
        emptySourcePath.mkdirs();
        return emptySourcePath.getAbsolutePath();
    }

    private void addClasspath() {
        if (!includeClasspath) {
            return;
        }

        Iterable<File> classpath = spec.getClasspath();
        if (classpath != null && classpath.iterator().hasNext()) {
            args.add("-classpath");
            args.add(Joiner.on(File.pathSeparatorChar).join(classpath));
        }
    }

    private void addSourceFiles() {
        if (!includeSourceFiles) {
            return;
        }

        for (File file : spec.getSource()) {
            args.add(file.getPath());
        }
    }
}
