/// \file Outliner.cpp

//
// This file is distributed under the MIT License. See LICENSE.md for details.
//

#include "llvm/ADT/SCCIterator.h"
#include "llvm/IR/GlobalValue.h"
#include "llvm/Support/Casting.h"
#include "llvm/Support/ScopedPrinter.h"
#include "llvm/Transforms/Utils/BasicBlockUtils.h"
#include "llvm/Transforms/Utils/Cloning.h"
#include "llvm/Transforms/Utils/CodeExtractor.h"
#include "llvm/Transforms/Utils/ValueMapper.h"

#include "revng/ADT/Queue.h"
#include "revng/EarlyFunctionAnalysis/CallGraph.h"
#include "revng/EarlyFunctionAnalysis/CallHandler.h"
#include "revng/EarlyFunctionAnalysis/Outliner.h"
#include "revng/Model/IRHelpers.h"
#include "revng/Support/IRHelpers.h"
#include "revng/Support/MetaAddress.h"
#include "revng/Support/OpaqueRegisterUser.h"

using namespace llvm;

namespace efa {

class OutlinedFunctionsMap {
private:
  Module *M = nullptr;
  std::map<MetaAddress, UniqueValuePtr<Function>> Map;
  std::set<MetaAddress> Banned;

public:
  OutlinedFunctionsMap(Module *M) : M(M) {}
  ~OutlinedFunctionsMap() { clear(); }

public:
  auto begin() { return Map.begin(); }
  auto end() { return Map.end(); }

public:
  void clear() { Map.clear(); }

public:
  Function *get(MetaAddress Entry) {
    using namespace llvm;

    auto It = Map.find(Entry);
    if (It == Map.end()) {
      LLVMContext &Context = M->getContext();
      auto *FT = FunctionType::get(Type::getVoidTy(Context), {}, false);
      Function *New = Function::Create(FT,
                                       llvm::GlobalObject::ExternalLinkage,
                                       0,
                                       {},
                                       M);
      Map.insert(It, { Entry, UniqueValuePtr<Function>(New) });
      return New;
    } else {
      return It->second.get();
    }
  }

  void set(MetaAddress Entry, Function *F) { Map[Entry].reset(F); }

public:
  bool isBanned(MetaAddress BB) const { return Banned.contains(BB); }

  bool banRecursiveFunctions();
};

bool OutlinedFunctionsMap::banRecursiveFunctions() {
  using namespace llvm;
  bool Result = false;

  CallGraph CallGraph;
  std::map<Function *, BasicBlockNode *> ReverseMap;

  BasicBlockNode EntryNode(MetaAddress::invalid());
  BasicBlockNode *Entry = CallGraph.addNode(EntryNode);
  CallGraph.setEntryNode(Entry);

  // Populate graph and map
  for (auto &[Block, Function] : Map) {
    // We don't care about the address
    BasicBlockNode Node(MetaAddress::invalid());
    auto *NewNode = CallGraph.addNode(Node);
    ReverseMap[Function.get()] = NewNode;
    Entry->addSuccessor(NewNode);
  }

  // Add edges
  for (auto &[_, Function] : Map) {
    BasicBlockNode *Caller = ReverseMap.at(Function.get());
    for (Instruction &I : instructions(Function.get())) {
      if (auto *Call = dyn_cast<CallInst>(&I)) {
        auto It = ReverseMap.find(getCalledFunction(Call));
        if (It != ReverseMap.end()) {
          Caller->addSuccessor(It->second);
        }
      }
    }
  }

  // Ban everyone part of an SCC
  auto It = scc_begin(&CallGraph);
  auto End = scc_end(&CallGraph);
  for (; It != End; ++It) {
    if (not It.hasCycle())
      continue;

    auto &SCCNodes = *It;
    Result = Result or (SCCNodes.size() > 0);
    for (auto &Node : SCCNodes)
      Banned.insert(Node->Address);
  }

  return Result;
}

void Outliner::integrateFunctionCallee(CallHandler *TheCallHandler,
                                       MetaAddress CallerFunction,
                                       llvm::BasicBlock *CallerBlock,
                                       llvm::CallInst *FunctionCall,
                                       llvm::CallInst *JumpToSymbol,
                                       MetaAddress Callee,
                                       OutlinedFunctionsMap &FunctionsMap) {
  llvm::LLVMContext &Context = M.getContext();

  auto [Summary, IsTailCall] = getCallSiteInfo(CallerFunction,
                                               CallerBlock,
                                               FunctionCall,
                                               JumpToSymbol,
                                               Callee);

  using namespace llvm;
  using llvm::BasicBlock;

  BasicBlock *BB = FunctionCall != nullptr ? FunctionCall->getParent() :
                                             JumpToSymbol->getParent();
  MetaAddress CallerBlockAddress = getBasicBlockAddress(getJumpTargetBlock(BB));
  revng_assert(CallerBlockAddress.isValid());

  bool TargetIsSymbol = JumpToSymbol != nullptr;
  Value *SymbolNamePointer = nullptr;
  if (TargetIsSymbol) {
    SymbolNamePointer = JumpToSymbol->getArgOperand(0);
  } else {
    using CPN = ConstantPointerNull;
    Type *I8Ptr = Type::getInt8PtrTy(Context);
    SymbolNamePointer = CPN::get(Type::getInt8PtrTy(Context));
  }

  if (FunctionCall == nullptr)
    IsTailCall = true;

  // Ignore any indirect tail call (i.e., not direct and not targeting a dynamic
  // symbol)
  if (IsTailCall and Callee.isInvalid() and SymbolNamePointer == nullptr)
    return;

  // The function call is replaced with 1) hooks that delimit the space of the
  // ABI analyses' traversals and 2) a summary of the registers clobbered by
  // that function.

  // What is the function type of the callee?
  using namespace model::FunctionAttribute;

  // Inline if 1) marked as inline, 2) not the caller function itself and 3)
  // not banned from inlining
  bool IsInline = false;
  bool IsCallToUnexpected = false;
  if (FunctionCall != nullptr) {
    auto *T = FunctionCall->getParent()->getTerminator();
    BasicBlock *CalleeBlock = getFunctionCallCallee(T);

    bool IsMarkedInline = Summary->Attributes.contains(Inline);
    bool IsCallingTheCaller = CallerFunction == Callee;
    bool IsBanned = FunctionsMap.isBanned(Callee);
    IsInline = IsMarkedInline and not IsCallingTheCaller and not IsBanned;
  }

  bool IsNoReturn = Summary->Attributes.contains(NoReturn);

  if (IsInline) {
    // Emit a call to the function to inline: a later step will inline this call
    revng_assert(Callee.isValid());
    auto *CI = CallInst::Create(FunctionsMap.get(Callee),
                                "",
                                BB->getTerminator());
  } else {
    Instruction *Term = BB->getTerminator();
    IRBuilder<> Builder(Term);

    if (FunctionCall != nullptr) {
      // This is a proper call, storing the return address somewhere: inject a
      // store clobbering the return address. This will make the instruction
      // saving the return address dead, the optimizer will collect it and then
      // we'll drop this store too, resulting in an IR free of return addresses
      // being written around.
      Value *LinkRegister = FunctionCall->getArgOperand(3);
      Value *Pointer = nullptr;
      Type *PointeeType = nullptr;

      // Are we using the link register or storing the return address on the top
      // of the stack?
      if (auto *CSV = dyn_cast_or_null<GlobalVariable>(LinkRegister)) {
        Pointer = CSV;
        PointeeType = CSV->getValueType();
      } else {
        PointeeType = GCBI.spReg()->getValueType();
        Pointer = Builder.CreateIntToPtr(createLoad(Builder, GCBI.spReg()),
                                         PointeeType->getPointerTo());
      }

      // Inject the store
      std::string Name = ("opaque_"
                          + to_string(PointeeType->getIntegerBitWidth()));
      auto *OpaqueReturnAddressFunction = OpaqueReturnAddress.get(PointeeType,
                                                                  PointeeType,
                                                                  {},
                                                                  Name);
      Builder.CreateStore(Builder.CreateCall(OpaqueReturnAddressFunction),
                          Pointer);
    }

    TheCallHandler->handleCall(CallerBlockAddress,
                               Builder,
                               Callee,
                               Summary->ClobberedRegisters,
                               Summary->ElectedFSO,
                               IsNoReturn,
                               IsTailCall,
                               SymbolNamePointer);

    if (IsNoReturn) {
      Term->eraseFromParent();
      Builder.SetInsertPoint(BB);
      TheCallHandler->handlePostNoReturn(Builder);
    }
  }

  // Erase markers
  if (FunctionCall != nullptr)
    eraseFromParent(FunctionCall);

  if (JumpToSymbol != nullptr)
    eraseFromParent(JumpToSymbol);
}

OutlinedFunction
Outliner::outlineFunctionInternal(CallHandler *TheCallHandler,
                                  const MetaAddress &FunctionAddress,
                                  OutlinedFunctionsMap &FunctionsToInline) {
  using namespace llvm;
  using llvm::BasicBlock;

  OutlinedFunction OutlinedFunction;
  OutlinedFunction.Address = FunctionAddress;
  OnceQueue<BasicBlock *> Queue;
  std::vector<BasicBlock *> BlocksToClone;

  auto *AnyPCBB = GCBI.anyPC();
  auto *UnexpectedPCBB = GCBI.unexpectedPC();
  Function *Root = AnyPCBB->getParent();

  BasicBlock *EntryBlock = GCBI.getBlockAt(FunctionAddress);
  bool HasEntryBlock = EntryBlock != nullptr;

  if (HasEntryBlock)
    Queue.insert(EntryBlock);

  Queue.insert(AnyPCBB);
  Queue.insert(UnexpectedPCBB);

  //
  // Collect list of blocks to clone
  //
  while (not Queue.empty()) {
    BasicBlock *Current = Queue.pop();
    BlocksToClone.emplace_back(Current);

    if (auto *FunctionCall = getMarker(Current, "function_call")) {
      // Compute callee
      MetaAddress PCCallee = MetaAddress::invalid();
      if (auto *Next = getFunctionCallCallee(Current))
        PCCallee = getBasicBlockAddress(Next);

      CallInst *JumpToSymbol = getMarker(Current, "jump_to_symbol");
      auto [Summary, IsTailCall] = getCallSiteInfo(FunctionAddress,
                                                   FunctionCall->getParent(),
                                                   FunctionCall,
                                                   JumpToSymbol,
                                                   PCCallee);

      // Unless it's NoReturn, enqueue the call fallthrough
      using namespace model::FunctionAttribute;
      bool IsNoReturn = Summary->Attributes.contains(NoReturn);
      if (not IsNoReturn and not IsTailCall) {
        Queue.insert(getFallthrough(Current));
      }
    } else {
      for (auto *Successor : successors(Current)) {
        if (not isPartOfRootDispatcher(Successor))
          Queue.insert(Successor);
      }
    }
  }

  //
  // Create a copy of all the basic blocks to outline in `root`
  //
  llvm::ValueToValueMapTy VMap;
  SmallVector<BasicBlock *, 8> BlocksToExtract;

  for (const auto &BB : BlocksToClone) {
    BasicBlock *Cloned = CloneBasicBlock(BB, VMap, Twine("_cloned"), Root);

    VMap[BB] = Cloned;
    BlocksToExtract.emplace_back(Cloned);
  }

  OutlinedFunction.AnyPCCloned = cast<BasicBlock>(VMap[AnyPCBB]);
  OutlinedFunction.UnexpectedPCCloned = cast<BasicBlock>(VMap[UnexpectedPCBB]);

  //
  // Update references in cloned basic blocks
  //
  remapInstructionsInBlocks(BlocksToExtract, VMap);

  //
  // Switch successor of function calls from callee to fallthrough
  //
  std::map<llvm::CallInst *, MetaAddress> CallToCallee;
  for (const auto &BB : BlocksToExtract) {
    if (CallInst *FunctionCall = getMarker(BB, "function_call")) {
      auto *Term = BB->getTerminator();

      // If the function callee is null, we are dealing with an indirect call
      MetaAddress PCCallee = MetaAddress::invalid();
      if (BasicBlock *Next = getFunctionCallCallee(Term))
        PCCallee = getBasicBlockAddress(Next);

      CallToCallee[FunctionCall] = PCCallee;

      using namespace model::FunctionAttribute;

      CallInst *JumpToSymbol = getMarker(BB, "jump_to_symbol");
      auto [CalleeSummary, IsTailCall] = getCallSiteInfo(FunctionAddress,
                                                         BB,
                                                         FunctionCall,
                                                         JumpToSymbol,
                                                         PCCallee);

      bool IsNoReturn = CalleeSummary->Attributes.contains(NoReturn);
      if (IsNoReturn) {
        auto *BB = Term->getParent();
        Term->eraseFromParent();
        IRBuilder<> Builder(BB);
        Builder.CreateUnreachable();

        // Ensure markers are still close to the terminator
        if (JumpToSymbol != nullptr)
          JumpToSymbol->moveBefore(BB->getTerminator());
        FunctionCall->moveBefore(BB->getTerminator());
      } else if (IsTailCall) {
        revng_assert(not cast<BranchInst>(Term)->isConditional());
        auto *Br = BranchInst::Create(cast<BasicBlock>(VMap[AnyPCBB]));
        ReplaceInstWithInst(Term, Br);
      } else {
        auto *Br = BranchInst::Create(getFallthrough(Term));
        ReplaceInstWithInst(Term, Br);
      }

      // To allow correct function extraction, there must not exist users of
      // BBs to be extracted, so we destroy the blockaddress of the
      // fall-through BB in the `function_call` marker.
      unsigned ArgNo = 0;
      PointerType *I8PtrTy = Type::getInt8PtrTy(M.getContext());
      Constant *I8NullPtr = ConstantPointerNull::get(I8PtrTy);
      for (Value *Arg : FunctionCall->args()) {
        if (isa<BlockAddress>(Arg)) {
          FunctionCall->setArgOperand(ArgNo, I8NullPtr);
          if (Arg->use_empty())
            cast<BlockAddress>(Arg)->destroyConstant();
        }
        ++ArgNo;
      }
    }
  }

  if (not HasEntryBlock) {
    auto *EntryBlock = BasicBlock::Create(Root->getContext(), "", Root);
    Function *AbortFunction = notNull(Root->getParent()->getFunction("_abort"));
    IRBuilder<> Builder(EntryBlock);
    emitCall(Builder,
             AbortFunction,
             "This starts at non-executable address",
             DebugLoc());
    BlocksToExtract.insert(BlocksToExtract.begin(), EntryBlock);
  }

  // Extract outlined function
  llvm::Function *F = CodeExtractor(BlocksToExtract).extractCodeRegion(CEAC);
  F->addFnAttr(Attribute::NullPointerIsValid);
  OutlinedFunction.Function = UniqueValuePtr<llvm::Function>(F);

  revng_assert(OutlinedFunction.Function != nullptr);
  revng_assert(OutlinedFunction.Function->arg_size() == 0);
  revng_assert(OutlinedFunction.Function->getReturnType()->isVoidTy());
  revng_assert(OutlinedFunction.Function->hasOneUser());

  // Remove the only user (call to the outlined function) in `root`
  auto It = OutlinedFunction.Function->user_begin();
  cast<Instruction>(*It)->getParent()->eraseFromParent();

  // Integrate function callee
  for (auto &BB : *OutlinedFunction.Function) {
    MetaAddress Callee;
    CallInst *FunctionCall = getMarker(&BB, "function_call");
    if (FunctionCall != nullptr)
      Callee = CallToCallee.at(FunctionCall);
    CallInst *JumpToSymbol = getMarker(&BB, "jump_to_symbol");
    // TODO: we don't integrate the call if it's a tail call (JumpToSymbol but
    //       no FunctionCall). Is this OK?
    if (FunctionCall != nullptr) {
      integrateFunctionCallee(TheCallHandler,
                              FunctionAddress,
                              &BB,
                              FunctionCall,
                              JumpToSymbol,
                              Callee,
                              FunctionsToInline);
    }
  }

  return OutlinedFunction;
}

void Outliner::createAnyPCHooks(CallHandler *TheCallHandler,
                                OutlinedFunction *OutlinedFunction) {
  using namespace llvm;
  LLVMContext &Context = M.getContext();

  SmallVector<Instruction *, 4> IndirectBranchPredecessors;
  if (OutlinedFunction->AnyPCCloned) {
    for (auto *Pred : predecessors(OutlinedFunction->AnyPCCloned)) {
      auto *Term = Pred->getTerminator();
      IndirectBranchPredecessors.emplace_back(Term);
    }
  }

  // Initialize ret-hook marker (needed for the ABI analyses on return values)
  // and fix pre-hook marker upon encountering a jump to `anypc`. Since we don't
  // know in advance whether it will be classified as a return or indirect tail
  // call, RegisterUsageAnalyses need to run on this potential call-site as
  // well. The results will not be merged eventually, if the indirect jump turns
  // out to be a proper return.
  for (auto *Term : IndirectBranchPredecessors) {
    auto *BB = Term->getParent();
    auto *JumpTargetBB = getJumpTargetBlock(BB);
    MetaAddress IndirectRetBBAddress = getBasicBlockAddress(JumpTargetBB);
    revng_assert(IndirectRetBBAddress.isValid());

    auto *Split = BB->splitBasicBlock(Term, BB->getName() + Twine("_anypc"));

    auto *JumpToAnyPC = Split->getTerminator();
    revng_assert(isa<BranchInst>(JumpToAnyPC));
    IRBuilder<> Builder(JumpToAnyPC);
    Builder.SetInsertPoint(JumpToAnyPC);

    using CPN = ConstantPointerNull;
    Value *SymbolName = CPN::get(Type::getInt8PtrTy(Context));
    CallInst *JumpToSymbol = getMarker(BB, "jump_to_symbol");

    auto [Summary, _] = getCallSiteInfo(OutlinedFunction->Address,
                                        BB,
                                        nullptr,
                                        JumpToSymbol,
                                        MetaAddress::invalid());

    if (JumpToSymbol != nullptr) {
      SymbolName = JumpToSymbol->getArgOperand(0);
      eraseFromParent(JumpToSymbol);
    }

    TheCallHandler->handleIndirectJump(Builder,
                                       IndirectRetBBAddress,
                                       Summary->ClobberedRegisters,
                                       SymbolName);
  }
}

class FixFunctionPreInlining {
private:
  unsigned InliningIndex = 0;
  std::set<std::pair<MetaAddress, llvm::Function *>> ToRestore;
  std::vector<MetaAddress> &InlinedFunctionsByIndex;

public:
  FixFunctionPreInlining(std::vector<MetaAddress> &InlinedFunctionsByIndex) :
    InlinedFunctionsByIndex(InlinedFunctionsByIndex) {}
  ~FixFunctionPreInlining() { revng_assert(ToRestore.size() == 0); }

public:
  void fix(const MetaAddress &CalleeAddress, llvm::CallBase *Caller) {
    Function *Callee = getCalledFunction(Caller);
    revng_assert(Callee != nullptr);
    setNewPCInlineIndex(CalleeAddress, Callee, ++InliningIndex);
    InlinedFunctionsByIndex.push_back(CalleeAddress);
    ToRestore.emplace(CalleeAddress, Callee);
  }

  void restore() {
    for (const auto &[Address, F] : ToRestore)
      setNewPCInlineIndex(Address, F, 0);
    ToRestore.clear();
  }

private:
  static void setNewPCInlineIndex(const MetaAddress &FunctionAddress,
                                  llvm::Function *F,
                                  unsigned InliningIndex) {
    revng_assert(FunctionAddress.isValid());

    for (llvm::Instruction &I : llvm::instructions(F)) {
      if (auto *NewPCCall = getCallTo(&I, "newpc")) {
        using namespace NewPCArguments;
        Value *Argument = NewPCCall->getArgOperand(InstructionID);
        auto OldID = BasicBlockID::fromValue(Argument);
        BasicBlockID NewID(OldID.start(), InliningIndex);
        NewPCCall->setArgOperand(InstructionID, NewID.toValue(F->getParent()));
      }
    }
  }
};

OutlinedFunction Outliner::outline(const MetaAddress &Entry,
                                   CallHandler *Handler) {
  using namespace llvm;

  OutlinedFunction Result;
  OutlinedFunctionsMap FunctionsToInline(&M);

  unsigned Attempts = 0;
  do {
    ++Attempts;
    revng_assert(Attempts < 3);

    // Restart isolating functions
    FunctionsToInline.clear();

    // Outline functions but do not perform inlining
    Result = outlineFunctionInternal(Handler, Entry, FunctionsToInline);

    //
    // Fixed point creation of function that needs to be inlined
    //
    bool SomethingNew = true;
    while (SomethingNew) {
      SomethingNew = false;
      for (const auto &[Address, F] : FunctionsToInline) {
        if (F->isDeclaration()) {
          Function *ToInline = createFunctionToInline(Handler,
                                                      Address,
                                                      FunctionsToInline);
          F->replaceAllUsesWith(ToInline);
          FunctionsToInline.set(Address, ToInline);
          SomethingNew = true;
        }
      }
    }

  } while (FunctionsToInline.banRecursiveFunctions());

  //
  // Fixed point inlining
  //
  bool SomethingNew = true;
  FixFunctionPreInlining FunctionFixer(Result.InlinedFunctionsByIndex);
  while (SomethingNew) {
    SomethingNew = false;
    for (auto &[Address, F] : FunctionsToInline) {
      SmallVector<CallBase *> ToInline;
      for (CallBase *Caller : callers(F.get()))
        ToInline.push_back(Caller);

      for (CallBase *Caller : ToInline) {
        FunctionFixer.fix(Address, Caller);
        InlineFunctionInfo IFI;
        bool Success = InlineFunction(*Caller, IFI).isSuccess();
        revng_assert(Success);
        SomethingNew = true;
      }
    }
  }

  FunctionFixer.restore();

  // Fix `unexpectedpc` of the callees to inline
  for (auto &I : instructions(Result.Function.get())) {
    if (CallInst *Call = getCallTo(&I, UnexpectedPCMarker.get())) {
      revng_assert(Result.UnexpectedPCCloned != nullptr);

      auto *Br = BranchInst::Create(Result.UnexpectedPCCloned);
      ReplaceInstWithInst(I.getParent()->getTerminator(), Br);
      Call->eraseFromParent();
      break;
    }
  }

  createAnyPCHooks(Handler, &Result);

  return Result;
}

llvm::Function *
Outliner::createFunctionToInline(CallHandler *TheCallHandler,
                                 const MetaAddress &Entry,
                                 OutlinedFunctionsMap &FunctionsToInline) {
  using namespace llvm;
  LLVMContext &Context = M.getContext();

  // Recreate outlined function
  OutlinedFunction OF = outlineFunctionInternal(TheCallHandler,
                                                Entry,
                                                FunctionsToInline);

  // Adjust `anypc` and `unexpectedpc` BBs of the function to inline
  revng_assert(OF.AnyPCCloned != nullptr);

  // Functions to inline must have one and only one broken return
  revng_assert(OF.AnyPCCloned->hasNPredecessors(1));

  // Replace the broken return with a `ret`
  auto *Br = OF.AnyPCCloned->getUniquePredecessor()->getTerminator();
  auto *Ret = ReturnInst::Create(Context);
  ReplaceInstWithInst(Br, Ret);

  if (OF.UnexpectedPCCloned != nullptr) {
    CallInst::Create(UnexpectedPCMarker.get(),
                     "",
                     OF.UnexpectedPCCloned->getTerminator());
  }

  return OF.releaseFunction();
}

std::pair<const FunctionSummary *, bool>
Outliner::getCallSiteInfo(MetaAddress CallerFunction,
                          llvm::BasicBlock *CallerBlock,
                          llvm::CallInst *FunctionCall,
                          llvm::CallInst *JumpToSymbol,
                          MetaAddress Callee) {
  using namespace llvm;
  using llvm::BasicBlock;

  Instruction *Term = CallerBlock->getTerminator();

  // Extract MetaAddress of JT of the call-site
  BasicBlockID
    CallSiteAddress = getBasicBlockID(getJumpTargetBlock(CallerBlock));
  revng_assert(CallSiteAddress.isValid());

  StringRef CalledSymbol;
  if (JumpToSymbol != nullptr) {
    // The target is a symbol
    CalledSymbol = extractFromConstantStringPtr(JumpToSymbol->getArgOperand(0));
  }

  return Oracle.getCallSite(CallerFunction,
                            CallSiteAddress,
                            Callee,
                            CalledSymbol);
}

} // namespace efa
