#include "mlir/Dialect/Linalg/IR/Linalg.h"
#include "mlir/IR/Matchers.h"

#include "llvm/ADT/SmallBitVector.h"
#include "llvm/ADT/SmallPtrSet.h"

#include "../Common.hpp"
#include "../Patterns.hpp"
#include "src/Accelerators/PIM/Common/PimCommon.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp"

using namespace mlir;

namespace onnx_mlir {
namespace {

struct ConstantSubviewCopy {
  DenseElementsAttr source;
  SmallVector<int64_t> offsets;
  SmallVector<int64_t> strides;
  Operation* copyOp = nullptr;
};

static FailureOr<DenseElementsAttr> transposeDenseElements(DenseElementsAttr denseAttr, ArrayRef<int64_t> perms) {
  auto tensorType = dyn_cast<RankedTensorType>(denseAttr.getType());
  if (!tensorType)
    return failure();

  int64_t rank = tensorType.getRank();
  if (static_cast<int64_t>(perms.size()) != rank)
    return failure();

  llvm::SmallBitVector seen(rank);
  SmallVector<int64_t> transposedShape;
  transposedShape.reserve(rank);
  for (int64_t perm : perms) {
    if (perm < 0 || perm >= rank || seen.test(perm))
      return failure();
    seen.set(perm);
    transposedShape.push_back(tensorType.getShape()[perm]);
  }

  auto transposedType = RankedTensorType::get(transposedShape, tensorType.getElementType());
  if (denseAttr.isSplat())
    return DenseElementsAttr::get(transposedType, denseAttr.getSplatValue<Attribute>());

  SmallVector<Attribute> originalValues(denseAttr.getValues<Attribute>());
  SmallVector<Attribute> transposedValues(originalValues.size());

  SmallVector<int64_t> originalStrides(rank, 1);
  SmallVector<int64_t> transposedStrides(rank, 1);
  for (int64_t dim = rank - 2; dim >= 0; --dim) {
    originalStrides[dim] = originalStrides[dim + 1] * tensorType.getShape()[dim + 1];
    transposedStrides[dim] = transposedStrides[dim + 1] * transposedShape[dim + 1];
  }

  SmallVector<int64_t> originalIndices(rank);
  SmallVector<int64_t> transposedIndices(rank);
  for (auto [linearIndex, value] : llvm::enumerate(originalValues)) {
    int64_t remaining = static_cast<int64_t>(linearIndex);
    for (int64_t dim = 0; dim < rank; ++dim) {
      originalIndices[dim] = remaining / originalStrides[dim];
      remaining %= originalStrides[dim];
    }

    for (int64_t dim = 0; dim < rank; ++dim)
      transposedIndices[dim] = originalIndices[perms[dim]];

    int64_t transposedLinearIndex = 0;
    for (int64_t dim = 0; dim < rank; ++dim)
      transposedLinearIndex += transposedIndices[dim] * transposedStrides[dim];

    transposedValues[transposedLinearIndex] = value;
  }

  return DenseElementsAttr::get(transposedType, transposedValues);
}

static FailureOr<Attribute> getConstantMapYield(linalg::MapOp mapOp) {
  if (!mapOp.getInputs().empty())
    return failure();

  auto yieldOp = dyn_cast<linalg::YieldOp>(mapOp.getMapper().front().getTerminator());
  if (!yieldOp || yieldOp.getNumOperands() != 1)
    return failure();

  Attribute attr;
  if (!matchPattern(yieldOp.getValues().front(), m_Constant(&attr)))
    return failure();
  return attr;
}

struct FoldConstantCoreMapPattern final : OpRewritePattern<linalg::MapOp> {
  using OpRewritePattern::OpRewritePattern;

  LogicalResult matchAndRewrite(linalg::MapOp mapOp, PatternRewriter& rewriter) const override {
    auto coreOp = mapOp->getParentOfType<pim::PimCoreOp>();
    if (!coreOp)
      return failure();

    auto initType = dyn_cast<MemRefType>(mapOp.getInit().getType());
    if (!initType || !initType.hasStaticShape())
      return failure();

    auto fillValue = getConstantMapYield(mapOp);
    if (failed(fillValue))
      return failure();

    auto tensorType = RankedTensorType::get(initType.getShape(), initType.getElementType());
    DenseElementsAttr splatAttr = DenseElementsAttr::get(tensorType, *fillValue);

    auto moduleOp = mapOp->getParentOfType<ModuleOp>();
    if (!moduleOp)
      return failure();

    auto globalOp = createFoldedGlobal(moduleOp, mapOp.getLoc(), initType, splatAttr, "pim_core_fill");

    OpBuilder::InsertionGuard guard(rewriter);
    rewriter.setInsertionPoint(coreOp);
    auto getGlobalOp = memref::GetGlobalOp::create(rewriter, mapOp.getLoc(), initType, globalOp.getName());

    rewriter.setInsertionPoint(mapOp);
    rewriter.replaceAllUsesExcept(mapOp.getInit(), getGlobalOp.getResult(), mapOp);
    rewriter.eraseOp(mapOp);
    return success();
  }
};

static FailureOr<DenseElementsAttr> foldConstantAlloc(memref::AllocOp allocOp, ModuleOp moduleOp) {
  auto allocType = dyn_cast<MemRefType>(allocOp.getType());
  if (!allocType || !allocType.hasStaticShape())
    return failure();

  auto resultTensorType = RankedTensorType::get(allocType.getShape(), allocType.getElementType());
  const int64_t numElements = resultTensorType.getNumElements();
  if (numElements < 0)
    return failure();

  Attribute fillValue;
  SmallVector<ConstantSubviewCopy> copies;
  llvm::SmallPtrSet<Operation*, 8> visitedAliases;
  SmallVector<Value> pendingAliases;
  pendingAliases.push_back(allocOp.getResult());

  while (!pendingAliases.empty()) {
    Value alias = pendingAliases.pop_back_val();
    for (Operation* user : alias.getUsers()) {
      if (!visitedAliases.insert(user).second)
        continue;

      if (auto mapOp = dyn_cast<linalg::MapOp>(user)) {
        if (mapOp.getInit() != alias)
          return failure();
        auto maybeFillValue = getConstantMapYield(mapOp);
        if (failed(maybeFillValue))
          return failure();
        if (fillValue && fillValue != *maybeFillValue)
          return failure();
        fillValue = *maybeFillValue;
        continue;
      }

      if (auto subviewOp = dyn_cast<memref::SubViewOp>(user)) {
        SmallVector<int64_t> offsets;
        SmallVector<int64_t> strides;
        offsets.reserve(subviewOp.getMixedOffsets().size());
        strides.reserve(subviewOp.getMixedStrides().size());
        for (OpFoldResult offset : subviewOp.getMixedOffsets()) {
          auto staticOffset = getConstantIntValue(offset);
          if (!staticOffset)
            return failure();
          offsets.push_back(*staticOffset);
        }
        for (OpFoldResult stride : subviewOp.getMixedStrides()) {
          auto staticStride = getConstantIntValue(stride);
          if (!staticStride)
            return failure();
          strides.push_back(*staticStride);
        }

        for (Operation* subviewUser : subviewOp->getUsers()) {
          if (auto copyOp = dyn_cast<memref::CopyOp>(subviewUser)) {
            if (copyOp.getTarget() != subviewOp.getResult())
              return failure();

            auto denseAttr = getDenseGlobalValue(moduleOp, copyOp.getSource());
            if (failed(denseAttr))
              return failure();
            copies.push_back({*denseAttr, offsets, strides, copyOp});
            continue;
          }
          return failure();
        }
        continue;
      }

      if (isa<pim::PimCoreOp, memref::DeallocOp>(user))
        continue;

      if (auto castOp = dyn_cast<memref::CastOp>(user)) {
        pendingAliases.push_back(castOp.getResult());
        continue;
      }

      return failure();
    }
  }

  if (!fillValue)
    return failure();

  SmallVector<Attribute> resultValues(numElements, fillValue);
  auto resultStrides = computeRowMajorStrides(resultTensorType.getShape());

  llvm::sort(copies, [](const ConstantSubviewCopy& lhs, const ConstantSubviewCopy& rhs) {
    return lhs.copyOp->isBeforeInBlock(rhs.copyOp);
  });

  for (const ConstantSubviewCopy& copy : copies) {
    auto sourceType = dyn_cast<RankedTensorType>(copy.source.getType());
    if (!sourceType || !sourceType.hasStaticShape())
      return failure();
    if (sourceType.getRank() != static_cast<int64_t>(copy.offsets.size())
        || sourceType.getRank() != static_cast<int64_t>(copy.strides.size()))
      return failure();

    auto sourceStrides = computeRowMajorStrides(sourceType.getShape());
    SmallVector<Attribute> sourceValues(copy.source.getValues<Attribute>());
    for (auto [linearIndex, value] : llvm::enumerate(sourceValues)) {
      SmallVector<int64_t> sourceIndices =
        delinearizeIndex(static_cast<int64_t>(linearIndex), sourceType.getShape(), sourceStrides);
      SmallVector<int64_t> resultIndices;
      resultIndices.reserve(sourceIndices.size());
      for (auto [offset, sourceIndex, stride] : llvm::zip_equal(copy.offsets, sourceIndices, copy.strides))
        resultIndices.push_back(offset + sourceIndex * stride);

      int64_t resultLinearIndex = linearizeIndex(resultIndices, resultStrides);
      resultValues[resultLinearIndex] = value;
    }
  }

  return DenseElementsAttr::get(resultTensorType, resultValues);
}

struct FoldConstantTransposePattern final : OpRewritePattern<pim::PimTransposeOp> {
  using OpRewritePattern::OpRewritePattern;

  LogicalResult matchAndRewrite(pim::PimTransposeOp transposeOp, PatternRewriter& rewriter) const override {
    auto resultType = dyn_cast<MemRefType>(transposeOp.getOutput().getType());
    if (!resultType || !resultType.hasStaticShape())
      return failure();

    auto sourceGetGlobal = transposeOp.getInput().getDefiningOp<memref::GetGlobalOp>();
    if (!sourceGetGlobal)
      return failure();

    auto moduleOp = transposeOp->getParentOfType<ModuleOp>();
    if (!moduleOp)
      return failure();

    auto sourceGlobal = lookupGlobalForGetGlobal(moduleOp, sourceGetGlobal);
    if (!sourceGlobal || !sourceGlobal.getConstant() || !sourceGlobal.getInitialValue())
      return failure();

    auto denseAttr = dyn_cast<DenseElementsAttr>(*sourceGlobal.getInitialValue());
    if (!denseAttr)
      return failure();

    SmallVector<int64_t> perms;
    perms.reserve(transposeOp.getPermutation().size());
    for (IntegerAttr attr : transposeOp.getPermutation().getAsRange<IntegerAttr>())
      perms.push_back(attr.getInt());
    FailureOr<DenseElementsAttr> transposedAttr = transposeDenseElements(denseAttr, perms);
    if (failed(transposedAttr))
      return failure();

    auto transposedShape = cast<RankedTensorType>(transposedAttr->getType()).getShape();
    if (!llvm::equal(transposedShape, resultType.getShape()))
      return failure();

    auto newGlobal = createFoldedGlobal(moduleOp,
                                        transposeOp.getLoc(),
                                        resultType,
                                        *transposedAttr,
                                        sourceGlobal.getName().str() + "__folded_transpose",
                                        sourceGlobal.getAlignmentAttr());

    rewriter.setInsertionPoint(transposeOp);
    auto newGetGlobal = memref::GetGlobalOp::create(rewriter, transposeOp.getLoc(), resultType, newGlobal.getName());

    bool isAlwaysWeight =
      !transposeOp->getUsers().empty()
      && llvm::all_of(transposeOp->getUsers(), [](Operation* user) { return isa<pim::PimCoreOp>(user); });
    if (isAlwaysWeight) {
      markWeightAlways(newGlobal);
      markWeightAlways(newGetGlobal);
    }

    rewriter.replaceOp(transposeOp, newGetGlobal.getResult());
    return success();
  }
};

struct FoldConstantAllocPattern final : OpRewritePattern<memref::AllocOp> {
  using OpRewritePattern::OpRewritePattern;

  LogicalResult matchAndRewrite(memref::AllocOp allocOp, PatternRewriter& rewriter) const override {
    auto moduleOp = allocOp->getParentOfType<ModuleOp>();
    if (!moduleOp)
      return failure();

    auto foldedAttr = foldConstantAlloc(allocOp, moduleOp);
    if (failed(foldedAttr))
      return failure();

    auto allocType = cast<MemRefType>(allocOp.getType());
    auto newGlobal = createFoldedGlobal(moduleOp, allocOp.getLoc(), allocType, *foldedAttr, "pim_folded_constant");

    rewriter.setInsertionPoint(allocOp);
    auto newGetGlobal = memref::GetGlobalOp::create(rewriter, allocOp.getLoc(), allocType, newGlobal.getName());

    SmallVector<Operation*> opsToErase;
    SmallVector<memref::CastOp> castsToReplace;
    bool allLiveUsersAreCoreOps = true;
    for (Operation* user : llvm::make_early_inc_range(allocOp->getUsers())) {
      if (isa<linalg::MapOp, memref::SubViewOp, memref::DeallocOp>(user)) {
        opsToErase.push_back(user);
        continue;
      }

      if (auto castOp = dyn_cast<memref::CastOp>(user)) {
        castsToReplace.push_back(castOp);
        continue;
      }

      if (!isa<pim::PimCoreOp>(user))
        return failure();
    }

    if (!llvm::all_of(castsToReplace, [](memref::CastOp castOp) {
          return llvm::all_of(castOp->getUsers(), [](Operation* user) { return isa<pim::PimCoreOp>(user); });
        })) {
      allLiveUsersAreCoreOps = false;
    }

    if (!llvm::all_of(allocOp->getUsers(), [](Operation* user) {
          return isa<linalg::MapOp, memref::SubViewOp, memref::DeallocOp, memref::CastOp, pim::PimCoreOp>(user);
        })) {
      return failure();
    }

    if (allLiveUsersAreCoreOps) {
      markWeightAlways(newGlobal);
      markWeightAlways(newGetGlobal);
    }

    llvm::SmallPtrSet<Operation*, 8> preservedUsers(opsToErase.begin(), opsToErase.end());
    for (memref::CastOp castOp : castsToReplace)
      preservedUsers.insert(castOp);
    rewriter.replaceAllUsesExcept(allocOp.getResult(), newGetGlobal.getResult(), preservedUsers);

    for (memref::CastOp castOp : castsToReplace) {
      rewriter.setInsertionPoint(castOp);
      Value replacementCast = memref::CastOp::create(rewriter, castOp.getLoc(), castOp.getType(), newGetGlobal);
      rewriter.replaceOp(castOp, replacementCast);
      if (allLiveUsersAreCoreOps)
        markWeightAlways(replacementCast.getDefiningOp());
    }

    for (Operation* op : llvm::make_early_inc_range(opsToErase)) {
      if (auto subviewOp = dyn_cast<memref::SubViewOp>(op))
        for (Operation* subviewUser : llvm::make_early_inc_range(subviewOp->getUsers()))
          rewriter.eraseOp(subviewUser);
      if (op->use_empty())
        rewriter.eraseOp(op);
    }

    if (allocOp.use_empty())
      rewriter.eraseOp(allocOp);
    return success();
  }
};

struct FoldConstantMemCpPattern final : OpRewritePattern<pim::PimMemCopyOp> {
  using OpRewritePattern::OpRewritePattern;

  LogicalResult matchAndRewrite(pim::PimMemCopyOp copyOp, PatternRewriter& rewriter) const override {
    if (copyOp->getParentOfType<pim::PimCoreOp>())
      return failure();

    auto allocOp = copyOp.getTarget().getDefiningOp<memref::AllocOp>();
    if (!allocOp)
      return failure();
    auto allocType = dyn_cast<MemRefType>(allocOp.getType());
    if (!allocType || !allocType.hasStaticShape())
      return failure();

    if (copyOp.getTargetOffset() != 0 || copyOp.getSourceOffset() != 0)
      return failure();

    auto srcSubview = getStaticSubviewInfo(copyOp.getSource());
    Value globalSource = succeeded(srcSubview) ? srcSubview->source : stripMemRefCasts(copyOp.getSource());

    auto moduleOp = copyOp->getParentOfType<ModuleOp>();
    if (!moduleOp)
      return failure();

    auto denseAttr = getDenseGlobalValue(moduleOp, globalSource);
    if (failed(denseAttr))
      return failure();

    DenseElementsAttr foldedAttr;
    if (succeeded(srcSubview)) {
      auto sourceType = dyn_cast<RankedTensorType>(denseAttr->getType());
      if (!sourceType || !sourceType.hasStaticShape())
        return failure();
      if (llvm::any_of(srcSubview->strides, [](int64_t stride) { return stride != 1; }))
        return failure();

      auto resultTensorType = RankedTensorType::get(allocType.getShape(), allocType.getElementType());
      const int64_t numResultElements = resultTensorType.getNumElements();
      auto sourceStrides = computeRowMajorStrides(sourceType.getShape());
      auto resultStrides = computeRowMajorStrides(resultTensorType.getShape());
      SmallVector<Attribute> sourceValues(denseAttr->getValues<Attribute>());
      SmallVector<Attribute> resultValues(numResultElements);

      for (int64_t i = 0; i < numResultElements; ++i) {
        auto resultIndices = delinearizeIndex(i, resultTensorType.getShape(), resultStrides);
        SmallVector<int64_t> sourceIndices;
        sourceIndices.reserve(resultIndices.size());
        for (auto [off, idx] : llvm::zip_equal(srcSubview->offsets, resultIndices))
          sourceIndices.push_back(off + idx);
        int64_t srcLinear = linearizeIndex(sourceIndices, sourceStrides);
        resultValues[i] = sourceValues[srcLinear];
      }
      foldedAttr = DenseElementsAttr::get(resultTensorType, resultValues);
    }
    else {
      auto resultTensorType = RankedTensorType::get(allocType.getShape(), allocType.getElementType());
      if (resultTensorType != denseAttr->getType())
        return failure();
      foldedAttr = *denseAttr;
    }

    bool allLiveUsersAreCores = true;
    for (Operation* user : allocOp->getUsers()) {
      if (user == copyOp)
        continue;
      if (isa<memref::DeallocOp>(user))
        continue;
      if (isa<pim::PimCoreOp>(user))
        continue;
      if (isa<memref::SubViewOp>(user)) {
        allLiveUsersAreCores = false;
        continue;
      }
      return failure();
    }

    auto newGlobal = createFoldedGlobal(moduleOp, allocOp.getLoc(), allocType, foldedAttr, "pim_folded_memcp");
    if (allLiveUsersAreCores)
      markWeightAlways(newGlobal);

    rewriter.setInsertionPoint(allocOp);
    auto newGetGlobal = memref::GetGlobalOp::create(rewriter, allocOp.getLoc(), allocType, newGlobal.getName());
    if (allLiveUsersAreCores)
      markWeightAlways(newGetGlobal);

    rewriter.replaceAllUsesWith(allocOp.getResult(), newGetGlobal.getResult());
    rewriter.eraseOp(copyOp);
    if (allocOp.use_empty())
      rewriter.eraseOp(allocOp);
    return success();
  }
};

} // namespace

void populateConstantFoldingConstantPatterns(RewritePatternSet& patterns) {
  patterns
    .add<FoldConstantTransposePattern, FoldConstantAllocPattern, FoldConstantCoreMapPattern, FoldConstantMemCpPattern>(
      patterns.getContext());
}

} // namespace onnx_mlir
