// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include <unistd.h>

#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <memory>
#include <optional>
#include <string>
#include <system_error>
#include <utility>

#include "common/bazel_working_dir.h"
#include "common/check.h"
#include "common/command_line.h"
#include "common/error.h"
#include "common/exe_path.h"
#include "common/filesystem.h"
#include "common/init_llvm.h"
#include "common/raw_string_ostream.h"
#include "common/version.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/ThreadPool.h"
#include "llvm/Support/Threading.h"
#include "llvm/Support/VirtualFileSystem.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/TargetParser/Triple.h"
#include "toolchain/base/install_paths.h"
#include "toolchain/driver/clang_runner.h"
#include "toolchain/driver/clang_runtimes.h"
#include "toolchain/driver/codegen_options.h"
#include "toolchain/driver/runtimes_cache.h"
#include "tools/cpp/runfiles/runfiles.h"

namespace Carbon {

namespace {
struct Options {
  static const CommandLine::CommandInfo Info;

  auto Build(CommandLine::CommandBuilder& b) -> void;

  bool verbose = false;
  bool force = false;
  bool threads = true;

  llvm::StringRef directory;
  CodegenOptions codegen_options;
};
}  // namespace

// Note that this is not constexpr so that it can include information generated
// in separate translation units and potentially overridden at link time in the
// version string.
const CommandLine::CommandInfo Options::Info = {
    .name = "bazel_build_clang_runtimes",
    .version = Version::ToolchainInfo,
    .help = R"""(
A dedicated tool for use with Bazel to build Carbon's _Clang_ runtimes.

This works similarly to the Carbon `build-runtimes` subcommand with some key
differences:

1) It only builds the Clang runtimes, not any Carbon-specific runtimes. This is
   important due to the next point...

2) It is a stand-alone command with minimal dependencies on Carbon to allow
   Bazel to cache the Clang-based runtimes across most changes to the Carbon
   toolchain when the LLVM version stays the same.

3) It removes any symlinks in the built runtimes tree into other parts of the
   installation that are not generated by this command. This allows a Bazel rule
   to re-create those in using Bazel-specific logic that connects those parts of
   the runtimes tree to their respective inputs.
)""",
};

auto Options::Build(CommandLine::CommandBuilder& b) -> void {
  b.AddFlag(
      {
          .name = "verbose",
          .short_name = "v",
          .help = "Enable verbose logging to the stderr stream.",
      },
      [&](CommandLine::FlagBuilder& arg_b) { arg_b.Set(&verbose); });

  b.AddFlag(
      {
          .name = "force",
          .help = R"""(
Force re-creating the provided output path from scratch

This will **remove** the provided output path and re-create it from scratch.
)""",
      },
      [&](CommandLine::FlagBuilder& arg_b) { arg_b.Set(&force); });

  b.AddFlag(
      {
          .name = "threads",
          .help = R"""(
Controls whether threads are used to build runtimes.

When enabled (the default), Carbon will try to build runtime libraries using
threads to parallelize the operation. How many threads is controlled
automatically by the system.

Disabling threads ensures a single threaded build of the runtimes which can help
when there are errors or other output.
)""",
      },
      [&](auto& arg_b) {
        arg_b.Default(true);
        arg_b.Set(&threads);
      });

  codegen_options.Build(b);

  b.AddStringPositionalArg(
      {
          .name = "output-directory",
          .help = R"""(
The directory to populate with runtime libraries suitable for the selected code
generation options.
)""",
      },
      [&](auto& arg_b) {
        arg_b.Required(true);
        arg_b.Set(&directory);
      });

  b.Do([] {});
}

static auto MakeInstallPaths(const std::filesystem::path& exe_path)
    -> InstallPaths {
  CARBON_CHECK(*Filesystem::Cwd().Access(exe_path,
                                         Filesystem::AccessCheckFlags::Execute),
               "Invoked with a non-executable `argv[0]`: {0}", exe_path);
  return InstallPaths::MakeForBazelRunfiles(exe_path.native());
}

static auto GetClangPath(const std::filesystem::path& exe_path)
    -> std::filesystem::path {
  std::string runtimes_error;
  using bazel::tools::cpp::runfiles::Runfiles;
  std::unique_ptr<Runfiles> runfiles(
      Runfiles::Create(exe_path.native(), &runtimes_error));
  CARBON_CHECK(runfiles != nullptr, "Failed to find runtimes tree: {0}",
               runtimes_error);
  std::filesystem::path clang_path =
      runfiles->Rlocation("llvm-project/clang/clang");
  CARBON_CHECK(!clang_path.empty());
  CARBON_CHECK(*Filesystem::Cwd().Access(clang_path));
  return clang_path;
}

static auto ParseOptions(int argc, char** argv) -> ErrorOr<Options> {
  Options options;
  llvm::SmallVector<llvm::StringRef> args(
      llvm::ArrayRef<char*>(argv, argc).drop_front());
  CARBON_ASSIGN_OR_RETURN(
      auto result, CommandLine::Parse(args, llvm::outs(), Options::Info,
                                      [&](CommandLine::CommandBuilder& b) {
                                        options.Build(b);
                                      }));
  if (result == CommandLine::ParseResult::MetaSuccess) {
    // Exit immediately with success if this was just a meta invocation.
#if !defined(__APPLE__)
    std::quick_exit(0);
#else
    // No `std::quick_exit` on macOS, despite the standard including it.
    _Exit(0);
#endif
  }
  return options;
}

// The actual `main` implementation. Can return an exit code or an `Error`
// (which causes EXIT_FAILURE).
//
// Note that this is primarily an internal utility for use with a specific set
// of Bazel rules, and so many errors are directly `CARBON_CHECK`-ed instead of
// propagated. Only basic command line errors are propagated using the `Error`
// side of the return. Other errors in the execution environment are `CHECK`-ed
// to provide useful backtraces when debugging.
static auto Main(int argc, char** argv) -> ErrorOr<int> {
  InitLLVM init_llvm(argc, argv);
  if (argc < 1) {
    return Error("Invoked without command line arguments");
  }

  std::filesystem::path exe_path = argv[0];
  exe_path = SetWorkingDirForBazelRun(exe_path);

  const auto install_paths = MakeInstallPaths(exe_path);

  CARBON_ASSIGN_OR_RETURN(Options options, ParseOptions(argc, argv));

  std::filesystem::path clang_path = GetClangPath(exe_path);
  auto fs = llvm::vfs::getRealFileSystem();
  llvm::raw_ostream* vlog_stream = options.verbose ? &llvm::errs() : nullptr;
  ClangRunner runner(&install_paths, fs, vlog_stream, std::move(clang_path));

  Runtimes::Cache::Features features = {
      .target = options.codegen_options.target.str()};

  llvm::SingleThreadExecutor single_thread({.ThreadsRequested = 1});
  std::optional<llvm::DefaultThreadPool> threads;
  llvm::ThreadPoolInterface* thread_pool = &single_thread;
  if (options.threads) {
    threads.emplace(llvm::optimal_concurrency());
    thread_pool = &*threads;
  }

  auto runtimes = *Runtimes::Make(options.directory.str(), vlog_stream);

  if (options.force) {
    // Remove existing runtimes to force a rebuild.
    runtimes.Remove(Runtimes::ClangResourceDir).Check();
    runtimes.Remove(Runtimes::LibUnwind).Check();
    runtimes.Remove(Runtimes::Libcxx).Check();
  }

  ClangResourceDirBuilder resource_dir_builder(
      &runner, thread_pool, llvm::Triple(features.target), &runtimes);
  ClangArchiveRuntimesBuilder<Runtimes::LibUnwind> lib_unwind_builder(
      &runner, thread_pool, llvm::Triple(features.target), &runtimes);
  ClangArchiveRuntimesBuilder<Runtimes::Libcxx> libcxx_builder(
      &runner, thread_pool, llvm::Triple(features.target), &runtimes);

  std::filesystem::path resource_dir_path =
      *std::move(resource_dir_builder).Wait();
  std::move(lib_unwind_builder).Wait().Check();
  std::move(libcxx_builder).Wait().Check();

  // Now remove the `include` symlink from the resource_dir. We'll re-create
  // this tree in the Bazel rule, as the symlink currently is an absolute
  // (non-hermetic) path. We want Bazel to manage this directory with links to
  // the actual input files.
  Filesystem::Dir resource_dir = *Filesystem::Cwd().OpenDir(resource_dir_path);
  resource_dir.Unlink("include").Check();

  return EXIT_SUCCESS;
}

}  // namespace Carbon

auto main(int argc, char** argv) -> int {
  auto result = Carbon::Main(argc, argv);
  if (result.ok()) {
    return *result;
  } else {
    llvm::errs() << "error: " << result.error() << "\n";
    return EXIT_FAILURE;
  }
}
