#ifndef CAFFE2_OPERATORS_MINMAX_OPS_H_
#define CAFFE2_OPERATORS_MINMAX_OPS_H_

#include "caffe2/core/context.h"
#include "caffe2/core/logging.h"
#include "caffe2/core/operator.h"
#include "caffe2/core/types.h"
#include "caffe2/utils/math.h"
#include <c10/util/irange.h>

namespace caffe2 {

template <typename T, class Context>
class MaxOp final : public Operator<Context> {
 public:
  USE_OPERATOR_CONTEXT_FUNCTIONS;

  USE_SIMPLE_CTOR_DTOR(MaxOp)

  bool RunOnDevice() override {
    const auto& X0 = Input(0);
    auto* Y = Output(0);
    Y->ResizeLike(X0);
    const T* X0_data = X0.template data<T>();
    T* Y_data = Y->template mutable_data<T>();
    const int N = X0.numel();
    if (InputSize() == 1) {
      if (Y != &X0) {
        context_.template CopySameDevice<T>(N, X0_data, Y_data);
      }
      return true;
    }
    const auto& X1 = Input(1);
    CAFFE_ENFORCE_EQ(
        X0.sizes(),
        Y->sizes(),
        "Description: Input #1, input dimension:",
        X1.sizes(),
        " should match output dimension: ",
        Y->sizes());
    const T* X1_data = X1.template data<T>();
    math::Max<T, Context>(N, X0_data, X1_data, Y_data, &context_);
    for (const auto i : c10::irange(2, InputSize())) {
      const auto& Xi = Input(i);
      CAFFE_ENFORCE_EQ(
          Xi.sizes(),
          Y->sizes(),
          "Description: Input #",
          i,
          ", input dimension:",
          Input(i).sizes(),
          " should match output dimension: ",
          Y->sizes());
      const T* Xi_data = Xi.template data<T>();
      math::Max<T, Context>(N, Y_data, Xi_data, Y_data, &context_);
    }
    return true;
  }
};

template <typename T, class Context>
class MinOp final : public Operator<Context> {
 public:
  USE_OPERATOR_CONTEXT_FUNCTIONS;

  USE_SIMPLE_CTOR_DTOR(MinOp)

  bool RunOnDevice() override {
    const auto& X0 = Input(0);
    auto* Y = Output(0);
    Y->ResizeLike(X0);
    const T* X0_data = X0.template data<T>();
    T* Y_data = Y->template mutable_data<T>();
    const int N = X0.numel();
    if (InputSize() == 1) {
      if (Y != &X0) {
        context_.template CopySameDevice<T>(N, X0_data, Y_data);
      }
      return true;
    }
    const auto& X1 = Input(1);
    CAFFE_ENFORCE_EQ(
        X0.sizes(),
        Y->sizes(),
        "Description: Input #1, input dimension:",
        X1.sizes(),
        " should match output dimension: ",
        Y->sizes());
    const T* X1_data = X1.template data<T>();
    math::Min<T, Context>(N, X0_data, X1_data, Y_data, &context_);
    for (const auto i : c10::irange(2, InputSize())) {
      const auto& Xi = Input(i);
      CAFFE_ENFORCE_EQ(
          Xi.sizes(),
          Y->sizes(),
          "Description: Input #",
          i,
          ", input dimension:",
          Input(i).sizes(),
          " should match output dimension: ",
          Y->sizes());
      const T* Xi_data = Xi.template data<T>();
      math::Min<T, Context>(N, Y_data, Xi_data, Y_data, &context_);
    }
    return true;
  }
};

template <typename T, class Context>
class SelectGradientOpBase : public Operator<Context> {
 public:
  USE_OPERATOR_CONTEXT_FUNCTIONS;
  USE_SIMPLE_CTOR_DTOR(SelectGradientOpBase)

  bool RunOnDevice() override;
};

template <typename T, class Context>
class MaxGradientOp final : public SelectGradientOpBase<T, Context> {
 public:
  template <class... Args>
  explicit MaxGradientOp(Args&&... args)
      : SelectGradientOpBase<T, Context>(std::forward<Args>(args)...) {}

  ~MaxGradientOp() = default;
};

template <typename T, class Context>
class MinGradientOp final : public SelectGradientOpBase<T, Context> {
 public:
  template <class... Args>
  explicit MinGradientOp(Args&&... args)
      : SelectGradientOpBase<T, Context>(std::forward<Args>(args)...) {}

  ~MinGradientOp() = default;
};

} // namespace caffe2

#endif // CAFFE2_OPERATORS_MINMAX_OPS_H_
