// 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 "iree/compiler/GlobalOptimization/Utils.h"

#include "mlir/Dialect/Linalg/IR/Linalg.h"
#include "mlir/Dialect/Linalg/Utils/Utils.h"
#include "mlir/Dialect/Tensor/IR/Tensor.h"

namespace mlir::iree_compiler::GlobalOptimization {

std::optional<CastOpInterface> getDefiningNonI1ExtendingCastOp(Value input) {
  // Returns true if the source of cast op has i1 element type.
  auto isI1Src = [](CastOpInterface castOp) -> bool {
    auto elemType = getElementTypeOrSelf(castOp->getOperandTypes()[0]);
    return elemType.getIntOrFloatBitWidth() == 1;
  };

  auto isExtending = [](CastOpInterface castOp) -> bool {
    auto inElemType = getElementTypeOrSelf(castOp->getOperandTypes()[0]);
    auto outElemType = getElementTypeOrSelf(castOp->getResultTypes()[0]);
    return inElemType.getIntOrFloatBitWidth() <
           outElemType.getIntOrFloatBitWidth();
  };

  std::optional<CastOpInterface> result = std::nullopt;
  auto castOp = input.getDefiningOp<CastOpInterface>();
  if (castOp) {
    if (!isI1Src(castOp) && isExtending(castOp)) {
      result = castOp;
    }
    return result;
  }
  auto genericOp = input.getDefiningOp<linalg::GenericOp>();
  if (!genericOp || genericOp.getNumDpsInputs() != 1 ||
      genericOp.getNumDpsInits() != 1 ||
      genericOp.getBody()->getOperations().size() != 2 ||
      !isElementwise(genericOp)) {
    return std::nullopt;
  }
  auto yieldOp = cast<linalg::YieldOp>(genericOp.getBody()->getTerminator());
  castOp = yieldOp->getOperand(0).getDefiningOp<CastOpInterface>();
  if (!castOp) {
    return std::nullopt;
  }
  Value castIn = castOp->getOperand(0);
  if (auto blockArg = dyn_cast<BlockArgument>(castIn)) {
    if (blockArg.getArgNumber() != 0) {
      return std::nullopt;
    }
  }
  if (!isI1Src(castOp) && isExtending(castOp)) {
    result = castOp;
  }
  return result;
}

std::optional<Type> getCastElemType(Value input) {
  std::optional<CastOpInterface> castOp =
      getDefiningNonI1ExtendingCastOp(input);
  if (!castOp) {
    return std::nullopt;
  }
  Type castSrcElemType = getElementTypeOrSelf(castOp.value()->getOperand(0));
  if (isa<arith::ExtUIOp>(castOp.value())) {
    int64_t bitWidth = castSrcElemType.getIntOrFloatBitWidth();
    return IntegerType::get(castOp->getContext(), bitWidth,
                            IntegerType::SignednessSemantics::Unsigned);
  }
  return castSrcElemType;
}

Value createGenericElementwiseCastOp(
    OpBuilder &builder, Location loc, Value input, CastOpInterface castOp,
    ArrayRef<NamedAttribute> attrs,
    std::optional<IREE::Encoding::EncodingAttr> encoding) {
  auto inputType = cast<RankedTensorType>(input.getType());
  SmallVector<AffineMap> maps(
      2, AffineMap::getMultiDimIdentityMap(inputType.getRank(),
                                           builder.getContext()));
  SmallVector<utils::IteratorType> iteratorTypes(inputType.getRank(),
                                                 utils::IteratorType::parallel);
  auto elementType = getElementTypeOrSelf(castOp->getResultTypes()[0]);
  auto castedType = inputType.clone(elementType);
  SmallVector<OpFoldResult> inputMixedSizes =
      tensor::getMixedSizes(builder, loc, input);
  Value init = encoding ? tensor::EmptyOp::create(builder, loc, inputMixedSizes,
                                                  elementType, *encoding)
                        : tensor::EmptyOp::create(builder, loc, inputMixedSizes,
                                                  elementType);
  return linalg::GenericOp::create(
             builder, loc, castedType, input, init, maps, iteratorTypes,
             [&](OpBuilder &b, Location nestedLoc, ValueRange args) {
               Value castRes =
                   b.create(nestedLoc, castOp->getName().getIdentifier(),
                            args[0], elementType)
                       ->getResult(0);
               linalg::YieldOp::create(b, nestedLoc, castRes);
             },
             attrs)
      .getResult(0);
}

Value sumReduceDimensionSubset(ImplicitLocOpBuilder &rewriter, Value val,
                               Type accETy, ArrayRef<bool> is_reduction) {
  auto context = val.getContext();
  RankedTensorType ty = cast<RankedTensorType>(val.getType());

  llvm::SmallVector<int64_t> staticSizes;
  SmallVector<Value> dynSizes;
  for (int i = 0, s = is_reduction.size(); i < s; i++) {
    if (is_reduction[i]) {
      continue;
    }

    staticSizes.push_back(ty.getDimSize(i));
    if (ty.isDynamicDim(i)) {
      dynSizes.push_back(tensor::DimOp::create(rewriter, val, i));
    }
  }

  // Create a zero-filled accumulator.
  Value initAcc =
      tensor::EmptyOp::create(rewriter, staticSizes, accETy, dynSizes);
  Value zeroInt = arith::ConstantIntOp::create(rewriter, accETy, 0).getResult();
  Value zeroAcc =
      linalg::FillOp::create(rewriter, zeroInt, initAcc).getResult(0);

  SmallVector<AffineExpr> filterExprs(ty.getRank());
  SmallVector<AffineExpr> outputExprs;
  SmallVector<utils::IteratorType> iterators;

  for (int i = 0, s = ty.getRank(); i < s; i++) {
    if (!is_reduction[i]) {
      auto expr = rewriter.getAffineDimExpr(iterators.size());
      iterators.push_back(utils::IteratorType::parallel);

      outputExprs.push_back(expr);
      filterExprs[i] = expr;
    }
  }

  for (int i = 0, s = filterExprs.size(); i < s; i++) {
    if (!filterExprs[i]) {
      auto expr = mlir::getAffineDimExpr(iterators.size(), context);
      iterators.push_back(utils::IteratorType::reduction);
      filterExprs[i] = expr;
    }
  }

  SmallVector<AffineMap> affineMaps{
      AffineMap::get(ty.getRank(), 0, filterExprs, context),
      AffineMap::get(ty.getRank(), 0, outputExprs, context)};

  return linalg::GenericOp::create(
             rewriter, zeroAcc.getType(), ValueRange{val}, ValueRange{zeroAcc},
             affineMaps, iterators,
             [=](OpBuilder &b, Location loc, ValueRange args) {
               Value ext = arith::ExtSIOp::create(b, loc, accETy, args[0]);
               Value sum = arith::AddIOp::create(b, loc, ext, args[1]);
               linalg::YieldOp::create(b, loc, sum);
             })
      .getResult(0);
}

} // namespace mlir::iree_compiler::GlobalOptimization
