// Copyright (c) 2013 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/path_output.h"

#include "base/strings/string_util.h"
#include "gn/filesystem_utils.h"
#include "gn/output_file.h"
#include "gn/string_utils.h"
#include "util/build_config.h"

PathOutput::PathOutput(const SourceDir& current_dir,
                       std::string_view source_root,
                       EscapingMode escaping)
    : current_dir_(current_dir) {
  inverse_current_dir_ = RebasePath("//", current_dir, source_root);
  if (!EndsWithSlash(inverse_current_dir_))
    inverse_current_dir_.push_back('/');
  options_.mode = escaping;
}

PathOutput::~PathOutput() = default;

void PathOutput::WriteFile(std::ostream& out, const SourceFile& file) const {
  WritePathStr(out, file.value());
}

void PathOutput::WriteDir(std::ostream& out,
                          const SourceDir& dir,
                          DirSlashEnding slash_ending) const {
  if (dir.value() == "/") {
    // Writing system root is always a slash (this will normally only come up
    // on Posix systems).
    if (slash_ending == DIR_NO_LAST_SLASH)
      out << "/.";
    else
      out << "/";
  } else if (dir.value() == "//") {
    // Writing out the source root.
    if (slash_ending == DIR_NO_LAST_SLASH) {
      // The inverse_current_dir_ will contain a [back]slash at the end, so we
      // can't just write it out.
      if (inverse_current_dir_.empty()) {
        out << ".";
      } else {
        out.write(inverse_current_dir_.c_str(),
                  inverse_current_dir_.size() - 1);
      }
    } else {
      if (inverse_current_dir_.empty())
        out << "./";
      else
        out << inverse_current_dir_;
    }
  } else if (dir == current_dir_) {
    // Writing the same directory. This needs special handling here since
    // we need to output something else other than the input.
    if (slash_ending == DIR_INCLUDE_LAST_SLASH)
      out << "./";
    else
      out << ".";
  } else if (slash_ending == DIR_INCLUDE_LAST_SLASH) {
    WritePathStr(out, dir.value());
  } else {
    // DIR_NO_LAST_SLASH mode, just trim the last char.
    WritePathStr(out,
                 std::string_view(dir.value().data(), dir.value().size() - 1));
  }
}

void PathOutput::WriteFile(std::ostream& out, const OutputFile& file) const {
  // Here we assume that the path is already preprocessed.
  EscapeStringToStream(out, file.value(), options_);
}

void PathOutput::WriteFiles(std::ostream& out,
                            const std::vector<SourceFile>& files) const {
  for (const auto& file : files) {
    out << " ";
    WriteFile(out, file);
  }
}

void PathOutput::WriteFiles(std::ostream& out,
                            const std::vector<OutputFile>& files) const {
  for (const auto& file : files) {
    out << " ";
    WriteFile(out, file);
  }
}

void PathOutput::WriteFiles(std::ostream& out,
                            const UniqueVector<OutputFile>& files) const {
  for (const auto& file : files) {
    out << " ";
    WriteFile(out, file);
  }
}

void PathOutput::WriteDir(std::ostream& out,
                          const OutputFile& file,
                          DirSlashEnding slash_ending) const {
  DCHECK(file.value().empty() || file.value()[file.value().size() - 1] == '/');

  switch (slash_ending) {
    case DIR_INCLUDE_LAST_SLASH:
      EscapeStringToStream(out, file.value(), options_);
      break;
    case DIR_NO_LAST_SLASH:
      if (!file.value().empty() &&
          file.value()[file.value().size() - 1] == '/') {
        // Trim trailing slash.
        EscapeStringToStream(
            out, std::string_view(file.value().data(), file.value().size() - 1),
            options_);
      } else {
        // Doesn't end with a slash, write the whole thing.
        EscapeStringToStream(out, file.value(), options_);
      }
      break;
  }
}

void PathOutput::WriteFile(std::ostream& out,
                           const base::FilePath& file) const {
  // Assume native file paths are always absolute.
  EscapeStringToStream(out, FilePathToUTF8(file), options_);
}

void PathOutput::WriteSourceRelativeString(std::ostream& out,
                                           std::string_view str) const {
  if (options_.mode == ESCAPE_NINJA_COMMAND) {
    // Shell escaping needs an intermediate string since it may end up
    // quoting the whole thing.
    std::string intermediate;
    intermediate.reserve(inverse_current_dir_.size() + str.size());
    intermediate.assign(inverse_current_dir_.c_str(),
                        inverse_current_dir_.size());
    intermediate.append(str.data(), str.size());

    EscapeStringToStream(
        out, std::string_view(intermediate.c_str(), intermediate.size()),
        options_);
  } else {
    // Ninja (and none) escaping can avoid the intermediate string and
    // reprocessing of the inverse_current_dir_.
    EscapeStringToStream(out, inverse_current_dir_, options_);
    EscapeStringToStream(out, str, options_);
  }
}

void PathOutput::WritePathStr(std::ostream& out, std::string_view str) const {
  DCHECK(str.size() > 0 && str[0] == '/');

  if (str.substr(0, current_dir_.value().size()) ==
      std::string_view(current_dir_.value())) {
    // The current dir is a prefix of the output file, so we can strip the
    // prefix and write out the result.
    EscapeStringToStream(out, str.substr(current_dir_.value().size()),
                         options_);
  } else if (str.size() >= 2 && str[1] == '/') {
    WriteSourceRelativeString(out, str.substr(2));
  } else {
// Input begins with one slash, don't write the current directory since
// it's system-absolute.
#if defined(OS_WIN)
    // On Windows, trim the leading slash, since the input for absolute
    // paths will look like "/C:/foo/bar.txt".
    EscapeStringToStream(out, str.substr(1), options_);
#else
    EscapeStringToStream(out, str, options_);
#endif
  }
}
