// Copyright 2015 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.android;

import static com.google.common.collect.Streams.concat;
import static java.util.stream.Collectors.toList;

import com.android.builder.core.VariantType;
import com.android.utils.StdLogger;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.devtools.build.android.Converters.DependencyAndroidDataListConverter;
import com.google.devtools.build.android.Converters.PathConverter;
import com.google.devtools.build.android.Converters.PathListConverter;
import com.google.devtools.build.android.Converters.SerializedAndroidDataListConverter;
import com.google.devtools.build.android.Converters.UnvalidatedAndroidDataConverter;
import com.google.devtools.build.android.Converters.VariantTypeConverter;
import com.google.devtools.build.android.aapt2.Aapt2ConfigOptions;
import com.google.devtools.build.android.aapt2.CompiledResources;
import com.google.devtools.build.android.aapt2.PackagedResources;
import com.google.devtools.build.android.aapt2.ProtoApk;
import com.google.devtools.build.android.aapt2.ResourceCompiler;
import com.google.devtools.build.android.aapt2.ResourceLinker;
import com.google.devtools.build.android.aapt2.StaticLibrary;
import com.google.devtools.common.options.Converters.CommaSeparatedOptionListConverter;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor;
import com.google.devtools.common.options.TriState;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;

/**
 * Provides an entry point for the resource processing using the AOSP build tools.
 *
 * <pre>
 * Example Usage:
 *   java/com/google/build/android/Aapt2ResourcePackagingAction\
 *      --sdkRoot path/to/sdk\
 *      --aapt path/to/sdk/aapt\
 *      --adb path/to/sdk/adb\
 *      --zipAlign path/to/sdk/zipAlign\
 *      --androidJar path/to/sdk/androidJar\
 *      --manifestOutput path/to/manifest\
 *      --primaryData path/to/resources:path/to/assets:path/to/manifest\
 *      --data p/t/res1:p/t/assets1:p/t/1/AndroidManifest.xml:p/t/1/R.txt:symbols,\
 *             p/t/res2:p/t/assets2:p/t/2/AndroidManifest.xml:p/t/2/R.txt:symbols\
 *      --packagePath path/to/write/archive.ap_\
 *      --srcJarOutput path/to/write/archive.srcjar
 * </pre>
 */
public class Aapt2ResourcePackagingAction {

  private static final StdLogger STD_LOGGER = new StdLogger(StdLogger.Level.WARNING);

  /** Flag specifications for this action. */
  public static final class Options extends OptionsBase {
    @Option(
        name = "primaryData",
        defaultValue = "null",
        converter = UnvalidatedAndroidDataConverter.class,
        category = "input",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help =
            "The directory containing the primary resource directory. The contents will override "
                + "the contents of any other resource directories during merging. The expected "
                + "format is "
                + UnvalidatedAndroidData.EXPECTED_FORMAT)
    public UnvalidatedAndroidData primaryData;

    @Option(
        name = "data",
        defaultValue = "",
        converter = DependencyAndroidDataListConverter.class,
        category = "input",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help =
            "Transitive Data dependencies. These values will be used if not defined in the "
                + "primary resources. The expected format is "
                + DependencyAndroidData.EXPECTED_FORMAT
                + "[,...]")
    public List<DependencyAndroidData> transitiveData;

    @Option(
        name = "directData",
        defaultValue = "",
        converter = DependencyAndroidDataListConverter.class,
        category = "input",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help =
            "Direct Data dependencies. These values will be used if not defined in the "
                + "primary resources. The expected format is "
                + DependencyAndroidData.EXPECTED_FORMAT
                + "[,...]")
    public List<DependencyAndroidData> directData;

    @Option(
        name = "assets",
        defaultValue = "",
        converter = SerializedAndroidDataListConverter.class,
        category = "input",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help =
            "Transitive asset dependencies. These can also be specified together with resources"
                + " using --data. Expected format: "
                + SerializedAndroidData.EXPECTED_FORMAT
                + "[,...]")
    public List<SerializedAndroidData> transitiveAssets;

    @Option(
        name = "directAssets",
        defaultValue = "",
        converter = SerializedAndroidDataListConverter.class,
        category = "input",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help =
            "Direct asset dependencies. These can also be specified together with resources using "
                + "--directData. Expected format: "
                + SerializedAndroidData.EXPECTED_FORMAT
                + "[,...]")
    public List<SerializedAndroidData> directAssets;

    @Option(
        name = "additionalApksToLinkAgainst",
        defaultValue = "null",
        category = "input",
        converter = PathListConverter.class,
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "List of APKs used during linking.")
    public List<Path> additionalApksToLinkAgainst;

    @Option(
        name = "packageId",
        defaultValue = "-1",
        category = "input",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Resource ID prefix; see AAPT2 documentation for --package-id.")
    public int packageId;

    @Option(
        name = "rOutput",
        defaultValue = "null",
        converter = PathConverter.class,
        category = "output",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Path to where the R.txt should be written.")
    public Path rOutput;

    @Option(
        name = "symbolsOut",
        oldName = "symbolsTxtOut",
        defaultValue = "null",
        converter = PathConverter.class,
        category = "output",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Path to where the symbols should be written.")
    public Path symbolsOut;

    @Option(
        name = "dataBindingInfoOut",
        defaultValue = "null",
        converter = PathConverter.class,
        category = "output",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Path to where data binding's layout info output should be written.")
    public Path dataBindingInfoOut;

    @Option(
        name = "packagePath",
        defaultValue = "null",
        converter = PathConverter.class,
        category = "output",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Path to the write the archive.")
    public Path packagePath;

    @Option(
        name = "resourcesOutput",
        defaultValue = "null",
        converter = PathConverter.class,
        category = "output",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Path to the write merged resources archive.")
    public Path resourcesOutput;

    @Option(
        name = "proguardOutput",
        defaultValue = "null",
        converter = PathConverter.class,
        category = "output",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Path for the proguard file.")
    public Path proguardOutput;

    @Option(
        name = "mainDexProguardOutput",
        defaultValue = "null",
        converter = PathConverter.class,
        category = "output",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Path for the main dex proguard file.")
    public Path mainDexProguardOutput;

    @Option(
        name = "manifestOutput",
        defaultValue = "null",
        converter = PathConverter.class,
        category = "output",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Path for the modified manifest.")
    public Path manifestOutput;

    @Option(
        name = "srcJarOutput",
        defaultValue = "null",
        converter = PathConverter.class,
        category = "output",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Path for the generated java source jar.")
    public Path srcJarOutput;

    @Option(
        name = "packageType",
        defaultValue = "DEFAULT",
        converter = VariantTypeConverter.class,
        category = "config",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help =
            "Variant configuration type for packaging the resources."
                + " Acceptable values DEFAULT, LIBRARY, ANDROID_TEST, UNIT_TEST")
    public VariantType packageType;

    @Option(
        name = "densities",
        defaultValue = "",
        converter = CommaSeparatedOptionListConverter.class,
        category = "config",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "A list of densities to filter the resource drawables by.")
    public List<String> densities;

    @Option(
        name = "packageForR",
        defaultValue = "null",
        category = "config",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Custom java package to generate the R symbols files.")
    public String packageForR;

    @Option(
        name = "applicationId",
        defaultValue = "null",
        category = "config",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Custom application id (package manifest) for the packaged manifest.")
    public String applicationId;

    @Option(
        name = "versionName",
        defaultValue = "null",
        category = "config",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Version name to stamp into the packaged manifest.")
    public String versionName;

    @Option(
        name = "versionCode",
        defaultValue = "-1",
        category = "config",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Version code to stamp into the packaged manifest.")
    public int versionCode;

    @Option(
        name = "throwOnResourceConflict",
        defaultValue = "false",
        category = "config",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "If passed, resource merge conflicts will be treated as errors instead of warnings")
    public boolean throwOnResourceConflict;

    @Option(
        name = "packageUnderTest",
        defaultValue = "null",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.NO_OP},
        help = "Unused/deprecated option.")
    public String packageUnderTest;

    @Option(
        name = "isTestWithResources",
        defaultValue = "false",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.NO_OP},
        help = "Unused/deprecated option.")
    public boolean isTestWithResources;

    @Option(
        name = "includeProguardLocationReferences",
        defaultValue = "false",
        documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
        effectTags = {OptionEffectTag.AFFECTS_OUTPUTS},
        help = "When generating proguard configurations, include location references.")
    public boolean includeProguardLocationReferences;
  }

  public static void main(String[] args) throws Exception {
    Profiler profiler = InMemoryProfiler.createAndStart("setup");
    OptionsParser optionsParser =
        OptionsParser.builder()
            .optionsClasses(
                Options.class, Aapt2ConfigOptions.class, ResourceProcessorCommonOptions.class)
            .argsPreProcessor(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault()))
            .build();
    optionsParser.parseAndExitUponError(args);

    Aapt2ConfigOptions aaptConfigOptions = optionsParser.getOptions(Aapt2ConfigOptions.class);
    Options options = optionsParser.getOptions(Options.class);

    Preconditions.checkArgument(
        options.packageId == -1 || (options.packageId >= 2 && options.packageId <= 255),
        "packageId must be in the range [2,255]");

    try (ScopedTemporaryDirectory scopedTmp =
            new ScopedTemporaryDirectory("android_resources_tmp");
        ExecutorServiceCloser executorService = ExecutorServiceCloser.createWithFixedPoolOf(15)) {
      final Path tmp = scopedTmp.getPath();
      final Path densityManifest = tmp.resolve("manifest-filtered/AndroidManifest.xml");
      final Path processedManifest = tmp.resolve("manifest-processed/AndroidManifest.xml");
      final Path symbols = tmp.resolve("symbols/symbols.bin");
      final Path databindingResourcesRoot =
          Files.createDirectories(tmp.resolve("android_data_binding_resources"));
      final Path compiledResources = Files.createDirectories(tmp.resolve("compiled"));
      final Path linkedOut = Files.createDirectories(tmp.resolve("linked"));
      final AndroidCompiledDataDeserializer dataDeserializer =
          AndroidCompiledDataDeserializer.create(/*includeFileContentsForValidation=*/ true);
      final ResourceCompiler compiler =
          ResourceCompiler.create(
              executorService,
              compiledResources,
              aaptConfigOptions.aapt2,
              aaptConfigOptions.buildToolsVersion,
              aaptConfigOptions.generatePseudoLocale);

      profiler.recordEndOf("setup").startTask("compile");
      CompiledResources compiled =
          options
              .primaryData
              .processDataBindings(
                  options.dataBindingInfoOut,
                  options.packageForR,
                  databindingResourcesRoot,
                  aaptConfigOptions.useDataBindingAndroidX)
              .compile(compiler, compiledResources)
              .processManifest(
                  manifest ->
                      AndroidManifestProcessor.with(STD_LOGGER)
                          .processManifest(
                              options.applicationId,
                              options.versionCode,
                              options.versionName,
                              manifest,
                              processedManifest,
                              optionsParser.getOptions(ResourceProcessorCommonOptions.class)
                                  .logWarnings))
              .processManifest(
                  manifest ->
                      new DensitySpecificManifestProcessor(options.densities, densityManifest)
                          .process(manifest));

      profiler.recordEndOf("compile").startTask("merge");

      // Checks for merge conflicts, and write the merged data out.
      final Path symbolsBin =
          AndroidResourceMerger.mergeDataToSymbols(
              ParsedAndroidData.loadedFrom(
                  DependencyInfo.DependencyType.PRIMARY,
                  ImmutableList.of(SerializedAndroidData.from(compiled)),
                  executorService,
                  dataDeserializer),
              new DensitySpecificManifestProcessor(options.densities, densityManifest)
                  .process(options.primaryData.getManifest()),
              ImmutableList.<SerializedAndroidData>builder()
                  .addAll(options.directData)
                  .addAll(options.directAssets)
                  .build(),
              ImmutableList.<SerializedAndroidData>builder()
                  .addAll(options.transitiveData)
                  .addAll(options.transitiveAssets)
                  .build(),
              options.packageType,
              symbols,
              dataDeserializer,
              options.throwOnResourceConflict,
              executorService);
      if (options.symbolsOut != null) {
        Files.copy(symbolsBin, options.symbolsOut);
      }

      profiler.recordEndOf("merge").startTask("link");
      // Write manifestOutput now before the dummy manifest is created.
      if (options.manifestOutput != null) {
        AndroidResourceOutputs.copyManifestToOutput(compiled, options.manifestOutput);
      }

      List<CompiledResources> compiledResourceDeps =
          // Last defined dependencies will overwrite previous one, so always place direct
          // after transitive.
          concat(options.transitiveData.stream(), options.directData.stream())
              .map(DependencyAndroidData::getCompiledSymbols)
              .collect(toList());

      // NB: "-A" options are in *decreasing* precedence, while "-R" options are in *increasing*
      // precedence.  While this is internally inconsistent, it matches AAPTv1's treatment of "-A".
      List<Path> assetDirs =
          concat(
                  options.primaryData.assetDirs.stream(),
                  concat(
                          options.directData.stream(),
                          options.directAssets.stream(),
                          options.transitiveData.stream(),
                          options.transitiveAssets.stream())
                      .flatMap(dep -> dep.assetDirs.stream()))
              .collect(toList());

      List<StaticLibrary> dependencies =
          Lists.newArrayList(StaticLibrary.from(aaptConfigOptions.androidJar));
      if (options.additionalApksToLinkAgainst != null) {
        dependencies.addAll(
            options.additionalApksToLinkAgainst.stream()
                .map(StaticLibrary::from)
                .collect(toList()));
      }

      final PackagedResources packagedResources =
          ResourceLinker.create(aaptConfigOptions.aapt2, executorService, linkedOut)
              .profileUsing(profiler)
              .customPackage(options.packageForR)
              .packageId(
                  options.packageId != -1 ? Optional.of(options.packageId) : Optional.empty())
              .outputAsProto(aaptConfigOptions.resourceTableAsProto)
              .dependencies(ImmutableList.copyOf(dependencies))
              .include(compiledResourceDeps)
              .withAssets(assetDirs)
              .buildVersion(aaptConfigOptions.buildToolsVersion)
              .conditionalKeepRules(aaptConfigOptions.conditionalKeepRules == TriState.YES)
              .filterToDensity(options.densities)
              .storeUncompressed(aaptConfigOptions.uncompressedExtensions)
              .debug(aaptConfigOptions.debug)
              .includeGeneratedLocales(aaptConfigOptions.generatePseudoLocale)
              .includeOnlyConfigs(aaptConfigOptions.resourceConfigs)
              .includeProguardLocationReferences(options.includeProguardLocationReferences)
              .link(compiled);
      profiler.recordEndOf("link").startTask("validate");

      ValidateAndLinkResourcesAction.checkVisibilityOfResourceReferences(
          ProtoApk.readFrom(packagedResources.proto()).getManifest(),
          compiled,
          compiledResourceDeps);

      profiler.recordEndOf("validate");

      if (options.packagePath != null) {
        copy(packagedResources.apk(), options.packagePath);
      }
      if (options.proguardOutput != null) {
        copy(packagedResources.proguardConfig(), options.proguardOutput);
      }
      if (options.mainDexProguardOutput != null) {
        copy(packagedResources.mainDexProguard(), options.mainDexProguardOutput);
      }
      if (options.srcJarOutput != null) {
        AndroidResourceOutputs.createSrcJar(
            packagedResources.javaSourceDirectory(), options.srcJarOutput, /* staticIds= */ false);
      }
      if (options.rOutput != null) {
        copy(packagedResources.rTxt(), options.rOutput);
      }
      if (options.resourcesOutput != null) {
        packagedResources.asArchive().writeTo(options.resourcesOutput, /* compress= */ false);
      }
    }
  }

  private static void copy(Path from, Path out) throws IOException {
    Files.createDirectories(out.getParent());
    Files.copy(from, out);
  }
}
