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

#include "base/command_line.h"
#include "tools/gn/builder.h"
#include "tools/gn/filesystem_utils.h"
#include "tools/gn/item.h"
#include "tools/gn/label.h"
#include "tools/gn/label_pattern.h"
#include "tools/gn/setup.h"
#include "tools/gn/standard_out.h"
#include "tools/gn/target.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 all_toolchains is false, 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 all_toolchains,
    std::vector<const Target*>* matches) {
  Value pattern_value(nullptr, label_pattern);

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

  if (!all_toolchains) {
    // 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 all_toolchains,
    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, all_toolchains,
                                              &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->loader()->default_toolchain_label(),
                               Value(nullptr, input), &err);
  if (err.has_error()) {
    err.PrintToStdout();
    return false;
  }

  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(
        input, setup->build_settings().root_path_utf8()));
  }

  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 == "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 swap to output.
  std::vector<const Target*> result;
  result.reserve(targets->size());

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

  targets->swap(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 swap to 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->swap(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(bool indent,
                              const std::vector<const Target*>& targets) {
  // 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) {
    if (indent)
      OutputString("  ");
    OutputString(file + "\n");
  }
}

void PrintTargetsAsLabels(bool indent,
                          const std::vector<const Target*>& targets) {
  // Putting the labels into a set automatically sorts them for us.
  std::set<Label> unique_labels;
  for (const 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.
    if (indent)
      OutputString("  ");
    OutputString(label.GetUserVisibleName(
        label.GetToolchainLabel() != default_tc_label));
    OutputString("\n");
  }
}

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

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

  SourceDir current_dir = SourceDirForCurrentDirectory(
      build_settings->root_path());
  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(), current_dir,
                                    build_settings->root_path_utf8());
    if (indent)
      OutputString("  ");
    OutputString(result);
    OutputString("\n");
  }
}

}  // 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(Args)
    INSERT_COMMAND(Check)
    INSERT_COMMAND(Clean)
    INSERT_COMMAND(Desc)
    INSERT_COMMAND(Gen)
    INSERT_COMMAND(Format)
    INSERT_COMMAND(Help)
    INSERT_COMMAND(Ls)
    INSERT_COMMAND(Refs)

    #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, label_string);
  Err err;
  Label label = Label::Resolve(SourceDirForCurrentDirectory(
                                   setup->build_settings().root_path()),
                               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.");
    return nullptr;
  }

  return target;
}

bool ResolveFromCommandLineInput(
    Setup* setup,
    const std::vector<std::string>& input,
    bool all_toolchains,
    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,
                                           all_toolchains, 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 (const 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 (const auto& target : input) {
    for (const auto& pattern : filter) {
      if (pattern.Matches(target->label())) {
        output->push_back(target);
        break;
      }
    }
  }
}

void FilterAndPrintTargets(bool indent, std::vector<const Target*>* targets) {
  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(indent, *targets);
      break;
    case TARGET_PRINT_LABEL:
      PrintTargetsAsLabels(indent, *targets);
      break;
    case TARGET_PRINT_OUTPUT:
      PrintTargetsAsOutputs(indent, *targets);
      break;
  }
}

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

}  // namespace commands