// Copyright 2022 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/Codegen/Utils/LinalgOpInfo.h"

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

using namespace mlir::linalg;

namespace mlir::iree_compiler {

/// Returns true if `map` is a tranpose. A transpose map is a projected
/// permutation with or without zeros in results where there exist at least two
/// dimensions di and dj such that di < dj and result_pos(di) > result_pos(dj).
/// Examples:
///
///  (d0, d1, d2) -> (d0, d2) is not a transpose map.
///  (d0, d1, d2) -> (d2, d0) is a transpose map.
///  (d0, d1, d2) -> (d1, d2) is not a transpose map.
///  (d0, d1, d2) -> (d0, 0, d1) is not a transpose map.
///  (d0, d1, d2) -> (d2, 0, d1) is a transpose map.
///  (d0, d1, d2) -> (d1, 0) is not a transpose map.
///
// TODO(dcaballe): Discern between "memcopy" transposes and "shuffle"
// transposes.
// TODO(dcaballe): Move to Affine utils?
static bool isTransposeMap(AffineMap map) {
  // A transpose map must be a projected permutation with or without
  // broadcasted/reduction dimensions.
  if (!map.isProjectedPermutation(/*allowZeroInResults=*/true)) {
    return false;
  }

  // Check that the projected permutation has at least two result dimensions
  // that are actually transposed by comparing its input position.
  unsigned prevDim = 0;
  for (AffineExpr expr : map.getResults()) {
    if (auto constExpr = dyn_cast<AffineConstantExpr>(expr)) {
      // Constant zero expression, guaranteed by 'allowZeroInResults' above.
      continue;
    } else if (auto dimExpr = dyn_cast<AffineDimExpr>(expr)) {
      if (prevDim > dimExpr.getPosition()) {
        return true;
      }
      prevDim = dimExpr.getPosition();
    } else {
      return false;
    }
  }

  return false;
}

/// The default filter passes all op operands.
static bool defaultTransposeMapFilter(AffineMap map) { return true; }

LinalgOpInfo::LinalgOpInfo(linalg::LinalgOp linalgOp)
    : transposeMapFilter(defaultTransposeMapFilter) {
  computeInfo(linalgOp);
}
LinalgOpInfo::LinalgOpInfo(linalg::LinalgOp linalgOp,
                           TransposeMapFilter transposeMapFilter)
    : transposeMapFilter(transposeMapFilter) {
  computeInfo(linalgOp);
}

/// Returns true if a LinalgOp implements a transpose.
// TODO(dcaballe):
//   * Consider transpose + reductions.
//   * Consider input and output transposes.
static SmallVector<OpOperand *>
computeTransposeInfo(LinalgOp linalgOp,
                     LinalgOpInfo::TransposeMapFilter transposeMapFilter) {
  SmallVector<OpOperand *> transposeOperands;

  // Reductions are not supported.
  if (linalgOp.getNumReductionLoops() > 0) {
    return transposeOperands;
  }

  // Inverse map to use transfer op permutation logic.
  AffineMap outputInversedMap = inversePermutation(
      linalgOp.getMatchingIndexingMap(linalgOp.getDpsInitOperand(0)));

  SmallVector<AffineMap> inputInversedMaps;
  for (OpOperand *linalgOperand : linalgOp.getDpsInputOperands()) {
    auto map = linalgOp.getMatchingIndexingMap(linalgOperand);
    if (!map.isProjectedPermutation(/*allowZeroInResults=*/true)) {
      return transposeOperands;
    }
    AffineMap inverseMap = inverseAndBroadcastProjectedPermutation(map);
    if (isTransposeMap(inverseMap) && transposeMapFilter(inverseMap)) {
      transposeOperands.push_back(linalgOperand);
    }
  }

  // Multiple outputs are not supported yet.
  if (linalgOp.getNumDpsInits() != 1) {
    return transposeOperands;
  }

  if (isTransposeMap(outputInversedMap) &&
      transposeMapFilter(outputInversedMap)) {
    transposeOperands.push_back(linalgOp.getDpsInitOperand(0));
  }

  return transposeOperands;
}

static bool computeReductionInfo(LinalgOp linalgOp) {
  return linalgOp.getNumReductionLoops() >= 1;
}

static bool computeDynamicInfo(LinalgOp linalgOp) {
  return linalgOp.hasDynamicShape();
}

void LinalgOpInfo::computeInfo(LinalgOp linalgOp) {
  transposeOperands = computeTransposeInfo(linalgOp, transposeMapFilter);
  reductionTrait = computeReductionInfo(linalgOp);
  dynamicTrait = computeDynamicInfo(linalgOp);
}

bool isMatmulOrBatchMatmul(linalg::LinalgOp linalgOp) {
  // (Batch) matmul should be a reduction op with 2/3 parallel dimensions.
  if (!linalg::isaContractionOpInterface(linalgOp) ||
      !llvm::is_contained({2u, 3u}, linalgOp.getNumParallelLoops()))
    return false;

  // Also exclude the case of matvec, which has only one non-unit parallel dim.
  // They should go down different pipelines.
  int nonUnitParallelDimCount = 0;
  SmallVector<int64_t, 4> bounds = linalgOp.getStaticLoopRanges();
  FailureOr<mlir::linalg::ContractionDimensions> contractionDims =
      mlir::linalg::inferContractionDims(linalgOp);
  assert(succeeded(contractionDims) && "Could not infer contraction dims");
  for (auto mDim : contractionDims->m) {
    nonUnitParallelDimCount += bounds[mDim] != 1;
  }
  for (auto nDim : contractionDims->n) {
    nonUnitParallelDimCount += bounds[nDim] != 1;
  }
  return nonUnitParallelDimCount > 1;
}

} // namespace mlir::iree_compiler
