//===--- RemoveUsingNamespace.cpp --------------------------------*- C++-*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "AST.h"
#include "FindTarget.h"
#include "Selection.h"
#include "SourceCode.h"
#include "refactor/Tweak.h"
#include "clang/AST/Decl.h"
#include "clang/AST/DeclBase.h"
#include "clang/AST/DeclCXX.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Tooling/Core/Replacement.h"
#include "clang/Tooling/Refactoring/RecursiveSymbolVisitor.h"
#include "llvm/ADT/ScopeExit.h"

namespace clang {
namespace clangd {
namespace {
/// Removes the 'using namespace' under the cursor and qualifies all accesses in
/// the current file. E.g.,
///   using namespace std;
///   vector<int> foo(std::map<int, int>);
/// Would become:
///   std::vector<int> foo(std::map<int, int>);
/// Currently limited to using namespace directives inside global namespace to
/// simplify implementation. Also the namespace must not contain using
/// directives.
class RemoveUsingNamespace : public Tweak {
public:
  const char *id() const override;

  bool prepare(const Selection &Inputs) override;
  Expected<Effect> apply(const Selection &Inputs) override;
  std::string title() const override;
  Intent intent() const override { return Refactor; }

private:
  const UsingDirectiveDecl *TargetDirective = nullptr;
};
REGISTER_TWEAK(RemoveUsingNamespace)

class FindSameUsings : public RecursiveASTVisitor<FindSameUsings> {
public:
  FindSameUsings(const UsingDirectiveDecl &Target,
                 std::vector<const UsingDirectiveDecl *> &Results)
      : TargetNS(Target.getNominatedNamespace()),
        TargetCtx(Target.getDeclContext()), Results(Results) {}

  bool VisitUsingDirectiveDecl(UsingDirectiveDecl *D) {
    if (D->getNominatedNamespace() != TargetNS ||
        D->getDeclContext() != TargetCtx)
      return true;
    Results.push_back(D);
    return true;
  }

private:
  const NamespaceDecl *TargetNS;
  const DeclContext *TargetCtx;
  std::vector<const UsingDirectiveDecl *> &Results;
};

/// Produce edit removing 'using namespace xxx::yyy' and the trailing semicolon.
llvm::Expected<tooling::Replacement>
removeUsingDirective(ASTContext &Ctx, const UsingDirectiveDecl *D) {
  auto &SM = Ctx.getSourceManager();
  llvm::Optional<Token> NextTok =
      Lexer::findNextToken(D->getEndLoc(), SM, Ctx.getLangOpts());
  if (!NextTok || NextTok->isNot(tok::semi))
    return llvm::createStringError(llvm::inconvertibleErrorCode(),
                                   "no semicolon after using-directive");
  // FIXME: removing the semicolon may be invalid in some obscure cases, e.g.
  //        if (x) using namespace std; else using namespace bar;
  return tooling::Replacement(
      SM,
      CharSourceRange::getTokenRange(D->getBeginLoc(), NextTok->getLocation()),
      "", Ctx.getLangOpts());
}

// Returns true iff the parent of the Node is a TUDecl.
bool isTopLevelDecl(const SelectionTree::Node *Node) {
  return Node->Parent && Node->Parent->ASTNode.get<TranslationUnitDecl>();
}

// Returns the first visible context that contains this DeclContext.
// For example: Returns ns1 for S1 and a.
// namespace ns1 {
// inline namespace ns2 { struct S1 {}; }
// enum E { a, b, c, d };
// }
const DeclContext *visibleContext(const DeclContext *D) {
  while (D->isInlineNamespace() || D->isTransparentContext())
    D = D->getParent();
  return D;
}

bool RemoveUsingNamespace::prepare(const Selection &Inputs) {
  // Find the 'using namespace' directive under the cursor.
  auto *CA = Inputs.ASTSelection.commonAncestor();
  if (!CA)
    return false;
  TargetDirective = CA->ASTNode.get<UsingDirectiveDecl>();
  if (!TargetDirective)
    return false;
  if (!dyn_cast<Decl>(TargetDirective->getDeclContext()))
    return false;
  // FIXME: Unavailable for namespaces containing using-namespace decl.
  // It is non-trivial to deal with cases where identifiers come from the inner
  // namespace. For example map has to be changed to aa::map.
  // namespace aa {
  //   namespace bb { struct map {}; }
  //   using namespace bb;
  // }
  // using namespace a^a;
  // int main() { map m; }
  // We need to make this aware of the transitive using-namespace decls.
  if (!TargetDirective->getNominatedNamespace()->using_directives().empty())
    return false;
  return isTopLevelDecl(CA);
}

Expected<Tweak::Effect> RemoveUsingNamespace::apply(const Selection &Inputs) {
  auto &Ctx = Inputs.AST->getASTContext();
  auto &SM = Ctx.getSourceManager();
  // First, collect *all* using namespace directives that redeclare the same
  // namespace.
  std::vector<const UsingDirectiveDecl *> AllDirectives;
  FindSameUsings(*TargetDirective, AllDirectives).TraverseAST(Ctx);

  SourceLocation FirstUsingDirectiveLoc;
  for (auto *D : AllDirectives) {
    if (FirstUsingDirectiveLoc.isInvalid() ||
        SM.isBeforeInTranslationUnit(D->getBeginLoc(), FirstUsingDirectiveLoc))
      FirstUsingDirectiveLoc = D->getBeginLoc();
  }

  // Collect all references to symbols from the namespace for which we're
  // removing the directive.
  std::vector<SourceLocation> IdentsToQualify;
  for (auto &D : Inputs.AST->getLocalTopLevelDecls()) {
    findExplicitReferences(D, [&](ReferenceLoc Ref) {
      if (Ref.Qualifier)
        return; // This reference is already qualified.

      for (auto *T : Ref.Targets) {
        if (!visibleContext(T->getDeclContext())
                 ->Equals(TargetDirective->getNominatedNamespace()))
          return;
      }
      SourceLocation Loc = Ref.NameLoc;
      if (Loc.isMacroID()) {
        // Avoid adding qualifiers before macro expansions, it's probably
        // incorrect, e.g.
        //   namespace std { int foo(); }
        //   #define FOO 1 + foo()
        //   using namespace foo; // provides matrix
        //   auto x = FOO; // Must not changed to auto x = std::FOO
        if (!SM.isMacroArgExpansion(Loc))
          return; // FIXME: report a warning to the users.
        Loc = SM.getFileLoc(Ref.NameLoc);
      }
      assert(Loc.isFileID());
      if (SM.getFileID(Loc) != SM.getMainFileID())
        return; // FIXME: report these to the user as warnings?
      if (SM.isBeforeInTranslationUnit(Loc, FirstUsingDirectiveLoc))
        return; // Directive was not visible before this point.
      IdentsToQualify.push_back(Loc);
    });
  }
  // Remove duplicates.
  llvm::sort(IdentsToQualify);
  IdentsToQualify.erase(
      std::unique(IdentsToQualify.begin(), IdentsToQualify.end()),
      IdentsToQualify.end());

  // Produce replacements to remove the using directives.
  tooling::Replacements R;
  for (auto *D : AllDirectives) {
    auto RemoveUsing = removeUsingDirective(Ctx, D);
    if (!RemoveUsing)
      return RemoveUsing.takeError();
    if (auto Err = R.add(*RemoveUsing))
      return std::move(Err);
  }
  // Produce replacements to add the qualifiers.
  std::string Qualifier = printUsingNamespaceName(Ctx, *TargetDirective) + "::";
  for (auto Loc : IdentsToQualify) {
    if (auto Err = R.add(tooling::Replacement(Ctx.getSourceManager(), Loc,
                                              /*Length=*/0, Qualifier)))
      return std::move(Err);
  }
  return Effect::mainFileEdit(SM, std::move(R));
}

std::string RemoveUsingNamespace::title() const {
  return std::string(
      llvm::formatv("Remove using namespace, re-qualify names instead."));
}
} // namespace
} // namespace clangd
} // namespace clang
