//===- TestXeGPUTransforms.cpp -- Test Vector transforms and lowerings ----===//
//
// 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 "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/GPU/IR/GPUDialect.h"
#include "mlir/Dialect/Index/IR/IndexDialect.h"
#include "mlir/Dialect/Math/IR/Math.h"
#include "mlir/Dialect/SCF/Transforms/Patterns.h"
#include "mlir/Dialect/Vector/Transforms/VectorTransforms.h"
#include "mlir/Dialect/XeGPU/IR/XeGPU.h"
#include "mlir/Dialect/XeGPU/Transforms/Transforms.h"
#include "mlir/Dialect/XeGPU/Transforms/XeGPULayoutImpl.h"
#include "mlir/Dialect/XeGPU/Utils/XeGPUUtils.h"
#include "mlir/Dialect/XeGPU/uArch/IntelGpuXe2.h"
#include "mlir/IR/BuiltinTypes.h"
#include "mlir/IR/Value.h"
#include "mlir/Pass/Pass.h"
#include "mlir/Pass/PassManager.h"
#include "mlir/Transforms/DialectConversion.h"
#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
#include "llvm/Support/raw_ostream.h"
#include <optional>

using namespace mlir;
using namespace mlir::xegpu;

namespace {

#define DEBUG_TYPE "test-xegpu-unroll"

struct TestXeGPUUnrollingPatterns
    : public PassWrapper<TestXeGPUUnrollingPatterns,
                         OperationPass<gpu::GPUModuleOp>> {
  MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestXeGPUUnrollingPatterns)

  StringRef getArgument() const final {
    return "test-xegpu-unrolling-patterns";
  }

  StringRef getDescription() const final {
    return "Test lowering patterns to unroll ops in the xegpu dialect";
  }

  void getDependentDialects(::mlir::DialectRegistry &registry) const override {
    registry.insert<memref::MemRefDialect>();
    registry.insert<xegpu::XeGPUDialect>();
    registry.insert<vector::VectorDialect>();
  }

  TestXeGPUUnrollingPatterns() = default;
  TestXeGPUUnrollingPatterns(const TestXeGPUUnrollingPatterns &pass) = default;

  void runOnOperation() override {
    MLIRContext *ctx = &getContext();
    xegpu::UnrollOptions options;
    options.setNativeShapeFn([&](Operation *op)
                                 -> std::optional<SmallVector<int64_t>> {
      if (isa<xegpu::CreateNdDescOp, xegpu::PrefetchNdOp, xegpu::LoadNdOp,
              xegpu::StoreNdOp, xegpu::PrefetchOp, xegpu::LoadGatherOp,
              xegpu::StoreScatterOp>(op)) {
        xegpu::TensorDescType tdescTy;
        if (auto createNdOp = dyn_cast<xegpu::CreateNdDescOp>(op)) {
          tdescTy = createNdOp.getType();
        } else if (auto prefetchNdOp = dyn_cast<xegpu::PrefetchNdOp>(op)) {
          tdescTy = prefetchNdOp.getTensorDescType();
        } else if (auto loadNdOp = dyn_cast<xegpu::LoadNdOp>(op)) {
          tdescTy = loadNdOp.getTensorDescType();
        } else if (auto storeNdOp = dyn_cast<xegpu::StoreNdOp>(op)) {
          tdescTy = storeNdOp.getTensorDescType();
        } else if (isa<xegpu::PrefetchOp, xegpu::LoadGatherOp,
                       xegpu::StoreScatterOp>(op)) {
          auto anchorOp = cast<xegpu::AnchorLayoutInterface>(op);
          auto layout =
              dyn_cast_or_null<xegpu::LayoutAttr>(anchorOp.getAnchorLayout());
          if (layout && layout.isForSubgroup()) {
            auto inst_data = layout.getEffectiveInstDataAsInt();
            if (!inst_data.empty())
              return SmallVector<int64_t>(inst_data.begin(), inst_data.end());
          }
          return std::nullopt;
        }

        if (auto layout = tdescTy.getLayoutAttr()) {
          auto inst_data = layout.getEffectiveInstDataAsInt();
          if (!inst_data.empty() && layout.isForSubgroup())
            return SmallVector<int64_t>(inst_data.begin(), inst_data.end());
        }
      }

      if (isa<xegpu::DpasOp>(op))
        return SmallVector<int64_t>{8, 16, 16};

      // For vector.multi_reduction, read tile shape from the layout attribute
      // on the source operand (layout_operand_0).
      if (isa<vector::MultiDimReductionOp>(op)) {
        xegpu::DistributeLayoutAttr layout =
            xegpu::getDistributeLayoutAttr(op->getOpOperand(0));
        if (layout) {
          auto instData = layout.getEffectiveInstDataAsInt();
          if (!instData.empty())
            return instData;
        }
        return std::nullopt;
      }

      return std::nullopt;
    });

    options.setUnrolledTypesFn(
        [&](ShapedType type, ArrayRef<int64_t> tileShape) -> SmallVector<Type> {
          Type elemTy = type.getElementType();
          Type newTy;

          // TensorDescType needs to drop the inst_data field in the layout
          // attribute
          if (auto tdescTy = dyn_cast<xegpu::TensorDescType>(type)) {
            Attribute encoding = tdescTy.getEncoding();
            auto layout = tdescTy.getLayoutAttr();

            if (layout) {
              if (layout.getEffectiveLaneLayoutAsInt().empty())
                layout = xegpu::LayoutAttr();
              else
                layout = layout.dropInstData();
            }

            newTy = xegpu::TensorDescType::get(ctx, tileShape, elemTy, encoding,
                                               layout);
            // compute the product of batch (higher) dimensions
            ArrayRef<int64_t> shape = type.getShape();
            int64_t batchCount =
                shape.size() > 2 ? computeProduct(shape.drop_back(2)) : 1;
            return SmallVector<Type>(batchCount, newTy);
          }

          newTy = type.clone(tileShape, elemTy);
          std::optional<SmallVector<int64_t>> ratio =
              computeShapeRatio(type.getShape(), tileShape);
          assert(ratio && "Expecting the ratio to be valid.");
          return SmallVector<Type>(computeProduct(*ratio), newTy);
        });

    RewritePatternSet patterns(ctx);

    populateXeGPUUnrollPatterns(patterns, options);
    (void)applyPatternsGreedily(getOperation(), std::move(patterns));
  }
};

#undef DEBUG_TYPE
#define DEBUG_TYPE "test-xegpu-layout-interface"

// Test pattern for distributing vector::StepOp from workgroup to subgroup.
// Validates DistributeLayoutAttr interfaces for offset computation
// abstraction between LayoutAttr and SliceAttr.
class TestStepOpPattern : public OpConversionPattern<vector::StepOp> {
  using OpConversionPattern<vector::StepOp>::OpConversionPattern;

  LogicalResult
  matchAndRewrite(vector::StepOp op, OneToNOpAdaptor adaptor,
                  ConversionPatternRewriter &rewriter) const override {

    auto layoutName = xegpu::getTemporaryLayoutName(op->getResult(0));
    auto sliceAttr = op->getAttrOfType<xegpu::SliceAttr>(layoutName);
    if (!sliceAttr || sliceAttr.getRank() != 1)
      return failure();

    std::optional<SmallVector<int64_t>> sgShape =
        sliceAttr.getEffectiveSgDataAsInt();
    if (!sgShape)
      return failure();

    Location loc = op.getLoc();
    VectorType type = op.getResult().getType();
    auto wgShape = type.getShape();

    Value sgId =
        gpu::SubgroupIdOp::create(rewriter, loc, /*upper_bound=*/nullptr);
    auto maybeOffsets =
        sliceAttr.computeDistributedCoords(rewriter, loc, sgId, wgShape);
    if (failed(maybeOffsets))
      return failure();

    VectorType newTy = type.cloneWith(*sgShape, type.getElementType());
    Value base = vector::StepOp::create(rewriter, loc, newTy);
    SmallVector<Value> newOps;
    for (auto offsets : *maybeOffsets) {
      Value bcast =
          vector::BroadcastOp::create(rewriter, loc, newTy, offsets[0]);
      Value add = arith::AddIOp::create(rewriter, loc, base, bcast);
      newOps.push_back(add);
    }
    rewriter.replaceOpWithMultiple(op, {newOps});
    return success();
  }
};

struct TestXeGPURecoverTemporaryLayouts
    : public PassWrapper<TestXeGPURecoverTemporaryLayouts,
                         OperationPass<gpu::GPUModuleOp>> {
  MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestXeGPURecoverTemporaryLayouts)

  StringRef getArgument() const final {
    return "test-xegpu-recover-temporary-layouts";
  }

  StringRef getDescription() const final {
    return "Test the implementation of XeGPU temporary layout recovery";
  }

  void getDependentDialects(::mlir::DialectRegistry &registry) const override {
    registry.insert<arith::ArithDialect>();
    registry.insert<memref::MemRefDialect>();
    registry.insert<xegpu::XeGPUDialect>();
    registry.insert<vector::VectorDialect>();
    registry.insert<gpu::GPUDialect>();
  }

  TestXeGPURecoverTemporaryLayouts() = default;
  TestXeGPURecoverTemporaryLayouts(const TestXeGPURecoverTemporaryLayouts &pass)
      : PassWrapper(pass) {}

  void runOnOperation() override {
    Operation *op = getOperation();
    if (!xegpu::recoverTemporaryLayouts(op))
      signalPassFailure();
  }
};

/// This test pass is intended to test the subgroup to lane distribution of
/// xegpu/vector/arith operations in isolation, it does not handle any
/// structural ops like scf.for etc.
struct TestXeGPUSgToLaneDistribute
    : public PassWrapper<TestXeGPUSgToLaneDistribute,
                         OperationPass<gpu::GPUModuleOp>> {
  MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestXeGPUSgToLaneDistribute)

  StringRef getArgument() const final {
    return "test-xegpu-sg-to-lane-distribute";
  }

  StringRef getDescription() const final {
    return "Test the implementation of XeGPU Subgroup to Lane Distribution";
  }

  void getDependentDialects(::mlir::DialectRegistry &registry) const override {
    registry.insert<arith::ArithDialect>();
    registry.insert<memref::MemRefDialect>();
    registry.insert<xegpu::XeGPUDialect>();
    registry.insert<vector::VectorDialect>();
    registry.insert<index::IndexDialect>();
    registry.insert<gpu::GPUDialect>();
  }

  TestXeGPUSgToLaneDistribute() = default;
  TestXeGPUSgToLaneDistribute(const TestXeGPUSgToLaneDistribute &pass)
      : PassWrapper(pass) {}

  void runOnOperation() override {
    Operation *op = getOperation();
    if (!xegpu::recoverTemporaryLayouts(op)) {
      signalPassFailure();
      return;
    }

    MLIRContext *ctx = &getContext();
    TypeConverter typeConverter;
    // Define type materializations using UnrealizedConversionCastOp.
    auto materializeCast = [&](mlir::OpBuilder &builder, mlir::Type type,
                               mlir::ValueRange inputs,
                               mlir::Location loc) -> mlir::Value {
      return UnrealizedConversionCastOp::create(builder, loc, type, inputs)
          .getResult(0);
    };
    typeConverter.addSourceMaterialization(materializeCast);
    typeConverter.addTargetMaterialization(materializeCast);

    ConversionTarget target(*ctx);
    RewritePatternSet patterns(ctx);
    xegpu::populateXeGPUSgToLaneDistributeTypeConversionAndLegality(
        typeConverter, patterns, target, op);
    (void)applyPartialConversion(op, target, std::move(patterns));
  }
};

struct TestXeGPUPropagateLayouts
    : public PassWrapper<TestXeGPUPropagateLayouts,
                         OperationPass<gpu::GPUModuleOp>> {
  MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestXeGPUPropagateLayouts)

  StringRef getArgument() const final { return "test-xegpu-propagate-layouts"; }

  StringRef getDescription() const final {
    return "Test the implementation of XeGPU propagate layouts.";
  }

  void getDependentDialects(::mlir::DialectRegistry &registry) const override {
    registry.insert<xegpu::XeGPUDialect>();
    registry.insert<gpu::GPUDialect>();
  }

  TestXeGPUPropagateLayouts() = default;
  TestXeGPUPropagateLayouts(const TestXeGPUPropagateLayouts &pass)
      : PassWrapper(pass) {}

  Option<std::string> layoutKind{
      *this, "layout-kind",
      llvm::cl::desc("Propagate `subgroup` / `inst` / `lane` level of xegpu "
                     "layouts."),
      llvm::cl::init("lane")};

  void runOnOperation() override {
    OpBuilder builder(getOperation());
    LayoutKind kind;
    if (layoutKind == "subgroup")
      kind = LayoutKind::Subgroup;
    else if (layoutKind == "inst")
      kind = LayoutKind::InstData;
    else if (layoutKind == "lane")
      kind = LayoutKind::Lane;
    else {
      signalPassFailure();
      return;
    }
    if (failed(xegpu::propagateLayouts(builder, getOperation(), kind, 32))) {
      signalPassFailure();
    }
  }
};

struct TestXeGPUResolveLayoutConflicts
    : public PassWrapper<TestXeGPUResolveLayoutConflicts,
                         OperationPass<gpu::GPUModuleOp>> {
  MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestXeGPUResolveLayoutConflicts)

  StringRef getArgument() const final {
    return "test-xegpu-resolve-layout-conflicts";
  }

  StringRef getDescription() const final {
    return "Test the implementation of XeGPU layout conflict resolution.";
  }

  void getDependentDialects(::mlir::DialectRegistry &registry) const override {
    registry.insert<xegpu::XeGPUDialect>();
    registry.insert<gpu::GPUDialect>();
  }

  TestXeGPUResolveLayoutConflicts() = default;
  TestXeGPUResolveLayoutConflicts(const TestXeGPUResolveLayoutConflicts &pass) =
      default;

  void runOnOperation() override {
    if (failed(xegpu::resolveLayoutConflicts(getOperation())))
      signalPassFailure();
  }
};

struct TestXeGPUArrayLengthOptimization
    : public PassWrapper<TestXeGPUArrayLengthOptimization,
                         OperationPass<gpu::GPUModuleOp>> {
  MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestXeGPUArrayLengthOptimization)

  StringRef getArgument() const final {
    return "test-xegpu-array-length-optimization";
  }

  StringRef getDescription() const final {
    return "Test XeGPU 2D block array load optimization patterns in isolation";
  }

  void getDependentDialects(::mlir::DialectRegistry &registry) const override {
    registry.insert<xegpu::XeGPUDialect>();
    registry.insert<vector::VectorDialect>();
  }

  TestXeGPUArrayLengthOptimization() = default;
  TestXeGPUArrayLengthOptimization(const TestXeGPUArrayLengthOptimization &pass)
      : PassWrapper(pass) {}

  void runOnOperation() override {
    RewritePatternSet patterns(&getContext());
    xegpu::populateXeGPUArrayLengthOptimizationPatterns(patterns);
    if (failed(applyPatternsGreedily(getOperation(), std::move(patterns))))
      signalPassFailure();
  }
};

struct TestXeGPULayoutInterface
    : public PassWrapper<TestXeGPULayoutInterface,
                         OperationPass<gpu::GPUModuleOp>> {
  MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestXeGPULayoutInterface)

  StringRef getArgument() const final { return "test-xegpu-layout-interface"; }

  StringRef getDescription() const final {
    return "Test the implementation of XeGPU Layout interfaces";
  }

  void getDependentDialects(::mlir::DialectRegistry &registry) const override {
    registry.insert<arith::ArithDialect>();
    registry.insert<memref::MemRefDialect>();
    registry.insert<xegpu::XeGPUDialect>();
    registry.insert<vector::VectorDialect>();
    registry.insert<index::IndexDialect>();
  }

  TestXeGPULayoutInterface() = default;
  TestXeGPULayoutInterface(const TestXeGPULayoutInterface &pass) = default;

  void runOnOperation() override {
    MLIRContext *ctx = &getContext();

    TypeConverter typeConverter;
    auto materializeCast = [&](mlir::OpBuilder &builder, mlir::Type type,
                               mlir::ValueRange inputs,
                               mlir::Location loc) -> mlir::Value {
      return UnrealizedConversionCastOp::create(builder, loc, type, inputs)
          .getResult(0);
    };
    typeConverter.addSourceMaterialization(materializeCast);
    typeConverter.addTargetMaterialization(materializeCast);

    RewritePatternSet patterns(ctx);
    patterns.add<TestStepOpPattern>(typeConverter, ctx);

    ConversionTarget target(*ctx);
    auto isLegal = [&](xegpu::SliceAttr layout) -> bool {
      return !layout || !layout.isForWorkgroup();
    };

    target.addDynamicallyLegalOp<vector::StepOp>(
        [&](vector::StepOp op) -> bool {
          auto layoutName = xegpu::getTemporaryLayoutName(op->getResult(0));
          auto sliceAttr = op->getAttrOfType<xegpu::SliceAttr>(layoutName);
          return isLegal(sliceAttr);
        });

    target.markUnknownOpDynamicallyLegal([](Operation *op) { return true; });

    (void)applyPartialConversion(getOperation(), target, std::move(patterns));
  }
};

struct TestXeGPUCoalesceGatherScatter
    : public PassWrapper<TestXeGPUCoalesceGatherScatter, OperationPass<>> {
  MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestXeGPUCoalesceGatherScatter)

  StringRef getArgument() const final {
    return "test-xegpu-coalesce-gather-scatter";
  }

  StringRef getDescription() const final {
    return "Test driver that turns the contiguity attribute into a lane_data "
           "layout on gather/scatter ops.";
  }

  void getDependentDialects(::mlir::DialectRegistry &registry) const override {
    registry.insert<arith::ArithDialect>();
    registry.insert<vector::VectorDialect>();
    registry.insert<xegpu::XeGPUDialect>();
  }

  TestXeGPUCoalesceGatherScatter() = default;
  TestXeGPUCoalesceGatherScatter(const TestXeGPUCoalesceGatherScatter &pass)
      : PassWrapper(pass) {}

  Option<unsigned> maxChunkSize{
      *this, "max-chunk-size",
      llvm::cl::desc("Upper bound on the produced lane_data FCD."),
      llvm::cl::init(8)};

  Option<bool> analyzeOnly{
      *this, "analyze-only",
      llvm::cl::desc("Only run the analysis (stamp the contiguity attribute); "
                     "do not apply."),
      llvm::cl::init(false)};

  void runOnOperation() override {
    xegpu::runContiguityAnalysis(getOperation());
    if (analyzeOnly)
      return;
    getOperation()->walk([&](Operation *op) {
      if (auto load = dyn_cast<xegpu::LoadGatherOp>(op))
        applyContiguity(load, maxChunkSize);
      else if (auto store = dyn_cast<xegpu::StoreScatterOp>(op))
        applyContiguity(store, maxChunkSize);
    });
  }

private:
  /// Build a `lane_layout`/`lane_data`/`inst_data` layout of rank `rank`, with
  /// the given lane_layout / lane_data on the innermost dim (1 elsewhere).
  /// `inst_data` is `lane_layout * lane_data` per dim.
  static xegpu::LayoutAttr buildLaneDataLayout(MLIRContext *ctx, unsigned rank,
                                               int64_t innerLaneLayout,
                                               int64_t innerLaneData) {
    SmallVector<int32_t> laneLayout(rank, 1);
    SmallVector<int32_t> laneData(rank, 1);
    SmallVector<int32_t> instData(rank, 1);
    laneLayout.back() = static_cast<int32_t>(innerLaneLayout);
    laneData.back() = static_cast<int32_t>(innerLaneData);
    instData.back() = static_cast<int32_t>(innerLaneLayout * innerLaneData);
    return xegpu::LayoutAttr::get(ctx, instData, laneLayout, laneData);
  }

  /// Minimal driver: read the `contiguity` attribute the analysis stamped and
  /// turn it into a `lane_data` layout. This is only a stand-in for the real
  /// consumer (layout propagation) so the analysis output can be checked
  /// end-to-end; it handles just the simple power-of-two case.
  template <typename OpTy>
  static void applyContiguity(OpTy op, unsigned maxChunkSize) {
    std::optional<uint64_t> contiguity = op.getContiguity();
    if (!contiguity)
      return;
    op.removeContiguityAttr();

    auto offsetsTy = dyn_cast<VectorType>(op.getOffsets().getType());
    auto valueTy = op.getValueType();
    if (!offsetsTy || !valueTy)
      return;
    const auto *uArch =
        xegpu::uArch::getUArch(xegpu::getChipStr(op).value_or(""));
    if (!uArch)
      return;
    int64_t subgroupSize = uArch->getSubgroupSize();

    // Tile size and subgroup size are powers of two, so the smaller is already
    // the largest power-of-two divisor; min() suffices throughout.
    int64_t inner = offsetsTy.getShape().back();
    int64_t laneLayout = std::min<int64_t>(subgroupSize, inner);
    int64_t perLane = inner / laneLayout;
    int64_t laneData =
        std::min<int64_t>({static_cast<int64_t>(*contiguity),
                           static_cast<int64_t>(maxChunkSize), perLane});
    if (laneData < 2)
      return;

    op.setLayoutAttr(buildLaneDataLayout(op.getContext(), valueTy.getRank(),
                                         laneLayout, laneData));
  }
};

} // namespace

namespace mlir {
namespace test {
void registerTestXeGPULowerings() {
  PassRegistration<TestXeGPUUnrollingPatterns>();
  PassRegistration<TestXeGPULayoutInterface>();
  PassRegistration<TestXeGPURecoverTemporaryLayouts>();
  PassRegistration<TestXeGPUSgToLaneDistribute>();
  PassRegistration<TestXeGPUPropagateLayouts>();
  PassRegistration<TestXeGPUResolveLayoutConflicts>();
  PassRegistration<TestXeGPUArrayLengthOptimization>();
  PassRegistration<TestXeGPUCoalesceGatherScatter>();
}
} // namespace test
} // namespace mlir
