//===- EditedSource.cpp - Collection of source edits ----------------------===//
//
// 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 "clang/Edit/EditedSource.h"
#include "clang/Basic/CharInfo.h"
#include "clang/Basic/LLVM.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Edit/Commit.h"
#include "clang/Edit/EditsReceiver.h"
#include "clang/Edit/FileOffset.h"
#include "clang/Lex/Lexer.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/Twine.h"
#include <cassert>
#include <tuple>
#include <utility>

using namespace clang;
using namespace edit;

void EditsReceiver::remove(CharSourceRange range) {
  replace(range, StringRef());
}

void EditedSource::deconstructMacroArgLoc(SourceLocation Loc,
                                          SourceLocation &ExpansionLoc,
                                          MacroArgUse &ArgUse) {
  assert(SourceMgr.isMacroArgExpansion(Loc));
  SourceLocation DefArgLoc =
      SourceMgr.getImmediateExpansionRange(Loc).getBegin();
  SourceLocation ImmediateExpansionLoc =
      SourceMgr.getImmediateExpansionRange(DefArgLoc).getBegin();
  ExpansionLoc = ImmediateExpansionLoc;
  while (SourceMgr.isMacroBodyExpansion(ExpansionLoc))
    ExpansionLoc =
        SourceMgr.getImmediateExpansionRange(ExpansionLoc).getBegin();
  SmallString<20> Buf;
  StringRef ArgName = Lexer::getSpelling(SourceMgr.getSpellingLoc(DefArgLoc),
                                         Buf, SourceMgr, LangOpts);
  ArgUse = MacroArgUse{nullptr, SourceLocation(), SourceLocation()};
  if (!ArgName.empty())
    ArgUse = {&IdentTable.get(ArgName), ImmediateExpansionLoc,
              SourceMgr.getSpellingLoc(DefArgLoc)};
}

void EditedSource::startingCommit() {}

void EditedSource::finishedCommit() {
  for (auto &ExpArg : CurrCommitMacroArgExps) {
    SourceLocation ExpLoc;
    MacroArgUse ArgUse;
    std::tie(ExpLoc, ArgUse) = ExpArg;
    auto &ArgUses = ExpansionToArgMap[ExpLoc];
    if (!llvm::is_contained(ArgUses, ArgUse))
      ArgUses.push_back(ArgUse);
  }
  CurrCommitMacroArgExps.clear();
}

StringRef EditedSource::copyString(const Twine &twine) {
  SmallString<128> Data;
  return copyString(twine.toStringRef(Data));
}

bool EditedSource::canInsertInOffset(SourceLocation OrigLoc, FileOffset Offs) {
  FileEditsTy::iterator FA = getActionForOffset(Offs);
  if (FA != FileEdits.end()) {
    if (FA->first != Offs)
      return false; // position has been removed.
  }

  if (SourceMgr.isMacroArgExpansion(OrigLoc)) {
    SourceLocation ExpLoc;
    MacroArgUse ArgUse;
    deconstructMacroArgLoc(OrigLoc, ExpLoc, ArgUse);
    auto I = ExpansionToArgMap.find(ExpLoc);
    if (I != ExpansionToArgMap.end() &&
        llvm::any_of(I->second, [&](const MacroArgUse &U) {
          return ArgUse.Identifier == U.Identifier &&
                 std::tie(ArgUse.ImmediateExpansionLoc, ArgUse.UseLoc) !=
                     std::tie(U.ImmediateExpansionLoc, U.UseLoc);
        })) {
      // Trying to write in a macro argument input that has already been
      // written by a previous commit for another expansion of the same macro
      // argument name. For example:
      //
      // \code
      //   #define MAC(x) ((x)+(x))
      //   MAC(a)
      // \endcode
      //
      // A commit modified the macro argument 'a' due to the first '(x)'
      // expansion inside the macro definition, and a subsequent commit tried
      // to modify 'a' again for the second '(x)' expansion. The edits of the
      // second commit will be rejected.
      return false;
    }
  }
  return true;
}

bool EditedSource::commitInsert(SourceLocation OrigLoc,
                                FileOffset Offs, StringRef text,
                                bool beforePreviousInsertions) {
  if (!canInsertInOffset(OrigLoc, Offs))
    return false;
  if (text.empty())
    return true;

  if (SourceMgr.isMacroArgExpansion(OrigLoc)) {
    MacroArgUse ArgUse;
    SourceLocation ExpLoc;
    deconstructMacroArgLoc(OrigLoc, ExpLoc, ArgUse);
    if (ArgUse.Identifier)
      CurrCommitMacroArgExps.emplace_back(ExpLoc, ArgUse);
  }

  FileEdit &FA = FileEdits[Offs];
  if (FA.Text.empty()) {
    FA.Text = copyString(text);
    return true;
  }

  if (beforePreviousInsertions)
    FA.Text = copyString(Twine(text) + FA.Text);
  else
    FA.Text = copyString(Twine(FA.Text) + text);

  return true;
}

bool EditedSource::commitInsertFromRange(SourceLocation OrigLoc,
                                   FileOffset Offs,
                                   FileOffset InsertFromRangeOffs, unsigned Len,
                                   bool beforePreviousInsertions) {
  if (Len == 0)
    return true;

  SmallString<128> StrVec;
  FileOffset BeginOffs = InsertFromRangeOffs;
  FileOffset EndOffs = BeginOffs.getWithOffset(Len);
  FileEditsTy::iterator I = FileEdits.upper_bound(BeginOffs);
  if (I != FileEdits.begin())
    --I;

  for (; I != FileEdits.end(); ++I) {
    FileEdit &FA = I->second;
    FileOffset B = I->first;
    FileOffset E = B.getWithOffset(FA.RemoveLen);

    if (BeginOffs == B)
      break;

    if (BeginOffs < E) {
      if (BeginOffs > B) {
        BeginOffs = E;
        ++I;
      }
      break;
    }
  }

  for (; I != FileEdits.end() && EndOffs > I->first; ++I) {
    FileEdit &FA = I->second;
    FileOffset B = I->first;
    FileOffset E = B.getWithOffset(FA.RemoveLen);

    if (BeginOffs < B) {
      bool Invalid = false;
      StringRef text = getSourceText(BeginOffs, B, Invalid);
      if (Invalid)
        return false;
      StrVec += text;
    }
    StrVec += FA.Text;
    BeginOffs = E;
  }

  if (BeginOffs < EndOffs) {
    bool Invalid = false;
    StringRef text = getSourceText(BeginOffs, EndOffs, Invalid);
    if (Invalid)
      return false;
    StrVec += text;
  }

  return commitInsert(OrigLoc, Offs, StrVec, beforePreviousInsertions);
}

void EditedSource::commitRemove(SourceLocation OrigLoc,
                                FileOffset BeginOffs, unsigned Len) {
  if (Len == 0)
    return;

  FileOffset EndOffs = BeginOffs.getWithOffset(Len);
  FileEditsTy::iterator I = FileEdits.upper_bound(BeginOffs);
  if (I != FileEdits.begin())
    --I;

  for (; I != FileEdits.end(); ++I) {
    FileEdit &FA = I->second;
    FileOffset B = I->first;
    FileOffset E = B.getWithOffset(FA.RemoveLen);

    if (BeginOffs < E)
      break;
  }

  FileOffset TopBegin, TopEnd;
  FileEdit *TopFA = nullptr;

  if (I == FileEdits.end()) {
    FileEditsTy::iterator
      NewI = FileEdits.insert(I, std::make_pair(BeginOffs, FileEdit()));
    NewI->second.RemoveLen = Len;
    return;
  }

  FileEdit &FA = I->second;
  FileOffset B = I->first;
  FileOffset E = B.getWithOffset(FA.RemoveLen);
  if (BeginOffs < B) {
    FileEditsTy::iterator
      NewI = FileEdits.insert(I, std::make_pair(BeginOffs, FileEdit()));
    TopBegin = BeginOffs;
    TopEnd = EndOffs;
    TopFA = &NewI->second;
    TopFA->RemoveLen = Len;
  } else {
    TopBegin = B;
    TopEnd = E;
    TopFA = &I->second;
    if (TopEnd >= EndOffs)
      return;
    unsigned diff = EndOffs.getOffset() - TopEnd.getOffset();
    TopEnd = EndOffs;
    TopFA->RemoveLen += diff;
    if (B == BeginOffs)
      TopFA->Text = StringRef();
    ++I;
  }

  while (I != FileEdits.end()) {
    FileEdit &FA = I->second;
    FileOffset B = I->first;
    FileOffset E = B.getWithOffset(FA.RemoveLen);

    if (B >= TopEnd)
      break;

    if (E <= TopEnd) {
      FileEdits.erase(I++);
      continue;
    }

    if (B < TopEnd) {
      unsigned diff = E.getOffset() - TopEnd.getOffset();
      TopEnd = E;
      TopFA->RemoveLen += diff;
      FileEdits.erase(I);
    }

    break;
  }
}

bool EditedSource::commit(const Commit &commit) {
  if (!commit.isCommitable())
    return false;

  struct CommitRAII {
    EditedSource &Editor;

    CommitRAII(EditedSource &Editor) : Editor(Editor) {
      Editor.startingCommit();
    }

    ~CommitRAII() {
      Editor.finishedCommit();
    }
  } CommitRAII(*this);

  for (edit::Commit::edit_iterator
         I = commit.edit_begin(), E = commit.edit_end(); I != E; ++I) {
    const edit::Commit::Edit &edit = *I;
    switch (edit.Kind) {
    case edit::Commit::Act_Insert:
      commitInsert(edit.OrigLoc, edit.Offset, edit.Text, edit.BeforePrev);
      break;
    case edit::Commit::Act_InsertFromRange:
      commitInsertFromRange(edit.OrigLoc, edit.Offset,
                            edit.InsertFromRangeOffs, edit.Length,
                            edit.BeforePrev);
      break;
    case edit::Commit::Act_Remove:
      commitRemove(edit.OrigLoc, edit.Offset, edit.Length);
      break;
    }
  }

  return true;
}

// Returns true if it is ok to make the two given characters adjacent.
static bool canBeJoined(char left, char right, const LangOptions &LangOpts) {
  // FIXME: Should use TokenConcatenation to make sure we don't allow stuff like
  // making two '<' adjacent.
  return !(Lexer::isAsciiIdentifierContinueChar(left, LangOpts) &&
           Lexer::isAsciiIdentifierContinueChar(right, LangOpts));
}

/// Returns true if it is ok to eliminate the trailing whitespace between
/// the given characters.
static bool canRemoveWhitespace(char left, char beforeWSpace, char right,
                                const LangOptions &LangOpts) {
  if (!canBeJoined(left, right, LangOpts))
    return false;
  if (isWhitespace(left) || isWhitespace(right))
    return true;
  if (canBeJoined(beforeWSpace, right, LangOpts))
    return false; // the whitespace was intentional, keep it.
  return true;
}

/// Check the range that we are going to remove and:
/// -Remove any trailing whitespace if possible.
/// -Insert a space if removing the range is going to mess up the source tokens.
static void adjustRemoval(const SourceManager &SM, const LangOptions &LangOpts,
                          SourceLocation Loc, FileOffset offs,
                          unsigned &len, StringRef &text) {
  assert(len && text.empty());
  SourceLocation BeginTokLoc = Lexer::GetBeginningOfToken(Loc, SM, LangOpts);
  if (BeginTokLoc != Loc)
    return; // the range is not at the beginning of a token, keep the range.

  bool Invalid = false;
  StringRef buffer = SM.getBufferData(offs.getFID(), &Invalid);
  if (Invalid)
    return;

  unsigned begin = offs.getOffset();
  unsigned end = begin + len;

  // Do not try to extend the removal if we're at the end of the buffer already.
  if (end == buffer.size())
    return;

  assert(begin < buffer.size() && end < buffer.size() && "Invalid range!");

  // FIXME: Remove newline.

  if (begin == 0) {
    if (buffer[end] == ' ')
      ++len;
    return;
  }

  if (buffer[end] == ' ') {
    assert((end + 1 != buffer.size() || buffer.data()[end + 1] == 0) &&
           "buffer not zero-terminated!");
    if (canRemoveWhitespace(/*left=*/buffer[begin-1],
                            /*beforeWSpace=*/buffer[end-1],
                            /*right=*/buffer.data()[end + 1], // zero-terminated
                            LangOpts))
      ++len;
    return;
  }

  if (!canBeJoined(buffer[begin-1], buffer[end], LangOpts))
    text = " ";
}

static void applyRewrite(EditsReceiver &receiver,
                         StringRef text, FileOffset offs, unsigned len,
                         const SourceManager &SM, const LangOptions &LangOpts,
                         bool shouldAdjustRemovals) {
  assert(offs.getFID().isValid());
  SourceLocation Loc = SM.getLocForStartOfFile(offs.getFID());
  Loc = Loc.getLocWithOffset(offs.getOffset());
  assert(Loc.isFileID());

  if (text.empty() && shouldAdjustRemovals)
    adjustRemoval(SM, LangOpts, Loc, offs, len, text);

  CharSourceRange range = CharSourceRange::getCharRange(Loc,
                                                     Loc.getLocWithOffset(len));

  if (text.empty()) {
    assert(len);
    receiver.remove(range);
    return;
  }

  if (len)
    receiver.replace(range, text);
  else
    receiver.insert(Loc, text);
}

void EditedSource::applyRewrites(EditsReceiver &receiver,
                                 bool shouldAdjustRemovals) {
  SmallString<128> StrVec;
  FileOffset CurOffs, CurEnd;
  unsigned CurLen;

  if (FileEdits.empty())
    return;

  FileEditsTy::iterator I = FileEdits.begin();
  CurOffs = I->first;
  StrVec = I->second.Text;
  CurLen = I->second.RemoveLen;
  CurEnd = CurOffs.getWithOffset(CurLen);
  ++I;

  for (FileEditsTy::iterator E = FileEdits.end(); I != E; ++I) {
    FileOffset offs = I->first;
    FileEdit act = I->second;
    assert(offs >= CurEnd);

    if (offs == CurEnd) {
      StrVec += act.Text;
      CurLen += act.RemoveLen;
      CurEnd.getWithOffset(act.RemoveLen);
      continue;
    }

    applyRewrite(receiver, StrVec, CurOffs, CurLen, SourceMgr, LangOpts,
                 shouldAdjustRemovals);
    CurOffs = offs;
    StrVec = act.Text;
    CurLen = act.RemoveLen;
    CurEnd = CurOffs.getWithOffset(CurLen);
  }

  applyRewrite(receiver, StrVec, CurOffs, CurLen, SourceMgr, LangOpts,
               shouldAdjustRemovals);
}

void EditedSource::clearRewrites() {
  FileEdits.clear();
  StrAlloc.Reset();
}

StringRef EditedSource::getSourceText(FileOffset BeginOffs, FileOffset EndOffs,
                                      bool &Invalid) {
  assert(BeginOffs.getFID() == EndOffs.getFID());
  assert(BeginOffs <= EndOffs);
  SourceLocation BLoc = SourceMgr.getLocForStartOfFile(BeginOffs.getFID());
  BLoc = BLoc.getLocWithOffset(BeginOffs.getOffset());
  assert(BLoc.isFileID());
  SourceLocation
    ELoc = BLoc.getLocWithOffset(EndOffs.getOffset() - BeginOffs.getOffset());
  return Lexer::getSourceText(CharSourceRange::getCharRange(BLoc, ELoc),
                              SourceMgr, LangOpts, &Invalid);
}

EditedSource::FileEditsTy::iterator
EditedSource::getActionForOffset(FileOffset Offs) {
  FileEditsTy::iterator I = FileEdits.upper_bound(Offs);
  if (I == FileEdits.begin())
    return FileEdits.end();
  --I;
  FileEdit &FA = I->second;
  FileOffset B = I->first;
  FileOffset E = B.getWithOffset(FA.RemoveLen);
  if (Offs >= B && Offs < E)
    return I;

  return FileEdits.end();
}
