// 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 <map>

#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "tools/gn/commands.h"
#include "tools/gn/input_file.h"
#include "tools/gn/parse_tree.h"
#include "tools/gn/setup.h"
#include "tools/gn/standard_out.h"
#include "tools/gn/tokenizer.h"

namespace commands {

namespace {

bool DoesLineBeginWithComment(const base::StringPiece& line) {
  // Skip whitespace.
  size_t i = 0;
  while (i < line.size() && IsAsciiWhitespace(line[i]))
    i++;

  return i < line.size() && line[i] == '#';
}

// Returns the offset of the beginning of the line identified by |offset|.
size_t BackUpToLineBegin(const std::string& data, size_t offset) {
  // Degenerate case of an empty line. Below we'll try to return the
  // character after the newline, but that will be incorrect in this case.
  if (offset == 0 || Tokenizer::IsNewline(data, offset))
    return offset;

  size_t cur = offset;
  do {
    cur --;
    if (Tokenizer::IsNewline(data, cur))
      return cur + 1;  // Want the first character *after* the newline.
  } while (cur > 0);
  return 0;
}

// Assumes DoesLineBeginWithComment().
std::string StripCommentFromLine(const base::StringPiece& line) {
  std::string ret = line.as_string();
  for (size_t i = 0; i < ret.size(); i++) {
    if (ret[i] == '#') {
      ret[i] = ' ';
      break;
    }
  }
  return ret;
}

// Tries to find the comment before the setting of the given value.
void GetContextForValue(const Value& value,
                        std::string* location_str,
                        std::string* comment) {
  Location location = value.origin()->GetRange().begin();
  const InputFile* file = location.file();
  if (!file)
    return;

  *location_str = file->name().value() + ":" +
      base::IntToString(location.line_number());

  const std::string& data = file->contents();
  size_t line_off =
      Tokenizer::ByteOffsetOfNthLine(data, location.line_number());

  while (line_off > 1) {
    line_off -= 2;  // Back up to end of previous line.
    size_t previous_line_offset = BackUpToLineBegin(data, line_off);

    base::StringPiece line(&data[previous_line_offset],
                           line_off - previous_line_offset + 1);
    if (!DoesLineBeginWithComment(line))
      break;

    comment->insert(0, StripCommentFromLine(line) + "\n");
    line_off = previous_line_offset;
  }
}

void PrintArgHelp(const base::StringPiece& name, const Value& value) {
  OutputString(name.as_string(), DECORATION_YELLOW);
  OutputString("  Default = " + value.ToString(true) + "\n");

  if (value.origin()) {
    std::string location, comment;
    GetContextForValue(value, &location, &comment);
    OutputString("    " + location + "\n" + comment);
  } else {
    OutputString("    (Internally set)\n");
  }
}

}  // namespace

extern const char kArgs[] = "args";
extern const char kArgs_HelpShort[] =
    "args: Display configurable arguments declared by the build.";
extern const char kArgs_Help[] =
    "gn args [arg name]\n"
    "  Displays all arguments declared by buildfiles along with their\n"
    "  description. Build arguments are anything in a declare_args() block\n"
    "  in any buildfile. The comment preceeding the declaration will be\n"
    "  displayed here (so comment well!).\n"
    "\n"
    "  These arguments can be overriden on the command-line:\n"
    "    --args=\"doom_melon_setting=5 component_build=1\"\n"
    "  or in a toolchain definition (see \"gn help buildargs\" for more on\n"
    "  how this all works).\n"
    "\n"
    "  If \"arg name\" is specified, only the information for that argument\n"
    "  will be displayed. Otherwise all arguments will be displayed.\n";

int RunArgs(const std::vector<std::string>& args) {
  Setup* setup = new Setup;
  setup->set_check_for_bad_items(false);
  if (!setup->DoSetup() || !setup->Run())
    return 1;

  const Scope::KeyValueMap& build_args =
      setup->build_settings().build_args().declared_arguments();

  if (args.size() == 1) {
    // Get help on a specific command.
    Scope::KeyValueMap::const_iterator found_arg = build_args.find(args[0]);
    if (found_arg == build_args.end()) {
      Err(Location(), "Unknown build arg.",
          "You asked for \"" + args[0] + "\" which I didn't find in any "
          "buildfile\nassociated with this build.");
      return 1;
    }
    PrintArgHelp(args[0], found_arg->second);
    return 0;
  } else if (args.size() > 1) {
    // Too many arguments.
    Err(Location(), "You're holding it wrong.",
        "Usage: \"gn args [arg name]\"").PrintToStdout();
    return 1;
  }

  // List all arguments. First put them in a regular map so they're sorted.
  std::map<base::StringPiece, Value> sorted_args;
  for (Scope::KeyValueMap::const_iterator i = build_args.begin();
       i != build_args.end(); ++i)
    sorted_args.insert(*i);

  for (std::map<base::StringPiece, Value>::iterator i = sorted_args.begin();
       i != sorted_args.end(); ++i) {
    PrintArgHelp(i->first, i->second);
    OutputString("\n");
  }

  return 0;
}

}  // namespace commands