// Copyright 2023 The IREE Authors
//
// Licensed 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 <random>

#include "iree/compiler/Reducer/Strategies/DeltaStrategies.h"

#include "iree/compiler/Dialect/Util/IR/UtilOps.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/Linalg/IR/Linalg.h"
#include "mlir/Dialect/Tensor/IR/Tensor.h"
#include "mlir/Pass/PassManager.h"
#include "mlir/Transforms/Passes.h"

using namespace mlir;
using namespace mlir::iree_compiler;
using namespace mlir::iree_compiler::Reducer;

void mlir::iree_compiler::Reducer::reduceLinalgOnTensorsDelta(
    ChunkManager &chunker, WorkItem &workItem) {
  ModuleOp module = workItem.getModule();

  /// TODO(Groverkss): This pass can work for any op producing a tensor result.
  /// Add support for more dialect operations.
  SmallVector<linalg::LinalgOp> linalgOps;
  SmallVector<linalg::LinalgOp> keepOps;
  module.walk([&](linalg::LinalgOp op) {
    if (!op.hasPureTensorSemantics()) {
      return;
    }
    // Op should have at least one tensor input, otherwise the operation is
    // already a fill-like operation.
    // TODO(Groverkss): Explore if we can remove in this case too.
    bool hasAtleastOneTensorInput = false;
    for (auto *input : op.getDpsInputOperands()) {
      if (isa<RankedTensorType>(input->get().getType())) {
        hasAtleastOneTensorInput = true;
        break;
      }
    }

    if (!hasAtleastOneTensorInput) {
      return;
    }

    // There should be only 1 tensor output.
    if (op.getNumDpsInits() != 1) {
      return;
    }
    if (!isa<RankedTensorType>(op.getDpsInitOperand(0)->get().getType())) {
      return;
    }

    if (!chunker.shouldFeatureBeKept()) {
      linalgOps.push_back(op);
    } else {
      keepOps.push_back(op);
    }
  });

  if (linalgOps.empty()) {
    return;
  }

  OpBuilder builder = workItem.getBuilder();

  // Insert util.optimization_barrier for results of keep dispatch ops.
  for (auto linalgOp : keepOps) {
    builder.setInsertionPointAfter(linalgOp);
    for (Value result : linalgOp->getResults()) {
      IREE::Util::OptimizationBarrierOp::create(builder, linalgOp.getLoc(),
                                                result);
    }
  }

  for (auto linalgOp : linalgOps) {
    builder.setInsertionPoint(linalgOp);
    Value out = linalgOp->getResult(0);
    Value init = linalgOp.getDpsInitOperand(0)->get();
    RankedTensorType outType = cast<RankedTensorType>(out.getType());
    Value newOut;

    // TODO(Groverkss): Add support for dynamic shapes using
    // ValueBoundsInterface.
    if (outType.hasStaticShape()) {
      for (auto *input : linalgOp.getDpsInputOperands()) {
        auto inType = dyn_cast<RankedTensorType>(input->get().getType());
        if (!inType) {
          continue;
        }

        // Check if we can replace an input directly with the output.
        if (inType == outType) {
          newOut = input->get();
          break;
        }
        // TODO(Groverkss): Add support for different shapes using
        // extract_slice/broadcast.
      }
    }

    if (newOut) {
      out.replaceAllUsesWith(newOut);
      linalgOp->erase();
      continue;
    }

    Type elType = outType.getElementType();
    // Builder::getZeroAttr only supports int/index/float element types. For
    // other element types (e.g. complex) we cannot synthesize a zero constant,
    // so leave the op untouched rather than crashing.
    if (!elType.isIntOrIndexOrFloat()) {
      continue;
    }
    // Build a constant 0 of the type.
    builder.setInsertionPoint(linalgOp);
    Value zero = arith::ConstantOp::create(builder, linalgOp.getLoc(),
                                           builder.getZeroAttr(elType));

    // Build linalg.fill for this out.
    newOut = linalg::FillOp::create(builder, linalgOp.getLoc(), zero, init)
                 .getResult(0);

    out.replaceAllUsesWith(newOut);
    linalgOp->erase();
  }

  PassManager pm(module.getContext());
  // Dead code eliminate.
  pm.addPass(createCSEPass());
  // De-duplicate identical fills.
  pm.addPass(createCanonicalizerPass());
  // Remove dead globals.
  pm.addPass(createSymbolDCEPass());
  if (failed(pm.run(module))) {
    return;
  }
}
