// Copyright 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 "extensions/browser/file_highlighter.h"

#include <stack>

#include "base/strings/utf_string_conversions.h"
#include "base/values.h"

namespace extensions {

namespace {

// Keys for a highlighted dictionary.
const char kBeforeHighlightKey[] = "beforeHighlight";
const char kHighlightKey[] = "highlight";
const char kAfterHighlightKey[] = "afterHighlight";

// Increment |index| to the position of the next quote ('"') in |str|, skipping
// over any escaped quotes. If no next quote is found, |index| is set to
// std::string::npos. Assumes |index| currently points to a quote.
void QuoteIncrement(const std::string& str, size_t* index) {
  size_t i = *index + 1;  // Skip over the first quote.
  bool found = false;
  while (!found && i < str.size()) {
    if (str[i] == '\\')
      i += 2;  // if we find an escaped character, skip it.
    else if (str[i] == '"')
      found = true;
    else
      ++i;
  }
  *index = found ? i : std::string::npos;
}

// Increment |index| by one if the next character is not a comment. Increment
// index until the end of the comment if it is a comment.
void CommentSafeIncrement(const std::string& str, size_t* index) {
  size_t i = *index;
  if (str[i] == '/' && i + 1 < str.size()) {
    // Eat a single-line comment.
    if (str[i + 1] == '/') {
      i += 2;  // Eat the '//'.
      while (i < str.size() && str[i] != '\n' && str[i] != '\r')
        ++i;
    } else if (str[i + 1] == '*') {  // Eat a multi-line comment.
      i += 3;  // Advance to the first possible comment end.
      while (i < str.size() && !(str[i - 1] == '*' && str[i] == '/'))
        ++i;
    }
  }
  *index = i + 1;
}

// Increment index until the end of the current "chunk"; a "chunk" is a JSON-
// style list, object, or string literal, without exceeding |end|. Assumes
// |index| currently points to a chunk's starting character ('{', '[', or '"').
void ChunkIncrement(const std::string& str, size_t* index, size_t end) {
  char c = str[*index];
  std::stack<char> stack;
  do {
    if (c == '"')
      QuoteIncrement(str, index);
    else if (c == '[')
      stack.push(']');
    else if (c == '{')
      stack.push('}');
    else if (!stack.empty() && c == stack.top())
      stack.pop();
    CommentSafeIncrement(str, index);
    c = str[*index];
  } while (!stack.empty() && *index < end);
}

}  // namespace

FileHighlighter::FileHighlighter(const std::string& contents)
    : contents_(contents), start_(0u), end_(contents_.size()) {
}

FileHighlighter::~FileHighlighter() {
}

std::string FileHighlighter::GetBeforeFeature() const {
  return contents_.substr(0, start_);
}

std::string FileHighlighter::GetFeature() const {
  return contents_.substr(start_, end_ - start_);
}

std::string FileHighlighter::GetAfterFeature() const {
  return contents_.substr(end_);
}

void FileHighlighter::SetHighlightedRegions(base::DictionaryValue* dict) const {
  std::string before_feature = GetBeforeFeature();
  if (!before_feature.empty())
    dict->SetString(kBeforeHighlightKey, base::UTF8ToUTF16(before_feature));

  std::string feature = GetFeature();
  if (!feature.empty())
    dict->SetString(kHighlightKey, base::UTF8ToUTF16(feature));

  std::string after_feature = GetAfterFeature();
  if (!after_feature.empty())
    dict->SetString(kAfterHighlightKey, base::UTF8ToUTF16(after_feature));
}

ManifestHighlighter::ManifestHighlighter(const std::string& manifest,
                                         const std::string& key,
                                         const std::string& specific)
    : FileHighlighter(manifest) {
  start_ = contents_.find('{') + 1;
  end_ = contents_.rfind('}');
  Parse(key, specific);
}

ManifestHighlighter::~ManifestHighlighter() {
}


void ManifestHighlighter::Parse(const std::string& key,
                                const std::string& specific) {
  // First, try to find the bounds of the full key.
  if (FindBounds(key, true) /* enforce at top level */ ) {
    // If we succeed, and we have a specific location, find the bounds of the
    // specific.
    if (!specific.empty())
      FindBounds(specific, false /* don't enforce at top level */ );

    // We may have found trailing whitespace. Don't use base::TrimWhitespace,
    // because we want to keep any whitespace we find - just not highlight it.
    size_t trim = contents_.find_last_not_of(" \t\n\r", end_ - 1);
    if (trim < end_ && trim > start_)
      end_ = trim + 1;
  } else {
    // If we fail, then we set start to end so that the highlighted portion is
    // empty.
    start_ = end_;
  }
}

bool ManifestHighlighter::FindBounds(const std::string& feature,
                                     bool enforce_at_top_level) {
  char c = '\0';
  while (start_ < end_) {
    c = contents_[start_];
    if (c == '"') {
      // The feature may be quoted.
      size_t quote_end = start_;
      QuoteIncrement(contents_, &quote_end);
      if (contents_.substr(start_ + 1, quote_end - 1 - start_) == feature) {
        FindBoundsEnd(feature, quote_end + 1);
        return true;
      } else {
        // If it's not the feature, then we can skip the quoted section.
        start_ = quote_end + 1;
      }
    } else if (contents_.substr(start_, feature.size()) == feature) {
        FindBoundsEnd(feature, start_ + feature.size() + 1);
        return true;
    } else if (enforce_at_top_level && (c == '{' || c == '[')) {
      // If we don't have to be at the top level, then we can skip any chunks
      // we find.
      ChunkIncrement(contents_, &start_, end_);
    } else {
      CommentSafeIncrement(contents_, &start_);
    }
  }
  return false;
}

void ManifestHighlighter::FindBoundsEnd(const std::string& feature,
                                        size_t local_start) {
  char c = '\0';
  while (local_start < end_) {
    c = contents_[local_start];
    // We're done when we find a terminating character (i.e., either a comma or
    // an ending bracket.
    if (c == ',' || c == '}' || c == ']') {
      end_ = local_start;
      return;
    }
    // We can skip any chunks we find, since we are looking for the end of the
    // current feature, and don't want to go any deeper.
    if (c == '"' || c == '{' || c == '[')
      ChunkIncrement(contents_, &local_start, end_);
    else
      CommentSafeIncrement(contents_, &local_start);
  }
}

SourceHighlighter::SourceHighlighter(const std::string& contents,
                                     size_t line_number)
    : FileHighlighter(contents) {
  Parse(line_number);
}

SourceHighlighter::~SourceHighlighter() {
}

void SourceHighlighter::Parse(size_t line_number) {
  // If line 0 is requested, highlight nothing.
  if (line_number == 0) {
    start_ = contents_.size();
    return;
  }

  for (size_t i = 1; i < line_number; ++i) {
    start_ = contents_.find('\n', start_);
    if (start_ == std::string::npos)
      break;
    start_ += 1;
  }

  end_ = contents_.find('\n', start_);

  // If we went off the end of the string (i.e., the line number was invalid),
  // then move start and end to the end of the string, so that the highlighted
  // portion is empty.
  start_ = start_ == std::string::npos ? contents_.size() : start_;
  end_ = end_ == std::string::npos ? contents_.size() : end_;
}

}  // namespace extensions