// 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/commands.h"

#include <optional>

#include "base/command_line.h"
#include "base/environment.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "gn/builder.h"
#include "gn/config_values_extractors.h"
#include "gn/filesystem_utils.h"
#include "gn/item.h"
#include "gn/label.h"
#include "gn/label_pattern.h"
#include "gn/setup.h"
#include "gn/standard_out.h"
#include "gn/target.h"
#include "util/build_config.h"

namespace commands {

namespace {

// Like above but the input string can be a pattern that matches multiple
// targets. If the input does not parse as a pattern, prints and error and
// returns false. If the pattern is valid, fills the vector (which might be
// empty if there are no matches) and returns true.
//
// If default_toolchain_only is true, a pattern with an unspecified toolchain
// will match the default toolchain only. If true, all toolchains will be
// matched.
bool ResolveTargetsFromCommandLinePattern(Setup* setup,
                                          const std::string& label_pattern,
                                          bool default_toolchain_only,
                                          std::vector<const Target*>* matches) {
  Value pattern_value(nullptr, label_pattern);

  Err err;
  LabelPattern pattern = LabelPattern::GetPattern(
      SourceDirForCurrentDirectory(setup->build_settings().root_path()),
      setup->build_settings().root_path_utf8(), pattern_value, &err);
  if (err.has_error()) {
    err.PrintToStdout();
    return false;
  }

  if (default_toolchain_only) {
    // By default a pattern with an empty toolchain will match all toolchains.
    // If the caller wants to default to the main toolchain only, set it
    // explicitly.
    if (pattern.toolchain().is_null()) {
      // No explicit toolchain set.
      pattern.set_toolchain(setup->loader()->default_toolchain_label());
    }
  }

  std::vector<LabelPattern> pattern_vector;
  pattern_vector.push_back(pattern);
  FilterTargetsByPatterns(setup->builder().GetAllResolvedTargets(),
                          pattern_vector, matches);
  return true;
}

// If there's an error, it will be printed and false will be returned.
bool ResolveStringFromCommandLineInput(
    Setup* setup,
    const SourceDir& current_dir,
    const std::string& input,
    bool default_toolchain_only,
    UniqueVector<const Target*>* target_matches,
    UniqueVector<const Config*>* config_matches,
    UniqueVector<const Toolchain*>* toolchain_matches,
    UniqueVector<SourceFile>* file_matches) {
  if (LabelPattern::HasWildcard(input)) {
    // For now, only match patterns against targets. It might be nice in the
    // future to allow the user to specify which types of things they want to
    // match, but it should probably only match targets by default.
    std::vector<const Target*> target_match_vector;
    if (!ResolveTargetsFromCommandLinePattern(
            setup, input, default_toolchain_only, &target_match_vector))
      return false;
    for (const Target* target : target_match_vector)
      target_matches->push_back(target);
    return true;
  }

  // Try to figure out what this thing is.
  Err err;
  Label label = Label::Resolve(
      current_dir, setup->build_settings().root_path_utf8(),
      setup->loader()->default_toolchain_label(), Value(nullptr, input), &err);
  if (err.has_error()) {
    // Not a valid label, assume this must be a file.
    err = Err();
    file_matches->push_back(current_dir.ResolveRelativeFile(
        Value(nullptr, input), &err, setup->build_settings().root_path_utf8()));
    if (err.has_error()) {
      err.PrintToStdout();
      return false;
    }
    return true;
  }

  const Item* item = setup->builder().GetItem(label);
  if (item) {
    if (const Config* as_config = item->AsConfig())
      config_matches->push_back(as_config);
    else if (const Target* as_target = item->AsTarget())
      target_matches->push_back(as_target);
    else if (const Toolchain* as_toolchain = item->AsToolchain())
      toolchain_matches->push_back(as_toolchain);
  } else {
    // Not an item, assume this must be a file.
    file_matches->push_back(current_dir.ResolveRelativeFile(
        Value(nullptr, input), &err, setup->build_settings().root_path_utf8()));
    if (err.has_error()) {
      err.PrintToStdout();
      return false;
    }
  }

  return true;
}

enum TargetPrintingMode {
  TARGET_PRINT_BUILDFILE,
  TARGET_PRINT_LABEL,
  TARGET_PRINT_OUTPUT,
};

// Retrieves the target printing mode based on the command line flags for the
// current process. Returns true on success. On error, prints a message to the
// console and returns false.
bool GetTargetPrintingMode(TargetPrintingMode* mode) {
  std::string switch_key = "as";
  const base::CommandLine* cmdline = base::CommandLine::ForCurrentProcess();

  if (!cmdline->HasSwitch(switch_key)) {
    // Default to labels.
    *mode = TARGET_PRINT_LABEL;
    return true;
  }

  std::string value = cmdline->GetSwitchValueASCII(switch_key);
  if (value == "buildfile") {
    *mode = TARGET_PRINT_BUILDFILE;
    return true;
  }
  if (value == "label") {
    *mode = TARGET_PRINT_LABEL;
    return true;
  }
  if (value == "output") {
    *mode = TARGET_PRINT_OUTPUT;
    return true;
  }

  Err(Location(), "Invalid value for \"--as\".",
      "I was expecting \"buildfile\", \"label\", or \"output\" but you\n"
      "said \"" +
          value + "\".")
      .PrintToStdout();
  return false;
}

// Returns the target type filter based on the command line flags for the
// current process. Returns true on success. On error, prints a message to the
// console and returns false.
//
// Target::UNKNOWN will be set if there is no filter. Target::ACTION_FOREACH
// will never be returned. Code applying the filters should apply Target::ACTION
// to both ACTION and ACTION_FOREACH.
bool GetTargetTypeFilter(Target::OutputType* type) {
  std::string switch_key = "type";
  const base::CommandLine* cmdline = base::CommandLine::ForCurrentProcess();

  if (!cmdline->HasSwitch(switch_key)) {
    // Default to unknown -> no filtering.
    *type = Target::UNKNOWN;
    return true;
  }

  std::string value = cmdline->GetSwitchValueASCII(switch_key);
  if (value == "group") {
    *type = Target::GROUP;
    return true;
  }
  if (value == "executable") {
    *type = Target::EXECUTABLE;
    return true;
  }
  if (value == "shared_library") {
    *type = Target::SHARED_LIBRARY;
    return true;
  }
  if (value == "loadable_module") {
    *type = Target::LOADABLE_MODULE;
    return true;
  }
  if (value == "static_library") {
    *type = Target::STATIC_LIBRARY;
    return true;
  }
  if (value == "source_set") {
    *type = Target::SOURCE_SET;
    return true;
  }
  if (value == "copy") {
    *type = Target::COPY_FILES;
    return true;
  }
  if (value == "action") {
    *type = Target::ACTION;
    return true;
  }

  Err(Location(), "Invalid value for \"--type\".").PrintToStdout();
  return false;
}

// Applies any testonly filtering specified on the command line to the given
// target set. On failure, prints an error and returns false.
bool ApplyTestonlyFilter(std::vector<const Target*>* targets) {
  const base::CommandLine* cmdline = base::CommandLine::ForCurrentProcess();
  std::string testonly_key = "testonly";

  if (targets->empty() || !cmdline->HasSwitch(testonly_key))
    return true;

  std::string testonly_value = cmdline->GetSwitchValueASCII(testonly_key);
  bool testonly = false;
  if (testonly_value == "true") {
    testonly = true;
  } else if (testonly_value != "false") {
    Err(Location(), "Bad value for --testonly.",
        "I was expecting --testonly=true or --testonly=false.")
        .PrintToStdout();
    return false;
  }

  // Filter into a copy of the vector, then replace the output.
  std::vector<const Target*> result;
  result.reserve(targets->size());

  for (const Target* target : *targets) {
    if (target->testonly() == testonly)
      result.push_back(target);
  }

  *targets = std::move(result);
  return true;
}

// Applies any target type filtering specified on the command line to the given
// target set. On failure, prints an error and returns false.
bool ApplyTypeFilter(std::vector<const Target*>* targets) {
  Target::OutputType type = Target::UNKNOWN;
  if (!GetTargetTypeFilter(&type))
    return false;
  if (targets->empty() || type == Target::UNKNOWN)
    return true;  // Nothing to filter out.

  // Filter into a copy of the vector, then replace the output.
  std::vector<const Target*> result;
  result.reserve(targets->size());

  for (const Target* target : *targets) {
    // Make "action" also apply to ACTION_FOREACH.
    if (target->output_type() == type ||
        (type == Target::ACTION &&
         target->output_type() == Target::ACTION_FOREACH))
      result.push_back(target);
  }

  *targets = std::move(result);
  return true;
}

// Returns the file path generating this item.
base::FilePath BuildFileForItem(const Item* item) {
  return item->defined_from()->GetRange().begin().file()->physical_name();
}

void PrintTargetsAsBuildfiles(const std::vector<const Target*>& targets,
                              base::ListValue* out) {
  // Output the set of unique source files.
  std::set<std::string> unique_files;
  for (const Target* target : targets)
    unique_files.insert(FilePathToUTF8(BuildFileForItem(target)));

  for (const std::string& file : unique_files) {
    out->AppendString(file);
  }
}

void PrintTargetsAsLabels(const std::vector<const Target*>& targets,
                          base::ListValue* out) {
  // Putting the labels into a set automatically sorts them for us.
  std::set<Label> unique_labels;
  for (auto* target : targets)
    unique_labels.insert(target->label());

  // Grab the label of the default toolchain from the first target.
  Label default_tc_label = targets[0]->settings()->default_toolchain_label();

  for (const Label& label : unique_labels) {
    // Print toolchain only for ones not in the default toolchain.
    out->AppendString(label.GetUserVisibleName(label.GetToolchainLabel() !=
                                               default_tc_label));
  }
}

void PrintTargetsAsOutputs(const std::vector<const Target*>& targets,
                           base::ListValue* out) {
  if (targets.empty())
    return;

  // Grab the build settings from a random target.
  const BuildSettings* build_settings =
      targets[0]->settings()->build_settings();

  for (const Target* target : targets) {
    // Use the link output file if there is one, otherwise fall back to the
    // dependency output file (for actions, for example).
    OutputFile output_file = target->link_output_file();
    if (output_file.value().empty())
      output_file = target->dependency_output_file();

    SourceFile output_as_source = output_file.AsSourceFile(build_settings);
    std::string result =
        RebasePath(output_as_source.value(), build_settings->build_dir(),
                   build_settings->root_path_utf8());
    out->AppendString(result);
  }
}

#if defined(OS_WIN)
// Git bash will remove the first "/" in "//" paths
// This also happens for labels assigned to command line parameters, e.g.
// --filters
// Fix "//" paths, but not absolute and relative paths
inline std::string FixGitBashLabelEdit(const std::string& label) {
  static std::unique_ptr<base::Environment> git_bash_env;
  if (!git_bash_env)
    git_bash_env = base::Environment::Create();

  std::string temp_label(label);

  if (git_bash_env->HasVar(
          "MSYSTEM") &&        // Only for MinGW based shells like Git Bash
      temp_label[0] == '/' &&  // Only fix for //foo paths, not /f:oo paths
      (temp_label.length() < 2 ||
       (temp_label[1] != '/' &&
        (temp_label.length() < 3 || temp_label[1] != ':'))))
    temp_label.insert(0, "/");
  return temp_label;
}
#else
// Only repair on Windows
inline std::string FixGitBashLabelEdit(const std::string& label) {
  return label;
}
#endif

std::optional<HowTargetContainsFile> TargetContainsFile(
    const Target* target,
    const SourceFile& file) {
  for (const auto& cur_file : target->sources()) {
    if (cur_file == file)
      return HowTargetContainsFile::kSources;
  }
  for (const auto& cur_file : target->public_headers()) {
    if (cur_file == file)
      return HowTargetContainsFile::kPublic;
  }
  for (ConfigValuesIterator iter(target); !iter.done(); iter.Next()) {
    for (const auto& cur_file : iter.cur().inputs()) {
      if (cur_file == file)
        return HowTargetContainsFile::kInputs;
    }
  }
  for (const auto& cur_file : target->data()) {
    if (cur_file == file.value())
      return HowTargetContainsFile::kData;
    if (cur_file.back() == '/' &&
        base::StartsWith(file.value(), cur_file, base::CompareCase::SENSITIVE))
      return HowTargetContainsFile::kData;
  }

  if (target->action_values().script().value() == file.value())
    return HowTargetContainsFile::kScript;

  std::vector<SourceFile> output_sources;
  target->action_values().GetOutputsAsSourceFiles(target, &output_sources);
  for (const auto& cur_file : output_sources) {
    if (cur_file == file)
      return HowTargetContainsFile::kOutput;
  }

  for (const auto& cur_file : target->computed_outputs()) {
    if (cur_file.AsSourceFile(target->settings()->build_settings()) == file)
      return HowTargetContainsFile::kOutput;
  }
  return std::nullopt;
}

}  // namespace

CommandInfo::CommandInfo()
    : help_short(nullptr), help(nullptr), runner(nullptr) {}

CommandInfo::CommandInfo(const char* in_help_short,
                         const char* in_help,
                         CommandRunner in_runner)
    : help_short(in_help_short), help(in_help), runner(in_runner) {}

const CommandInfoMap& GetCommands() {
  static CommandInfoMap info_map;
  if (info_map.empty()) {
#define INSERT_COMMAND(cmd) \
  info_map[k##cmd] = CommandInfo(k##cmd##_HelpShort, k##cmd##_Help, &Run##cmd);

    INSERT_COMMAND(Analyze)
    INSERT_COMMAND(Args)
    INSERT_COMMAND(Check)
    INSERT_COMMAND(Clean)
    INSERT_COMMAND(Desc)
    INSERT_COMMAND(Gen)
    INSERT_COMMAND(Format)
    INSERT_COMMAND(Help)
    INSERT_COMMAND(Meta)
    INSERT_COMMAND(Ls)
    INSERT_COMMAND(Outputs)
    INSERT_COMMAND(Path)
    INSERT_COMMAND(Refs)
    INSERT_COMMAND(CleanStale);

#undef INSERT_COMMAND
  }
  return info_map;
}

const Target* ResolveTargetFromCommandLineString(
    Setup* setup,
    const std::string& label_string) {
  // Need to resolve the label after we know the default toolchain.
  Label default_toolchain = setup->loader()->default_toolchain_label();
  Value arg_value(nullptr, FixGitBashLabelEdit(label_string));
  Err err;
  Label label = Label::Resolve(
      SourceDirForCurrentDirectory(setup->build_settings().root_path()),
      setup->build_settings().root_path_utf8(), default_toolchain, arg_value,
      &err);
  if (err.has_error()) {
    err.PrintToStdout();
    return nullptr;
  }

  const Item* item = setup->builder().GetItem(label);
  if (!item) {
    Err(Location(), "Label not found.",
        label.GetUserVisibleName(false) + " not found.")
        .PrintToStdout();
    return nullptr;
  }

  const Target* target = item->AsTarget();
  if (!target) {
    Err(Location(), "Not a target.",
        "The \"" + label.GetUserVisibleName(false) +
            "\" thing\n"
            "is not a target. Somebody should probably implement this command "
            "for "
            "other\nitem types.")
        .PrintToStdout();
    return nullptr;
  }

  return target;
}

bool ResolveFromCommandLineInput(
    Setup* setup,
    const std::vector<std::string>& input,
    bool default_toolchain_only,
    UniqueVector<const Target*>* target_matches,
    UniqueVector<const Config*>* config_matches,
    UniqueVector<const Toolchain*>* toolchain_matches,
    UniqueVector<SourceFile>* file_matches) {
  if (input.empty()) {
    Err(Location(), "You need to specify a label, file, or pattern.")
        .PrintToStdout();
    return false;
  }

  SourceDir cur_dir =
      SourceDirForCurrentDirectory(setup->build_settings().root_path());
  for (const auto& cur : input) {
    if (!ResolveStringFromCommandLineInput(
            setup, cur_dir, cur, default_toolchain_only, target_matches,
            config_matches, toolchain_matches, file_matches))
      return false;
  }
  return true;
}

void FilterTargetsByPatterns(const std::vector<const Target*>& input,
                             const std::vector<LabelPattern>& filter,
                             std::vector<const Target*>* output) {
  for (auto* target : input) {
    for (const auto& pattern : filter) {
      if (pattern.Matches(target->label())) {
        output->push_back(target);
        break;
      }
    }
  }
}

void FilterTargetsByPatterns(const std::vector<const Target*>& input,
                             const std::vector<LabelPattern>& filter,
                             UniqueVector<const Target*>* output) {
  for (auto* target : input) {
    for (const auto& pattern : filter) {
      if (pattern.Matches(target->label())) {
        output->push_back(target);
        break;
      }
    }
  }
}

void FilterOutTargetsByPatterns(const std::vector<const Target*>& input,
                                const std::vector<LabelPattern>& filter,
                                std::vector<const Target*>* output) {
  for (auto* target : input) {
    bool match = false;
    for (const auto& pattern : filter) {
      if (pattern.Matches(target->label())) {
        match = true;
        break;
      }
    }
    if (!match) {
      output->push_back(target);
    }
  }
}

bool FilterPatternsFromString(const BuildSettings* build_settings,
                              const std::string& label_list_string,
                              std::vector<LabelPattern>* filters,
                              Err* err) {
  std::vector<std::string> tokens = base::SplitString(
      label_list_string, ";", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
  SourceDir root_dir("//");

  filters->reserve(tokens.size());
  for (const std::string& token : tokens) {
    LabelPattern pattern = LabelPattern::GetPattern(
        root_dir, build_settings->root_path_utf8(),
        Value(nullptr, FixGitBashLabelEdit(token)), err);
    if (err->has_error())
      return false;
    filters->push_back(pattern);
  }

  return true;
}

void FilterAndPrintTargets(std::vector<const Target*>* targets,
                           base::ListValue* out) {
  if (targets->empty())
    return;

  if (!ApplyTestonlyFilter(targets))
    return;
  if (!ApplyTypeFilter(targets))
    return;

  TargetPrintingMode printing_mode = TARGET_PRINT_LABEL;
  if (targets->empty() || !GetTargetPrintingMode(&printing_mode))
    return;
  switch (printing_mode) {
    case TARGET_PRINT_BUILDFILE:
      PrintTargetsAsBuildfiles(*targets, out);
      break;
    case TARGET_PRINT_LABEL:
      PrintTargetsAsLabels(*targets, out);
      break;
    case TARGET_PRINT_OUTPUT:
      PrintTargetsAsOutputs(*targets, out);
      break;
  }
}

void FilterAndPrintTargets(bool indent, std::vector<const Target*>* targets) {
  base::ListValue tmp;
  FilterAndPrintTargets(targets, &tmp);
  for (const auto& value : tmp) {
    std::string string;
    value.GetAsString(&string);
    if (indent)
      OutputString("  ");
    OutputString(string);
    OutputString("\n");
  }
}

void FilterAndPrintTargetSet(bool indent,
                             const std::set<const Target*>& targets) {
  std::vector<const Target*> target_vector(targets.begin(), targets.end());
  FilterAndPrintTargets(indent, &target_vector);
}

void FilterAndPrintTargetSet(const std::set<const Target*>& targets,
                             base::ListValue* out) {
  std::vector<const Target*> target_vector(targets.begin(), targets.end());
  FilterAndPrintTargets(&target_vector, out);
}

void GetTargetsContainingFile(Setup* setup,
                              const std::vector<const Target*>& all_targets,
                              const SourceFile& file,
                              bool default_toolchain_only,
                              std::vector<TargetContainingFile>* matches) {
  Label default_toolchain = setup->loader()->default_toolchain_label();
  for (auto* target : all_targets) {
    if (default_toolchain_only) {
      // Only check targets in the default toolchain.
      if (target->label().GetToolchainLabel() != default_toolchain)
        continue;
    }
    if (auto how = TargetContainsFile(target, file))
      matches->emplace_back(target, *how);
  }
}

}  // namespace commands
