/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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 com.android.build.gradle.internal.transforms;

import static com.google.common.base.Preconditions.checkNotNull;

import com.android.annotations.NonNull;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.SecondaryFile;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformException;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.build.gradle.ProguardFiles;
import com.android.build.gradle.internal.CompileOptions;
import com.android.build.gradle.internal.TaskManager;
import com.android.build.gradle.internal.core.GradleVariantConfiguration;
import com.android.build.gradle.internal.dsl.CoreAnnotationProcessorOptions;
import com.android.build.gradle.internal.pipeline.TransformManager;
import com.android.build.gradle.internal.scope.GlobalScope;
import com.android.build.gradle.internal.scope.VariantScope;
import com.android.build.gradle.tasks.JackPreDexTransform;
import com.android.build.gradle.tasks.factory.AbstractCompilesUtil;
import com.android.builder.core.AndroidBuilder;
import com.android.builder.core.JackProcessOptions;
import com.android.ide.common.process.ProcessException;
import com.android.jack.api.ConfigNotSupportedException;
import com.android.jack.api.v01.CompilationException;
import com.android.jack.api.v01.ConfigurationException;
import com.android.jack.api.v01.UnrecoverableException;
import com.android.repository.Revision;
import com.android.sdklib.BuildToolInfo;
import com.android.utils.StringHelper;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

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

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Transform for compiling using Jack.
 *
 * <p>Transform inputs:</p>
 * <ul>
 *     <li>Jack format libraries, produced by the for packaged libs {@link JackPreDexTransform}</li>
 *     <li>Jack format classpath, produced by the for runtime libs {@link JackPreDexTransform}
 *         i.e. normally the android.jar</li>
 * </ul>
 * <p>Secondary inputs:</p>
 * <ul>
 *     <li>Java source files to be compiled by Jack.</li>
 *     <li>Proguard config files</li>
 *     <li>JarJar config files</li>
 *     <li>The Jack binary</li>
 * </ul>
 * <p>Transform outputs:</p>
 * <ul>
 *     <li>Dex files for the whole app, including its dependencies</li>
 * </ul>
 * <p>Secondary outputs:</p>
 * <ul>
 *     <li>Proguard mapping file (for decoding stacktraces and test compilation)</li>
 *     <li>Jack file for test compilation</li>
 * </ul>
 */
public class JackTransform extends Transform {
    private Project project;

    private AndroidBuilder androidBuilder;

    private JackProcessOptions options;

    private boolean jackInProcess;

    private final List<ConfigurableFileTree> sourceFileTrees = Lists.newArrayList();

    @NonNull
    @Override
    public String getName() {
        return "jack";
    }

    @NonNull
    @Override
    public Collection<SecondaryFile> getSecondaryFiles() {
        ImmutableList.Builder<SecondaryFile> builder = ImmutableList.builder();
        builder.addAll(
                Iterables.transform(options.getProguardFiles(), SecondaryFile::nonIncremental));
        builder.addAll(
                Iterables.transform(options.getJarJarRuleFiles(), SecondaryFile::nonIncremental));
        builder.addAll(
                Iterables.transform(
                        options.getAnnotationProcessorClassPath(), SecondaryFile::nonIncremental));
        builder.addAll(Iterables.transform(getSourceFiles(), SecondaryFile::incremental));

        builder.add(
                SecondaryFile.nonIncremental(
                        new File(androidBuilder
                                .getTargetInfo()
                                .getBuildTools()
                                .getPath(BuildToolInfo.PathId.JACK))));

        return builder.build();
    }

    @NonNull
    @Override
    public Collection<File> getSecondaryFileOutputs() {
        ImmutableList.Builder<File> builder = ImmutableList.builder();
        if (options.getOutputFile() != null) {
            builder.add(options.getOutputFile());
        }
        if (options.getMappingFile() != null) {
            builder.add(options.getMappingFile());
        }
        return builder.build();
    }

    @NonNull
    @Override
    public Map<String, Object> getParameterInputs() {
        Map<String, Object> params = Maps.newHashMap();
        params.put("javaResourcesFolder", options.getResourceDirectories());
        params.put("isDebugLog", options.isDebugLog());
        params.put("multiDexEnabled", options.isMultiDex());
        params.put("minSdkVersion", options.getMinSdkVersion());
        params.put("javaMaxHeapSize", options.getJavaMaxHeapSize());
        params.put("sourceCompatibility", options.getSourceCompatibility());
        params.put("buildToolsRev",
                androidBuilder.getTargetInfo().getBuildTools().getRevision().toString());
        return params;
    }

    @NonNull
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_JACK;
    }

    @NonNull
    @Override
    public Set<QualifiedContent.ContentType> getOutputTypes() {
        return TransformManager.CONTENT_DEX;
    }

    @NonNull
    @Override
    public Set<QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    @NonNull
    @Override
    public Set<QualifiedContent.Scope> getReferencedScopes() {
        return Sets.immutableEnumSet(
                QualifiedContent.Scope.PROVIDED_ONLY,
                QualifiedContent.Scope.TESTED_CODE);
    }

    @Override
    public boolean isIncremental() {
        return false;
    }

    @Override
    public void transform(@NonNull final TransformInvocation transformInvocation)
            throws TransformException, InterruptedException, IOException {
        try {
            runJack(transformInvocation);
        } catch (ProcessException | ConfigNotSupportedException | CompilationException
                | ClassNotFoundException | UnrecoverableException | ConfigurationException e) {
            throw new TransformException(e);
        }
    }

    private void runJack(@NonNull TransformInvocation transformInvocation)
            throws ProcessException, IOException, ConfigNotSupportedException,
            ClassNotFoundException, CompilationException, ConfigurationException,
            UnrecoverableException {

        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        checkNotNull(outputProvider);
        final File outDirectory = outputProvider.getContentLocation(
                "main",
                getOutputTypes(),
                getScopes(),
                Format.DIRECTORY);

        options.setDexOutputDirectory(outDirectory);
        options.setClasspaths(
                TransformInputUtil.getAllFiles(transformInvocation.getReferencedInputs()));
        options.setImportFiles(TransformInputUtil.getAllFiles(transformInvocation.getInputs()));
        options.setInputFiles(getSourceFiles());

        androidBuilder.convertByteCodeUsingJack(options, jackInProcess);
    }

    private Collection<File> getSourceFiles() {
        Collection<File> sourceFiles = Lists.newArrayList();
        for (ConfigurableFileTree fileTree : sourceFileTrees) {
            sourceFiles.addAll(fileTree.getFiles());
        }
        return sourceFiles;
    }

    public File getMappingFile() {
        return options.getMappingFile();
    }

    public void addSource(File sourceFolder) {
        sourceFileTrees.add(project.fileTree(sourceFolder));
    }

    public JackTransform(
            final VariantScope scope,
            final boolean isDebugLog,
            final boolean compileJavaSources) {

        options = new JackProcessOptions();

        options.setDebugLog(isDebugLog);
        GlobalScope globalScope = scope.getGlobalScope();

        androidBuilder = globalScope.getAndroidBuilder();

        project = globalScope.getProject();
        if (compileJavaSources) {
            sourceFileTrees.addAll(scope.getVariantData().getJavaSources());
        }
        final GradleVariantConfiguration config = scope.getVariantData().getVariantConfiguration();
        options.setJavaMaxHeapSize(globalScope.getExtension().getDexOptions().getJavaMaxHeapSize());
        options.setJumboMode(globalScope.getExtension().getDexOptions().getJumboMode());
        boolean isDebuggable = scope.getVariantConfiguration().getBuildType().isDebuggable();
        options.setDebuggable(isDebuggable);
        options.setDexOptimize(
                Objects.firstNonNull(
                        globalScope.getExtension().getDexOptions().getOptimize(), !isDebuggable));
        options.setMultiDex(config.isMultiDexEnabled());
        options.setMinSdkVersion(config.getMinSdkVersion().getApiLevel());
        if (!Boolean.FALSE.equals(globalScope.getExtension().getCompileOptions().getIncremental())
                && androidBuilder.getTargetInfo().getBuildTools().getRevision().compareTo(
                        new Revision(24, 0, 3), Revision.PreviewComparison.IGNORE) >= 0) {
            String taskName = StringHelper.combineAsCamelCase(
                    ImmutableList.of(
                            "transformJackWith",
                            getName(),
                            "for",
                            scope.getFullVariantName()));
            options.setIncrementalDir(scope.getIncrementalDir(taskName));
        }
        options.setOutputFile(scope.getJackClassesZip());
        options.setResourceDirectories(ImmutableList.of(scope.getJavaResourcesDestinationDir()));

        CoreAnnotationProcessorOptions annotationProcessorOptions =
                config.getJavaCompileOptions().getAnnotationProcessorOptions();
        checkNotNull(annotationProcessorOptions.getIncludeCompileClasspath());
        options.setAnnotationProcessorClassPath(
                Lists.newArrayList(
                        scope.getVariantData().getVariantDependency()
                                .resolveAndGetAnnotationProcessorClassPath(
                                        annotationProcessorOptions.getIncludeCompileClasspath(),
                                        androidBuilder.getErrorReporter())));
        options.setAnnotationProcessorNames(annotationProcessorOptions.getClassNames());
        options.setAnnotationProcessorOptions(annotationProcessorOptions.getArguments());
        options.setAnnotationProcessorOutputDirectory(scope.getAnnotationProcessorOutputDir());
        options.setEcjOptionFile(scope.getJackEcjOptionsFile());
        options.setAdditionalParameters(config.getJackOptions().getAdditionalParameters());

        jackInProcess = config.getJackOptions().isJackInProcess();

        if (config.getBuildType().isTestCoverageEnabled()) {
            options.setCoverageMetadataFile(scope.getJackCoverageMetadataFile());
        }

        if (config.isMinifyEnabled()) {
            // since all the output use the same resources, we can use the first output
            // to query for a proguard file.
            File sdkDir = scope.getGlobalScope().getSdkHandler().getAndCheckSdkFolder();
            checkNotNull(sdkDir);
            File defaultProguardFile = ProguardFiles.getDefaultProguardFile(
                    TaskManager.DEFAULT_PROGUARD_CONFIG_FILE, project);

            Set<File> proguardFiles = config.getProguardFiles(true /*includeLibs*/,
                    ImmutableList.of(defaultProguardFile));
            File proguardResFile = scope.getProcessAndroidResourcesProguardOutputFile();
            proguardFiles.add(proguardResFile);
            // for tested app, we only care about their aapt config since the base
            // configs are the same files anyway.
            if (scope.getTestedVariantData() != null) {
                proguardResFile = scope.getTestedVariantData().getScope()
                        .getProcessAndroidResourcesProguardOutputFile();
                proguardFiles.add(proguardResFile);
            }
            options.setProguardFiles(proguardFiles);
            options.setMappingFile(new File(scope.getProguardOutputFolder(), "mapping.txt"));
        }

        ImmutableList.Builder<File> jarJarRuleFiles = ImmutableList.builder();
        for (File file : config.getJarJarRuleFiles()) {
            jarJarRuleFiles.add(project.file(file));
        }
        options.setJarJarRuleFiles(jarJarRuleFiles.build());

        CompileOptions compileOptions = scope.getGlobalScope().getExtension().getCompileOptions();
        AbstractCompilesUtil.setDefaultJavaVersion(
                compileOptions,
                scope.getGlobalScope().getExtension().getCompileSdkVersion(),
                true /*jackEnabled*/);
        options.setSourceCompatibility(compileOptions.getSourceCompatibility().toString());
    }
}
