// Copyright 2018 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 "gn/compile_commands_writer.h"

#include <sstream>

#include "base/json/string_escape.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "gn/builder.h"
#include "gn/c_substitution_type.h"
#include "gn/c_tool.h"
#include "gn/config_values_extractors.h"
#include "gn/deps_iterator.h"
#include "gn/escape.h"
#include "gn/filesystem_utils.h"
#include "gn/ninja_target_command_util.h"
#include "gn/path_output.h"
#include "gn/string_output_buffer.h"
#include "gn/substitution_writer.h"

// Structure of JSON output file
// [
//   {
//      "directory": "The build directory."
//      "file": "The main source file processed by this compilation step.
//               Must be absolute or relative to the above build directory."
//      "command": "The compile command executed."
//   }
//   ...
// ]

namespace {

#if defined(OS_WIN)
const char kPrettyPrintLineEnding[] = "\r\n";
#else
const char kPrettyPrintLineEnding[] = "\n";
#endif

struct CompileFlags {
  std::string includes;
  std::string defines;
  std::string cflags;
  std::string cflags_c;
  std::string cflags_cc;
  std::string cflags_objc;
  std::string cflags_objcc;
  std::string framework_dirs;
  std::string frameworks;
};

// Helper template function to call RecursiveTargetConfigToStream<std::string>
// and return the JSON-escaped resulting string.
//
// NOTE: The Windows compiler cannot properly deduce the first parameter type
// so pass it at each call site to ensure proper builds for this platform.
template <typename T, typename Writer>
std::string FlagsGetter(const Target* target,
                        const std::vector<T>& (ConfigValues::*getter)() const,
                        const Writer& writer) {
  std::string result;
  std::ostringstream out;
  RecursiveTargetConfigToStream<T>(target, getter, writer, out);
  base::EscapeJSONString(out.str(), false, &result);
  return result;
};

void SetupCompileFlags(const Target* target,
                       PathOutput& path_output,
                       EscapeOptions opts,
                       CompileFlags& flags) {
  bool has_precompiled_headers =
      target->config_values().has_precompiled_headers();

  flags.defines =
      FlagsGetter<std::string>(target, &ConfigValues::defines,
                               DefineWriter(ESCAPE_COMPILATION_DATABASE));

  flags.framework_dirs =
      FlagsGetter<SourceDir>(target, &ConfigValues::framework_dirs,
                             FrameworkDirsWriter(path_output, "-F"));

  flags.frameworks = FlagsGetter<std::string>(
      target, &ConfigValues::frameworks,
      FrameworksWriter(ESCAPE_COMPILATION_DATABASE, "-framework"));
  flags.frameworks += FlagsGetter<std::string>(
      target, &ConfigValues::weak_frameworks,
      FrameworksWriter(ESCAPE_COMPILATION_DATABASE, "-weak_framework"));

  flags.includes = FlagsGetter<SourceDir>(target, &ConfigValues::include_dirs,
                                          IncludeWriter(path_output));

  // Helper lambda to call WriteOneFlag() and return the resulting
  // escaped JSON string.
  auto one_flag = [&](const Substitution* substitution,
                      bool has_precompiled_headers, const char* tool_name,
                      const std::vector<std::string>& (ConfigValues::*getter)()
                          const) -> std::string {
    std::string result;
    std::ostringstream out;
    WriteOneFlag(target, substitution, has_precompiled_headers, tool_name,
                 getter, opts, path_output, out, /*write_substitution=*/false);
    base::EscapeJSONString(out.str(), false, &result);
    return result;
  };

  flags.cflags = one_flag(&CSubstitutionCFlags, false, Tool::kToolNone,
                          &ConfigValues::cflags);

  flags.cflags_c = one_flag(&CSubstitutionCFlagsC, has_precompiled_headers,
                            CTool::kCToolCc, &ConfigValues::cflags_c);

  flags.cflags_cc = one_flag(&CSubstitutionCFlagsCc, has_precompiled_headers,
                             CTool::kCToolCxx, &ConfigValues::cflags_cc);

  flags.cflags_objc =
      one_flag(&CSubstitutionCFlagsObjC, has_precompiled_headers,
               CTool::kCToolObjC, &ConfigValues::cflags_objc);

  flags.cflags_objcc =
      one_flag(&CSubstitutionCFlagsObjCc, has_precompiled_headers,
               CTool::kCToolObjCxx, &ConfigValues::cflags_objcc);
}

void WriteFile(const SourceFile& source,
               PathOutput& path_output,
               std::ostream& out) {
  std::ostringstream rel_source_path;
  out << "    \"file\": \"";
  path_output.WriteFile(out, source);
}

void WriteDirectory(std::string build_dir, std::ostream& out) {
  out << "\",";
  out << kPrettyPrintLineEnding;
  out << "    \"directory\": \"";
  out << build_dir;
  out << "\",";
}

void WriteCommand(const Target* target,
                  const SourceFile& source,
                  const CompileFlags& flags,
                  std::vector<OutputFile>& tool_outputs,
                  PathOutput& path_output,
                  SourceFile::Type source_type,
                  const char* tool_name,
                  EscapeOptions opts,
                  std::ostream& out) {
  EscapeOptions no_quoting(opts);
  no_quoting.inhibit_quoting = true;
  const Tool* tool = target->toolchain()->GetTool(tool_name);

  out << kPrettyPrintLineEnding;
  out << "    \"command\": \"";

  for (const auto& range : tool->command().ranges()) {
    // TODO: this is emitting a bonus space prior to each substitution.
    if (range.type == &SubstitutionLiteral) {
      EscapeJSONStringToStream(out, range.literal, no_quoting);
    } else if (range.type == &SubstitutionOutput) {
      path_output.WriteFiles(out, tool_outputs);
    } else if (range.type == &CSubstitutionDefines) {
      out << flags.defines;
    } else if (range.type == &CSubstitutionFrameworkDirs) {
      out << flags.framework_dirs;
    } else if (range.type == &CSubstitutionFrameworks) {
      out << flags.frameworks;
    } else if (range.type == &CSubstitutionIncludeDirs) {
      out << flags.includes;
    } else if (range.type == &CSubstitutionCFlags) {
      out << flags.cflags;
    } else if (range.type == &CSubstitutionCFlagsC) {
      if (source_type == SourceFile::SOURCE_C)
        out << flags.cflags_c;
    } else if (range.type == &CSubstitutionCFlagsCc) {
      if (source_type == SourceFile::SOURCE_CPP)
        out << flags.cflags_cc;
    } else if (range.type == &CSubstitutionCFlagsObjC) {
      if (source_type == SourceFile::SOURCE_M)
        out << flags.cflags_objc;
    } else if (range.type == &CSubstitutionCFlagsObjCc) {
      if (source_type == SourceFile::SOURCE_MM)
        out << flags.cflags_objcc;
    } else if (range.type == &SubstitutionLabel ||
               range.type == &SubstitutionLabelName ||
               range.type == &SubstitutionLabelNoToolchain ||
               range.type == &SubstitutionRootGenDir ||
               range.type == &SubstitutionRootOutDir ||
               range.type == &SubstitutionTargetGenDir ||
               range.type == &SubstitutionTargetOutDir ||
               range.type == &SubstitutionTargetOutputName ||
               range.type == &SubstitutionSource ||
               range.type == &SubstitutionSourceNamePart ||
               range.type == &SubstitutionSourceFilePart ||
               range.type == &SubstitutionSourceDir ||
               range.type == &SubstitutionSourceRootRelativeDir ||
               range.type == &SubstitutionSourceGenDir ||
               range.type == &SubstitutionSourceOutDir ||
               range.type == &SubstitutionSourceTargetRelative) {
      EscapeStringToStream(out,
                           SubstitutionWriter::GetCompilerSubstitution(
                               target, source, range.type),
                           opts);
    } else {
      // Other flags shouldn't be relevant to compiling C/C++/ObjC/ObjC++
      // source files.
      NOTREACHED() << "Unsupported substitution for this type of target : "
                   << range.type->name;
      continue;
    }
  }
}

void OutputJSON(const BuildSettings* build_settings,
                std::vector<const Target*>& all_targets,
                std::ostream& out) {
  out << '[';
  out << kPrettyPrintLineEnding;
  bool first = true;
  auto build_dir = build_settings->GetFullPath(build_settings->build_dir())
                       .StripTrailingSeparators();
  std::vector<OutputFile> tool_outputs;  // Prevent reallocation in loop.

  EscapeOptions opts;
  opts.mode = ESCAPE_NINJA_PREFORMATTED_COMMAND;

  for (const auto* target : all_targets) {
    if (!target->IsBinary())
      continue;

    // Precompute values that are the same for all sources in a target to avoid
    // computing for every source.

    PathOutput path_output(
        target->settings()->build_settings()->build_dir(),
        target->settings()->build_settings()->root_path_utf8(),
        ESCAPE_NINJA_COMMAND);

    CompileFlags flags;
    SetupCompileFlags(target, path_output, opts, flags);

    for (const auto& source : target->sources()) {
      // If this source is not a C/C++/ObjC/ObjC++ source (not header) file,
      // continue as it does not belong in the compilation database.
      SourceFile::Type source_type = source.type();
      if (source_type != SourceFile::SOURCE_CPP &&
          source_type != SourceFile::SOURCE_C &&
          source_type != SourceFile::SOURCE_M &&
          source_type != SourceFile::SOURCE_MM)
        continue;

      const char* tool_name = Tool::kToolNone;
      if (!target->GetOutputFilesForSource(source, &tool_name, &tool_outputs))
        continue;

      if (!first) {
        out << ',';
        out << kPrettyPrintLineEnding;
      }
      first = false;
      out << "  {";
      out << kPrettyPrintLineEnding;

      WriteFile(source, path_output, out);
      WriteDirectory(base::StringPrintf("%" PRIsFP, PATH_CSTR(build_dir)), out);
      WriteCommand(target, source, flags, tool_outputs, path_output,
                   source_type, tool_name, opts, out);
      out << "\"";
      out << kPrettyPrintLineEnding;
      out << "  }";
    }
  }

  out << kPrettyPrintLineEnding;
  out << "]";
  out << kPrettyPrintLineEnding;
}

}  // namespace

std::string CompileCommandsWriter::RenderJSON(
    const BuildSettings* build_settings,
    std::vector<const Target*>& all_targets) {
  StringOutputBuffer json;
  std::ostream out(&json);
  OutputJSON(build_settings, all_targets, out);
  return json.str();
}

bool CompileCommandsWriter::RunAndWriteFiles(
    const BuildSettings* build_settings,
    const Builder& builder,
    const std::string& file_name,
    const std::string& target_filters,
    bool quiet,
    Err* err) {
  SourceFile output_file = build_settings->build_dir().ResolveRelativeFile(
      Value(nullptr, file_name), err);
  if (output_file.is_null())
    return false;

  base::FilePath output_path = build_settings->GetFullPath(output_file);

  std::vector<const Target*> all_targets = builder.GetAllResolvedTargets();

  std::set<std::string> target_filters_set;
  for (auto& target :
       base::SplitString(target_filters, ",", base::TRIM_WHITESPACE,
                         base::SPLIT_WANT_NONEMPTY)) {
    target_filters_set.insert(target);
  }

  StringOutputBuffer json;
  std::ostream output_to_json(&json);
  if (target_filters_set.empty()) {
    OutputJSON(build_settings, all_targets, output_to_json);
  } else {
    std::vector<const Target*> preserved_targets =
        FilterTargets(all_targets, target_filters_set);
    OutputJSON(build_settings, preserved_targets, output_to_json);
  }

  if (!json.ContentsEqual(output_path)) {
    if (!json.WriteToFile(output_path, err))
      return false;
  }
  return true;
}

std::vector<const Target*> CompileCommandsWriter::FilterTargets(
    const std::vector<const Target*>& all_targets,
    const std::set<std::string>& target_filters_set) {
  std::vector<const Target*> preserved_targets;

  std::set<const Target*> visited;
  for (auto& target : all_targets) {
    if (target_filters_set.count(target->label().name())) {
      VisitDeps(target, &visited);
    }
  }

  preserved_targets.reserve(visited.size());
  // Preserve the original ordering of all_targets
  // to allow easier debugging and testing.
  for (auto& target : all_targets) {
    if (visited.count(target)) {
      preserved_targets.push_back(target);
    }
  }
  return preserved_targets;
}

void CompileCommandsWriter::VisitDeps(const Target* target,
                                      std::set<const Target*>* visited) {
  if (!visited->count(target)) {
    visited->insert(target);
    for (const auto& pair : target->GetDeps(Target::DEPS_ALL)) {
      VisitDeps(pair.ptr, visited);
    }
  }
}
