// Copyright 2017 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 static com.google.devtools.build.lib.analysis.OutputGroupInfo.INTERNAL_SUFFIX;

import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.actions.ActionEnvironment;
import com.google.devtools.build.lib.actions.ActionOwner;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.CommandLines;
import com.google.devtools.build.lib.actions.ExecutionRequirements;
import com.google.devtools.build.lib.actions.ParamFileInfo;
import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType;
import com.google.devtools.build.lib.actions.ResourceSetOrBuilder;
import com.google.devtools.build.lib.actions.RunfilesSupplier;
import com.google.devtools.build.lib.analysis.FilesToRunProvider;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.config.CoreOptions.OutputPathsMode;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException;
import com.google.devtools.build.lib.rules.java.DeployArchiveBuilder;
import com.google.devtools.build.lib.rules.java.JavaInfo;
import com.google.devtools.build.lib.rules.java.JavaSemantics;
import com.google.devtools.build.lib.rules.java.JavaTargetAttributes;
import java.util.Map;
import javax.annotation.Nullable;

/** Encapsulates the logic for creating actions for mobile-install. */
public final class AndroidBinaryMobileInstall {

  /** Data class for the resource apks created for mobile-install. */
  static final class MobileInstallResourceApks {
    final ResourceApk incrementalResourceApk;
    final ResourceApk splitResourceApk;

    MobileInstallResourceApks(ResourceApk incrementalResourceApk, ResourceApk splitResourceApk) {
      this.incrementalResourceApk = incrementalResourceApk;
      this.splitResourceApk = splitResourceApk;
    }
  }

  static MobileInstallResourceApks createMobileInstallResourceApks(
      RuleContext ruleContext, AndroidDataContext dataContext, StampedAndroidManifest manifest)
      throws RuleErrorException, InterruptedException {
    Map<String, String> manifestValues = StampedAndroidManifest.getManifestValues(ruleContext);

    ResourceApk incrementalResourceApk =
        ProcessedAndroidData.processIncrementalBinaryDataFrom(
                ruleContext,
                dataContext,
                manifest.addMobileInstallStubApplication(ruleContext),
                ruleContext.getImplicitOutputArtifact(
                    AndroidRuleClasses.ANDROID_INCREMENTAL_RESOURCES_APK),
                getMobileInstallArtifact(ruleContext, "merged_incremental_resources.bin"),
                "incremental",
                manifestValues)
            // Intentionally skip building an R class JAR - incremental binaries handle this
            // separately.
            .withValidatedResources(null);

    ResourceApk splitResourceApk =
        ProcessedAndroidData.processIncrementalBinaryDataFrom(
                ruleContext,
                dataContext,
                manifest.createSplitManifest(ruleContext, "android_resources", false),
                getMobileInstallArtifact(ruleContext, "android_resources.ap_"),
                getMobileInstallArtifact(ruleContext, "merged_split_resources.bin"),
                "incremental_split",
                manifestValues)
            // Intentionally skip building an R class JAR - incremental binaries handle this
            // separately.
            .withValidatedResources(null);
    ruleContext.assertNoErrors();

    return new MobileInstallResourceApks(incrementalResourceApk, splitResourceApk);
  }

  static void addMobileInstall(
      RuleContext ruleContext,
      RuleConfiguredTargetBuilder ruleConfiguredTargetBuilder,
      Artifact javaResourceJar,
      ImmutableList<Artifact> shardDexZips,
      JavaSemantics javaSemantics,
      NativeLibs nativeLibs,
      ResourceApk resourceApk,
      MobileInstallResourceApks mobileInstallResourceApks,
      FilesToRunProvider resourceExtractor,
      NestedSet<Artifact> nativeLibsAar,
      ImmutableList<Artifact> signingKeys,
      @Nullable Artifact signingLineage,
      ImmutableList<Artifact> additionalMergedManifests)
      throws InterruptedException, RuleErrorException {

    Artifact incrementalApk =
        ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_BINARY_INCREMENTAL_APK);

    Artifact fullDeployMarker =
        ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.FULL_DEPLOY_MARKER);
    Artifact incrementalDeployMarker =
        ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.INCREMENTAL_DEPLOY_MARKER);
    Artifact splitDeployMarker =
        ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.SPLIT_DEPLOY_MARKER);

    Artifact incrementalDexManifest =
        ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.DEX_MANIFEST);
    ruleContext.registerAction(
        new SpawnAction.Builder()
            .useDefaultShellEnvironment()
            .setMnemonic("AndroidDexManifest")
            .setProgressMessage(
                "Generating incremental installation manifest for %s", ruleContext.getLabel())
            .setExecutable(ruleContext.getExecutablePrerequisite("$build_incremental_dexmanifest"))
            .addOutput(incrementalDexManifest)
            .addInputs(shardDexZips)
            .addCommandLine(
                CustomCommandLine.builder()
                    .addExecPath(incrementalDexManifest)
                    .addExecPaths(shardDexZips)
                    .build(),
                ParamFileInfo.builder(ParameterFileType.UNQUOTED).build())
            .build(ruleContext));

    Artifact stubData =
        ruleContext.getImplicitOutputArtifact(
            AndroidRuleClasses.MOBILE_INSTALL_STUB_APPLICATION_DATA);
    Artifact stubDex = getStubDex(ruleContext, javaSemantics, false);
    ruleContext.assertNoErrors();

    ApkActionsBuilder incrementalActionsBuilder =
        ApkActionsBuilder.create("incremental apk")
            .setClassesDex(stubDex)
            .addInputZip(mobileInstallResourceApks.incrementalResourceApk.getArtifact())
            .setJavaResourceZip(javaResourceJar, resourceExtractor)
            .addInputZips(nativeLibsAar.toList())
            .setJavaResourceFile(stubData)
            .setSignedApk(incrementalApk)
            .setSigningKeys(signingKeys)
            .setZipalignApk(true)
            .setSigningLineageFile(signingLineage);

    incrementalActionsBuilder.registerActions(ruleContext);

    Artifact argsArtifact =
        ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.MOBILE_INSTALL_ARGS);
    ruleContext.registerAction(new WriteAdbArgsAction(ruleContext.getActionOwner(), argsArtifact));

    createInstallAction(
        ruleContext,
        /* incremental= */ false,
        fullDeployMarker,
        argsArtifact,
        incrementalDexManifest,
        shardDexZips,
        mobileInstallResourceApks.incrementalResourceApk.getArtifact(),
        incrementalApk,
        nativeLibs,
        stubData);

    createInstallAction(
        ruleContext,
        /* incremental= */ true,
        incrementalDeployMarker,
        argsArtifact,
        incrementalDexManifest,
        shardDexZips,
        mobileInstallResourceApks.incrementalResourceApk.getArtifact(),
        incrementalApk,
        nativeLibs,
        stubData);

    NestedSetBuilder<Artifact> splitApkSetBuilder = NestedSetBuilder.compileOrder();

    // Put the Android resource APK first so that this split gets installed first.
    //
    // This avoids some logcat spam during installation, because otherwise the Android package
    // manager would complain about references to missing resources in the manifest during the
    // installation of each split (said references would eventually get installed, but it cannot
    // know that in advance)
    Artifact resourceSplitApk = getMobileInstallArtifact(ruleContext, "android_resources.apk");
    ApkActionsBuilder.create("split Android resource apk")
        .addInputZip(mobileInstallResourceApks.splitResourceApk.getArtifact())
        .setSignedApk(resourceSplitApk)
        .setSigningKeys(signingKeys)
        .setSigningLineageFile(signingLineage)
        .setZipalignApk(true)
        .registerActions(ruleContext);
    splitApkSetBuilder.add(resourceSplitApk);

    for (int i = 0; i < shardDexZips.size(); i++) {
      String splitName = "dex" + (i + 1);
      Artifact splitApkResources =
          createSplitApkResources(ruleContext, resourceApk.getProcessedManifest(), splitName, true);
      Artifact splitApk = getMobileInstallArtifact(ruleContext, splitName + ".apk");
      ApkActionsBuilder.create("split dex apk " + (i + 1))
          .setClassesDex(shardDexZips.get(i))
          .addInputZip(splitApkResources)
          .setSignedApk(splitApk)
          .setSigningKeys(signingKeys)
          .setSigningLineageFile(signingLineage)
          .registerActions(ruleContext);
      splitApkSetBuilder.add(splitApk);
    }

    Artifact nativeSplitApkResources =
        createSplitApkResources(ruleContext, resourceApk.getProcessedManifest(), "native", false);
    Artifact nativeSplitApk = getMobileInstallArtifact(ruleContext, "native.apk");
    ApkActionsBuilder.create("split native apk")
        .addInputZip(nativeSplitApkResources)
        .setNativeLibs(nativeLibs)
        .setSignedApk(nativeSplitApk)
        .setSigningKeys(signingKeys)
        .setSigningLineageFile(signingLineage)
        .registerActions(ruleContext);
    splitApkSetBuilder.add(nativeSplitApk);

    Artifact javaSplitApkResources =
        createSplitApkResources(
            ruleContext, resourceApk.getProcessedManifest(), "java_resources", false);
    Artifact javaSplitApk = getMobileInstallArtifact(ruleContext, "java_resources.apk");
    ApkActionsBuilder.create("split Java resource apk")
        .addInputZip(javaSplitApkResources)
        .setJavaResourceZip(javaResourceJar, resourceExtractor)
        .setSignedApk(javaSplitApk)
        .setSigningKeys(signingKeys)
        .setSigningLineageFile(signingLineage)
        .registerActions(ruleContext);
    splitApkSetBuilder.add(javaSplitApk);

    Artifact splitMainApkResources = getMobileInstallArtifact(ruleContext, "split_main.ap_");
    ruleContext.registerAction(
        new SpawnAction.Builder()
            .useDefaultShellEnvironment()
            .setMnemonic("AndroidStripResources")
            .setProgressMessage("Stripping resources from split main apk")
            .setExecutable(ruleContext.getExecutablePrerequisite("$strip_resources"))
            .addInput(resourceApk.getArtifact())
            .addOutput(splitMainApkResources)
            .addCommandLine(
                CustomCommandLine.builder()
                    .addExecPath("--input_resource_apk", resourceApk.getArtifact())
                    .addExecPath("--output_resource_apk", splitMainApkResources)
                    .build())
            .build(ruleContext));

    NestedSet<Artifact> splitApks = splitApkSetBuilder.build();
    Artifact splitMainApk = getMobileInstallArtifact(ruleContext, "split_main.apk");
    Artifact splitStubDex = getStubDex(ruleContext, javaSemantics, true);
    ruleContext.assertNoErrors();
    ApkActionsBuilder.create("split main apk")
        .setClassesDex(splitStubDex)
        .addInputZip(splitMainApkResources)
        .addInputZips(nativeLibsAar.toList())
        .setSignedApk(splitMainApk)
        .setSigningKeys(signingKeys)
        .setSigningLineageFile(signingLineage)
        .setZipalignApk(true)
        .registerActions(ruleContext);
    splitApkSetBuilder.add(splitMainApk);
    NestedSet<Artifact> allSplitApks = splitApkSetBuilder.build();

    createSplitInstallAction(
        ruleContext, splitDeployMarker, argsArtifact, splitMainApk, splitApks, stubData);

    Artifact incrementalDeployInfo =
        ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.DEPLOY_INFO_INCREMENTAL);
    AndroidDeployInfoAction.createDeployInfoAction(
        ruleContext,
        incrementalDeployInfo,
        resourceApk.getManifest(),
        additionalMergedManifests,
        ImmutableList.of());

    Artifact splitDeployInfo =
        ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.DEPLOY_INFO_SPLIT);
    AndroidDeployInfoAction.createDeployInfoAction(
        ruleContext,
        splitDeployInfo,
        resourceApk.getManifest(),
        additionalMergedManifests,
        ImmutableList.of());

    NestedSet<Artifact> fullInstallOutputGroup =
        NestedSetBuilder.<Artifact>stableOrder()
            .add(fullDeployMarker)
            .add(incrementalDeployInfo)
            .build();

    NestedSet<Artifact> incrementalInstallOutputGroup =
        NestedSetBuilder.<Artifact>stableOrder()
            .add(incrementalDeployMarker)
            .add(incrementalDeployInfo)
            .build();

    NestedSet<Artifact> splitInstallOutputGroup =
        NestedSetBuilder.<Artifact>stableOrder()
            .addTransitive(allSplitApks)
            .add(splitDeployMarker)
            .add(splitDeployInfo)
            .build();

    ruleConfiguredTargetBuilder
        .addOutputGroup("mobile_install_full" + INTERNAL_SUFFIX, fullInstallOutputGroup)
        .addOutputGroup(
            "mobile_install_incremental" + INTERNAL_SUFFIX, incrementalInstallOutputGroup)
        .addOutputGroup("mobile_install_split" + INTERNAL_SUFFIX, splitInstallOutputGroup)
        .addOutputGroup("android_incremental_deploy_info", incrementalDeployInfo);
  }

  @Nullable
  private static Artifact getStubDex(
      RuleContext ruleContext, JavaSemantics javaSemantics, boolean split)
      throws InterruptedException, RuleErrorException {
    String attribute =
        split ? "$incremental_split_stub_application" : "$incremental_stub_application";

    TransitiveInfoCollection dep = ruleContext.getPrerequisite(attribute);
    if (dep == null) {
      ruleContext.attributeError(attribute, "Stub application cannot be found");
      return null;
    }

    if (!JavaInfo.isJavaTarget(dep)) {
      ruleContext.attributeError(attribute, "'" + dep.getLabel() + "' should be a Java target");
      return null;
    }

    JavaTargetAttributes attributes =
        new JavaTargetAttributes.Builder(javaSemantics)
            .addRuntimeClassPathEntries(JavaInfo.transitiveRuntimeJars(dep))
            .build();

    Function<Artifact, Artifact> desugaredJars = Functions.identity();
    if (AndroidCommon.getAndroidConfig(ruleContext).desugarJava8()) {
      desugaredJars =
          AndroidBinary.collectDesugaredJarsFromAttributes(ruleContext, ImmutableList.of(attribute))
              .build()
              .collapseToFunction();
    }
    Artifact stubDeployJar =
        getMobileInstallArtifact(ruleContext, split ? "split_stub_deploy.jar" : "stub_deploy.jar");
    new DeployArchiveBuilder(javaSemantics, ruleContext)
        .setOutputJar(stubDeployJar)
        .setAttributes(attributes)
        .setDerivedJarFunction(desugaredJars)
        .setCheckDesugarDeps(AndroidCommon.getAndroidConfig(ruleContext).checkDesugarDeps())
        .build();

    Artifact stubDex =
        getMobileInstallArtifact(
            ruleContext,
            split ? "split_stub_application/classes.dex.zip" : "stub_application/classes.dex.zip");
    AndroidCommon.createDexAction(ruleContext, stubDeployJar, stubDex, ImmutableList.of(), 0, null);

    return stubDex;
  }

  private static void createInstallAction(
      RuleContext ruleContext,
      boolean incremental,
      Artifact marker,
      Artifact argsArtifact,
      Artifact dexManifest,
      ImmutableList<Artifact> shardDexZips,
      Artifact resourceApk,
      Artifact apk,
      NativeLibs nativeLibs,
      Artifact stubDataFile)
      throws RuleErrorException {

    FilesToRunProvider adb = AndroidSdkProvider.fromRuleContext(ruleContext).getAdb();
    SpawnAction.Builder builder =
        new InstallActionBuilder()
            .useDefaultShellEnvironment()
            .setExecutable(ruleContext.getExecutablePrerequisite("$incremental_install"))
            .setMnemonic("AndroidInstall")
            .setProgressMessage(
                "Installing %s%s", ruleContext.getLabel(), (incremental ? " incrementally" : ""))
            .setExecutionInfo(ImmutableMap.of(ExecutionRequirements.NO_REMOTE, ""))
            .addTool(adb)
            .addOutput(marker)
            .addInput(dexManifest)
            .addInputs(shardDexZips)
            .addInput(resourceApk)
            .addInput(stubDataFile)
            .addInput(argsArtifact);

    CustomCommandLine.Builder commandLine =
        CustomCommandLine.builder()
            .addExecPath("--output_marker", marker)
            .addExecPath("--dexmanifest", dexManifest)
            .addExecPath("--resource_apk", resourceApk)
            .addExecPath("--stub_datafile", stubDataFile)
            .addExecPath("--adb", adb.getExecutable())
            .addExecPath("--flagfile", argsArtifact);

    if (!incremental) {
      builder.addInput(apk);
      commandLine.addExecPath("--apk", apk);
    }

    for (Map.Entry<String, NestedSet<Artifact>> arch : nativeLibs.getMap().entrySet()) {
      for (Artifact lib : arch.getValue().toList()) {
        builder.addInput(lib);
        commandLine.add("--native_lib").addFormatted("%s:%s", arch.getKey(), lib);
      }
    }

    builder.addCommandLine(commandLine.build());
    ruleContext.registerAction(builder.build(ruleContext));
  }

  private static void createSplitInstallAction(
      RuleContext ruleContext,
      Artifact marker,
      Artifact argsArtifact,
      Artifact splitMainApk,
      NestedSet<Artifact> splitApks,
      Artifact stubDataFile)
      throws RuleErrorException {
    FilesToRunProvider adb = AndroidSdkProvider.fromRuleContext(ruleContext).getAdb();
    SpawnAction.Builder builder =
        new InstallActionBuilder()
            .useDefaultShellEnvironment()
            .setExecutable(ruleContext.getExecutablePrerequisite("$incremental_install"))
            .addTool(adb)
            .setMnemonic("AndroidInstall")
            .setProgressMessage("Installing %s using split apks", ruleContext.getLabel())
            .setExecutionInfo(ImmutableMap.of(ExecutionRequirements.NO_REMOTE, ""))
            .addTool(adb)
            .addOutput(marker)
            .addInput(stubDataFile)
            .addInput(argsArtifact)
            .addInput(splitMainApk)
            .addTransitiveInputs(splitApks);
    CustomCommandLine.Builder commandLine =
        CustomCommandLine.builder()
            .addExecPath("--output_marker", marker)
            .addExecPath("--stub_datafile", stubDataFile)
            .addExecPath("--adb", adb.getExecutable())
            .addExecPath("--flagfile", argsArtifact)
            .addExecPath("--split_main_apk", splitMainApk)
            .addExecPaths("--split_apk", splitApks);

    builder.addCommandLine(commandLine.build());
    ruleContext.registerAction(builder.build(ruleContext));
  }

  private static Artifact createSplitApkResources(
      RuleContext ruleContext,
      ProcessedAndroidManifest mainManifest,
      String splitName,
      boolean hasCode)
      throws InterruptedException, RuleErrorException {
    Artifact splitManifest =
        mainManifest.createSplitManifest(ruleContext, splitName, hasCode).getManifest();
    Artifact splitResources = getMobileInstallArtifact(ruleContext, "split_" + splitName + ".ap_");
    AndroidSdkProvider sdk = AndroidSdkProvider.fromRuleContext(ruleContext);
    ruleContext.registerAction(
        new SpawnAction.Builder()
            .useDefaultShellEnvironment()
            .setExecutable(sdk.getAapt())
            .setMnemonic("AaptSplitResourceApk")
            .setProgressMessage("Generating resource apk for split %s", splitName)
            .addOutput(splitResources)
            .addInput(splitManifest)
            .addInput(sdk.getAndroidJar())
            .addCommandLine(
                CustomCommandLine.builder()
                    .add("package")
                    .addExecPath("-F", splitResources)
                    .addExecPath("-M", splitManifest)
                    .addExecPath("-I", sdk.getAndroidJar())
                    .build())
            .build(ruleContext));

    return splitResources;
  }

  /** Returns an intermediate artifact used to support mobile-install. */
  private static Artifact getMobileInstallArtifact(RuleContext ruleContext, String baseName) {
    return ruleContext.getUniqueDirectoryArtifact(
        "_mobile_install", baseName, ruleContext.getBinOrGenfilesDirectory());
  }

  private static final class InstallAction extends SpawnAction {
    InstallAction(
        ActionOwner owner,
        NestedSet<Artifact> tools,
        NestedSet<Artifact> inputs,
        Iterable<? extends Artifact> outputs,
        ResourceSetOrBuilder resourceSetOrBuilder,
        CommandLines commandLines,
        ActionEnvironment env,
        ImmutableMap<String, String> executionInfo,
        CharSequence progressMessage,
        RunfilesSupplier runfilesSupplier,
        String mnemonic) {
      super(
          owner,
          tools,
          inputs,
          outputs,
          resourceSetOrBuilder,
          commandLines,
          env,
          executionInfo,
          progressMessage,
          runfilesSupplier,
          mnemonic,
          OutputPathsMode.OFF);
    }

    @Override
    public boolean executeUnconditionally() {
      // We cannot know if the user connected a new device, uninstalled the app from the device or
      // did anything strange to it, so we always run this action.
      return true;
    }
  }

  private static final class InstallActionBuilder extends SpawnAction.Builder {
    @Override
    protected SpawnAction createSpawnAction(
        ActionOwner owner,
        NestedSet<Artifact> tools,
        NestedSet<Artifact> inputsAndTools,
        ImmutableSet<Artifact> outputs,
        ResourceSetOrBuilder resourceSetOrBuilder,
        CommandLines commandLines,
        ActionEnvironment env,
        @Nullable BuildConfigurationValue configuration,
        ImmutableMap<String, String> executionInfo,
        CharSequence progressMessage,
        RunfilesSupplier runfilesSupplier,
        String mnemonic) {
      return new InstallAction(
          owner,
          tools,
          inputsAndTools,
          outputs,
          resourceSetOrBuilder,
          commandLines,
          env,
          executionInfo,
          progressMessage,
          runfilesSupplier,
          mnemonic);
    }
  }

  private AndroidBinaryMobileInstall() {}
}
