//===-- asmstmt.cpp -------------------------------------------------------===//
//
//                         LDC – the LLVM D compiler
//
// This file originates from work by David Friedman for GDC released under
// the GPL 2 and Artistic licenses. See the LICENSE file for details.
//
//===----------------------------------------------------------------------===//

#include "dmd/declaration.h"
#include "dmd/dsymbol.h"
#include "dmd/errors.h"
#include "dmd/ldcbindings.h"
#include "dmd/scope.h"
#include "dmd/statement.h"
#include "gen/dvalue.h"
#include "gen/functions.h"
#include "gen/irstate.h"
#include "gen/llvm.h"
#include "gen/llvmhelpers.h"
#include "gen/logger.h"
#include "gen/tollvm.h"
#include "ir/irfunction.h"
#include "llvm/IR/InlineAsm.h"
#include <cassert>
#include <cstring>
#include <deque>
#include <string>
#include <sstream>

typedef enum {
  Arg_Integer,
  Arg_Pointer,
  Arg_Memory,
  Arg_FrameRelative,
  Arg_LocalSize,
  Arg_Dollar
} AsmArgType;

typedef enum { Mode_Input, Mode_Output, Mode_Update } AsmArgMode;

struct AsmArg {
  Expression *expr;
  AsmArgType type;
  AsmArgMode mode;
  AsmArg(AsmArgType type, Expression *expr, AsmArgMode mode) {
    this->type = type;
    this->expr = expr;
    this->mode = mode;
  }
};

struct AsmCode {
  std::string insnTemplate;
  std::vector<AsmArg> args;
  std::vector<bool> regs;
  unsigned dollarLabel;
  int clobbersMemory;
  explicit AsmCode(int n_regs) {
    regs.resize(n_regs, false);
    dollarLabel = 0;
    clobbersMemory = 0;
  }
};

struct AsmParserCommon {
  virtual ~AsmParserCommon() = default;
  virtual void run(Scope *sc, InlineAsmStatement *asmst) = 0;
  virtual std::string getRegName(int i) = 0;
};
AsmParserCommon *asmparser = nullptr;

#include "asm-x86.h" // x86 assembly parser
#define ASM_X86_64
#include "asm-x86.h" // x86_64 assembly parser
#undef ASM_X86_64

/**
 * Replaces <<func>> with the name of the currently codegen'd function.
 *
 * This kludge is required to handle labels correctly, as the instruction
 * strings for jumps, … are generated during semantic3, but attribute inference
 * might change the function type (and hence the mangled name) right at the end
 * of semantic3.
 */
static void replace_func_name(IRState *p, std::string &insnt) {
  static const std::string needle("<<func>>");

  const char *mangle = mangleExact(p->func()->decl);

  size_t pos;
  while (std::string::npos != (pos = insnt.find(needle))) {
    // This will only happen for few instructions, and only once for those.
    insnt.replace(pos, needle.size(), mangle);
  }
}

Statement *gccAsmSemantic(GccAsmStatement *s, Scope *sc);

Statement *asmSemantic(AsmStatement *s, Scope *sc) {
  if (!s->tokens) {
    return nullptr;
  }

  sc->func->hasReturnExp |= 8;

  // GCC-style asm starts with a string literal or a `(`
  if (s->tokens->value == TOK::string_ ||
      s->tokens->value == TOK::leftParenthesis) {
    auto gas = createGccAsmStatement(s->loc, s->tokens);
    return gccAsmSemantic(gas, sc);
  }

  // this is DMD-style asm
  sc->func->hasReturnExp |= 32;

  auto ias = createInlineAsmStatement(s->loc, s->tokens);
  s = ias;

  bool err = false;
  llvm::Triple const &t = *global.params.targetTriple;
  if (!(t.getArch() == llvm::Triple::x86 ||
        t.getArch() == llvm::Triple::x86_64)) {
    s->error(
        "DMD-style `asm { op; }` statements are not supported for the \"%s\" "
        "architecture.",
        t.getArchName().str().c_str());
    errorSupplemental(s->loc, "Use GDC-style `asm { \"op\" : …; }` syntax or "
                              "`ldc.llvmasm.__asm` instead.");
    err = true;
  }
  if (!global.params.useInlineAsm) {
    s->error(
        "the `asm` statement is not allowed when the -noasm switch is used");
    err = true;
  }
  if (err) {
    if (!global.gag) {
      fatal();
    }
    return s;
  }

  // puts(toChars());

  if (!asmparser) {
    if (t.getArch() == llvm::Triple::x86) {
      asmparser = new AsmParserx8632::AsmParser;
    } else if (t.getArch() == llvm::Triple::x86_64) {
      asmparser = new AsmParserx8664::AsmParser;
    }
  }

  asmparser->run(sc, ias);

  return s;
}

void AsmStatement_toIR(InlineAsmStatement *stmt, IRState *irs) {
  IF_LOG Logger::println("InlineAsmStatement::toIR(): %s", stmt->loc.toChars());
  LOG_SCOPE;

  // sanity check
  assert((irs->func()->decl->hasReturnExp & 40) == 40);

  // get asm block
  IRAsmBlock *asmblock = irs->asmBlock;
  assert(asmblock);

  // debug info
  gIR->DBuilder.EmitStopPoint(stmt->loc);

  if (!stmt->asmcode) {
    return;
  }

  static std::string i_cns = "i";
  static std::string p_cns = "i";
  static std::string m_cns = "*m";
  static std::string mw_cns = "=*m";
  static std::string mrw_cns = "+*m";
  static std::string memory_name = "memory";

  AsmCode *code = static_cast<AsmCode *>(stmt->asmcode);
  std::vector<LLValue *> input_values;
  std::vector<std::string> input_constraints;
  std::vector<LLValue *> output_values;
  std::vector<std::string> output_constraints;
  std::vector<std::string> clobbers;

  // FIXME
  //#define HOST_WIDE_INT long
  // HOST_WIDE_INT var_frame_offset; // "frame_offset" is a macro
  bool clobbers_mem = code->clobbersMemory;
  int input_idx = 0;
  int n_outputs = 0;
  int arg_map[10];

  assert(code->args.size() <= 10);

  auto arg = code->args.begin();
  for (unsigned i = 0; i < code->args.size(); i++, ++arg) {
    bool is_input = true;
    LLValue *arg_val = nullptr;
    std::string cns;

    switch (arg->type) {
    case Arg_Integer:
      arg_val = DtoRVal(arg->expr);
    do_integer:
      cns = i_cns;
      break;
    case Arg_Pointer:
      assert(arg->expr->isVarExp());
      arg_val = DtoRVal(arg->expr);
      cns = p_cns;

      break;
    case Arg_Memory:
      arg_val = DtoRVal(arg->expr);

      switch (arg->mode) {
      case Mode_Input:
        cns = m_cns;
        break;
      case Mode_Output:
        cns = mw_cns;
        is_input = false;
        break;
      case Mode_Update:
        cns = mrw_cns;
        is_input = false;
        break;
      }
      break;
    case Arg_FrameRelative:
      // FIXME
      llvm_unreachable("Arg_FrameRelative not supported.");
    /*          if (auto ve = arg->expr->isVarExp())
                    arg_val = ve->var->toSymbol()->Stree;
                else
                    assert(0);
                if ( getFrameRelativeValue(arg_val, & var_frame_offset) ) {
    //              arg_val = irs->integerConstant(var_frame_offset);
                    cns = i_cns;
                } else {
                    this->error("%s", "argument not frame relative");
                    return;
                }
                if (arg->mode != Mode_Input)
                    clobbers_mem = true;
                break;*/
    case Arg_LocalSize:
      // FIXME
      llvm_unreachable("Arg_LocalSize not supported.");
      /*          var_frame_offset = cfun->x_frame_offset;
                  if (var_frame_offset < 0)
                      var_frame_offset = - var_frame_offset;
                  arg_val = irs->integerConstant( var_frame_offset );*/
      goto do_integer;
    default:
      llvm_unreachable("Unknown inline asm reference type.");
    }

    if (is_input) {
      arg_map[i] = --input_idx;
      input_values.push_back(arg_val);
      input_constraints.push_back(cns);
    } else {
      arg_map[i] = n_outputs++;
      output_values.push_back(arg_val);
      output_constraints.push_back(cns);
    }
  }

  // Telling GCC that callee-saved registers are clobbered makes it preserve
  // those registers.   This changes the stack from what a naked function
  // expects.

  // FIXME
  //    if (!irs->func->isNaked()) {
  assert(asmparser);
  for (size_t i = 0; i < code->regs.size(); i++) {
    if (code->regs[i]) {
      clobbers.push_back(asmparser->getRegName(i));
    }
  }
  if (clobbers_mem) {
    clobbers.push_back(memory_name);
  }
  //    }

  // Remap argument numbers
  for (unsigned i = 0; i < code->args.size(); i++) {
    if (arg_map[i] < 0) {
      arg_map[i] = -arg_map[i] - 1 + n_outputs;
    }
  }

  bool pct = false;
  auto p = code->insnTemplate.begin();
  auto q = code->insnTemplate.end();
  // printf("start: %.*s\n", code->insnTemplateLen, code->insnTemplate);
  while (p < q) {
    if (pct) {
      if (*p >= '0' && *p <= '9') {
        // %% doesn't check against nargs
        *p = '0' + arg_map[*p - '0'];
        pct = false;
      } else if (*p == '$') {
        pct = false;
      }
      // assert(*p == '%');// could be 'a', etc. so forget it..
    } else if (*p == '$') {
      pct = true;
    }
    ++p;
  }

  IF_LOG {
    Logger::cout() << "final asm: " << code->insnTemplate << '\n';
    std::ostringstream ss;

    ss << "GCC-style output constraints: {";
    for (const auto &oc : output_constraints) {
      ss << " " << oc;
    }
    ss << " }";
    Logger::println("%s", ss.str().c_str());

    ss.str("");
    ss << "GCC-style input constraints: {";
    for (const auto &ic : input_constraints) {
      ss << " " << ic;
    }
    ss << " }";
    Logger::println("%s", ss.str().c_str());

    ss.str("");
    ss << "GCC-style clobbers: {";
    for (const auto &c : clobbers) {
      ss << " " << c;
    }
    ss << " }";
    Logger::println("%s", ss.str().c_str());
  }

  // rewrite GCC-style constraints to LLVM-style constraints
  std::string llvmOutConstraints;
  std::string llvmInConstraints;
  int n = 0;
  for (auto &oc : output_constraints) {
    // rewrite update constraint to in and out constraints
    if (oc[0] == '+') {
      assert(oc == mrw_cns && "What else are we updating except memory?");
      /* LLVM doesn't support updating operands, so split into an input
       * and an output operand.
       */

      // Change update operand to pure output operand.
      oc = mw_cns;

      // Add input operand with same value, with original as "matching
      // output".
      std::ostringstream ss;
      ss << '*' << (n + asmblock->outputcount);
      // Must be at the back; unused operands before used ones screw up
      // numbering.
      input_constraints.push_back(ss.str());
      input_values.push_back(output_values[n]);
    }
    llvmOutConstraints += oc;
    llvmOutConstraints += ",";
    n++;
  }
  asmblock->outputcount += n;

  for (const auto &ic : input_constraints) {
    llvmInConstraints += ic;
    llvmInConstraints += ",";
  }

  std::string clobstr;
  for (const auto &c : clobbers) {
    clobstr = "~{" + c + "},";
    asmblock->clobs.insert(clobstr);
  }

  IF_LOG {
    {
      Logger::println("Output values:");
      LOG_SCOPE
      size_t i = 0;
      for (auto ov : output_values) {
        Logger::cout() << "Out " << i++ << " = " << *ov << '\n';
      }
    }
    {
      Logger::println("Input values:");
      LOG_SCOPE
      size_t i = 0;
      for (auto iv : input_values) {
        Logger::cout() << "In  " << i++ << " = " << *iv << '\n';
      }
    }
  }

  // excessive commas are removed later...

  replace_func_name(irs, code->insnTemplate);

  // push asm statement
  auto asmStmt = new IRAsmStmt;
  asmStmt->code = code->insnTemplate;
  asmStmt->out_c = llvmOutConstraints;
  asmStmt->in_c = llvmInConstraints;
  asmStmt->out.insert(asmStmt->out.begin(), output_values.begin(),
                      output_values.end());
  asmStmt->in.insert(asmStmt->in.begin(), input_values.begin(),
                     input_values.end());
  asmStmt->isBranchToLabel = stmt->isBranchToLabel;
  asmblock->s.push_back(asmStmt);
}

//////////////////////////////////////////////////////////////////////////////

// rewrite argument indices to the block scope indices
static void remap_outargs(std::string &insnt, size_t nargs, size_t idx) {
  static const std::string digits[10] = {"0", "1", "2", "3", "4",
                                         "5", "6", "7", "8", "9"};
  assert(nargs <= 10);

  static const std::string prefix("<<out");
  static const std::string suffix(">>");
  std::string argnum;
  std::string needle;
  char buf[10];
  for (unsigned i = 0; i < nargs; i++) {
    needle = prefix + digits[i] + suffix;
    size_t pos = insnt.find(needle);
    if (std::string::npos != pos) {
      sprintf(buf, "%llu", static_cast<unsigned long long>(idx++));
    }
    while (std::string::npos != (pos = insnt.find(needle))) {
      insnt.replace(pos, needle.size(), buf);
    }
  }
}

// rewrite argument indices to the block scope indices
static void remap_inargs(std::string &insnt, size_t nargs, size_t idx) {
  static const std::string digits[10] = {"0", "1", "2", "3", "4",
                                         "5", "6", "7", "8", "9"};
  assert(nargs <= 10);

  static const std::string prefix("<<in");
  static const std::string suffix(">>");
  std::string argnum;
  std::string needle;
  char buf[10];
  for (unsigned i = 0; i < nargs; i++) {
    needle = prefix + digits[i] + suffix;
    size_t pos = insnt.find(needle);
    if (std::string::npos != pos) {
      sprintf(buf, "%llu", static_cast<unsigned long long>(idx++));
    }
    while (std::string::npos != (pos = insnt.find(needle))) {
      insnt.replace(pos, needle.size(), buf);
    }
  }
}

void CompoundAsmStatement_toIR(CompoundAsmStatement *stmt, IRState *p) {
  IF_LOG Logger::println("CompoundAsmStatement::toIR(): %s",
                         stmt->loc.toChars());
  LOG_SCOPE;

  const bool isCompoundGccAsmStatement =
      (stmt->statements && stmt->statements->length &&
       stmt->statements->front()->isGccAsmStatement());
  if (isCompoundGccAsmStatement) {
    for (Statement *s : *stmt->statements) {
      if (auto gas = s->isGccAsmStatement()) {
        Statement_toIR(gas, p);
      } else {
        s->error("DMD-style assembly statement unsupported within GCC-style "
                 "`asm` block");
        fatal();
      }
    }

    return;
  }

  // disable inlining by default
  if (!p->func()->decl->allowInlining) {
    p->func()->setNeverInline();
  }

  // create asm block structure
  assert(!p->asmBlock);
  auto asmblock = new IRAsmBlock(stmt);
  assert(asmblock);
  p->asmBlock = asmblock;

  // do asm statements
  for (Statement *s : *stmt->statements) {
    if (s) {
      if (s->isGccAsmStatement()) {
        s->error("GCC-style assembly statement unsupported within DMD-style "
                 "`asm` block");
        fatal();
      }
      Statement_toIR(s, p);
    }
  }

  // build forwarder for in-asm branches to external labels
  // this additional asm code sets the __llvm_jump_target variable
  // to a unique value that will identify the jump target in
  // a post-asm switch

  // maps each goto destination to its special value
  std::map<LabelDsymbol *, int> gotoToVal;

  // location of the special value determining the goto label
  // will be set if post-asm dispatcher block is needed
  LLValue *jump_target = nullptr;

  {
    FuncDeclaration *fd = gIR->func()->decl;
    OutBuffer mangleBuf;
    mangleToBuffer(fd, &mangleBuf);
    const char *fdmangle = mangleBuf.peekChars();

    // we use a simple static counter to make sure the new end labels are
    // unique
    static size_t uniqueLabelsId = 0;
    std::ostringstream asmGotoEndLabel;
    printLabelName(asmGotoEndLabel, fdmangle, "_llvm_asm_end");
    asmGotoEndLabel << uniqueLabelsId++;

    // initialize the setter statement we're going to build
    auto outSetterStmt = new IRAsmStmt;
    std::string asmGotoEnd = "\n\tjmp " + asmGotoEndLabel.str() + "\n";
    std::ostringstream code;
    code << asmGotoEnd;

    int n_goto = 1;

    for (IRAsmStmt *a : asmblock->s) {
      // skip non-branch statements
      LabelDsymbol *const targetLabel = a->isBranchToLabel;
      if (!targetLabel) {
        continue;
      }
      Identifier *const ident = targetLabel->ident;

      // if internal, no special handling is necessary, skip
      if (llvm::any_of(asmblock->internalLabels,
                       [ident](Identifier *i) { return i->equals(ident); })) {
        continue;
      }

      // if we already set things up for this branch target, skip
      if (gotoToVal.find(targetLabel) != gotoToVal.end()) {
        continue;
      }

      // record that the jump needs to be handled in the post-asm dispatcher
      gotoToVal[targetLabel] = n_goto;

      // provide an in-asm target for the branch and set value
      IF_LOG Logger::println(
          "statement '%s' references outer label '%s': creating forwarder",
          a->code.c_str(), ident->toChars());
      printLabelName(code, fdmangle, ident->toChars());
      code << ":\n\t";
      code << "movl $<<in" << n_goto << ">>, $<<out0>>\n";
      // FIXME: Store the value -> label mapping somewhere, so it can be
      // referenced later
      outSetterStmt->in.push_back(DtoConstUint(n_goto));
      outSetterStmt->in_c += "i,";
      code << asmGotoEnd;

      ++n_goto;
    }
    if (code.str() != asmGotoEnd) {
      // finalize code
      outSetterStmt->code = code.str();
      outSetterStmt->code += asmGotoEndLabel.str() + ":\n";

      // create storage for and initialize the temporary
      jump_target = DtoAllocaDump(DtoConstUint(0), 0, "__llvm_jump_target");
      // setup variable for output from asm
      outSetterStmt->out_c = "=*m,";
      outSetterStmt->out.push_back(jump_target);

      asmblock->s.push_back(outSetterStmt);
    } else {
      delete outSetterStmt;
    }
  }

  // build a fall-off-end-properly asm statement

  FuncDeclaration *thisfunc = p->func()->decl;
  bool useabiret = false;
  p->asmBlock->asmBlock->abiret = nullptr;
  if (thisfunc->fbody->endsWithAsm() == stmt &&
      thisfunc->type->nextOf()->ty != TY::Tvoid) {
    // there can't be goto forwarders in this case
    assert(gotoToVal.empty());
    emitABIReturnAsmStmt(asmblock, stmt->loc, thisfunc);
    useabiret = true;
  }

  // build asm block
  std::vector<LLValue *> outargs;
  std::vector<LLValue *> inargs;
  std::vector<LLType *> outtypes;
  std::vector<LLType *> intypes;
  std::string out_c;
  std::string in_c;
  std::string clobbers;
  std::string code;
  size_t asmIdx = asmblock->retn;

  Logger::println("do outputs");
  size_t n = asmblock->s.size();
  for (size_t i = 0; i < n; ++i) {
    IRAsmStmt *a = asmblock->s[i];
    assert(a);
    size_t onn = a->out.size();
    for (size_t j = 0; j < onn; ++j) {
      outargs.push_back(a->out[j]);
      outtypes.push_back(a->out[j]->getType());
    }
    if (!a->out_c.empty()) {
      out_c += a->out_c;
    }
    remap_outargs(a->code, onn + a->in.size(), asmIdx);
    asmIdx += onn;
  }

  Logger::println("do inputs");
  for (size_t i = 0; i < n; ++i) {
    IRAsmStmt *a = asmblock->s[i];
    assert(a);
    size_t inn = a->in.size();
    for (size_t j = 0; j < inn; ++j) {
      inargs.push_back(a->in[j]);
      intypes.push_back(a->in[j]->getType());
    }
    if (!a->in_c.empty()) {
      in_c += a->in_c;
    }
    remap_inargs(a->code, inn + a->out.size(), asmIdx);
    asmIdx += inn;
    if (!code.empty()) {
      code += "\n\t";
    }
    code += a->code;
  }
  asmblock->s.clear();

  // append inputs
  out_c += in_c;

  // append clobbers
  for (const auto &c : asmblock->clobs) {
    out_c += c;
  }

  // remove excessive comma
  if (!out_c.empty()) {
    out_c.resize(out_c.size() - 1);
  }

  IF_LOG {
    Logger::println("code = \"%s\"", code.c_str());
    Logger::println("constraints = \"%s\"", out_c.c_str());
  }

  // build return types
  LLType *retty;
  if (asmblock->retn) {
    retty = asmblock->retty;
  } else {
    retty = llvm::Type::getVoidTy(gIR->context());
  }

  // build argument types
  std::vector<LLType *> types;
  types.insert(types.end(), outtypes.begin(), outtypes.end());
  types.insert(types.end(), intypes.begin(), intypes.end());
  llvm::FunctionType *fty = llvm::FunctionType::get(retty, types, false);
  IF_LOG Logger::cout() << "function type = " << *fty << '\n';

  std::vector<LLValue *> args;
  args.insert(args.end(), outargs.begin(), outargs.end());
  args.insert(args.end(), inargs.begin(), inargs.end());

  IF_LOG {
    Logger::cout() << "Arguments:" << '\n';
    Logger::indent();
    size_t i = 0;
    for (auto arg : args) {
      Stream cout = Logger::cout();
      cout << '$' << i << " ==> " << *arg;
      if (!llvm::isa<llvm::Instruction>(arg) &&
          !llvm::isa<LLGlobalValue>(arg)) {
        cout << '\n';
      }
      ++i;
    }
    Logger::undent();
  }

  llvm::InlineAsm *ia = llvm::InlineAsm::get(fty, code, out_c, true);

  auto call = p->createInlineAsmCall(stmt->loc, ia, args);
  if (!retty->isVoidTy()) {
    call->setName("asm");
  }

  IF_LOG Logger::cout() << "Complete asm statement: " << *call << '\n';

  // capture abi return value
  if (useabiret) {
    IRAsmBlock *block = p->asmBlock;
    if (block->retfixup) {
      block->asmBlock->abiret = (*block->retfixup)(p->ir, call);
    } else if (p->asmBlock->retemu) {
      block->asmBlock->abiret = DtoLoad(block->asmBlock->abiret);
    } else {
      block->asmBlock->abiret = call;
    }
  }

  p->asmBlock = nullptr;

  // if asm contained external branches, emit goto forwarder code
  if (!gotoToVal.empty()) {
    assert(jump_target);

    // make new blocks
    llvm::BasicBlock *bb = p->insertBB("afterasmgotoforwarder");

    auto val = DtoLoad(jump_target, "__llvm_jump_target_value");
    llvm::SwitchInst *sw = p->ir->CreateSwitch(val, bb, gotoToVal.size());

    // add all cases
    for (const auto &pair : gotoToVal) {
      llvm::BasicBlock *casebb = p->insertBBBefore(bb, "case");
      sw->addCase(LLConstantInt::get(llvm::IntegerType::get(gIR->context(), 32),
                                     pair.second),
                  casebb);

      p->ir->SetInsertPoint(casebb);
      DtoGoto(stmt->loc, pair.first);
    }

    p->ir->SetInsertPoint(bb);
  }
}

//////////////////////////////////////////////////////////////////////////////

void AsmStatement_toNakedIR(InlineAsmStatement *stmt, IRState *irs) {
  IF_LOG Logger::println("InlineAsmStatement::toNakedIR(): %s",
                         stmt->loc.toChars());
  LOG_SCOPE;

  // is there code?
  if (!stmt->asmcode) {
    return;
  }
  AsmCode *code = static_cast<AsmCode *>(stmt->asmcode);

  // build asm stmt
  replace_func_name(irs, code->insnTemplate);
  irs->nakedAsm << "\t" << code->insnTemplate << std::endl;
}
