// 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.lib.rules.android;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.rules.android.AndroidDataConverter.JoinerType;
import com.google.devtools.build.lib.rules.android.databinding.DataBindingContext;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Collections;
import java.util.List;

/** Builder for creating resource processing action. */
public class AndroidResourcesProcessorBuilder {

  @SerializationConstant @VisibleForSerialization
  static final AndroidDataConverter<ValidatedAndroidResources> AAPT2_RESOURCE_DEP_TO_ARG_NO_PARSE =
      AndroidDataConverter.<ValidatedAndroidResources>builder(JoinerType.COLON_COMMA)
          .withRoots(ValidatedAndroidResources::getResourceRoots)
          .withEmpty()
          .withArtifact(ValidatedAndroidResources::getManifest)
          .maybeWithArtifact(ValidatedAndroidResources::getAapt2RTxt)
          .maybeWithArtifact(ValidatedAndroidResources::getCompiledSymbols)
          .build();

  private ResourceDependencies resourceDependencies = ResourceDependencies.empty();
  private AssetDependencies assetDependencies = AssetDependencies.empty();

  private Artifact proguardOut;
  private Artifact mainDexProguardOut;
  private boolean conditionalKeepRules;
  private Artifact rTxtOut;
  private Artifact sourceJarOut;
  private boolean debug = false;
  private ResourceFilterFactory resourceFilterFactory = ResourceFilterFactory.empty();
  private List<String> uncompressedExtensions = Collections.emptyList();
  private Artifact apkOut;
  private String customJavaPackage;
  private String versionCode;
  private String applicationId;
  private String versionName;
  private Artifact symbols;
  private Artifact dataBindingInfoZip;

  private Artifact manifestOut;
  private Artifact mergedResourcesOut;
  private boolean isLibrary;
  private boolean crunchPng = true;
  private boolean throwOnResourceConflict;
  private String packageUnderTest;
  private boolean isTestWithResources = false;
  private boolean includeProguardLocationReferences = false;

  /**
   * The output zip for resource-processed data binding expressions (i.e. a zip of .xml files).
   *
   * <p>If null, data binding processing is skipped (and data binding expressions aren't allowed in
   * layout resources).
   */
  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setDataBindingInfoZip(Artifact zip) {
    this.dataBindingInfoZip = zip;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder withResourceDependencies(
      ResourceDependencies resourceDeps) {
    this.resourceDependencies = resourceDeps;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder withAssetDependencies(AssetDependencies assetDeps) {
    this.assetDependencies = assetDeps;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setUncompressedExtensions(
      List<String> uncompressedExtensions) {
    this.uncompressedExtensions = uncompressedExtensions;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setCrunchPng(boolean crunchPng) {
    this.crunchPng = crunchPng;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setResourceFilterFactory(
      ResourceFilterFactory resourceFilterFactory) {
    this.resourceFilterFactory = resourceFilterFactory;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setDebug(boolean debug) {
    this.debug = debug;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setProguardOut(Artifact proguardCfg) {
    this.proguardOut = proguardCfg;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder conditionalKeepRules(boolean conditionalKeepRules) {
    this.conditionalKeepRules = conditionalKeepRules;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setMainDexProguardOut(Artifact mainDexProguardCfg) {
    this.mainDexProguardOut = mainDexProguardCfg;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setIncludeProguardLocationReferences(
      boolean includeProguardLocationReferences) {
    this.includeProguardLocationReferences = includeProguardLocationReferences;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setRTxtOut(Artifact rTxtOut) {
    this.rTxtOut = rTxtOut;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setSymbols(Artifact symbols) {
    this.symbols = symbols;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setApkOut(Artifact apkOut) {
    this.apkOut = apkOut;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setSourceJarOut(Artifact sourceJarOut) {
    this.sourceJarOut = sourceJarOut;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setManifestOut(Artifact manifestOut) {
    this.manifestOut = manifestOut;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setMergedResourcesOut(Artifact mergedResourcesOut) {
    this.mergedResourcesOut = mergedResourcesOut;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setLibrary(boolean isLibrary) {
    this.isLibrary = isLibrary;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setThrowOnResourceConflict(
      boolean throwOnResourceConflict) {
    this.throwOnResourceConflict = throwOnResourceConflict;
    return this;
  }

  /**
   * Creates and registers an action that processes only transitive data.
   *
   * <p>Local resources and assets will be completely ignored by this action.
   *
   * @return a {@link ResourceApk} containing the processed resource, asset, and manifest
   *     information.
   */
  public ResourceApk buildWithoutLocalResources(
      AndroidDataContext dataContext,
      StampedAndroidManifest manifest,
      DataBindingContext dataBindingContext) {

    build(
        dataContext, AndroidResources.empty(), AndroidAssets.empty(), manifest, dataBindingContext);

    return ResourceApk.fromTransitiveResources(
        resourceDependencies,
        assetDependencies,
        manifest.withProcessedManifest(manifestOut == null ? manifest.getManifest() : manifestOut),
        rTxtOut,
        dataBindingContext);
  }

  public ProcessedAndroidData build(
      AndroidDataContext dataContext,
      AndroidResources primaryResources,
      AndroidAssets primaryAssets,
      StampedAndroidManifest primaryManifest,
      DataBindingContext dataBindingContext) {

    // Wrap the new manifest, if any
    ProcessedAndroidManifest processedManifest =
        new ProcessedAndroidManifest(
            manifestOut == null ? primaryManifest.getManifest() : manifestOut,
            primaryManifest.getPackage(),
            primaryManifest.isExported());

    // In databinding v2, this strips out the databinding and generates the layout info file.
    AndroidResources databindingProcessedResources = dataBindingContext.processResources(
        dataContext, primaryResources, processedManifest.getPackage());

    createAapt2ApkAction(
        dataContext,
        databindingProcessedResources,
        primaryAssets,
        primaryManifest,
        dataBindingContext.usesAndroidX());

    // Wrap the parsed resources
    ParsedAndroidResources parsedResources =
        ParsedAndroidResources.of(
            databindingProcessedResources,
            symbols,
            /* compiledSymbols = */ null,
            dataContext.getLabel(),
            processedManifest,
            dataBindingContext);

    // Wrap the parsed and merged assets
    ParsedAndroidAssets parsedAssets =
        ParsedAndroidAssets.of(
            primaryAssets, symbols, /* compiledSymbols = */ null, dataContext.getLabel());
    MergedAndroidAssets mergedAssets =
        MergedAndroidAssets.of(parsedAssets, mergedResourcesOut, assetDependencies);

    return ProcessedAndroidData.of(
        parsedResources,
        mergedAssets,
        processedManifest,
        rTxtOut,
        sourceJarOut,
        apkOut,
        dataBindingInfoZip,
        resourceDependencies,
        proguardOut,
        mainDexProguardOut);
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setJavaPackage(String customJavaPackage) {
    this.customJavaPackage = customJavaPackage;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setVersionCode(String versionCode) {
    this.versionCode = versionCode;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setApplicationId(String applicationId) {
    if (applicationId != null && !applicationId.isEmpty()) {
      this.applicationId = applicationId;
    }
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setVersionName(String versionName) {
    this.versionName = versionName;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setPackageUnderTest(String packageUnderTest) {
    this.packageUnderTest = packageUnderTest;
    return this;
  }

  @CanIgnoreReturnValue
  public AndroidResourcesProcessorBuilder setIsTestWithResources(boolean isTestWithResources) {
    this.isTestWithResources = isTestWithResources;
    return this;
  }

  private void createAapt2ApkAction(
      AndroidDataContext dataContext,
      AndroidResources primaryResources,
      AndroidAssets primaryAssets,
      StampedAndroidManifest primaryManifest,
      boolean useDataBindingAndroidX) {
    BusyBoxActionBuilder builder =
        BusyBoxActionBuilder.create(dataContext, "AAPT2_PACKAGE").addAapt();

    if (resourceDependencies != null) {
      builder
          .addTransitiveFlag(
              "--data",
              resourceDependencies.getTransitiveResourceContainers(),
              AAPT2_RESOURCE_DEP_TO_ARG_NO_PARSE)
          .addTransitiveFlag(
              "--directData",
              resourceDependencies.getDirectResourceContainers(),
              AAPT2_RESOURCE_DEP_TO_ARG_NO_PARSE)
          .addTransitiveInputValues(resourceDependencies.getTransitiveResources())
          .addTransitiveInputValues(resourceDependencies.getTransitiveManifests())
          .addTransitiveInputValues(resourceDependencies.getTransitiveAapt2RTxt())
          .addTransitiveInputValues(resourceDependencies.getTransitiveCompiledSymbols());
    }

    if (assetDependencies != null && !assetDependencies.getTransitiveAssets().isEmpty()) {
      builder
          .addTransitiveFlag(
              "--directAssets",
              assetDependencies.getDirectParsedAssets(),
              AndroidDataConverter.COMPILED_ASSET_CONVERTER)
          .addTransitiveFlag(
              "--assets",
              assetDependencies.getTransitiveParsedAssets(),
              AndroidDataConverter.COMPILED_ASSET_CONVERTER)
          .addTransitiveInputValues(assetDependencies.getTransitiveAssets())
          .addTransitiveInputValues(assetDependencies.getTransitiveCompiledSymbols());
    }

    builder.maybeAddFlag("--conditionalKeepRules", conditionalKeepRules);
    builder.maybeAddFlag("--includeProguardLocationReferences", includeProguardLocationReferences);
    configureCommonFlags(
            dataContext,
            primaryResources,
            primaryAssets,
            primaryManifest,
            useDataBindingAndroidX,
            builder)
        .buildAndRegister("Processing Android resources", "AndroidAapt2");
  }

  private BusyBoxActionBuilder configureCommonFlags(
      AndroidDataContext dataContext,
      AndroidResources primaryResources,
      AndroidAssets primaryAssets,
      StampedAndroidManifest primaryManifest,
      boolean useDataBindingAndroidX,
      BusyBoxActionBuilder builder) {

    return builder
        .addInput(
            "--primaryData",
            String.format(
                "%s:%s:%s",
                AndroidDataConverter.rootsToString(primaryResources.getResourceRoots()),
                AndroidDataConverter.rootsToString(primaryAssets.getAssetRoots()),
                primaryManifest.getManifest().getExecPathString()),
            Iterables.concat(
                primaryResources.getResources(),
                primaryAssets.getAssets(),
                ImmutableList.of(primaryManifest.getManifest())))
        .maybeAddFlag("--buildToolsVersion", dataContext.getSdk().getBuildToolsVersion())
        .addAndroidJar()
        .maybeAddFlag("--packageType", isLibrary)
        .maybeAddFlag("LIBRARY", isLibrary)
        .maybeAddOutput("--rOutput", rTxtOut)
        .maybeAddOutput("--symbolsOut", symbols)
        .maybeAddOutput("--srcJarOutput", sourceJarOut)
        .maybeAddOutput("--proguardOutput", proguardOut)
        .maybeAddOutput("--mainDexProguardOutput", mainDexProguardOut)
        .maybeAddOutput("--manifestOutput", manifestOut)
        .maybeAddOutput("--resourcesOutput", mergedResourcesOut)
        .maybeAddOutput("--packagePath", apkOut)

        // Always pass density and resource configuration filter strings to execution, even when
        // filtering in analysis. Filtering in analysis cannot remove resources from Filesets, and,
        // in addition, aapt needs access to resource filters to generate pseudolocalized resources
        // and because its resource filtering is somewhat stricter for locales, and resource
        // processing needs access to densities to add them to the manifest.
        .maybeAddFlag("--resourceConfigs", resourceFilterFactory.getConfigurationFilterString())
        .maybeAddFlag("--useDataBindingAndroidX", useDataBindingAndroidX)
        .maybeAddFlag("--densities", resourceFilterFactory.getDensityString())
        .maybeAddVectoredFlag("--uncompressedExtensions", uncompressedExtensions)
        .maybeAddFlag("--useAaptCruncher=no", !crunchPng)
        .maybeAddFlag("--debug", debug)
        .maybeAddFlag("--versionCode", versionCode)
        .maybeAddFlag("--versionName", versionName)
        .maybeAddFlag("--applicationId", applicationId)
        .maybeAddOutput("--dataBindingInfoOut", dataBindingInfoZip)
        .maybeAddFlag("--packageForR", customJavaPackage)
        .maybeAddFlag("--throwOnResourceConflict", throwOnResourceConflict)
        .maybeAddFlag("--packageUnderTest", packageUnderTest)
        .maybeAddFlag("--isTestWithResources", isTestWithResources);
  }
}
