// Copyright 2016 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.rules.android;

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.FileProvider;
import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.RunfilesProvider;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
import com.google.devtools.build.lib.rules.java.JavaCommon;
import com.google.devtools.build.lib.rules.java.JavaCompilationArgsProvider;
import com.google.devtools.build.lib.rules.java.JavaCompilationArtifacts;
import com.google.devtools.build.lib.rules.java.JavaHelper;
import com.google.devtools.build.lib.rules.java.JavaProvider;
import com.google.devtools.build.lib.rules.java.JavaRuleOutputJarsProvider;
import com.google.devtools.build.lib.rules.java.JavaRuleOutputJarsProvider.OutputJar;
import com.google.devtools.build.lib.rules.java.JavaRuntimeJarProvider;
import com.google.devtools.build.lib.rules.java.JavaSemantics;
import com.google.devtools.build.lib.rules.java.JavaToolchainProvider;
import com.google.devtools.build.lib.rules.java.Jvm;
import com.google.devtools.build.lib.vfs.PathFragment;

/**
 * An implementation for the aar_import rule.
 *
 * AAR files are zip archives that contain an Android Manifest, JARs, resources, assets, native
 * libraries, Proguard configuration and lint jars. Currently the aar_import rule supports AARs with
 * an AndroidManifest.xml, classes.jar, libs/, res/ and jni/. Assets are not yet supported.
 *
 * @see <a href="http://tools.android.com/tech-docs/new-build-system/aar-format">AAR Format</a>
 */
public class AarImport implements RuleConfiguredTargetFactory {
  private static final String ANDROID_MANIFEST = "AndroidManifest.xml";
  private static final String MERGED_JAR = "classes_and_libs_merged.jar";

  private final JavaSemantics javaSemantics;

  protected AarImport(JavaSemantics javaSemantics) {
    this.javaSemantics = javaSemantics;
  }

  @Override
  public ConfiguredTarget create(RuleContext ruleContext)
      throws InterruptedException, RuleErrorException {
    if (!AndroidSdkProvider.verifyPresence(ruleContext)) {
      return null;
    }

    RuleConfiguredTargetBuilder ruleBuilder = new RuleConfiguredTargetBuilder(ruleContext);
    Artifact aar = ruleContext.getPrerequisiteArtifact("aar", Mode.TARGET);

    Artifact allAarJars = createAarTreeArtifact(ruleContext, "jars");
    Artifact jarMergingParams = createAarArtifact(ruleContext, "jar_merging_params");
    ruleContext.registerAction(
        createAarEmbeddedJarsExtractorActions(ruleContext, aar, allAarJars, jarMergingParams));
    Artifact mergedJar = createAarArtifact(ruleContext, MERGED_JAR);
    ruleContext.registerAction(
        createAarJarsMergingActions(ruleContext, allAarJars, mergedJar, jarMergingParams));

    // AndroidManifest.xml is required in every AAR.
    Artifact androidManifestArtifact = createAarArtifact(ruleContext, ANDROID_MANIFEST);
    ruleContext.registerAction(createSingleFileExtractorActions(
        ruleContext, aar, ANDROID_MANIFEST, androidManifestArtifact));

    Artifact resourcesManifest = createAarArtifact(ruleContext, "resource_manifest");
    ruleContext.registerAction(
        createManifestExtractorActions(ruleContext, aar, "res/.*", resourcesManifest));

    Artifact resources = createAarTreeArtifact(ruleContext, "resources");
    ruleContext.registerAction(
        createManifestFileEntriesExtractorActions(ruleContext, aar, resourcesManifest, resources));

    ApplicationManifest androidManifest =
        ApplicationManifest.fromExplicitManifest(ruleContext, androidManifestArtifact);

    FileProvider resourcesProvider = new FileProvider(
        new NestedSetBuilder<Artifact>(Order.NAIVE_LINK_ORDER).add(resources).build());

    Artifact resourcesZip =
        ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_RESOURCES_ZIP);

    ResourceApk resourceApk =
        androidManifest.packWithDataAndResources(
            ruleContext,
            new LocalResourceContainer.Builder(ruleContext)
                .withResources(ImmutableList.of(resourcesProvider))
                .build(),
            ResourceDependencies.fromRuleDeps(ruleContext, JavaCommon.isNeverLink(ruleContext)),
            ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_R_TXT),
            ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_LOCAL_SYMBOLS),
            ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_PROCESSED_MANIFEST),
            resourcesZip,
            /* alwaysExportManifest = */ true);

    // There isn't really any use case for building an aar_import target on its own, so the files to
    // build could be empty. The resources zip and merged jars are added here as a sanity check for
    // Bazel developers so that `bazel build java/com/my_aar_import` will fail if the resource
    // processing or jar merging steps fail.
    NestedSetBuilder<Artifact> filesToBuildBuilder =
        NestedSetBuilder.<Artifact>stableOrder().add(resourcesZip).add(mergedJar);

    Artifact nativeLibs = createAarArtifact(ruleContext, "native_libs.zip");
    ruleContext.registerAction(createAarNativeLibsFilterActions(ruleContext, aar, nativeLibs));

    JavaRuleOutputJarsProvider.Builder jarProviderBuilder = new JavaRuleOutputJarsProvider.Builder()
        .addOutputJar(mergedJar, null, ImmutableList.<Artifact>of());
    for (TransitiveInfoCollection export : ruleContext.getPrerequisites("exports", Mode.TARGET)) {
      for (OutputJar jar :
          JavaProvider.getProvider(JavaRuleOutputJarsProvider.class, export).getOutputJars()) {
        jarProviderBuilder.addOutputJar(jar);
        filesToBuildBuilder.add(jar.getClassJar());
      }
    }

    ImmutableList<TransitiveInfoCollection> targets =
        ImmutableList.<TransitiveInfoCollection>copyOf(
            ruleContext.getPrerequisites("exports", Mode.TARGET));
    JavaCommon common =
        new JavaCommon(
            ruleContext,
            javaSemantics,
            /* sources = */ ImmutableList.<Artifact>of(),
            /* compileDeps = */ targets,
            /* runtimeDeps = */ targets,
            /* bothDeps = */ targets);
    common.setJavaCompilationArtifacts(
        new JavaCompilationArtifacts.Builder()
            .addRuntimeJar(mergedJar)
            .addCompileTimeJar(mergedJar)
            .build());

    return ruleBuilder
        .setFilesToBuild(filesToBuildBuilder.build())
        .addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY)
        .addProvider(
            AndroidResourcesProvider.class, resourceApk.toResourceProvider(ruleContext.getLabel()))
        .addProvider(
            NativeLibsZipsProvider.class,
            new NativeLibsZipsProvider(
                AndroidCommon.collectTransitiveNativeLibsZips(ruleContext).add(nativeLibs).build()))
        .addProvider(
            JavaRuntimeJarProvider.class, new JavaRuntimeJarProvider(ImmutableList.of(mergedJar)))
        .addProvider(
            JavaCompilationArgsProvider.class,
            JavaCompilationArgsProvider.create(
                common.collectJavaCompilationArgs(
                    /* recursive = */ false,
                    JavaCommon.isNeverLink(ruleContext),
                    /* srcLessDepsExport = */ false),
                common.collectJavaCompilationArgs(
                    /* recursive = */ true,
                    JavaCommon.isNeverLink(ruleContext),
                    /* srcLessDepsExport = */ false)))
        .addProvider(JavaRuleOutputJarsProvider.class, jarProviderBuilder.build())
        .build();
  }

  private static Action[] createSingleFileExtractorActions(RuleContext ruleContext, Artifact aar,
      String filename, Artifact outputArtifact) {
    return new SpawnAction.Builder()
        .setExecutable(ruleContext.getExecutablePrerequisite("$zipper", Mode.HOST))
        .setMnemonic("AarFileExtractor")
        .setProgressMessage("Extracting " + filename + " from " + aar.getFilename())
        .addArgument("x")
        .addInputArgument(aar)
        .addArgument("-d")
        .addOutput(outputArtifact)
        .addArgument(outputArtifact.getExecPath().getParentDirectory().getPathString())
        .addArgument(filename)
        .build(ruleContext);
  }

  private static Action[] createManifestFileEntriesExtractorActions(RuleContext ruleContext,
      Artifact aar, Artifact manifest, Artifact outputTree) {
    return new SpawnAction.Builder()
        .setExecutable(ruleContext.getExecutablePrerequisite("$zipper", Mode.HOST))
        .setMnemonic("AarManifestFileEntriesExtractor")
        .addArgument("x")
        .addInputArgument(aar)
        .addArgument("-d")
        .addOutputArgument(outputTree)
        .addArgument("@" + manifest.getExecPathString())
        .addInput(manifest)
        .build(ruleContext);
  }

  private static Action[] createAarEmbeddedJarsExtractorActions(RuleContext ruleContext,
      Artifact aar, Artifact jarsTreeArtifact, Artifact singleJarParamFile) {
    return new SpawnAction.Builder()
        .setExecutable(
            ruleContext.getExecutablePrerequisite("$aar_embedded_jars_extractor", Mode.HOST))
        .setMnemonic("AarEmbeddedJarsExtractor")
        .setProgressMessage("Extracting classes.jar and libs/*.jar from " + aar.getFilename())
        .addArgument("--input_aar")
        .addInputArgument(aar)
        .addArgument("--output_dir")
        .addOutputArgument(jarsTreeArtifact)
        .addArgument("--output_singlejar_param_file")
        .addOutputArgument(singleJarParamFile)
        .build(ruleContext);
  }

  private static Action[] createAarJarsMergingActions(RuleContext ruleContext,
      Artifact jarsTreeArtifact, Artifact mergedJar, Artifact paramFile) {
    return singleJarSpawnActionBuilder(ruleContext)
        .setMnemonic("AarJarsMerger")
        .setProgressMessage("Merging AAR embedded jars")
        .addInput(jarsTreeArtifact)
        .addArgument("--output")
        .addOutputArgument(mergedJar)
        .addArgument("--dont_change_compression")
        .addInput(paramFile)
        .addArgument("@" + paramFile.getExecPathString())
        .build(ruleContext);
  }

  private static Action[] createManifestExtractorActions(RuleContext ruleContext, Artifact aar,
      String filenameRegexp, Artifact manifest) {
    return new SpawnAction.Builder()
        .setExecutable(ruleContext.getExecutablePrerequisite("$zip_manifest_creator", Mode.HOST))
        .setMnemonic("ZipManifestCreator")
        .setProgressMessage(
            "Creating manifest for " + aar.getFilename() + " matching " + filenameRegexp)
        .addArgument(filenameRegexp)
        .addInputArgument(aar)
        .addOutputArgument(manifest)
        .build(ruleContext);
  }

  private static Action[] createAarNativeLibsFilterActions(RuleContext ruleContext, Artifact aar,
      Artifact outputZip) {
    SpawnAction.Builder actionBuilder = new SpawnAction.Builder()
        .setExecutable(
            ruleContext.getExecutablePrerequisite("$aar_native_libs_zip_creator", Mode.HOST))
        .setMnemonic("AarNativeLibsFilter")
        .setProgressMessage("Filtering AAR native libs by architecture")
        .addArgument("--input_aar")
        .addInputArgument(aar)
        .addArgument("--cpu")
        .addArgument(ruleContext.getConfiguration().getCpu())
        .addArgument("--output_zip")
        .addOutputArgument(outputZip);
    return actionBuilder.build(ruleContext);
  }

  private static Artifact createAarArtifact(RuleContext ruleContext, String name) {
    return ruleContext.getUniqueDirectoryArtifact(
        "_aar", name, ruleContext.getBinOrGenfilesDirectory());
  }

  private static Artifact createAarTreeArtifact(RuleContext ruleContext, String name) {
    PathFragment rootRelativePath = ruleContext.getUniqueDirectory("_aar/unzipped/" + name);
    return ruleContext.getTreeArtifact(rootRelativePath, ruleContext.getBinOrGenfilesDirectory());
  }

  // Adds the appropriate SpawnAction options depending on if SingleJar is a jar or not.
  private static SpawnAction.Builder singleJarSpawnActionBuilder(RuleContext ruleContext) {
    SpawnAction.Builder builder = new SpawnAction.Builder();
    Artifact singleJar = JavaToolchainProvider.fromRuleContext(ruleContext).getSingleJar();
    if (singleJar.getFilename().endsWith(".jar")) {
      builder
          .setJarExecutable(
              ruleContext.getHostConfiguration().getFragment(Jvm.class).getJavaExecutable(),
              singleJar,
              JavaToolchainProvider.fromRuleContext(ruleContext).getJvmOptions())
          .addTransitiveInputs(JavaHelper.getHostJavabaseInputs(ruleContext));
    } else {
      builder.setExecutable(singleJar);
    }
    return builder;
  }
}
