#include "elementwise_linear_op.h"

namespace caffe2 {

template<>
bool ElementwiseLinearOp<float, CPUContext>::RunOnDevice(){
  const auto& X = Input(0);
  const auto& a = Input(1);
  const auto& b = Input(2);

  const auto canonical_axis = X.canonical_axis_index(axis_);
  const int N = X.size_to_dim(canonical_axis);
  const int D = X.size_from_dim(canonical_axis);

  CAFFE_ENFORCE_EQ(a.dim(), 1, a.dim());
  CAFFE_ENFORCE_EQ(a.size(0), D, a.dim());
  CAFFE_ENFORCE_EQ(b.dim(), 1, b.dim());
  CAFFE_ENFORCE_EQ(b.size(0), D, b.dim());

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

  const float* X_data = X.data<float>();
  const float* a_data = a.data<float>();
  const float* b_data = b.data<float>();
  float* Y_data = Y->template mutable_data<float>();

  int p = 0;
  for (int n = 0; n < N; ++n) {
    for (int d = 0; d < D; ++d) {
      Y_data[p] = X_data[p] * a_data[d] + b_data[d];
      p++;
    }
  }
  return true;
}

template<>
bool ElementwiseLinearGradientOp<float, CPUContext>::RunOnDevice(){
  const auto& g_o = Input(0);
  const auto& X = Input(1);
  const auto& a = Input(2);

  const auto canonical_axis = X.canonical_axis_index(axis_);
  const int N = X.size_to_dim(canonical_axis);
  const int D = X.size_from_dim(canonical_axis);

  CAFFE_ENFORCE_EQ(a.dim(), 1, a.dim());
  CAFFE_ENFORCE_EQ(a.size(0), D, a.dim());

  auto* g_X = Output(0, X.sizes(), at::dtype<float>());
  auto* g_a = Output(1, a.sizes(), at::dtype<float>());
  auto* g_b = Output(2, a.sizes(), at::dtype<float>());

  const float* g_o_data = g_o.data<float>();
  const float* X_data = X.data<float>();
  const float* a_data = a.data<float>();
  float* g_X_data = g_X->template mutable_data<float>();
  float* g_a_data = g_a->template mutable_data<float>();
  float* g_b_data = g_b->template mutable_data<float>();

  math::Set<float, CPUContext>(g_a->numel(), 0.f, g_a_data, &context_);
  math::Set<float, CPUContext>(g_b->numel(), 0.f, g_b_data, &context_);

  int p = 0;
  for (int n = 0; n < N; ++n) {
    for (int d = 0; d < D; ++d) {
      g_X_data[p] = g_o_data[p] * a_data[d];
      g_a_data[d] += g_o_data[p] * X_data[p];
      g_b_data[d] += g_o_data[p];
      p++;
    }
  }
  return true;
}

REGISTER_CPU_OPERATOR(
  ElementwiseLinear,
  ElementwiseLinearOp<float, CPUContext>);
REGISTER_CPU_OPERATOR(
  ElementwiseLinearGradient,
  ElementwiseLinearGradientOp<float, CPUContext>);

OPERATOR_SCHEMA(ElementwiseLinear)
    .NumInputs(3)
    .NumOutputs(1)
    .SetDoc(R"DOC(
This op computes the elementwise linear combination of a batch of input vectors with a weight vector and bias vector. As input, the op takes an input tensor $X$ of shape $NxD$, a weight vector $w$ of length $D$, and a bias vector $b$ of length $D$. Here, $N$ represents the batch size and $D$ represents the length of the feature vectors. The output, $Y$, is a tensor of shape $NxD$ and is calculated as

$$Y_{ij} = X_{ij}w_j + b_j \ for \ i\in{N}, j\in{D}$$

Github Links:
- https://github.com/pytorch/pytorch/blob/master/caffe2/operators/elementwise_linear_op.h
- https://github.com/pytorch/pytorch/blob/master/caffe2/operators/elementwise_linear_op.cc

<details>

<summary> <b>Example</b> </summary>

**Code**

```

workspace.ResetWorkspace()

op = core.CreateOperator(
    "ElementwiseLinear",
    ["X", "w", "b"],
    ["Y"]
)

// Create X
X = np.array([[1,2,3,4,5],[6,8,9,16,10]])
print("X:\n",X)

// Create w
w = np.array([1,1/2.,1/3.,1/4.,1/5.])
print("w:\n",w)

// Create b
b = np.array([1.,1.,1.,1.,1.])
print("b:\n",b)


// Feed X & w & b into workspace
workspace.FeedBlob("X", X.astype(np.float32))
workspace.FeedBlob("w", w.astype(np.float32))
workspace.FeedBlob("b", b.astype(np.float32))

// Run op
workspace.RunOperatorOnce(op)

// Collect Output
print("Y:\n", workspace.FetchBlob("Y"))

```

**Result**

```

X:
 [[ 1  2  3  4  5]
 [ 6  8  9 16 10]]
w:
 [1.  0.5  0.33333333 0.25  0.2]
b:
 [1. 1. 1. 1. 1.]
Y:
 [[2. 2. 2. 2. 2.]
 [7. 5. 4. 5. 3.]]

```

</details>

  )DOC")
    .Input(
        0,
        "X",
        "2D input tensor of size $NxD$. This input represents the input data to be operated on.")
    .Input(
        1,
        "w",
        "1D scaling factors, or weights, of size $D$. This input contains the weights that will be multiplied by the data.")
    .Input(
        2,
        "b",
        "1D biases of size $D$. This input contains the biases that will be added to the products of the weights and data.")
    .Output(
        0,
        "Y",
        "2D output tensor of size $NxD$. Calculated as described above.")
    .Arg(
        "axis",
        "*(type: int; default: 1)* Describes the axis of the inputs; defaults to one because the 0th axis most likely describes the batch size.")
    .InheritOnnxSchema();

OPERATOR_SCHEMA(ElementwiseLinearGradient)
  .NumInputs(3)
  .NumOutputs(3);

struct GetElementwiseLinearGradient : public GradientMakerBase {
  using GradientMakerBase::GradientMakerBase;
  vector<OperatorDef> GetGradientDefs() override {
    return SingleGradientDef(
      "ElementwiseLinearGradient",
      "",
      vector<string>{GO(0), I(0), I(1)},
      vector<string>{GI(0), GI(1), GI(2)});
    }
};

REGISTER_GRADIENT(
  ElementwiseLinear,
  GetElementwiseLinearGradient
);

}  // namespace caffe2
