// Copyright 2014 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/c_include_iterator.h"

#include "base/logging.h"
#include "base/macros.h"
#include "base/strings/string_util.h"
#include "tools/gn/input_file.h"
#include "tools/gn/location.h"

namespace {

enum IncludeType {
  INCLUDE_NONE,
  INCLUDE_SYSTEM,  // #include <...>
  INCLUDE_USER     // #include "..."
};

// Returns a new string piece referencing the same buffer as the argument, but
// with leading space trimmed. This only checks for space and tab characters
// since we're dealing with lines in C source files.
base::StringPiece TrimLeadingWhitespace(const base::StringPiece& str) {
  size_t new_begin = 0;
  while (new_begin < str.size() &&
         (str[new_begin] == ' ' || str[new_begin] == '\t'))
    new_begin++;
  return str.substr(new_begin);
}

// We don't want to count comment lines and preprocessor lines toward our
// "max lines to look at before giving up" since the beginnings of some files
// may have a lot of comments.
//
// We only handle C-style "//" comments since this is the normal commenting
// style used in Chrome, and do so pretty stupidly. We don't want to write a
// full C++ parser here, we're just trying to get a good heuristic for checking
// the file.
//
// We assume the line has leading whitespace trimmed. We also assume that empty
// lines have already been filtered out.
bool ShouldCountTowardNonIncludeLines(const base::StringPiece& line) {
  if (base::StartsWith(line, "//", base::CompareCase::SENSITIVE))
    return false;  // Don't count comments.
  if (base::StartsWith(line, "/*", base::CompareCase::SENSITIVE) ||
      base::StartsWith(line, " *", base::CompareCase::SENSITIVE))
    return false;  // C-style comment blocks with stars along the left side.
  if (base::StartsWith(line, "#", base::CompareCase::SENSITIVE))
    return false;  // Don't count preprocessor.
  if (base::ContainsOnlyChars(line, base::kWhitespaceASCII))
    return false;  // Don't count whitespace lines.
  return true;     // Count everything else.
}

// Given a line, checks to see if it looks like an include or import and
// extract the path. The type of include is returned. Returns INCLUDE_NONE on
// error or if this is not an include line.
//
// The 1-based character number on the line that the include was found at
// will be filled into *begin_char.
IncludeType ExtractInclude(const base::StringPiece& line,
                           base::StringPiece* path,
                           int* begin_char) {
  static const char kInclude[] = "include";
  static const size_t kIncludeLen = arraysize(kInclude) - 1;  // No null.
  static const char kImport[] = "import";
  static const size_t kImportLen = arraysize(kImport) - 1;  // No null.

  base::StringPiece trimmed = TrimLeadingWhitespace(line);
  if (trimmed.empty())
    return INCLUDE_NONE;

  if (trimmed[0] != '#')
    return INCLUDE_NONE;

  trimmed = TrimLeadingWhitespace(trimmed.substr(1));

  base::StringPiece contents;
  if (base::StartsWith(trimmed, base::StringPiece(kInclude, kIncludeLen),
                       base::CompareCase::SENSITIVE))
    contents = TrimLeadingWhitespace(trimmed.substr(kIncludeLen));
  else if (base::StartsWith(trimmed, base::StringPiece(kImport, kImportLen),
                            base::CompareCase::SENSITIVE))
    contents = TrimLeadingWhitespace(trimmed.substr(kImportLen));

  if (contents.empty())
    return INCLUDE_NONE;

  IncludeType type = INCLUDE_NONE;
  char terminating_char = 0;
  if (contents[0] == '"') {
    type = INCLUDE_USER;
    terminating_char = '"';
  } else if (contents[0] == '<') {
    type = INCLUDE_SYSTEM;
    terminating_char = '>';
  } else {
    return INCLUDE_NONE;
  }

  // Count everything to next "/> as the contents.
  size_t terminator_index = contents.find(terminating_char, 1);
  if (terminator_index == base::StringPiece::npos)
    return INCLUDE_NONE;

  *path = contents.substr(1, terminator_index - 1);
  // Note: one based so we do "+ 1".
  *begin_char = static_cast<int>(path->data() - line.data()) + 1;
  return type;
}

// Returns true if this line has a "nogncheck" comment associated with it.
bool HasNoCheckAnnotation(const base::StringPiece& line) {
  return line.find("nogncheck") != base::StringPiece::npos;
}

}  // namespace

const int CIncludeIterator::kMaxNonIncludeLines = 10;

CIncludeIterator::CIncludeIterator(const InputFile* input)
    : input_file_(input),
      file_(input->contents()),
      offset_(0),
      line_number_(0),
      lines_since_last_include_(0) {}

CIncludeIterator::~CIncludeIterator() = default;

bool CIncludeIterator::GetNextIncludeString(base::StringPiece* out,
                                            LocationRange* location) {
  base::StringPiece line;
  int cur_line_number = 0;
  while (lines_since_last_include_ <= kMaxNonIncludeLines &&
         GetNextLine(&line, &cur_line_number)) {
    base::StringPiece include_contents;
    int begin_char;
    IncludeType type = ExtractInclude(line, &include_contents, &begin_char);
    if (type == INCLUDE_USER && !HasNoCheckAnnotation(line)) {
      // Only count user includes for now.
      *out = include_contents;
      *location = LocationRange(
          Location(input_file_, cur_line_number, begin_char,
                   -1 /* TODO(scottmg): Is this important? */),
          Location(input_file_, cur_line_number,
                   begin_char + static_cast<int>(include_contents.size()),
                   -1 /* TODO(scottmg): Is this important? */));

      lines_since_last_include_ = 0;
      return true;
    }

    if (ShouldCountTowardNonIncludeLines(line))
      lines_since_last_include_++;
  }
  return false;
}

bool CIncludeIterator::GetNextLine(base::StringPiece* line, int* line_number) {
  if (offset_ == file_.size())
    return false;

  size_t begin = offset_;
  while (offset_ < file_.size() && file_[offset_] != '\n')
    offset_++;
  line_number_++;

  *line = file_.substr(begin, offset_ - begin);
  *line_number = line_number_;

  // If we didn't hit EOF, skip past the newline for the next one.
  if (offset_ < file_.size())
    offset_++;
  return true;
}
