// 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 <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <map>

#include "base/command_line.h"
#include "base/environment.h"
#include "base/files/file_util.h"
#include "base/json/json_writer.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "gn/commands.h"
#include "gn/filesystem_utils.h"
#include "gn/input_file.h"
#include "gn/parse_tree.h"
#include "gn/setup.h"
#include "gn/standard_out.h"
#include "gn/tokenizer.h"
#include "gn/trace.h"
#include "util/build_config.h"

#if defined(OS_WIN)
#include <windows.h>

#include <shellapi.h>
#endif

namespace commands {

namespace {

const char kSwitchList[] = "list";
const char kSwitchShort[] = "short";
const char kSwitchOverridesOnly[] = "overrides-only";
const char kSwitchJson[] = "json";

bool DoesLineBeginWithComment(std::string_view line) {
  // Skip whitespace.
  size_t i = 0;
  while (i < line.size() && base::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(), this strips the # character from the
// beginning and normalizes preceding whitespace.
std::string StripHashFromLine(std::string_view line, bool pad) {
  // Replace the # sign and everything before it with 3 spaces, so that a
  // normal comment that has a space after the # will be indented 4 spaces
  // (which makes our formatting come out nicely). If the comment is indented
  // from there, we want to preserve that indenting.
  std::string line_stripped(line.substr(line.find('#') + 1));
  if (pad)
    return "   " + line_stripped;

  // If not padding, strip the leading space if present.
  if (!line_stripped.empty() && line_stripped[0] == ' ')
    return line_stripped.substr(1);
  return line_stripped;
}

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

  *location_str = file->name().value();
  *line_no = 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);

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

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

// Prints the value and origin for a default value. Default values always list
// an origin and if there is no origin, print a message about it being
// internally set. Overrides can't be internally set so the location handling
// is a bit different.
//
// The default value also contains the docstring.
void PrintDefaultValueInfo(std::string_view name, const Value& value) {
  OutputString(value.ToString(true) + "\n");
  if (value.origin()) {
    int line_no;
    std::string location, comment;
    GetContextForValue(value, &location, &line_no, &comment);
    OutputString("      From " + location + ":" + base::IntToString(line_no) +
                 "\n");
    if (!comment.empty())
      OutputString("\n" + comment);
  } else {
    OutputString("      (Internally set; try `gn help " + std::string(name) +
                 "`.)\n");
  }
}

// Override value is null if there is no override.
void PrintArgHelp(std::string_view name, const Args::ValueWithOverride& val) {
  OutputString(std::string(name), DECORATION_YELLOW);
  OutputString("\n");

  if (val.has_override) {
    // Override present, print both it and the default.
    OutputString("    Current value = " + val.override_value.ToString(true) +
                 "\n");
    if (val.override_value.origin()) {
      int line_no;
      std::string location, comment;
      GetContextForValue(val.override_value, &location, &line_no, &comment);
      OutputString("      From " + location + ":" + base::IntToString(line_no) +
                   "\n");
    }
    OutputString("    Overridden from the default = ");
    PrintDefaultValueInfo(name, val.default_value);
  } else {
    // No override.
    OutputString("    Current value (from the default) = ");
    PrintDefaultValueInfo(name, val.default_value);
  }
}

void BuildArgJson(base::Value& dict,
                  std::string_view name,
                  const Args::ValueWithOverride& arg,
                  bool short_only) {
  assert(dict.is_dict());

  // Fetch argument name.
  dict.SetKey("name", base::Value(name));

  // Fetch overridden value information (if present).
  if (arg.has_override) {
    base::DictionaryValue override_dict;
    override_dict.SetKey("value",
                         base::Value(arg.override_value.ToString(true)));
    if (arg.override_value.origin() && !short_only) {
      int line_no;
      std::string location, comment;
      GetContextForValue(arg.override_value, &location, &line_no, &comment,
                         /*pad_comment=*/false);
      override_dict.SetKey("file", base::Value(location));
      override_dict.SetKey("line", base::Value(line_no));
    }
    dict.SetKey("current", std::move(override_dict));
  }

  // Fetch default value information, and comment (if present).
  base::DictionaryValue default_dict;
  std::string comment;
  default_dict.SetKey("value", base::Value(arg.default_value.ToString(true)));
  if (arg.default_value.origin() && !short_only) {
    int line_no;
    std::string location;
    GetContextForValue(arg.default_value, &location, &line_no, &comment,
                       /*pad_comment=*/false);
    default_dict.SetKey("file", base::Value(location));
    default_dict.SetKey("line", base::Value(line_no));
  }
  dict.SetKey("default", std::move(default_dict));
  if (!comment.empty() && !short_only)
    dict.SetKey("comment", base::Value(comment));
}

int ListArgs(const std::string& build_dir) {
  // Deliberately leaked to avoid expensive process teardown.
  Setup* setup = new Setup;
  if (!setup->DoSetup(build_dir, false) || !setup->Run())
    return 1;

  Args::ValueWithOverrideMap args =
      setup->build_settings().build_args().GetAllArguments();
  std::string list_value =
      base::CommandLine::ForCurrentProcess()->GetSwitchValueString(kSwitchList);
  if (!list_value.empty()) {
    // List just the one specified as the parameter to --list.
    auto found = args.find(list_value);
    if (found == args.end()) {
      Err(Location(), "Unknown build argument.",
          "You asked for \"" + list_value +
              "\" which I didn't find in any "
              "build file\nassociated with this build.")
          .PrintToStdout();
      return 1;
    }

    // Delete everything from the map except the one requested.
    Args::ValueWithOverrideMap::value_type preserved = *found;
    args.clear();
    args.insert(preserved);
  }

  // Cache this to avoid looking it up for each |arg| in the loops below.
  const bool overrides_only =
      base::CommandLine::ForCurrentProcess()->HasSwitch(kSwitchOverridesOnly);
  const bool short_only =
      base::CommandLine::ForCurrentProcess()->HasSwitch(kSwitchShort);

  if (base::CommandLine::ForCurrentProcess()->HasSwitch(kSwitchJson)) {
    // Convert all args to JSON, serialize and print them
    auto list = std::make_unique<base::ListValue>();
    for (const auto& arg : args) {
      if (overrides_only && !arg.second.has_override)
        continue;
      list->GetList().emplace_back(base::DictionaryValue());
      BuildArgJson(list->GetList().back(), arg.first, arg.second, short_only);
    }
    std::string s;
    base::JSONWriter::WriteWithOptions(
        *list.get(), base::JSONWriter::OPTIONS_PRETTY_PRINT, &s);
    OutputString(s);
    return 0;
  }

  if (short_only) {
    // Short <key>=<current_value> output.
    for (const auto& arg : args) {
      if (overrides_only && !arg.second.has_override)
        continue;
      OutputString(std::string(arg.first));
      OutputString(" = ");
      if (arg.second.has_override)
        OutputString(arg.second.override_value.ToString(true));
      else
        OutputString(arg.second.default_value.ToString(true));
      OutputString("\n");
    }
    return 0;
  }

  // Long output.
  for (const auto& arg : args) {
    if (overrides_only && !arg.second.has_override)
      continue;
    PrintArgHelp(arg.first, arg.second);
    OutputString("\n");
  }

  return 0;
}

#if defined(OS_WIN)

bool RunEditor(const base::FilePath& file_to_edit) {
  SHELLEXECUTEINFO info;
  memset(&info, 0, sizeof(info));
  info.cbSize = sizeof(info);
  info.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_CLASSNAME;
  info.lpFile = reinterpret_cast<LPCWSTR>(file_to_edit.value().c_str());
  info.nShow = SW_SHOW;
  info.lpClass = L".txt";
  if (!::ShellExecuteEx(&info)) {
    Err(Location(), "Couldn't run editor.",
        "Just edit \"" + FilePathToUTF8(file_to_edit) + "\" manually instead.")
        .PrintToStdout();
    return false;
  }

  if (!info.hProcess) {
    // Windows re-used an existing process.
    OutputString("\"" + FilePathToUTF8(file_to_edit) +
                 "\" opened in editor, save it and press <Enter> when done.\n");
    getchar();
  } else {
    OutputString("Waiting for editor on \"" + FilePathToUTF8(file_to_edit) +
                 "\"...\n");
    ::WaitForSingleObject(info.hProcess, INFINITE);
    ::CloseHandle(info.hProcess);
  }
  return true;
}

#else  // POSIX

bool RunEditor(const base::FilePath& file_to_edit) {
  const char* editor_ptr = getenv("GN_EDITOR");
  if (!editor_ptr)
    editor_ptr = getenv("VISUAL");
  if (!editor_ptr)
    editor_ptr = getenv("EDITOR");
  if (!editor_ptr)
    editor_ptr = "vi";

  std::string cmd(editor_ptr);
  cmd.append(" \"");

  // Its impossible to do this properly since we don't know the user's shell,
  // but quoting and escaping internal quotes should handle 99.999% of all
  // cases.
  std::string escaped_name = file_to_edit.value();
  base::ReplaceSubstringsAfterOffset(&escaped_name, 0, "\"", "\\\"");
  cmd.append(escaped_name);
  cmd.push_back('"');

  OutputString("Waiting for editor on \"" + file_to_edit.value() + "\"...\n");
  return system(cmd.c_str()) == 0;
}

#endif

int EditArgsFile(const std::string& build_dir) {
  {
    // Scope the setup. We only use it for some basic state. We'll do the
    // "real" build below in the gen command.
    Setup setup;
    // Don't fill build arguments. We're about to edit the file which supplies
    // these in the first place.
    setup.set_fill_arguments(false);
    if (!setup.DoSetup(build_dir, true))
      return 1;

    // Ensure the file exists. Need to normalize path separators since on
    // Windows they can come out as forward slashes here, and that confuses some
    // of the commands.
    BuildSettings build_settings = setup.build_settings();
    base::FilePath arg_file =
        build_settings.GetFullPath(setup.GetBuildArgFile())
            .NormalizePathSeparators();
    if (!base::PathExists(arg_file)) {
      std::string argfile_default_contents =
          "# Build arguments go here.\n"
          "# See \"gn args <out_dir> --list\" for available build "
          "arguments.\n";

      SourceFile template_path = build_settings.arg_file_template_path();
      if (!template_path.is_null()) {
        base::FilePath full_path =
            build_settings.GetFullPath(template_path).NormalizePathSeparators();
        if (!base::PathExists(full_path)) {
          Err err =
              Err(Location(), std::string("Can't load arg_file_template:\n  ") +
                                  template_path.value());
          err.PrintToStdout();
          return 1;
        }

        // Ignore the return code; if the read fails (unlikely), we'll just
        // use the default contents.
        base::ReadFileToString(full_path, &argfile_default_contents);
      }
#if defined(OS_WIN)
      // Use Windows lineendings for this file since it will often open in
      // Notepad which can't handle Unix ones.
      base::ReplaceSubstringsAfterOffset(&argfile_default_contents, 0, "\n",
                                         "\r\n");
#endif
      base::CreateDirectory(arg_file.DirName());
      base::WriteFile(arg_file, argfile_default_contents.c_str(),
                      static_cast<int>(argfile_default_contents.size()));
    }

    ScopedTrace editor_trace(TraceItem::TRACE_SETUP, "Waiting for editor");
    if (!RunEditor(arg_file))
      return 1;
  }

  // Now do a normal "gen" command.
  OutputString("Generating files...\n");
  std::vector<std::string> gen_commands;
  gen_commands.push_back(build_dir);
  return RunGen(gen_commands);
}

}  // namespace

const char kArgs[] = "args";
const char kArgs_HelpShort[] =
    "args: Display or configure arguments declared by the build.";
const char kArgs_Help[] =
    R"(gn args: (command-line tool)

  Display or configure arguments declared by the build.

    gn args <out_dir> [--list] [--short] [--args] [--overrides-only]

  See also "gn help buildargs" for a more high-level overview of how
  build arguments work.

Usage

  gn args <out_dir>
      Open the arguments for the given build directory in an editor. If the
      given build directory doesn't exist, it will be created and an empty args
      file will be opened in the editor. You would type something like this
      into that file:
          enable_doom_melon=false
          os="android"

      To find your editor on Posix, GN will search the environment variables in
      order: GN_EDITOR, VISUAL, and EDITOR. On Windows GN will open the command
      associated with .txt files.

      Note: you can edit the build args manually by editing the file "args.gn"
      in the build directory and then running "gn gen <out_dir>".

  gn args <out_dir> --list[=<exact_arg>] [--short] [--overrides-only] [--json]
      Lists all build arguments available in the current configuration, or, if
      an exact_arg is specified for the list flag, just that one build
      argument.

      The output will list the declaration location, current value for the
      build, default value (if different than the current value), and comment
      preceding the declaration.

      If --short is specified, only the names and current values will be
      printed.

      If --overrides-only is specified, only the names and current values of
      arguments that have been overridden (i.e. non-default arguments) will
      be printed. Overrides come from the <out_dir>/args.gn file and //.gn

      If --json is specified, the output will be emitted in json format.
      JSON schema for output:
      [
        {
          "name": variable_name,
          "current": {
            "value": overridden_value,
            "file": file_name,
            "line": line_no
          },
          "default": {
            "value": default_value,
            "file": file_name,
            "line": line_no
          },
          "comment": comment_string
        },
        ...
      ]

Examples

  gn args out/Debug
    Opens an editor with the args for out/Debug.

  gn args out/Debug --list --short
    Prints all arguments with their default values for the out/Debug
    build.

  gn args out/Debug --list --short --overrides-only
    Prints overridden arguments for the out/Debug build.

  gn args out/Debug --list=target_cpu
    Prints information about the "target_cpu" argument for the "
   "out/Debug
    build.

  gn args --list --args="os=\"android\" enable_doom_melon=true"
    Prints all arguments with the default values for a build with the
    given arguments set (which may affect the values of other
    arguments).
)";

int RunArgs(const std::vector<std::string>& args) {
  if (args.size() != 1) {
    Err(Location(), "Exactly one build dir needed.",
        "Usage: \"gn args <out_dir>\"\n"
        "Or see \"gn help args\" for more variants.")
        .PrintToStdout();
    return 1;
  }

  if (base::CommandLine::ForCurrentProcess()->HasSwitch(kSwitchList))
    return ListArgs(args[0]);
  return EditArgsFile(args[0]);
}

}  // namespace commands
