// Copyright 2021 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.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.analysis.PlatformOptions;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.config.BuildOptionsView;
import com.google.devtools.build.lib.analysis.config.CoreOptions;
import com.google.devtools.build.lib.analysis.config.FragmentOptions;
import com.google.devtools.build.lib.analysis.config.transitions.SplitTransition;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.rules.android.AndroidConfiguration.ConfigurationDistinguisher;
import com.google.devtools.build.lib.rules.cpp.CppOptions;
import com.google.devtools.build.lib.starlarkbuildapi.android.AndroidSplitTransitionApi;
import java.util.List;
import net.starlark.java.eval.Printer;

/** Android Split configuration transition for properly handling native dependencies */
final class AndroidSplitTransition implements SplitTransition, AndroidSplitTransitionApi {

  @Override
  public ImmutableSet<Class<? extends FragmentOptions>> requiresOptionFragments() {
    return ImmutableSet.of(
        AndroidConfiguration.Options.class,
        CoreOptions.class,
        CppOptions.class,
        PlatformOptions.class);
  }

  @Override
  public ImmutableMap<String, BuildOptions> split(
      BuildOptionsView buildOptions, EventHandler eventHandler) {

    AndroidConfiguration.Options androidOptions =
        buildOptions.get(AndroidConfiguration.Options.class);

    CppOptions cppOptions = buildOptions.get(CppOptions.class);
    /*
     * The intended order of checks is:
     *  - When --incompatible_enable_android_toolchain_resolution is set:
     *    - --android_platforms
     *      - Split using the values of this flag as the target platform
     *      - If this is unset, use the first value from --platforms.
     *        - If this isn't a valid Android platform, an error will be thrown during the build.
     *  - Fall back to legacy flag logic:
     *    - --fat_apk_cpus
     *      - Split using the values of this flag as --cpu
     *      - If this is unset, fall though.
     *    - --android_cpu
     *      - Don't split, just use the value of this flag as --cpu
     *      - This will not update the output path to include "-android".
     *      - If this is unset, fall though.
     *    - Default
     *      - This will not update the output path to include "-android".
     *      - Don't split, using the same previously set --cpu value.
     */
    if (androidOptions.incompatibleUseToolchainResolution) {
      // Always use --android_platforms when toolchain resolution is enabled.
      List<Label> platformsToSplit = androidOptions.androidPlatforms;
      if (platformsToSplit.isEmpty()) {
        // If --android_platforms is unset, instead use only the first value from --platforms.
        Label targetPlatform =
            Iterables.getFirst(buildOptions.get(PlatformOptions.class).platforms, null);
        platformsToSplit = ImmutableList.of(targetPlatform);
      }
      return handleAndroidPlatforms(buildOptions, androidOptions, platformsToSplit);
    }

    // Fall back to the legacy flags.
    if (!androidOptions.fatApkCpus.isEmpty()) {
      return handleFatApkCpus(buildOptions, androidOptions);
    } else if (!androidOptions.cpu.isEmpty()
        && androidOptions.androidCrosstoolTop != null
        && !androidOptions.androidCrosstoolTop.equals(cppOptions.crosstoolTop)) {
      return handleAndroidCpu(buildOptions, androidOptions);
    } else {
      return handleDefaultSplit(buildOptions, buildOptions.get(CoreOptions.class).cpu);
    }
  }

  private void addNonCpuSplits(
      ImmutableMap.Builder<String, BuildOptions> result,
      String name,
      BuildOptionsView buildOptions) {

    AndroidConfiguration.Options androidOptions =
        buildOptions.get(AndroidConfiguration.Options.class);
    if (!androidOptions.fatApkHwasan) {
      return;
    }

    if (name.contains("arm64-v8a")) {
      BuildOptionsView hwasanSplitOptions = buildOptions.clone();

      // A HWASAN build is different from a regular one in these ways:
      // - The native library install directory gets a "-hwasan" suffix
      // - Some compiler/linker command line options are different (defined in the Android C++
      //   toolchain)
      // - The name of the output directory is changed so that HWASAN and non-HWASAN artifacts
      //   do not conflict
      hwasanSplitOptions.get(CppOptions.class).outputDirectoryTag = "hwasan";
      hwasanSplitOptions.get(AndroidConfiguration.Options.class).hwasan = true;

      result.put(name + "-hwasan", hwasanSplitOptions.underlying());
    }
  }

  /**
   * Splits the configuration based on the values of --android_platforms. Each split will set the
   * --platforms flag to one value from --android_platforms, as well as clean up a few other flags
   * around native CC builds.
   */
  private ImmutableMap<String, BuildOptions> handleAndroidPlatforms(
      BuildOptionsView buildOptions,
      AndroidConfiguration.Options androidOptions,
      List<Label> androidPlatforms) {
    ImmutableMap.Builder<String, BuildOptions> result = ImmutableMap.builder();
    for (Label platform : ImmutableSortedSet.copyOf(androidPlatforms)) {
      BuildOptionsView splitOptions = buildOptions.clone();

      // Disable fat APKs for the child configurations.
      splitOptions.get(AndroidConfiguration.Options.class).fatApkCpus = ImmutableList.of();
      splitOptions.get(AndroidConfiguration.Options.class).androidPlatforms = ImmutableList.of();

      // The cpu flag will be set by platform mapping if a mapping exists.
      splitOptions.get(PlatformOptions.class).platforms = ImmutableList.of(platform);
      setCcFlagsFromAndroid(androidOptions, splitOptions);
      result.put(platform.getName(), splitOptions.underlying());

      addNonCpuSplits(result, platform.getName(), splitOptions);
    }
    return result.buildOrThrow();
  }

  /** Returns a single-split transition that uses the "--cpu" and does not change any flags. */
  private ImmutableMap<String, BuildOptions> handleDefaultSplit(
      BuildOptionsView buildOptions, String cpu) {
    // Avoid a clone when nothing changes.
    ImmutableMap.Builder<String, BuildOptions> result = ImmutableMap.builder();
    result.put(cpu, buildOptions.underlying());
    addNonCpuSplits(result, cpu, buildOptions);
    return result.buildOrThrow();
  }

  /**
   * Returns a transition that sets "--cpu" to the value of "--android_cpu" and sets other C++ flags
   * based on the corresponding Android flags.
   */
  private ImmutableMap<String, BuildOptions> handleAndroidCpu(
      BuildOptionsView buildOptions, AndroidConfiguration.Options androidOptions) {
    BuildOptionsView splitOptions = buildOptions.clone();
    splitOptions.get(CoreOptions.class).cpu = androidOptions.cpu;
    setCcFlagsFromAndroid(androidOptions, splitOptions);
    // Ensure platforms aren't set so that platform mapping can take place.
    splitOptions.get(PlatformOptions.class).platforms = ImmutableList.of();
    // Because configuration is based on cpu flags we need to disable C++ toolchain resolution
    splitOptions.get(CppOptions.class).enableCcToolchainResolution = false;
    return handleDefaultSplit(splitOptions, androidOptions.cpu);
  }

  /**
   * Returns a multi-split transition that sets "--cpu" with the values of "--fat_apk_cpu" and sets
   * other C++ flags based on the corresponding Android flags.
   */
  private ImmutableMap<String, BuildOptions> handleFatApkCpus(
      BuildOptionsView buildOptions, AndroidConfiguration.Options androidOptions) {
    ImmutableMap.Builder<String, BuildOptions> result = ImmutableMap.builder();
    for (String cpu : ImmutableSortedSet.copyOf(androidOptions.fatApkCpus)) {
      BuildOptionsView splitOptions = buildOptions.clone();
      // Disable fat APKs for the child configurations.
      splitOptions.get(AndroidConfiguration.Options.class).fatApkCpus = ImmutableList.of();
      splitOptions.get(AndroidConfiguration.Options.class).androidPlatforms = ImmutableList.of();

      // Set the cpu & android_cpu.
      // TODO(bazel-team): --android_cpu doesn't follow --cpu right now; it should.
      splitOptions.get(AndroidConfiguration.Options.class).cpu = cpu;
      splitOptions.get(CoreOptions.class).cpu = cpu;
      setCcFlagsFromAndroid(androidOptions, splitOptions);
      // Ensure platforms aren't set so that platform mapping can take place.
      splitOptions.get(PlatformOptions.class).platforms = ImmutableList.of();
      // Because configuration is based on cpu flags we need to disable C++ toolchain resolution
      splitOptions.get(CppOptions.class).enableCcToolchainResolution = false;
      result.put(cpu, splitOptions.underlying());
      addNonCpuSplits(result, cpu, splitOptions);
    }
    return result.buildOrThrow();
  }

  private void setCcFlagsFromAndroid(
      AndroidConfiguration.Options androidOptions, BuildOptionsView newOptions) {

    // Set the CC options needed to build native code.
    CppOptions newCppOptions = newOptions.get(CppOptions.class);
    newCppOptions.cppCompiler = androidOptions.cppCompiler;
    newCppOptions.libcTopLabel = androidOptions.androidLibcTopLabel;
    newCppOptions.dynamicMode = androidOptions.dynamicMode;

    if (androidOptions.androidCrosstoolTop != null) {
      newCppOptions.crosstoolTop = androidOptions.androidCrosstoolTop;
    }

    newOptions.get(AndroidConfiguration.Options.class).configurationDistinguisher =
        ConfigurationDistinguisher.ANDROID;
  }

  @Override
  public boolean isImmutable() {
    return true;
  }

  @Override
  public void repr(Printer printer) {
    printer.append("android_common.multi_cpu_configuration");
  }
}
