// Copyright 2026 The GN 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/build_file_editor.h"

#include <algorithm>

#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "gn/build_settings.h"
#include "gn/command_format.h"
#include "gn/edit_subcommands.h"
#include "gn/filesystem_utils.h"
#include "gn/input_file.h"
#include "gn/label.h"
#include "gn/loader.h"
#include "gn/parse_tree.h"
#include "gn/parser.h"
#include "gn/scope.h"
#include "gn/string_atom.h"
#include "gn/tokenizer.h"
#include "gn/value.h"

namespace {

std::optional<std::string> AsStringLiteral(const ParseNode* node) {
  auto* literal = node->AsLiteral();
  if (!literal || literal->value().type() != Token::STRING) {
    return std::nullopt;
  }
  Scope scope(static_cast<const Settings*>(nullptr));
  Err err;
  Value v = literal->Execute(&scope, &err);
  // Because we provide an empty scope, "${b}" will result in an error.
  if (err.has_error() || v.type() != Value::STRING) {
    return std::nullopt;
  }
  return std::move(v.string_value());
}

// Resolves a single LabelPattern to matching SourceFiles.
Result<std::vector<SourceFile>> ResolvePatternToFiles(
    const BuildSettings* build_settings,
    const Loader* loader,
    const LabelPattern& pattern) {
  std::vector<SourceFile> matched_files;
  auto add_dir = [&](const SourceDir& dir) {
    auto build_file = loader->BuildFileForLabel(Label(dir, "dummy"));
    if (base::PathExists(build_settings->GetFullPath(build_file))) {
      matched_files.push_back(build_file);
    }
  };

  add_dir(pattern.dir());

  if (pattern.type() == LabelPattern::MATCH ||
      pattern.type() == LabelPattern::DIRECTORY) {
    if (matched_files.empty()) {
      return Err(
          Location(),
          "Build file does not exist: " +
              loader->BuildFileForLabel(Label(pattern.dir(), "dummy")).value());
    }
    return matched_files;
  }

  base::FilePath disk_path = build_settings->GetFullPath(pattern.dir());
  if (!base::DirectoryExists(disk_path)) {
    return Err(Location(),
               "Directory does not exist: " + pattern.dir().value());
  }

  base::FileEnumerator traverser(disk_path, /*recursive=*/true,
                                 base::FileEnumerator::DIRECTORIES);
  for (base::FilePath current = traverser.Next(); !current.empty();
       current = traverser.Next()) {
    base::FilePath relative;
    if (build_settings->root_path().AppendRelativePath(current, &relative)) {
      std::string source_path = "//" + FilePathToUTF8(relative) + "/";
      NormalizePath(&source_path);
      add_dir(SourceDir(source_path));
    }
  }

  return matched_files;
}

}  // namespace

bool TreeNode::is_conditional() const {
  DCHECK(!stack_.empty()) << "stack should never be empty";
  for (auto it = stack_.rbegin() + 1; it != stack_.rend(); ++it) {
    if ((*it)->AsCondition()) {
      return true;
    }
    // Stop at the target boundary.
    if ((*it)->AsFunctionCall()) {
      break;
    }
  }
  return false;
}

bool TreeNode::is_modification() const {
  if (const auto* op = node()->AsBinaryOp()) {
    return op->op().type() == Token::PLUS_EQUALS ||
           op->op().type() == Token::MINUS_EQUALS;
  }
  return false;
}

void TreeNode::add_todo(EditState& state, const EditTarget& target) const {
  const std::vector<std::string> lines = {
      "# TODO(gn edit: " + state.context + "):",
      "# This would normally be deleted but is conditional.",
      "# Manual intervention is required to decide whether it should "
      "actually be deleted.",
  };
  for (const auto& line : lines) {
    StringAtom atom(line);
    Token comment_token(node()->GetRange().begin(), Token::LINE_COMMENT,
                        atom.str());
    node()->comments_mutable()->append_before(std::move(comment_token));
  }
  state.needs_manual_review.insert(target.label);
}

void TreeNode::RemoveSelf(EditState& state, const EditTarget& target) const {
  if (is_conditional()) {
    add_todo(state, target);
  } else {
    RemoveSelf();
  }
}

void TreeNode::RemoveSelf() const {
  DCHECK(parent());
  if (auto* block = parent()->AsBlockMut()) {
    auto& stmts = block->statements();
    for (auto it = stmts.begin(); it != stmts.end(); ++it) {
      if (it->get() == node()) {
        stmts.erase(it);
        return;
      }
    }
  } else if (auto* list = parent()->AsListMut()) {
    auto& items = list->contents();
    for (auto it = items.begin(); it != items.end(); ++it) {
      if (it->get() == node()) {
        items.erase(it);
        return;
      }
    }
  } else {
    CHECK(false) << "Unsupported type to remove from";
  }
  CHECK(false) << "child node not found in parent's children";
}

LabelMatcher::LabelMatcher(SourceDir source_dir,
                           const std::vector<LabelPattern>& patterns)
    : source_dir_(std::move(source_dir)), globbed_(false) {
  for (const auto& pattern : patterns) {
    if (pattern.type() == LabelPattern::RECURSIVE_DIRECTORY &&
        source_dir_.value().starts_with(pattern.dir().value())) {
      globbed_ = true;
    } else if (pattern.type() == LabelPattern::DIRECTORY &&
               source_dir_ == pattern.dir()) {
      globbed_ = true;
    } else if (pattern.type() == LabelPattern::MATCH &&
               pattern.dir() == source_dir_) {
      used_[pattern.name()] = false;
    }
  }
}

LabelMatcher::MatchType LabelMatcher::matches(const std::string& name) {
  if (auto it = used_.find(name); it != used_.end()) {
    it->second = true;  // Mark as used.
    return EXACT;
  }
  return globbed_ ? GLOB : NONE;
}

Err LabelMatcher::done() const {
  std::vector<std::string> unused;
  for (const auto& [name, used] : used_) {
    if (!used) {
      unused.push_back(name);
    }
  }
  if (!unused.empty()) {
    std::sort(unused.begin(), unused.end());
    std::string msg = "Target(s) not found: ";
    for (size_t i = 0; i < unused.size(); ++i) {
      if (i > 0)
        msg += ", ";
      msg += Label(source_dir_, unused[i]).GetUserVisibleName(false);
    }
    return Err(Location(), msg);
  }
  return Ok();
}

std::vector<TreeNode> EditTarget::assignments(std::string_view attr) const {
  return FindStatement<TreeNode>(
      block, [attr](TreeNode& node_ref) -> std::optional<TreeNode> {
        if (const auto* op = node_ref->AsBinaryOp()) {
          if (op->op().type() == Token::EQUAL ||
              op->op().type() == Token::PLUS_EQUALS ||
              op->op().type() == Token::MINUS_EQUALS) {
            if (const auto* left = op->left()->AsIdentifier()) {
              if (left->value().value() == attr) {
                return node_ref;
              }
            }
          }
        }
        return std::nullopt;
      });
}

void EditTarget::add_warning(EditState& state, std::string_view message) const {
  std::string full_message = "Target \"" + label.GetUserVisibleName(false) +
                             "\" " + std::string(message);
  state.warnings.push_back(Err(node.node()->GetRange().begin(), full_message));
}

Result<BuildFile> BuildFile::Create(const BuildSettings* build_settings,
                                    const SourceFile& source_file,
                                    const std::vector<LabelPattern>& patterns) {
  auto input_file = std::make_unique<InputFile>(source_file);
  base::FilePath full_path = build_settings->GetFullPath(source_file);
  if (!input_file->Load(full_path)) {
    return Err(Location(), "Could not load file: " + source_file.value());
  }

  Err err;
  std::vector<Token> tokens = Tokenizer::Tokenize(input_file.get(), &err);
  RETURN_IF_ERROR(err);

  std::unique_ptr<ParseNode> tree_root = Parser::Parse(tokens, &err);
  RETURN_IF_ERROR(err);

  LabelMatcher label_matcher(source_file.GetDir(), patterns);
  return BuildFile(build_settings, source_file, std::move(input_file),
                   std::move(tree_root), std::move(label_matcher));
}

Location BuildFile::location() const {
  return Location(input_file_.get(), 1, 1);
}

std::vector<EditTarget> BuildFile::targets() {
  return FindStatement<EditTarget>(
      tree_root_.get(),
      [this](TreeNode& node_ref) -> std::optional<EditTarget> {
        if (auto* func = node_ref->AsFunctionCallMut()) {
          if (func->block() && func->args() &&
              func->args()->contents().size() == 1) {
            if (auto name =
                    AsStringLiteral(func->args()->contents()[0].get())) {
              auto match_type = label_matcher_.matches(*name);
              if (match_type != LabelMatcher::NONE) {
                return EditTarget{
                    .is_explicit = match_type == LabelMatcher::EXACT,
                    .label = Label(source_file_.GetDir(), *name),
                    .node = node_ref,
                    .block = func->block(),
                };
              }
            }
          }
        }
        return std::nullopt;
      });
}

std::unique_ptr<ParseNode> BuildFile::to_node(const Value& value) {
  auto file = std::make_unique<InputFile>(SourceFile("//dummy"));
  file->SetContents(value.ToString(true));

  Err err;
  std::vector<Token> tokens = Tokenizer::Tokenize(file.get(), &err);
  for (auto& token : tokens) {
    token.set_location(this->location());
  }
  auto parsed = Parser::ParseExpression(tokens, &err);
  extra_files_.push_back(std::move(file));
  // value.ToString() must return something parsable as input to GN.
  DCHECK(!err.has_error());
  return parsed;
}

std::unique_ptr<IdentifierNode> BuildFile::create_identifier(
    std::string_view value) {
  StringAtom atom(value);
  return std::make_unique<IdentifierNode>(
      Token(location(), Token::IDENTIFIER, atom.str()));
}

std::unique_ptr<BinaryOpNode> BuildFile::create_assignment(
    std::string_view name,
    std::unique_ptr<ParseNode> value) {
  auto left = create_identifier(name);

  auto assign = std::make_unique<BinaryOpNode>();
  assign->set_op(Token(location(), Token::EQUAL, "="));
  assign->set_left(std::move(left));
  assign->set_right(std::move(value));

  return assign;
}

Result<bool> BuildFile::Write() {
  ASSIGN_OR_RETURN(std::string formatted, commands::FormatNodeToString(root()));
  if (input_file_->contents() == formatted) {
    return false;
  }
  base::FilePath file_path = build_settings_->GetFullPath(source_file());
  if (base::WriteFile(file_path, formatted.data(),
                      static_cast<int>(formatted.size())) == -1) {
    return Err(Location(),
               "Failed to write to file: " + FilePathToUTF8(file_path));
  }
  return true;
}

BuildFile::BuildFile(const BuildSettings* build_settings,
                     SourceFile source_file,
                     std::unique_ptr<InputFile> input_file,
                     std::unique_ptr<ParseNode> tree_root,
                     LabelMatcher label_matcher)
    : build_settings_(build_settings),
      source_file_(std::move(source_file)),
      input_file_(std::move(input_file)),
      tree_root_(std::move(tree_root)),
      label_matcher_(std::move(label_matcher)) {}

Result<std::vector<BuildFile>> ResolvePatternsToBuildFiles(
    const BuildSettings* build_settings,
    const Loader* loader,
    const std::vector<LabelPattern>& patterns) {
  std::set<SourceFile> seen;
  std::vector<BuildFile> result;

  for (const LabelPattern& pattern : patterns) {
    ASSIGN_OR_RETURN(auto files,
                     ResolvePatternToFiles(build_settings, loader, pattern));

    for (const SourceFile& file : files) {
      if (seen.insert(file).second) {
        ASSIGN_OR_RETURN(auto parsed,
                         BuildFile::Create(build_settings, file, patterns));
        result.push_back(std::move(parsed));
      }
    }
  }
  return result;
}
