//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
///
/// \file
/// This file provides utilities to put braces around a statement.
///
//===----------------------------------------------------------------------===//

#include "BracesAroundStatement.h"
#include "../utils/LexerUtils.h"
#include "LexerUtils.h"
#include "clang/AST/ASTContext.h"
#include "clang/Basic/CharInfo.h"
#include "clang/Basic/LangOptions.h"
#include "clang/Lex/Lexer.h"

namespace clang::tidy::utils {

BraceInsertionHints::operator bool() const { return DiagnosticPos.isValid(); }

bool BraceInsertionHints::offersFixIts() const {
  return OpeningBracePos.isValid() && ClosingBracePos.isValid();
}

unsigned BraceInsertionHints::resultingCompoundLineExtent(
    const SourceManager &SourceMgr) const {
  return SourceMgr.getSpellingLineNumber(ClosingBracePos) -
         SourceMgr.getSpellingLineNumber(OpeningBracePos);
}

FixItHint BraceInsertionHints::openingBraceFixIt() const {
  return OpeningBracePos.isValid()
             ? FixItHint::CreateInsertion(OpeningBracePos, " {")
             : FixItHint();
}

FixItHint BraceInsertionHints::closingBraceFixIt() const {
  return ClosingBracePos.isValid()
             ? FixItHint::CreateInsertion(ClosingBracePos, ClosingBrace)
             : FixItHint();
}

static tok::TokenKind getTokenKind(SourceLocation Loc, const SourceManager &SM,
                                   const LangOptions &LangOpts) {
  Token Tok;
  const SourceLocation Beginning =
      Lexer::GetBeginningOfToken(Loc, SM, LangOpts);
  const bool Invalid = Lexer::getRawToken(Beginning, Tok, SM, LangOpts);
  assert(!Invalid && "Expected a valid token.");

  if (Invalid)
    return tok::NUM_TOKENS;

  return Tok.getKind();
}

static SourceLocation findEndLocation(const Stmt &S, const SourceManager &SM,
                                      const LangOptions &LangOpts) {
  SourceLocation Loc = lexer::getUnifiedEndLoc(S, SM, LangOpts);
  if (!Loc.isValid())
    return Loc;

  // Start searching right after S.
  Loc = Loc.getLocWithOffset(1);

  for (;;) {
    assert(Loc.isValid());
    while (isHorizontalWhitespace(*SM.getCharacterData(Loc)))
      Loc = Loc.getLocWithOffset(1);

    if (isVerticalWhitespace(*SM.getCharacterData(Loc))) {
      // EOL, insert brace before.
      break;
    }
    const tok::TokenKind TokKind = getTokenKind(Loc, SM, LangOpts);
    if (TokKind != tok::comment) {
      // Non-comment token, insert brace before.
      break;
    }

    const SourceLocation TokEndLoc =
        Lexer::getLocForEndOfToken(Loc, 0, SM, LangOpts);
    const SourceRange TokRange(Loc, TokEndLoc);
    const StringRef Comment = Lexer::getSourceText(
        CharSourceRange::getTokenRange(TokRange), SM, LangOpts);
    if (Comment.starts_with("/*") && Comment.contains('\n')) {
      // Multi-line block comment, insert brace before.
      break;
    }
    // else: Trailing comment, insert brace after the newline.

    // Fast-forward current token.
    Loc = TokEndLoc;
  }
  return Loc;
}

BraceInsertionHints getBraceInsertionsHints(const Stmt *const S,
                                            const LangOptions &LangOpts,
                                            const SourceManager &SM,
                                            SourceLocation StartLoc,
                                            SourceLocation EndLocHint) {
  // 1) If there's a corresponding "else" or "while", the check inserts "} "
  // right before that token.
  // 2) If there's a multi-line block comment starting on the same line after
  // the location we're inserting the closing brace at, or there's a non-comment
  // token, the check inserts "\n}" right before that token.
  // 3) Otherwise the check finds the end of line (possibly after some block or
  // line comments) and inserts "\n}" right before that EOL.
  if (!S || isa<CompoundStmt>(S)) {
    // Already inside braces.
    return {};
  }

  // When TreeTransform, Stmt in constexpr IfStmt will be transform to NullStmt.
  // This NullStmt can be detected according to beginning token.
  const SourceLocation StmtBeginLoc = S->getBeginLoc();
  if (isa<NullStmt>(S) && StmtBeginLoc.isValid() &&
      getTokenKind(StmtBeginLoc, SM, LangOpts) == tok::l_brace)
    return {};

  if (StartLoc.isInvalid())
    return {};

  const Stmt *InnerS = S;
  while (const auto *AS = dyn_cast<AttributedStmt>(InnerS))
    InnerS = AS->getSubStmt();

  SourceLocation InsertLoc = StartLoc;
  if (S != InnerS) {
    if (std::optional<Token> Tok = utils::lexer::getPreviousToken(
            InnerS->getBeginLoc(), SM, LangOpts, /*SkipComments=*/true)) {
      InsertLoc = Tok->getLocation();
    }
  }

  // Convert StartLoc to file location, if it's on the same macro expansion
  // level as the start of the statement. We also need file locations for
  // Lexer::getLocForEndOfToken working properly.
  StartLoc = Lexer::makeFileCharRange(CharSourceRange::getCharRange(
                                          InsertLoc, InnerS->getBeginLoc()),
                                      SM, LangOpts)
                 .getBegin();
  if (StartLoc.isInvalid())
    return {};
  StartLoc = Lexer::getLocForEndOfToken(StartLoc, 0, SM, LangOpts);

  // StartLoc points at the location of the opening brace to be inserted.
  SourceLocation EndLoc;
  StringRef ClosingInsertion;
  if (EndLocHint.isValid()) {
    EndLoc = EndLocHint;
    ClosingInsertion = "} ";
  } else {
    EndLoc = findEndLocation(*S, SM, LangOpts);
    ClosingInsertion = "\n}";
  }

  assert(StartLoc.isValid());

  // Change only if StartLoc and EndLoc are on the same macro expansion level.
  // This will also catch invalid EndLoc.
  // Example: LLVM_DEBUG( for(...) do_something() );
  // In this case fix-it cannot be provided as the semicolon which is not
  // visible here is part of the macro. Adding braces here would require adding
  // another semicolon.
  if (Lexer::makeFileCharRange(
          CharSourceRange::getTokenRange(SourceRange(
              SM.getSpellingLoc(StartLoc), SM.getSpellingLoc(EndLoc))),
          SM, LangOpts)
          .isInvalid())
    return {StartLoc};
  return {StartLoc, EndLoc, ClosingInsertion};
}

} // namespace clang::tidy::utils
