// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "tools/gn/xcode_writer.h"

#include <iomanip>
#include <map>
#include <memory>
#include <sstream>
#include <string>
#include <utility>

#include "base/environment.h"
#include "base/logging.h"
#include "base/sha1.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "tools/gn/args.h"
#include "tools/gn/build_settings.h"
#include "tools/gn/builder.h"
#include "tools/gn/commands.h"
#include "tools/gn/deps_iterator.h"
#include "tools/gn/filesystem_utils.h"
#include "tools/gn/settings.h"
#include "tools/gn/source_file.h"
#include "tools/gn/target.h"
#include "tools/gn/value.h"
#include "tools/gn/variables.h"
#include "tools/gn/xcode_object.h"

namespace {

using TargetToFileList = std::unordered_map<const Target*, Target::FileList>;
using TargetToTarget = std::unordered_map<const Target*, const Target*>;
using TargetToPBXTarget = std::unordered_map<const Target*, PBXTarget*>;

const char kEarlGreyFileNameIdentifier[] = "egtest.mm";
const char kXCTestFileNameIdentifier[] = "xctest.mm";
const char kXCTestModuleTargetNamePostfix[] = "_module";
const char kXCUITestRunnerTargetNamePostfix[] = "_runner";

struct SafeEnvironmentVariableInfo {
  const char* name;
  bool capture_at_generation;
};

SafeEnvironmentVariableInfo kSafeEnvironmentVariables[] = {
    {"HOME", true}, {"LANG", true},    {"PATH", true},
    {"USER", true}, {"TMPDIR", false},
};

XcodeWriter::TargetOsType GetTargetOs(const Args& args) {
  const Value* target_os_value = args.GetArgOverride(variables::kTargetOs);
  if (target_os_value) {
    if (target_os_value->type() == Value::STRING) {
      if (target_os_value->string_value() == "ios")
        return XcodeWriter::WRITER_TARGET_OS_IOS;
    }
  }
  return XcodeWriter::WRITER_TARGET_OS_MACOS;
}

std::string GetBuildScript(const std::string& target_name,
                           const std::string& ninja_extra_args,
                           base::Environment* environment) {
  std::stringstream script;
  script << "echo note: \"Compile and copy " << target_name << " via ninja\"\n"
         << "exec ";

  // Launch ninja with a sanitized environment (Xcode sets many environment
  // variable overridding settings, including the SDK, thus breaking hermetic
  // build).
  script << "env -i ";
  for (const auto& variable : kSafeEnvironmentVariables) {
    script << variable.name << "=\"";

    std::string value;
    if (variable.capture_at_generation)
      environment->GetVar(variable.name, &value);

    if (!value.empty())
      script << value;
    else
      script << "$" << variable.name;
    script << "\" ";
  }

  script << "ninja -C .";
  if (!ninja_extra_args.empty())
    script << " " << ninja_extra_args;
  if (!target_name.empty())
    script << " " << target_name;
  script << "\nexit 1\n";
  return script.str();
}

bool IsApplicationTarget(const Target* target) {
  return target->output_type() == Target::CREATE_BUNDLE &&
         target->bundle_data().product_type() ==
             "com.apple.product-type.application";
}

bool IsXCUITestRunnerTarget(const Target* target) {
  return IsApplicationTarget(target) &&
         base::EndsWith(target->label().name(),
                        kXCUITestRunnerTargetNamePostfix,
                        base::CompareCase::SENSITIVE);
}

bool IsXCTestModuleTarget(const Target* target) {
  return target->output_type() == Target::CREATE_BUNDLE &&
         target->bundle_data().product_type() ==
             "com.apple.product-type.bundle.unit-test" &&
         base::EndsWith(target->label().name(), kXCTestModuleTargetNamePostfix,
                        base::CompareCase::SENSITIVE);
}

bool IsXCUITestModuleTarget(const Target* target) {
  return target->output_type() == Target::CREATE_BUNDLE &&
         target->bundle_data().product_type() ==
             "com.apple.product-type.bundle.ui-testing" &&
         base::EndsWith(target->label().name(), kXCTestModuleTargetNamePostfix,
                        base::CompareCase::SENSITIVE);
}

bool IsXCTestFile(const SourceFile& file) {
  return base::EndsWith(file.GetName(), kEarlGreyFileNameIdentifier,
                        base::CompareCase::SENSITIVE) ||
         base::EndsWith(file.GetName(), kXCTestFileNameIdentifier,
                        base::CompareCase::SENSITIVE);
}

const Target* FindApplicationTargetByName(
    const std::string& target_name,
    const std::vector<const Target*>& targets) {
  for (const Target* target : targets) {
    if (target->label().name() == target_name) {
      DCHECK(IsApplicationTarget(target));
      return target;
    }
  }
  NOTREACHED();
  return nullptr;
}

// Adds |base_pbxtarget| as a dependency of |dependent_pbxtarget| in the
// generated Xcode project.
void AddPBXTargetDependency(const PBXTarget* base_pbxtarget,
                            PBXTarget* dependent_pbxtarget,
                            const PBXProject* project) {
  auto container_item_proxy =
      std::make_unique<PBXContainerItemProxy>(project, base_pbxtarget);
  auto dependency = std::make_unique<PBXTargetDependency>(
      base_pbxtarget, std::move(container_item_proxy));

  dependent_pbxtarget->AddDependency(std::move(dependency));
}

// Adds the corresponding test application target as dependency of xctest or
// xcuitest module target in the generated Xcode project.
void AddDependencyTargetForTestModuleTargets(
    const std::vector<const Target*>& targets,
    const TargetToPBXTarget& bundle_target_to_pbxtarget,
    const PBXProject* project) {
  for (const Target* target : targets) {
    if (!IsXCTestModuleTarget(target) && !IsXCUITestModuleTarget(target))
      continue;

    const Target* test_application_target = FindApplicationTargetByName(
        target->bundle_data().xcode_test_application_name(), targets);
    const PBXTarget* test_application_pbxtarget =
        bundle_target_to_pbxtarget.at(test_application_target);
    PBXTarget* module_pbxtarget = bundle_target_to_pbxtarget.at(target);
    DCHECK(test_application_pbxtarget);
    DCHECK(module_pbxtarget);

    AddPBXTargetDependency(test_application_pbxtarget, module_pbxtarget,
                           project);
  }
}

// Searches the list of xctest files recursively under |target|.
void SearchXCTestFilesForTarget(const Target* target,
                                TargetToFileList* xctest_files_per_target) {
  // Early return if already visited and processed.
  if (xctest_files_per_target->find(target) != xctest_files_per_target->end())
    return;

  Target::FileList xctest_files;
  for (const SourceFile& file : target->sources()) {
    if (IsXCTestFile(file)) {
      xctest_files.push_back(file);
    }
  }

  // Call recursively on public and private deps.
  for (const auto& t : target->public_deps()) {
    SearchXCTestFilesForTarget(t.ptr, xctest_files_per_target);
    const Target::FileList& deps_xctest_files =
        (*xctest_files_per_target)[t.ptr];
    xctest_files.insert(xctest_files.end(), deps_xctest_files.begin(),
                        deps_xctest_files.end());
  }

  for (const auto& t : target->private_deps()) {
    SearchXCTestFilesForTarget(t.ptr, xctest_files_per_target);
    const Target::FileList& deps_xctest_files =
        (*xctest_files_per_target)[t.ptr];
    xctest_files.insert(xctest_files.end(), deps_xctest_files.begin(),
                        deps_xctest_files.end());
  }

  // Sort xctest_files to remove duplicates.
  std::sort(xctest_files.begin(), xctest_files.end());
  xctest_files.erase(std::unique(xctest_files.begin(), xctest_files.end()),
                     xctest_files.end());

  xctest_files_per_target->insert(std::make_pair(target, xctest_files));
}

// Add all source files for indexing, both private and public.
void AddSourceFilesToProjectForIndexing(
    const std::vector<const Target*>& targets,
    PBXProject* project,
    SourceDir source_dir,
    const BuildSettings* build_settings) {
  std::vector<SourceFile> sources;
  for (const Target* target : targets) {
    for (const SourceFile& source : target->sources()) {
      if (IsStringInOutputDir(build_settings->build_dir(), source.value()))
        continue;

      sources.push_back(source);
    }

    if (target->all_headers_public())
      continue;

    for (const SourceFile& source : target->public_headers()) {
      if (IsStringInOutputDir(build_settings->build_dir(), source.value()))
        continue;

      sources.push_back(source);
    }
  }

  // Sort sources to ensure determinism of the project file generation and
  // remove duplicate reference to the source files (can happen due to the
  // bundle_data targets).
  std::sort(sources.begin(), sources.end());
  sources.erase(std::unique(sources.begin(), sources.end()), sources.end());

  for (const SourceFile& source : sources) {
    std::string source_file = RebasePath(source.value(), source_dir,
                                         build_settings->root_path_utf8());
    project->AddSourceFileToIndexingTarget(source_file, source_file,
                                           CompilerFlags::NONE);
  }
}

// Add xctest files to the "Compiler Sources" of corresponding test module
// native targets.
void AddXCTestFilesToTestModuleTarget(const Target::FileList& xctest_file_list,
                                      PBXNativeTarget* native_target,
                                      PBXProject* project,
                                      SourceDir source_dir,
                                      const BuildSettings* build_settings) {
  for (const SourceFile& source : xctest_file_list) {
    std::string source_path = RebasePath(source.value(), source_dir,
                                         build_settings->root_path_utf8());

    // Test files need to be known to Xcode for proper indexing and for
    // discovery of tests function for XCTest and XCUITest, but the compilation
    // is done via ninja and thus must prevent Xcode from compiling the files by
    // adding '-help' as per file compiler flag.
    project->AddSourceFile(source_path, source_path, CompilerFlags::HELP,
                           native_target);
  }
}

class CollectPBXObjectsPerClassHelper : public PBXObjectVisitor {
 public:
  CollectPBXObjectsPerClassHelper() = default;

  void Visit(PBXObject* object) override {
    DCHECK(object);
    objects_per_class_[object->Class()].push_back(object);
  }

  const std::map<PBXObjectClass, std::vector<const PBXObject*>>&
  objects_per_class() const {
    return objects_per_class_;
  }

 private:
  std::map<PBXObjectClass, std::vector<const PBXObject*>> objects_per_class_;

  DISALLOW_COPY_AND_ASSIGN(CollectPBXObjectsPerClassHelper);
};

std::map<PBXObjectClass, std::vector<const PBXObject*>>
CollectPBXObjectsPerClass(PBXProject* project) {
  CollectPBXObjectsPerClassHelper visitor;
  project->Visit(visitor);
  return visitor.objects_per_class();
}

class RecursivelyAssignIdsHelper : public PBXObjectVisitor {
 public:
  RecursivelyAssignIdsHelper(const std::string& seed)
      : seed_(seed), counter_(0) {}

  void Visit(PBXObject* object) override {
    std::stringstream buffer;
    buffer << seed_ << " " << object->Name() << " " << counter_;
    std::string hash = base::SHA1HashString(buffer.str());
    DCHECK_EQ(hash.size() % 4, 0u);

    uint32_t id[3] = {0, 0, 0};
    const uint32_t* ptr = reinterpret_cast<const uint32_t*>(hash.data());
    for (size_t i = 0; i < hash.size() / 4; i++)
      id[i % 3] ^= ptr[i];

    object->SetId(base::HexEncode(id, sizeof(id)));
    ++counter_;
  }

 private:
  std::string seed_;
  int64_t counter_;

  DISALLOW_COPY_AND_ASSIGN(RecursivelyAssignIdsHelper);
};

void RecursivelyAssignIds(PBXProject* project) {
  RecursivelyAssignIdsHelper visitor(project->Name());
  project->Visit(visitor);
}

}  // namespace

// static
bool XcodeWriter::RunAndWriteFiles(const std::string& workspace_name,
                                   const std::string& root_target_name,
                                   const std::string& ninja_extra_args,
                                   const std::string& dir_filters_string,
                                   const BuildSettings* build_settings,
                                   const Builder& builder,
                                   Err* err) {
  const XcodeWriter::TargetOsType target_os =
      GetTargetOs(build_settings->build_args());

  PBXAttributes attributes;
  switch (target_os) {
    case XcodeWriter::WRITER_TARGET_OS_IOS:
      attributes["SDKROOT"] = "iphoneos";
      attributes["TARGETED_DEVICE_FAMILY"] = "1,2";
      break;
    case XcodeWriter::WRITER_TARGET_OS_MACOS:
      attributes["SDKROOT"] = "macosx";
      break;
  }

  const std::string source_path =
      base::FilePath::FromUTF8Unsafe(
          RebasePath("//", build_settings->build_dir()))
          .StripTrailingSeparators()
          .AsUTF8Unsafe();

  std::string config_name = build_settings->build_dir()
                                .Resolve(base::FilePath())
                                .StripTrailingSeparators()
                                .BaseName()
                                .AsUTF8Unsafe();
  DCHECK(!config_name.empty());

  std::string::size_type separator = config_name.find('-');
  if (separator != std::string::npos)
    config_name = config_name.substr(0, separator);

  std::vector<const Target*> targets;
  std::vector<const Target*> all_targets = builder.GetAllResolvedTargets();
  if (!XcodeWriter::FilterTargets(build_settings, all_targets,
                                  dir_filters_string, &targets, err)) {
    return false;
  }

  XcodeWriter workspace(workspace_name);
  workspace.CreateProductsProject(targets, all_targets, attributes, source_path,
                                  config_name, root_target_name,
                                  ninja_extra_args, build_settings, target_os);

  return workspace.WriteFiles(build_settings, err);
}

XcodeWriter::XcodeWriter(const std::string& name) : name_(name) {
  if (name_.empty())
    name_.assign("all");
}

XcodeWriter::~XcodeWriter() = default;

// static
bool XcodeWriter::FilterTargets(const BuildSettings* build_settings,
                                const std::vector<const Target*>& all_targets,
                                const std::string& dir_filters_string,
                                std::vector<const Target*>* targets,
                                Err* err) {
  // Filter targets according to the semicolon-delimited list of label patterns,
  // if defined, first.
  targets->reserve(all_targets.size());
  if (dir_filters_string.empty()) {
    *targets = all_targets;
  } else {
    std::vector<LabelPattern> filters;
    if (!commands::FilterPatternsFromString(build_settings, dir_filters_string,
                                            &filters, err)) {
      return false;
    }

    commands::FilterTargetsByPatterns(all_targets, filters, targets);
  }

  // Filter out all target of type EXECUTABLE that are direct dependency of
  // a BUNDLE_DATA target (under the assumption that they will be part of a
  // CREATE_BUNDLE target generating an application bundle). Sort the list
  // of targets per pointer to use binary search for the removal.
  std::sort(targets->begin(), targets->end());

  for (const Target* target : all_targets) {
    if (!target->settings()->is_default())
      continue;

    if (target->output_type() != Target::BUNDLE_DATA)
      continue;

    for (const auto& pair : target->GetDeps(Target::DEPS_LINKED)) {
      if (pair.ptr->output_type() != Target::EXECUTABLE)
        continue;

      auto iter = std::lower_bound(targets->begin(), targets->end(), pair.ptr);
      if (iter != targets->end() && *iter == pair.ptr)
        targets->erase(iter);
    }
  }

  // Sort the list of targets per-label to get a consistent ordering of them
  // in the generated Xcode project (and thus stability of the file generated).
  std::sort(targets->begin(), targets->end(),
            [](const Target* a, const Target* b) {
              return a->label().name() < b->label().name();
            });

  return true;
}

void XcodeWriter::CreateProductsProject(
    const std::vector<const Target*>& targets,
    const std::vector<const Target*>& all_targets,
    const PBXAttributes& attributes,
    const std::string& source_path,
    const std::string& config_name,
    const std::string& root_target,
    const std::string& ninja_extra_args,
    const BuildSettings* build_settings,
    TargetOsType target_os) {
  std::unique_ptr<PBXProject> main_project(
      new PBXProject("products", config_name, source_path, attributes));

  std::vector<const Target*> bundle_targets;
  TargetToPBXTarget bundle_target_to_pbxtarget;

  std::string build_path;
  std::unique_ptr<base::Environment> env(base::Environment::Create());
  SourceDir source_dir("//");
  AddSourceFilesToProjectForIndexing(all_targets, main_project.get(),
                                     source_dir, build_settings);
  main_project->AddAggregateTarget(
      "All", GetBuildScript(root_target, ninja_extra_args, env.get()));

  // Needs to search for xctest files under the application targets, and this
  // variable is used to store the results of visited targets, thus making the
  // search more efficient.
  TargetToFileList xctest_files_per_target;

  for (const Target* target : targets) {
    switch (target->output_type()) {
      case Target::EXECUTABLE:
        if (target_os == XcodeWriter::WRITER_TARGET_OS_IOS)
          continue;

        main_project->AddNativeTarget(
            target->label().name(), "compiled.mach-o.executable",
            target->output_name().empty() ? target->label().name()
                                          : target->output_name(),
            "com.apple.product-type.tool",
            GetBuildScript(target->label().name(), ninja_extra_args,
                           env.get()));
        break;

      case Target::CREATE_BUNDLE: {
        if (target->bundle_data().product_type().empty())
          continue;

        // For XCUITest, two CREATE_BUNDLE targets are generated:
        // ${target_name}_runner and ${target_name}_module, however, Xcode
        // requires only one target named ${target_name} to run tests.
        if (IsXCUITestRunnerTarget(target))
          continue;
        std::string pbxtarget_name = target->label().name();
        if (IsXCUITestModuleTarget(target)) {
          std::string target_name = target->label().name();
          pbxtarget_name = target_name.substr(
              0, target_name.rfind(kXCTestModuleTargetNamePostfix));
        }

        PBXAttributes xcode_extra_attributes =
            target->bundle_data().xcode_extra_attributes();

        const std::string& target_output_name =
            RebasePath(target->bundle_data()
                           .GetBundleRootDirOutput(target->settings())
                           .value(),
                       build_settings->build_dir());
        PBXNativeTarget* native_target = main_project->AddNativeTarget(
            pbxtarget_name, std::string(), target_output_name,
            target->bundle_data().product_type(),
            GetBuildScript(pbxtarget_name, ninja_extra_args, env.get()),
            xcode_extra_attributes);

        bundle_targets.push_back(target);
        bundle_target_to_pbxtarget.insert(
            std::make_pair(target, native_target));

        if (!IsXCTestModuleTarget(target) && !IsXCUITestModuleTarget(target))
          continue;

        // For XCTest, test files are compiled into the application bundle.
        // For XCUITest, test files are compiled into the test module bundle.
        const Target* target_with_xctest_files = nullptr;
        if (IsXCTestModuleTarget(target)) {
          target_with_xctest_files = FindApplicationTargetByName(
              target->bundle_data().xcode_test_application_name(), targets);
        } else if (IsXCUITestModuleTarget(target)) {
          target_with_xctest_files = target;
        } else {
          NOTREACHED();
        }

        SearchXCTestFilesForTarget(target_with_xctest_files,
                                   &xctest_files_per_target);
        const Target::FileList& xctest_file_list =
            xctest_files_per_target[target_with_xctest_files];

        // Add xctest files to the "Compiler Sources" of corresponding xctest
        // and xcuitest native targets for proper indexing and for discovery of
        // tests function.
        AddXCTestFilesToTestModuleTarget(xctest_file_list, native_target,
                                         main_project.get(), source_dir,
                                         build_settings);
        break;
      }

      default:
        break;
    }
  }

  // Adding the corresponding test application target as a dependency of xctest
  // or xcuitest module target in the generated Xcode project so that the
  // application target is re-compiled when compiling the test module target.
  AddDependencyTargetForTestModuleTargets(
      bundle_targets, bundle_target_to_pbxtarget, main_project.get());

  projects_.push_back(std::move(main_project));
}

bool XcodeWriter::WriteFiles(const BuildSettings* build_settings, Err* err) {
  for (const auto& project : projects_) {
    if (!WriteProjectFile(build_settings, project.get(), err))
      return false;
  }

  SourceFile xcworkspacedata_file =
      build_settings->build_dir().ResolveRelativeFile(
          Value(nullptr, name_ + ".xcworkspace/contents.xcworkspacedata"), err);
  if (xcworkspacedata_file.is_null())
    return false;

  std::stringstream xcworkspacedata_string_out;
  WriteWorkspaceContent(xcworkspacedata_string_out);

  return WriteFileIfChanged(build_settings->GetFullPath(xcworkspacedata_file),
                            xcworkspacedata_string_out.str(), err);
}

bool XcodeWriter::WriteProjectFile(const BuildSettings* build_settings,
                                   PBXProject* project,
                                   Err* err) {
  SourceFile pbxproj_file = build_settings->build_dir().ResolveRelativeFile(
      Value(nullptr, project->Name() + ".xcodeproj/project.pbxproj"), err);
  if (pbxproj_file.is_null())
    return false;

  std::stringstream pbxproj_string_out;
  WriteProjectContent(pbxproj_string_out, project);

  if (!WriteFileIfChanged(build_settings->GetFullPath(pbxproj_file),
                          pbxproj_string_out.str(), err))
    return false;

  return true;
}

void XcodeWriter::WriteWorkspaceContent(std::ostream& out) {
  out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
      << "<Workspace version = \"1.0\">\n";
  for (const auto& project : projects_) {
    out << "  <FileRef location = \"group:" << project->Name()
        << ".xcodeproj\"></FileRef>\n";
  }
  out << "</Workspace>\n";
}

void XcodeWriter::WriteProjectContent(std::ostream& out, PBXProject* project) {
  RecursivelyAssignIds(project);

  out << "// !$*UTF8*$!\n"
      << "{\n"
      << "\tarchiveVersion = 1;\n"
      << "\tclasses = {\n"
      << "\t};\n"
      << "\tobjectVersion = 46;\n"
      << "\tobjects = {\n";

  for (auto& pair : CollectPBXObjectsPerClass(project)) {
    out << "\n"
        << "/* Begin " << ToString(pair.first) << " section */\n";
    std::sort(pair.second.begin(), pair.second.end(),
              [](const PBXObject* a, const PBXObject* b) {
                return a->id() < b->id();
              });
    for (auto* object : pair.second) {
      object->Print(out, 2);
    }
    out << "/* End " << ToString(pair.first) << " section */\n";
  }

  out << "\t};\n"
      << "\trootObject = " << project->Reference() << ";\n"
      << "}\n";
}