// 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.packages.util;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteStreams;
import com.google.devtools.build.lib.testutil.TestConstants;
import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig;
import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig.CToolchain;
import com.google.protobuf.TextFormat;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

/**
 * Creates mock BUILD files required for the objc rules.
 */
public final class MockObjcSupport {

  private static final ImmutableList<String> DEFAULT_OSX_CROSSTOOL_DEPS_DIRS =
      ImmutableList.of("third_party/bazel/tools/osx/crosstool");
  private static final String DEFAULT_OSX_CROSSTOOL_DIR = "tools/osx/crosstool";
  private static final String MOCK_OSX_CROSSTOOL_FILE =
      "com/google/devtools/build/lib/packages/util/MOCK_OSX_CROSSTOOL";

  /** The label of the {@code xcode_config} target used in test to enumerate available xcodes. */
  public static final String XCODE_VERSION_CONFIG =
      TestConstants.TOOLS_REPOSITORY + "//tools/objc:host_xcodes";

  /**
   * The build label for the mock OSX crosstool configuration.
   */
  public static final String DEFAULT_OSX_CROSSTOOL =
      "//" + DEFAULT_OSX_CROSSTOOL_DIR + ":crosstool";

  public static final String DEFAULT_XCODE_VERSION = "7.3.1";
  public static final String DEFAULT_IOS_SDK_VERSION = "8.4";

  /** Returns the set of flags required to build objc libraries using the mock OSX crosstool. */
  public static ImmutableList<String> requiredObjcCrosstoolFlags() {
    ImmutableList.Builder<String> argsBuilder = ImmutableList.builder();
    argsBuilder.addAll(TestConstants.OSX_CROSSTOOL_FLAGS);

    // TODO(b/68751876): Set --apple_crosstool_top and --crosstool_top using the
    // AppleCrosstoolTransition
    argsBuilder
        .add("--xcode_version_config=" + MockObjcSupport.XCODE_VERSION_CONFIG)
        .add("--apple_crosstool_top=" + MockObjcSupport.DEFAULT_OSX_CROSSTOOL)
        .add("--crosstool_top=" + MockObjcSupport.DEFAULT_OSX_CROSSTOOL);

    // TODO(b/32411441): This flag will be flipped off by default imminently, at which point
    // this can be removed. The flag itself is for safe rollout of a backwards incompatible change.
    argsBuilder.add("--noexperimental_objc_provider_from_linked");
    return argsBuilder.build();
  }

  /**
   * Sets up the support for building ObjC.
   * Any partial toolchain line will be merged into every toolchain stanza in the crosstool
   * loaded from file.
   */
  public static void setup(
      MockToolsConfig config, String... partialToolchainLines) throws IOException {
    for (String tool :
        ImmutableSet.of(
            "actoolwrapper",
            "bundlemerge",
            "objc_dummy.mm",
            "device_debug_entitlements.plist",
            "gcov",
            "ibtoolwrapper",
            "momcwrapper",
            "plmerge",
            "realpath",
            "swiftstdlibtoolwrapper",
            "testrunner",
            "xcrunwrapper.sh",
            "mcov",
            "libtool")) {
      config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/objc/" + tool);
    }
    config.create(
        TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/objc/BUILD",
        "package(default_visibility=['//visibility:public'])",
        "exports_files(glob(['**']))",
        "filegroup(name = 'default_provisioning_profile', srcs = ['foo.mobileprovision'])",
        "filegroup(name = 'compile_protos', srcs = ['compile_protos.py'])",
        "filegroup(name = 'protobuf_compiler_wrapper', srcs = ['protobuf_compiler_wrapper.sh'])",
        "filegroup(name = 'protobuf_compiler', srcs = ['protobuf_compiler_helper.py'])",
        "filegroup(",
        "  name = 'protobuf_compiler_support',",
        "  srcs = ['proto_support', 'protobuf_compiler_helper.py'],",
        ")",
        "sh_binary(name = 'xcrunwrapper', srcs = ['xcrunwrapper.sh'])",
        "apple_binary(name = 'xctest_appbin', platform_type = 'ios', deps = [':dummy_lib'])",
        "filegroup(name = 'xctest_infoplist', srcs = ['xctest.plist'])",
        "filegroup(name = 'j2objc_dead_code_pruner', srcs = ['j2objc_dead_code_pruner.py'])",
        "filegroup(",
        "  name = 'protobuf_well_known_types',",
        String.format(
            "  srcs = ['%s//objcproto:well_known_type.proto'],", TestConstants.TOOLS_REPOSITORY),
        ")",
        "xcode_config(name = 'host_xcodes',",
        "  default = ':version7_3_1',",
        "  versions = [':version7_3_1', 'version5_0', 'version7_3', 'version5_8'])",
        "xcode_version(",
        "  name = 'version7_3_1',",
        "  version = '" + DEFAULT_XCODE_VERSION + "',",
        "  default_ios_sdk_version = \"" + DEFAULT_IOS_SDK_VERSION + "\",",
        ")",
        "xcode_version(",
        "  name = 'version7_3',",
        "  version = '7.3',",
        ")",
        "xcode_version(",
        "  name = 'version5_0',",
        "  version = '5.0',",
        ")",
        "xcode_version(",
        "  name = 'version5_8',",
        "  version = '5.8',",
        ")",
        "objc_library(name = 'dummy_lib', srcs = ['objc_dummy.mm'])",
        "alias(name = 'protobuf_lib', actual = '//objcproto:protobuf_lib')");
    // If the bazel tools repository is not in the workspace, also create a workspace tools/objc
    // package with a few lingering dependencies.
    // TODO(b/64537078): Move these dependencies underneath the tools workspace.
    if (TestConstants.TOOLS_REPOSITORY_SCRATCH.length() > 0) {
      config.create(
          "tools/objc/BUILD",
          "package(default_visibility=['//visibility:public'])",
          "exports_files(glob(['**']))",
          "apple_binary(name = 'xctest_appbin', platform_type = 'ios', deps = [':dummy_lib'])",
          "filegroup(name = 'default_provisioning_profile', srcs = ['foo.mobileprovision'])",
          "filegroup(name = 'xctest_infoplist', srcs = ['xctest.plist'])");
    }
    config.create(
        TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/objc/foo.mobileprovision", "No such luck");
    config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/objc/compile_protos.py");
    config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/objc/xctest.plist");
    config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/objc/proto_support");
    config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/objc/j2objc_dead_code_pruner.py");
    config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/objc/header_scanner");
    createCrosstoolPackage(config, partialToolchainLines);
    setupObjcProto(config);
  }

  /** Sets up the support for building protocol buffers for ObjC. */
  private static void setupObjcProto(MockToolsConfig config) throws IOException {
    config.create(
        TestConstants.TOOLS_REPOSITORY_SCRATCH + "objcproto/BUILD",
        "package(default_visibility=['//visibility:public'])",
        "objc_library(",
        "  name = 'protobuf_lib',",
        "  srcs = ['empty.m'],",
        "  hdrs = ['include/header.h'],",
        "  includes = ['include'],",
        ")",
        "exports_files(['well_known_type.proto'])",
        "proto_library(",
        "  name = 'well_known_type_proto',",
        "  srcs = ['well_known_type.proto'],",
        ")");
    config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "objcproto/empty.m");
    config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "objcproto/empty.cc");
    config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "objcproto/well_known_type.proto");
  }

  /**
   * Test setup method which creates a package containing the mock OSX crosstool. The crosstool
   * will be available at {@link #DEFAULT_OSX_CROSSTOOL}.
   */
  public static void createCrosstoolPackage(
      MockToolsConfig config, String... partialToolchainLines) throws IOException {
    if (config.isRealFileSystem()) {
      for (String depDir : DEFAULT_OSX_CROSSTOOL_DEPS_DIRS) {
        config.linkTools(depDir);
      }
      config.linkTools(DEFAULT_OSX_CROSSTOOL_DIR);
    } else {
      // Read the crosstool file into crosstoolString.
      InputStream crosstoolFileStream =
          MockObjcSupport.class.getClassLoader().getResourceAsStream(MOCK_OSX_CROSSTOOL_FILE);
      String crosstoolString =
          new String(ByteStreams.toByteArray(crosstoolFileStream), StandardCharsets.UTF_8);

      // Create a config builder and merge the crosstoolString into it.
      CrosstoolConfig.CrosstoolRelease.Builder configBuilder =
          CrosstoolConfig.CrosstoolRelease.newBuilder();
      TextFormat.merge(crosstoolString, configBuilder);

      // Merge partialToolchainLines into the builder.
      CToolchain.Builder toolchainBuilder = CToolchain.newBuilder();
      TextFormat.merge(Joiner.on("\n").join(partialToolchainLines), toolchainBuilder);
      CToolchain partialToolchain = toolchainBuilder.buildPartial();
      for (CToolchain.Builder partialToolchainBuilder :
          configBuilder.getToolchainBuilderList()) {
        partialToolchainBuilder.mergeFrom(partialToolchain);
      }

      // Extract the modified crosstoolString and set things up so
      // that tests can use a crosstool parsed from it.
      crosstoolString = TextFormat.printToString(configBuilder);
      config.overwrite(DEFAULT_OSX_CROSSTOOL_DIR + "/CROSSTOOL", crosstoolString);

      // Create special lines specifying the compiler map entry for
      // each toolchain.
      StringBuilder compilerMap = new StringBuilder();
      for (CToolchain toolchain : configBuilder.build().getToolchainList()) {
        compilerMap.append(String.format("'%s|%s': ':cc-compiler-%s',\n",
            toolchain.getTargetCpu(), toolchain.getCompiler(), toolchain.getTargetCpu()));
      }

      // Create the test BUILD file.
      ImmutableList.Builder<String> crosstoolBuild =
          ImmutableList.<String>builder()
              .add(
                  "package(default_visibility=['//visibility:public'])",
                  "exports_files(glob(['**']))",
                  "cc_toolchain_suite(",
                  "    name = 'crosstool',",
                  "    toolchains = { " + compilerMap + " },",
                  ")",
                  "",
                  "cc_library(",
                  "    name = 'custom_malloc',",
                  ")",
                  "",
                  "filegroup(",
                  "    name = 'empty',",
                  "    srcs = [],",
                  ")",
                  "",
                  "filegroup(",
                  "    name = 'link',",
                  "    srcs = [",
                  "        'ar',",
                  "        'libempty.a',",
                  String.format("        '%s//tools/objc:libtool'", TestConstants.TOOLS_REPOSITORY),
                  "    ],",
                  ")");

      for (String arch :
          ImmutableList.of(
              // TODO(b/64492583): Remove 'darwin', as this is legacy and redundant with
              // darwin_x86_64.
              "darwin",
              // TODO(b/36471772): Remove 'k8' once unit tests do not require a host configuration
              // transition from the apple crosstool configuration.
              "x64_windows",
              "ios_x86_64",
              "ios_i386",
              "ios_armv7",
              "ios_arm64",
              "darwin_x86_64",
              "watchos_i386",
              "watchos_armv7k",
              "tvos_x86_64",
              "tvos_arm64",
              // TODO(b/36471772): Remove 'k8' once unit tests do not require a host configuration
              // transition from the apple crosstool configuration.
              "k8")) {
        crosstoolBuild.add(
            "apple_cc_toolchain(",
            "    name = 'cc-compiler-" + arch + "',",
            "    all_files = ':empty',",
            "    ar_files = ':empty',",
            "    as_files = ':empty',",
            "    compiler_files = ':empty',",
            "    cpu = 'ios',",
            "    dwp_files = ':empty',",
            "    dynamic_runtime_libs = [':empty'],",
            "    linker_files = ':link',",
            "    objcopy_files = ':empty',",
            "    static_runtime_libs = [':empty'],",
            "    strip_files = ':empty',",
            "    supports_param_files = 0,",
            ")",
            "toolchain(name = 'cc-toolchain-" + arch + "',",
            "    exec_compatible_with = [],",
            "    target_compatible_with = [],",
            "    toolchain = ':cc-compiler-" + arch + "',",
            "    toolchain_type = '"
                + TestConstants.TOOLS_REPOSITORY
                + "//tools/cpp:toolchain_type'",
            ")");

        // Add the newly-created toolchain to the WORKSPACE.
        config.append(
            "WORKSPACE",
            "register_toolchains('//" + DEFAULT_OSX_CROSSTOOL_DIR + ":cc-toolchain-" + arch + "')");
      }

      config.create(DEFAULT_OSX_CROSSTOOL_DIR + "/BUILD",
          Joiner.on("\n").join(crosstoolBuild.build()));
    }
  }

  /** Test setup for the Apple SDK targets that are used in tests. */
  public static void setupAppleSdks(MockToolsConfig config) throws IOException {
    config.create(
        "third_party/apple_sdks/BUILD",
        "package(default_visibility=['//visibility:public'])\n"
            + "licenses([\"notice\"])\n"
            + "filegroup(name = \"apple_sdk_compile\")");
  }
}
