#include "caffe2/operators/boolean_unmask_ops.h"
#include "caffe2/core/operator.h"
#include "caffe2/core/tensor.h"

namespace caffe2 {

template <>
bool BooleanUnmaskOp<CPUContext>::RunOnDevice() {
  int maskSize = Input(0).numel();
  int numMasks = InputSize() / 2;
  auto& valueMeta = Input(1).dtype();

  auto* valuesOut = Output(0);
  valuesOut->Resize(maskSize);
  auto* valuesOutPtr = (char*)valuesOut->raw_mutable_data(valueMeta);

  std::vector<int> nextValueIndices(numMasks, 0);
  for (int maskOffset = 0; maskOffset < maskSize; ++maskOffset) {
    bool maskFound = false;
    for (int maskIndex = 0; maskIndex < numMasks; ++maskIndex) {
      auto& mask = Input(maskIndex * 2);
      CAFFE_ENFORCE_EQ(mask.dim(), 1);
      CAFFE_ENFORCE_EQ(mask.numel(), maskSize);
      const auto* maskPtr = mask.template data<bool>();

      auto& values = Input(maskIndex * 2 + 1);
      CAFFE_ENFORCE_EQ(values.dim(), 1);
      const auto* valuesPtr = (char*)values.raw_data();

      if (maskPtr[maskOffset]) {
        auto& valueIndex = nextValueIndices[maskIndex];
        CAFFE_ENFORCE_LT(valueIndex, values.numel());
        auto* src = valuesPtr + (valueIndex++) * valueMeta.itemsize();
        auto* dst = valuesOutPtr + maskOffset * valueMeta.itemsize();
        std::copy(src, src + valueMeta.itemsize(), dst);
        maskFound = true;
        break;
      }
    }
    CAFFE_ENFORCE(
        maskFound, "All masks have False at position ", maskOffset, ".");
  }
  // check all indices match value length
  for (int i = 0; i < numMasks; ++i) {
    auto& values = Input(i * 2 + 1);
    CAFFE_ENFORCE_EQ(
        values.numel(),
        nextValueIndices[i],
        "The number of true at mask ",
        i,
        " does not match the corresponding value size.");
  }
  return true;
}

REGISTER_CPU_OPERATOR(BooleanUnmask, BooleanUnmaskOp<CPUContext>);

OPERATOR_SCHEMA(BooleanUnmask)
    .NumInputs([](int n) { return n > 0 && n % 2 == 0; })
    .NumOutputs(1)
    .SetDoc(R"DOC(
Given a series of masks and values, reconstruct values together according to masks. A comprehensive example:
```
mask1   = True, False, True, False, False
values1 = 1.0, 3.0
mask2   = False, True, False, False, False
values2 = 2.0
mask3   = False, False, False, True, True
values3 = 4.0, 5.0
```

Reconstruct by:

```
output = net.BooleanUnmask([mask1, values1, mask2, values2, mask3, values3], ["output"])
output = 1.0, 2.0, 3.0, 4.0, 5.0
```

Note that for all mask positions, there must be at least one True. This is not allowed:

```
mask1   = True, False
values1 = 1.0
mask2   = False, False
values2 =

output = net.BooleanUnmask([mask1, values1, mask2, values2], ["output"])
```

If there are multiple True values for a field, we accept the first value, and no longer expect a value for that location:

```
mask1   = True, False
values1 = 1.0
mask2   = True, True
values2 = 2.0

output = net.BooleanUnmask([mask1, values1, mask2, values2], ["output"])
output = 1.0, 2.0
```

*** Note that we alternate `data` and `mask` inputs

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

<details>

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

**Code**

```

workspace.ResetWorkspace()

op = core.CreateOperator(
    "BooleanUnmask",
    ["mask1", "data1", "mask2", "data2"],
    ["unmasked_data"]
)

workspace.FeedBlob("mask1", np.array([True,False,False,True,True,False]))
workspace.FeedBlob("data1", np.array([1,4,5]))
workspace.FeedBlob("mask2", np.array([False,True,True,False,False,True]))
workspace.FeedBlob("data2", np.array([2,3,6]))

print("data1:", workspace.FetchBlob("data1"))
print("mask1:", workspace.FetchBlob("mask1"))
print("data2:", workspace.FetchBlob("data2"))
print("mask2:", workspace.FetchBlob("mask2"))
workspace.RunOperatorOnce(op)
print("unmasked_data:", workspace.FetchBlob("unmasked_data"))

```

**Result**

```

data1: [1 4 5]
mask1: [ True False False  True  True False]
data2: [2 3 6]
mask2: [False  True  True False False  True]
unmasked_data: [1 2 3 4 5 6]

```

</details>
)DOC")
    .Input(0,"data","(*Tensor*): 1D input tensor(s)")
    .Input(1,"mask","(*Tensor`<bool>`*): 1D boolean mask tensor(s)")
    .Output(0, "unmasked_data", "(*Tensor*): 1D tensor of same type as `data` input that contains the unmasked input tensor");

NO_GRADIENT(BooleanUnmask)
}
