//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// Emit OpenACC Stmt nodes as CIR code.
//
//===----------------------------------------------------------------------===//

#include "CIRGenBuilder.h"
#include "CIRGenFunction.h"
#include "mlir/Dialect/OpenACC/OpenACC.h"
#include "clang/AST/OpenACCClause.h"
#include "clang/AST/StmtOpenACC.h"

using namespace clang;
using namespace clang::CIRGen;
using namespace cir;
using namespace mlir::acc;

template <typename Op, typename TermOp>
mlir::LogicalResult CIRGenFunction::emitOpenACCOpAssociatedStmt(
    mlir::Location start, mlir::Location end, OpenACCDirectiveKind dirKind,
    llvm::ArrayRef<const OpenACCClause *> clauses, const Stmt *associatedStmt) {
  mlir::LogicalResult res = mlir::success();

  llvm::SmallVector<mlir::Type> retTy;
  llvm::SmallVector<mlir::Value> operands;
  auto op = Op::create(builder, start, retTy, operands);

  emitOpenACCClauses(op, dirKind, clauses);

  {
    mlir::Block &block = op.getRegion().emplaceBlock();
    mlir::OpBuilder::InsertionGuard guardCase(builder);
    builder.setInsertionPointToEnd(&block);

    LexicalScope ls{*this, start, builder.getInsertionBlock()};
    res = emitStmt(associatedStmt, /*useCurrentScope=*/true);

    TermOp::create(builder, end);
  }
  return res;
}

namespace {
template <typename Op> struct CombinedType;
template <> struct CombinedType<ParallelOp> {
  static constexpr mlir::acc::CombinedConstructsType value =
      mlir::acc::CombinedConstructsType::ParallelLoop;
};
template <> struct CombinedType<SerialOp> {
  static constexpr mlir::acc::CombinedConstructsType value =
      mlir::acc::CombinedConstructsType::SerialLoop;
};
template <> struct CombinedType<KernelsOp> {
  static constexpr mlir::acc::CombinedConstructsType value =
      mlir::acc::CombinedConstructsType::KernelsLoop;
};
} // namespace

template <typename Op, typename TermOp>
mlir::LogicalResult CIRGenFunction::emitOpenACCOpCombinedConstruct(
    mlir::Location start, mlir::Location end, OpenACCDirectiveKind dirKind,
    llvm::ArrayRef<const OpenACCClause *> clauses, const Stmt *loopStmt) {
  mlir::LogicalResult res = mlir::success();

  llvm::SmallVector<mlir::Type> retTy;
  llvm::SmallVector<mlir::Value> operands;

  auto computeOp = Op::create(builder, start, retTy, operands);
  computeOp.setCombinedAttr(builder.getUnitAttr());
  mlir::acc::LoopOp loopOp;

  // First, emit the bodies of both operations, with the loop inside the body of
  // the combined construct.
  {
    mlir::Block &block = computeOp.getRegion().emplaceBlock();
    mlir::OpBuilder::InsertionGuard guardCase(builder);
    builder.setInsertionPointToEnd(&block);

    LexicalScope ls{*this, start, builder.getInsertionBlock()};
    auto loopOp = LoopOp::create(builder, start, retTy, operands);
    loopOp.setCombinedAttr(mlir::acc::CombinedConstructsTypeAttr::get(
        builder.getContext(), CombinedType<Op>::value));

    {
      mlir::Block &innerBlock = loopOp.getRegion().emplaceBlock();
      mlir::OpBuilder::InsertionGuard guardCase(builder);
      builder.setInsertionPointToEnd(&innerBlock);

      LexicalScope ls{*this, start, builder.getInsertionBlock()};
      ActiveOpenACCLoopRAII activeLoop{*this, &loopOp};

      res = emitStmt(loopStmt, /*useCurrentScope=*/true);

      mlir::acc::YieldOp::create(builder, end);
    }

    emitOpenACCClauses(computeOp, loopOp, dirKind, clauses);

    updateLoopOpParallelism(loopOp, /*isOrphan=*/false, dirKind);

    TermOp::create(builder, end);
  }

  return res;
}

template <typename Op>
Op CIRGenFunction::emitOpenACCOp(
    mlir::Location start, OpenACCDirectiveKind dirKind,
    llvm::ArrayRef<const OpenACCClause *> clauses) {
  llvm::SmallVector<mlir::Type> retTy;
  llvm::SmallVector<mlir::Value> operands;
  auto op = Op::create(builder, start, retTy, operands);

  emitOpenACCClauses(op, dirKind, clauses);
  return op;
}

mlir::LogicalResult
CIRGenFunction::emitOpenACCComputeConstruct(const OpenACCComputeConstruct &s) {
  mlir::Location start = getLoc(s.getSourceRange().getBegin());
  mlir::Location end = getLoc(s.getSourceRange().getEnd());

  switch (s.getDirectiveKind()) {
  case OpenACCDirectiveKind::Parallel:
    return emitOpenACCOpAssociatedStmt<ParallelOp, mlir::acc::YieldOp>(
        start, end, s.getDirectiveKind(), s.clauses(), s.getStructuredBlock());
  case OpenACCDirectiveKind::Serial:
    return emitOpenACCOpAssociatedStmt<SerialOp, mlir::acc::YieldOp>(
        start, end, s.getDirectiveKind(), s.clauses(), s.getStructuredBlock());
  case OpenACCDirectiveKind::Kernels:
    return emitOpenACCOpAssociatedStmt<KernelsOp, mlir::acc::TerminatorOp>(
        start, end, s.getDirectiveKind(), s.clauses(), s.getStructuredBlock());
  default:
    llvm_unreachable("invalid compute construct kind");
  }
}

mlir::LogicalResult
CIRGenFunction::emitOpenACCDataConstruct(const OpenACCDataConstruct &s) {
  mlir::Location start = getLoc(s.getSourceRange().getBegin());
  mlir::Location end = getLoc(s.getSourceRange().getEnd());

  return emitOpenACCOpAssociatedStmt<DataOp, mlir::acc::TerminatorOp>(
      start, end, s.getDirectiveKind(), s.clauses(), s.getStructuredBlock());
}

mlir::LogicalResult
CIRGenFunction::emitOpenACCInitConstruct(const OpenACCInitConstruct &s) {
  mlir::Location start = getLoc(s.getSourceRange().getBegin());
  emitOpenACCOp<InitOp>(start, s.getDirectiveKind(), s.clauses());
  return mlir::success();
}

mlir::LogicalResult
CIRGenFunction::emitOpenACCSetConstruct(const OpenACCSetConstruct &s) {
  mlir::Location start = getLoc(s.getSourceRange().getBegin());
  emitOpenACCOp<SetOp>(start, s.getDirectiveKind(), s.clauses());
  return mlir::success();
}

mlir::LogicalResult CIRGenFunction::emitOpenACCShutdownConstruct(
    const OpenACCShutdownConstruct &s) {
  mlir::Location start = getLoc(s.getSourceRange().getBegin());
  emitOpenACCOp<ShutdownOp>(start, s.getDirectiveKind(), s.clauses());
  return mlir::success();
}

mlir::LogicalResult
CIRGenFunction::emitOpenACCWaitConstruct(const OpenACCWaitConstruct &s) {
  mlir::Location start = getLoc(s.getSourceRange().getBegin());
  auto waitOp = emitOpenACCOp<WaitOp>(start, s.getDirectiveKind(), s.clauses());

  auto createIntExpr = [this](const Expr *intExpr) {
    mlir::Value expr = emitScalarExpr(intExpr);
    mlir::Location exprLoc = cgm.getLoc(intExpr->getBeginLoc());

    mlir::IntegerType targetType = mlir::IntegerType::get(
        &getMLIRContext(), getContext().getIntWidth(intExpr->getType()),
        intExpr->getType()->isSignedIntegerOrEnumerationType()
            ? mlir::IntegerType::SignednessSemantics::Signed
            : mlir::IntegerType::SignednessSemantics::Unsigned);

    auto conversionOp = mlir::UnrealizedConversionCastOp::create(
        builder, exprLoc, targetType, expr);
    return conversionOp.getResult(0);
  };

  // Emit the correct 'wait' clauses.
  {
    mlir::OpBuilder::InsertionGuard guardCase(builder);
    builder.setInsertionPoint(waitOp);

    if (s.hasDevNumExpr())
      waitOp.getWaitDevnumMutable().append(createIntExpr(s.getDevNumExpr()));

    for (Expr *QueueExpr : s.getQueueIdExprs())
      waitOp.getWaitOperandsMutable().append(createIntExpr(QueueExpr));
  }

  return mlir::success();
}

mlir::LogicalResult CIRGenFunction::emitOpenACCCombinedConstruct(
    const OpenACCCombinedConstruct &s) {
  mlir::Location start = getLoc(s.getSourceRange().getBegin());
  mlir::Location end = getLoc(s.getSourceRange().getEnd());

  switch (s.getDirectiveKind()) {
  case OpenACCDirectiveKind::ParallelLoop:
    return emitOpenACCOpCombinedConstruct<ParallelOp, mlir::acc::YieldOp>(
        start, end, s.getDirectiveKind(), s.clauses(), s.getLoop());
  case OpenACCDirectiveKind::SerialLoop:
    return emitOpenACCOpCombinedConstruct<SerialOp, mlir::acc::YieldOp>(
        start, end, s.getDirectiveKind(), s.clauses(), s.getLoop());
  case OpenACCDirectiveKind::KernelsLoop:
    return emitOpenACCOpCombinedConstruct<KernelsOp, mlir::acc::TerminatorOp>(
        start, end, s.getDirectiveKind(), s.clauses(), s.getLoop());
  default:
    llvm_unreachable("invalid compute construct kind");
  }
}

mlir::LogicalResult CIRGenFunction::emitOpenACCHostDataConstruct(
    const OpenACCHostDataConstruct &s) {
  mlir::Location start = getLoc(s.getSourceRange().getBegin());
  mlir::Location end = getLoc(s.getSourceRange().getEnd());

  return emitOpenACCOpAssociatedStmt<HostDataOp, mlir::acc::TerminatorOp>(
      start, end, s.getDirectiveKind(), s.clauses(), s.getStructuredBlock());
}

mlir::LogicalResult CIRGenFunction::emitOpenACCEnterDataConstruct(
    const OpenACCEnterDataConstruct &s) {
  mlir::Location start = getLoc(s.getSourceRange().getBegin());
  emitOpenACCOp<EnterDataOp>(start, s.getDirectiveKind(), s.clauses());
  return mlir::success();
}

mlir::LogicalResult CIRGenFunction::emitOpenACCExitDataConstruct(
    const OpenACCExitDataConstruct &s) {
  mlir::Location start = getLoc(s.getSourceRange().getBegin());
  emitOpenACCOp<ExitDataOp>(start, s.getDirectiveKind(), s.clauses());
  return mlir::success();
}

mlir::LogicalResult
CIRGenFunction::emitOpenACCUpdateConstruct(const OpenACCUpdateConstruct &s) {
  mlir::Location start = getLoc(s.getSourceRange().getBegin());
  emitOpenACCOp<UpdateOp>(start, s.getDirectiveKind(), s.clauses());
  return mlir::success();
}

mlir::LogicalResult
CIRGenFunction::emitOpenACCCacheConstruct(const OpenACCCacheConstruct &s) {
  // The 'cache' directive 'may' be at the top of a loop by standard, but
  // doesn't have to be. Additionally, there is nothing that requires this be a
  // loop affected by an OpenACC pragma. Sema doesn't do any level of
  // enforcement here, since it isn't particularly valuable to do so thanks to
  // that. Instead, we treat cache as a 'noop' if there is no acc.loop to apply
  // it to.
  if (!activeLoopOp)
    return mlir::success();

  mlir::acc::LoopOp loopOp = *activeLoopOp;

  mlir::OpBuilder::InsertionGuard guard(builder);
  builder.setInsertionPoint(loopOp);

  for (const Expr *var : s.getVarList()) {
    CIRGenFunction::OpenACCDataOperandInfo opInfo =
        getOpenACCDataOperandInfo(var);

    auto cacheOp = CacheOp::create(builder, opInfo.beginLoc, opInfo.varValue,
                                   /*structured=*/false, /*implicit=*/false,
                                   opInfo.name, opInfo.bounds);

    loopOp.getCacheOperandsMutable().append(cacheOp.getResult());
  }

  return mlir::success();
}

const VarDecl *getLValueDecl(const Expr *e) {
  // We are going to assume that after stripping implicit casts, that the LValue
  // is just a DRE around the var-decl.

  e = e->IgnoreImpCasts();

  const auto *dre = cast<DeclRefExpr>(e);
  return cast<VarDecl>(dre->getDecl());
}

static mlir::acc::AtomicReadOp
emitAtomicRead(CIRGenFunction &cgf, CIRGenBuilderTy &builder,
               mlir::Location start,
               const OpenACCAtomicConstruct::SingleStmtInfo &inf) {
  // Atomic 'read' only permits 'v = x', where v and x are both scalar L
  // values. The getAssociatedStmtInfo strips off implicit casts, which
  // includes implicit conversions and L-to-R-Value conversions, so we can
  // just emit it as an L value.  The Flang implementation has no problem with
  // different types, so it appears that the dialect can handle the
  // conversions.
  mlir::Value v = cgf.emitLValue(inf.V).getPointer();
  mlir::Value x = cgf.emitLValue(inf.X).getPointer();
  mlir::Type resTy = cgf.convertType(inf.V->getType());
  return mlir::acc::AtomicReadOp::create(builder, start, x, v, resTy,
                                         /*ifCond=*/{});
}

static mlir::acc::AtomicWriteOp
emitAtomicWrite(CIRGenFunction &cgf, CIRGenBuilderTy &builder,
                mlir::Location start,
                const OpenACCAtomicConstruct::SingleStmtInfo &inf) {
  mlir::Value x = cgf.emitLValue(inf.X).getPointer();
  mlir::Value expr = cgf.emitAnyExpr(inf.RefExpr).getValue();
  return mlir::acc::AtomicWriteOp::create(builder, start, x, expr,
                                          /*ifCond=*/{});
}

static std::pair<mlir::LogicalResult, mlir::acc::AtomicUpdateOp>
emitAtomicUpdate(CIRGenFunction &cgf, CIRGenBuilderTy &builder,
                 mlir::Location start, mlir::Location end,
                 const OpenACCAtomicConstruct::SingleStmtInfo &inf) {
  mlir::Value x = cgf.emitLValue(inf.X).getPointer();
  auto op = mlir::acc::AtomicUpdateOp::create(builder, start, x, /*ifCond=*/{});

  mlir::LogicalResult res = mlir::success();
  {
    mlir::OpBuilder::InsertionGuard guardCase(builder);
    mlir::Type argTy = cast<cir::PointerType>(x.getType()).getPointee();
    std::array<mlir::Type, 1> recipeType{argTy};
    std::array<mlir::Location, 1> recipeLoc{start};
    auto *recipeBlock = builder.createBlock(
        &op.getRegion(), op.getRegion().end(), recipeType, recipeLoc);
    builder.setInsertionPointToEnd(recipeBlock);
    // Since we have an initial value that we know is a scalar type, we can
    // just emit the entire statement here after sneaking-in our 'alloca' in
    // the right place, then loading out of it. Flang does a lot less work
    // (probably does its own emitting!), but we have more complicated AST
    // nodes to worry about, so we can just count on opt to remove the extra
    // alloca/load/store set.
    auto alloca = cir::AllocaOp::create(
        builder, start, x.getType(), argTy, "x_var",
        cgf.cgm.getSize(
            cgf.getContext().getTypeAlignInChars(inf.X->getType())));

    alloca.setInitAttr(builder.getUnitAttr());
    builder.CIRBaseBuilderTy::createStore(start, recipeBlock->getArgument(0),
                                          alloca);

    const VarDecl *xval = getLValueDecl(inf.X);
    CIRGenFunction::DeclMapRevertingRAII declMapRAII{cgf, xval};
    cgf.replaceAddrOfLocalVar(
        xval, Address{alloca, argTy, cgf.getContext().getDeclAlign(xval)});

    res = cgf.emitStmt(inf.WholeExpr, /*useCurrentScope=*/true);

    auto load = cir::LoadOp::create(builder, start, {alloca});
    mlir::acc::YieldOp::create(builder, end, {load});
  }

  return {res, op};
}

mlir::LogicalResult
CIRGenFunction::emitOpenACCAtomicConstruct(const OpenACCAtomicConstruct &s) {
  // While Atomic is an 'associated statement' construct, it 'steals' the
  // expression it is associated with rather than emitting it inside of it.  So
  // it has custom emit logic.
  mlir::Location start = getLoc(s.getSourceRange().getBegin());
  mlir::Location end = getLoc(s.getSourceRange().getEnd());
  OpenACCAtomicConstruct::StmtInfo inf = s.getAssociatedStmtInfo();

  switch (s.getAtomicKind()) {
  case OpenACCAtomicKind::Read: {
    assert(inf.Form == OpenACCAtomicConstruct::StmtInfo::StmtForm::Read);
    mlir::acc::AtomicReadOp op =
        emitAtomicRead(*this, builder, start, inf.First);
    emitOpenACCClauses(op, s.getDirectiveKind(), s.clauses());
    return mlir::success();
  }
  case OpenACCAtomicKind::Write: {
    assert(inf.Form == OpenACCAtomicConstruct::StmtInfo::StmtForm::Write);
    auto op = emitAtomicWrite(*this, builder, start, inf.First);
    emitOpenACCClauses(op, s.getDirectiveKind(), s.clauses());
    return mlir::success();
  }
  case OpenACCAtomicKind::None:
  case OpenACCAtomicKind::Update: {
    assert(inf.Form == OpenACCAtomicConstruct::StmtInfo::StmtForm::Update);
    auto [res, op] = emitAtomicUpdate(*this, builder, start, end, inf.First);
    emitOpenACCClauses(op, s.getDirectiveKind(), s.clauses());
    return res;
  }
  case OpenACCAtomicKind::Capture: {
    // Atomic-capture is made up of two statements, either an update = read,
    // read + update, or read + write.  As a result, the IR represents the
    // capture region as having those two 'inside' of it.
    auto op = mlir::acc::AtomicCaptureOp::create(builder, start, /*ifCond=*/{});
    emitOpenACCClauses(op, s.getDirectiveKind(), s.clauses());
    mlir::LogicalResult res = mlir::success();
    {
      mlir::OpBuilder::InsertionGuard guardCase(builder);

      mlir::Block *block =
          builder.createBlock(&op.getRegion(), op.getRegion().end(), {}, {});

      builder.setInsertionPointToStart(block);

      auto terminator = mlir::acc::TerminatorOp::create(builder, end);

      // The AtomicCaptureOp only permits the two acc.atomic.* operations inside
      // of it, so all other parts of the expression need to be emitted before
      // the AtomicCaptureOp, then moved into place.
      builder.setInsertionPoint(op);

      switch (inf.Form) {
      default:
        llvm_unreachable("invalid form for Capture");
      case OpenACCAtomicConstruct::StmtInfo::StmtForm::ReadWrite: {
        mlir::acc::AtomicReadOp first =
            emitAtomicRead(*this, builder, start, inf.First);
        mlir::acc::AtomicWriteOp second =
            emitAtomicWrite(*this, builder, start, inf.Second);

        first->moveBefore(terminator);
        second->moveBefore(terminator);
        break;
      }
      case OpenACCAtomicConstruct::StmtInfo::StmtForm::ReadUpdate: {
        mlir::acc::AtomicReadOp first =
            emitAtomicRead(*this, builder, start, inf.First);
        auto [this_res, second] =
            emitAtomicUpdate(*this, builder, start, end, inf.Second);
        res = this_res;

        first->moveBefore(terminator);
        second->moveBefore(terminator);
        break;
      }
      case OpenACCAtomicConstruct::StmtInfo::StmtForm::UpdateRead: {
        auto [this_res, first] =
            emitAtomicUpdate(*this, builder, start, end, inf.First);
        res = this_res;
        mlir::acc::AtomicReadOp second =
            emitAtomicRead(*this, builder, start, inf.Second);

        first->moveBefore(terminator);
        second->moveBefore(terminator);
        break;
      }
      }
    }
    return res;
  }
  }

  llvm_unreachable("unknown OpenACC atomic kind");
}
