#include "caffe2/operators/batch_permutation_op.h"

#include <cstring>
#include <vector>

#ifdef USE_MKLDNN
#include <caffe2/ideep/operators/operator_fallback_ideep.h>
#include <caffe2/ideep/utils/ideep_operator.h>
#endif

namespace caffe2 {

template <bool forwards>
void batch_permutation_loop(
    const int N,
    const int K,
    const float* src,
    const int* indices,
    float* dst) {
  // NOLINTNEXTLINE(cppcoreguidelines-narrowing-conversions,bugprone-narrowing-conversions)
  long numBytes = K * sizeof(float);
  if (forwards) {
#ifdef _OPENMP
#pragma omp parallel for
#endif
    for (int n = 0; n < N; n++) {
      int origIdx = n * K;
      int permuteIdx = indices[n] * K;
      std::memcpy(dst + origIdx, src + permuteIdx, numBytes);
    }
  } else {
    std::vector<int> backward_indices(N);
    for (int i = 0; i < N; ++i) {
      backward_indices[indices[i]] = i;
    }
    for (int n = 0; n < N; n++) {
      int permuteIdx = n * K;
      int origIdx = backward_indices[n] * K;
      std::memcpy(dst + permuteIdx, src + origIdx, numBytes);
    }
  }
}

template <>
bool BatchPermutationOp<float, CPUContext>::RunOnDevice() {
  auto& X = Input(0);
  auto& indices = Input(1);

  CAFFE_ENFORCE(indices.dim() == 1, "indices must be 1-d");
  CAFFE_ENFORCE(
      X.dim32(0) == indices.dim32(0),
      "X.dim32(0) must be equal to indices.dim32(0)",
      "(",
      X.dim32(0),
      " vs. ",
      indices.dim32(0),
      ")");

  auto* Y = Output(0, X.sizes(), at::dtype<float>());

  if (X.dim32(0) > 0) {
    batch_permutation_loop<true>(
        X.dim32(0),
        // NOLINTNEXTLINE(cppcoreguidelines-narrowing-conversions,bugprone-narrowing-conversions)
        X.numel() / X.dim32(0),
        X.data<float>(),
        indices.data<int>(),
        Y->mutable_data<float>());
  }
  return true;
}

template <>
bool BatchPermutationGradientOp<float, CPUContext>::RunOnDevice() {
  auto& indices = Input(0);
  auto& dY = Input(1);

  auto* dX = Output(0, dY.sizes(), at::dtype<float>());

  if (dY.dim32(0) > 0) {
    batch_permutation_loop<false>(
        dY.dim32(0),
        // NOLINTNEXTLINE(cppcoreguidelines-narrowing-conversions,bugprone-narrowing-conversions)
        dY.numel() / dY.dim32(0),
        dY.data<float>(),
        indices.data<int>(),
        dX->mutable_data<float>());
  }
  return true;
}

#ifdef USE_MKLDNN
REGISTER_IDEEP_OPERATOR(
    BatchPermutation,
    IDEEPFallbackOp<BatchPermutationOp<float, CPUContext>>);
#endif

REGISTER_CPU_OPERATOR(BatchPermutation, BatchPermutationOp<float, CPUContext>);
REGISTER_CPU_OPERATOR(
    BatchPermutationGradient,
    BatchPermutationGradientOp<float, CPUContext>);

// Input: X, indices; Output: Y
OPERATOR_SCHEMA(BatchPermutation)
    .NumInputs(2)
    .NumOutputs(1)
    .SetDoc(R"DOC(
Batch permutation of an input tensor X given input indices. First dimension of
X equals batch size N. The indices stores a be permutation of N.
The output Y is a tensor of same shape as X, with data re-ordered according to
the indices within the batch size.

Example of batch permutation on a 2-D tensor with batch size 4:
  X = [
    [1, 5, 2, 3, 4, 6, 0],
    [4, 3, 3, 5, 2, 3, 1],
    [2, 2, 3, 6, 0, 0, 1],
    [0, 0, 1, 1, 2, 2, 3]
  ]
  indices = [2, 0, 1, 3]
  Y = [
    [2, 2, 3, 6, 0, 0, 1],
    [1, 5, 2, 3, 4, 6, 0],
    [4, 3, 3, 5, 2, 3, 1],
    [0, 0, 1, 1, 2, 2, 3]
  ]

Example of batch permutation on a 3-D tensor with batch size 4:
  X = [
    [[1, 5, 2], [3, 4, 6, 0]],
    [[4, 3, 3], [5, 2, 3, 1]],
    [[2, 2, 3], [6, 0, 0, 1]],
    [[0, 0, 1], [1, 2, 2, 3]]
  ]
  indices = [2, 0, 1, 3]
  Y = [
    [[2, 2, 3], [6, 0, 0, 1]],
    [[1, 5, 2], [3, 4, 6, 0]],
    [[4, 3, 3], [5, 2, 3, 1]],
    [[0, 0, 1], [1, 2, 2, 3]]
  ]
)DOC")
    .Input(0, "X", "Input tensor, where 1st dimension equals batch size")
    .Input(1, "indices", "Input indices of batch to permute")
    .Output(0, "Y", "Output permuted tensor");
// Input: indices, dY (aka "gradOutput"); Output: dX (aka "gradInput")
OPERATOR_SCHEMA(BatchPermutationGradient).NumInputs(2).NumOutputs(1);

class GetBatchPermutationGradient : public GradientMakerBase {
  using GradientMakerBase::GradientMakerBase;
  vector<OperatorDef> GetGradientDefs() override {
    return SingleGradientDef(
        "BatchPermutationGradient",
        "",
        vector<string>{I(1), GO(0)},
        vector<string>{GI(0)});
  }
};

REGISTER_GRADIENT(BatchPermutation, GetBatchPermutationGradient);

} // namespace caffe2

using BatchPermutationOpFloatCPU =
    caffe2::BatchPermutationOp<float, caffe2::CPUContext>;

C10_EXPORT_CAFFE2_OP_TO_C10_CPU(
    BatchPermutation,
    "_caffe2::BatchPermutation(Tensor X, Tensor indices) -> Tensor",
    BatchPermutationOpFloatCPU);
