#include "caffe2/onnx/backend.h"
#include "caffe2/core/logging.h"
#include "caffe2/core/operator.h"
#include "caffe2/onnx/device.h"
#include "caffe2/onnx/helper.h"
#include "caffe2/utils/map_utils.h"
#include "caffe2/utils/proto_utils.h"

#ifndef C10_MOBILE
#include "onnx/checker.h"
#endif

#include "google/protobuf/io/coded_stream.h"
#include "google/protobuf/io/zero_copy_stream_impl_lite.h"

#include <cmath>
#include <iostream>
#include <limits>
#include <sstream>
#include <unordered_map>
#include <unordered_set>

namespace caffe2 {
namespace onnx {

namespace {

bool AlmostEqual(double a, double b) {
  constexpr static double kEps = 1e-15;
  return (fabs(a - b) < kEps);
}

template <class T>
bool TryConvertingTensorRawValues(
    const TensorProto& onnx_tensor,
    ::google::protobuf::RepeatedField<T>* field) {
  if (!onnx_tensor.has_raw_data()) {
    return false;
  }

  size_t raw_size = onnx_tensor.raw_data().size();
  CAFFE_ENFORCE_EQ(raw_size % sizeof(T), 0);

  size_t num_elements = raw_size / sizeof(T);
  const void* src_ptr = static_cast<const void*>(onnx_tensor.raw_data().data());
  field->Resize(num_elements, 0);
  void* target_ptr = static_cast<void*>(field->mutable_data());
  memcpy(target_ptr, src_ptr, raw_size);

  return true;
}

bool IsOperator(const std::string& op_type) {
  // pull in all the operators upon first invocation
  // Intentional leaky
  static std::set<std::string>* ops_ =
      new std::set<std::string>(caffe2::GetRegisteredOperators());
  return ops_->count(caffe2::OpRegistryKey(op_type, "DEFAULT"));
}

caffe2::DeviceOption GetDeviceOption(const Device& onnx_device) {
  static const std::unordered_map<DeviceType, caffe2::DeviceType> m = {
      {DeviceType::CPU, caffe2::DeviceType::CPU},
      {DeviceType::CUDA, caffe2::DeviceType::CUDA}};
  caffe2::DeviceOption d;
  d.set_device_type(static_cast<int32_t>(m.at(onnx_device.type)));
  d.set_device_id(onnx_device.device_id);
  return d;
}

template <class T, class U>
U LookUpWithDefault(
    const std::unordered_map<T, U>& map,
    const T& key,
    const U& default_value) {
  const auto it = map.find(key);
  if (it == map.end()) {
    return default_value;
  } else {
    return it->second;
  }
}

void UpdateNames(
    std::shared_ptr<DummyName> dummy,
    const caffe2::OperatorDef& op) {
  for (const auto& n : op.input()) {
    dummy->AddName(n);
  }
  for (const auto& n : op.output()) {
    dummy->AddName(n);
  }
}

void BuildOperator(
    caffe2::OperatorDef* c2_op,
    const std::string& op_type,
    const std::vector<std::string>& inputs,
    const std::vector<std::string>& outputs,
    const std::vector<caffe2::Argument>& args) {
  c2_op->set_name("");
  c2_op->set_type(op_type);
  for (const auto& input : inputs) {
    c2_op->add_input(input);
  }
  for (const auto& output : outputs) {
    c2_op->add_output(output);
  }
  for (const auto& arg : args) {
    auto* tmp = c2_op->add_arg();
    tmp->CopyFrom(arg);
  }
}

void BuildOperator(
    caffe2::OperatorDef* c2_op,
    const std::string& op_type,
    const std::vector<std::string>& inputs,
    const std::vector<std::string>& outputs) {
  std::vector<caffe2::Argument> empty;
  BuildOperator(c2_op, op_type, inputs, outputs, empty);
}

void CopyOnnxAttrValueToCaffe2Arg(
    caffe2::Argument* arg,
    const AttributeProto& attr) {
  if (attr.has_f()) {
    arg->set_f(attr.f());
  } else if (attr.has_i()) {
    arg->set_i(attr.i());
  } else if (attr.has_s()) {
    arg->set_s(attr.s());
  } else if (attr.has_t()) {
    // For proto, we convert it to serialized string
    std::string buffer;
    attr.t().SerializeToString(&buffer);
    arg->set_s(buffer);
  } else if (attr.floats_size()) {
    arg->mutable_floats()->CopyFrom(attr.floats());
  } else if (attr.ints_size()) {
    arg->mutable_ints()->CopyFrom(attr.ints());
  } else if (attr.strings_size()) {
    arg->mutable_strings()->CopyFrom(attr.strings());
  } else {
    CAFFE_THROW("Unsupported ONNX attribute: ", attr.name());
  }
}
} // namespace

OnnxAttributes::OnnxAttributes(const NodeProto& node) {
  for (const auto& attr : node.attribute()) {
    onnx_attrs_.emplace(attr.name(), &attr);
  }
}

template <>
int64_t OnnxAttributes::get(const std::string& key) const {
  int64_t value = 0;
  const auto it = onnx_attrs_.find(key);
  if (it != onnx_attrs_.end()) {
    const AttributeProto& attr = *it->second;
    value = attr.i();
  }
  return value;
}

template <>
float OnnxAttributes::get(const std::string& key) const {
  float value = 0.0;
  const auto it = onnx_attrs_.find(key);
  if (it != onnx_attrs_.end()) {
    const AttributeProto& attr = *it->second;
    value = attr.f();
  }
  return value;
}

template <>
::google::protobuf::RepeatedPtrField<std::string> OnnxAttributes::get(
    const std::string& key) const {
  ::google::protobuf::RepeatedPtrField<std::string> value;
  const auto it = onnx_attrs_.find(key);
  if (it != onnx_attrs_.end()) {
    const AttributeProto& attr = *it->second;
    value.CopyFrom(attr.strings());
  }
  return value;
}

template <>
::google::protobuf::RepeatedField<::google::protobuf::int64>
OnnxAttributes::get(const std::string& key) const {
  ::google::protobuf::RepeatedField<::google::protobuf::int64> value;
  const auto it = onnx_attrs_.find(key);
  if (it != onnx_attrs_.end()) {
    const AttributeProto& attr = *it->second;
    value.CopyFrom(attr.ints());
  }
  return value;
}

template <>
::google::protobuf::RepeatedField<float> OnnxAttributes::get(
    const std::string& key) const {
  ::google::protobuf::RepeatedField<float> value;
  const auto it = onnx_attrs_.find(key);
  if (it != onnx_attrs_.end()) {
    const AttributeProto& attr = *it->second;
    value.CopyFrom(attr.floats());
  }
  return value;
}

template <>
const TensorProto* OnnxAttributes::get(const std::string& key) const {
  const TensorProto* value = nullptr;
  const auto it = onnx_attrs_.find(key);
  if (it != onnx_attrs_.end()) {
    const AttributeProto& attr = *it->second;
    value = &attr.t();
  }
  return value;
}

::google::protobuf::RepeatedPtrField<caffe2::Argument>
OnnxAttributes::OnnxAttrToCaffe2Arg(
    std::function<std::string(const std::string&)> mapper) const {
  ::google::protobuf::RepeatedPtrField<caffe2::Argument> args;
  for (const auto& kv : onnx_attrs_) {
    // If the attribute was rewritten, we use it instead. Note that the
    // rewritten attribute still has the unmapped name
    const auto& attr = rewritten_onnx_attrs_.count(kv.first)
        ? rewritten_onnx_attrs_.at(kv.first)
        : (*kv.second);
    auto* arg = args.Add();
    arg->set_name(mapper(attr.name()));
    CopyOnnxAttrValueToCaffe2Arg(arg, attr);
  }
  for (const auto& kv : rewritten_onnx_attrs_) {
    // If rewritten attribute doesn't appear in the original attributes, this is
    // a newlly added one and we need to add this to argument too
    if (!onnx_attrs_.count(kv.first)) {
      const auto& attr = kv.second;
      auto* arg = args.Add();
      arg->set_name(mapper(attr.name()));
      CopyOnnxAttrValueToCaffe2Arg(arg, attr);
    }
  }

  return args;
}

const std::unordered_map<std::string, int>&
Caffe2Backend::get_broken_operators() const {
  const static std::unordered_map<std::string, int> kBrokenOperators{};
  return kBrokenOperators;
}

// Temporary hack for RNN related operators, as we don't have C++ interface in
// C2 to build those operators yet
const std::unordered_set<std::string>& Caffe2Backend::get_rnn_operators()
    const {
  const static std::unordered_set<std::string> kRNNOperators{
      "LSTM", "GRU", "RNN"};
  return kRNNOperators;
}

// Operators that are different between Caffe2 and
// ONNX but only in their name.
// In most cases, this should be empty - as the effort of ONNX is
// to unify the operator definitions.
const std::unordered_map<std::string, std::string>&
Caffe2Backend::get_renamed_operators() const {
  const static std::unordered_map<std::string, std::string> kRenamedOperators{
      {"Caffe2ConvTranspose", "ConvTranspose"},
      {"GlobalMaxPool", "MaxPool"},
      {"GlobalAveragePool", "AveragePool"},
      {"Pad", "PadImage"},
      {"Neg", "Negative"},
      {"BatchNormalization", "SpatialBN"},
      {"InstanceNormalization", "InstanceNorm"},
      {"MatMul", "BatchMatMul"},
      {"Upsample", "ResizeNearest"},
      {"Identity", "Copy"},
      {"InstanceNormalization", "InstanceNorm"},
      {"Equal", "EQ"},
      {"Less", "LT"},
      {"Greater", "GT"},
      {"Unsqueeze", "ExpandDims"},
      {"Tile", "NumpyTile"},
      {"DynamicSlice", "Slice"},
      {"ConstantOfShape", "ConstantFill"},
      {"RandomNormal", "GaussianFill"},
      {"RandomNormalLike", "GaussianFill"}};
  return kRenamedOperators;
}

const std::unordered_map<std::string, std::string>&
Caffe2Backend::get_renamed_attrs() const {
  const static std::unordered_map<std::string, std::string> kRenamedAttrs{
      {"kernel_shape", "kernels"}};
  return kRenamedAttrs;
}

const std::
    unordered_map<std::string, std::unordered_map<std::string, std::string>>&
    Caffe2Backend::get_per_op_renamed_attrs() const {
  const static std::
      unordered_map<std::string, std::unordered_map<std::string, std::string>>
          kPerOpRenamedAttrs = {
              {"Squeeze", {{"axes", "dims"}}},
              {"Unsqueeze", {{"axes", "dims"}}},
              {"Transpose", {{"perm", "axes"}}},
              {"ConvTranspose", {{"output_padding", "adjs"}}},
              {"Selu", {{"gamma", "scale"}}}};

  return kPerOpRenamedAttrs;
}

// operators whose behavior is different beyond renaming
// the value is an attribute of this class that is a
// function from ToffeIR node_def to caffe2 op_def
const std::unordered_map<std::string, Caffe2Backend::SpecialOpConverter>&
Caffe2Backend::get_special_operators() const {
  const static std::
      unordered_map<std::string, Caffe2Backend::SpecialOpConverter>
          kSpecialOperators = {
              {"ArgMax", &Caffe2Backend::CreateArgMaxMin},
              {"ArgMin", &Caffe2Backend::CreateArgMaxMin},
              {"Cast", &Caffe2Backend::CreateCast},
              {"Constant", &Caffe2Backend::CreateConstant},
              {"ConstantOfShape", &Caffe2Backend::CreateConstantOfShape},
              {"Conv", &Caffe2Backend::CreateConvPoolOpBase},
              {"AveragePool", &Caffe2Backend::CreateConvPoolOpBase},
              {"GlobalAveragePool", &Caffe2Backend::CreateConvPoolOpBase},
              {"GlobalMaxPool", &Caffe2Backend::CreateConvPoolOpBase},
              {"MaxPool", &Caffe2Backend::CreateConvPoolOpBase},
              {"Reshape", &Caffe2Backend::CreateReshape},
              {"Int8Reshape", &Caffe2Backend::CreateReshape},
              {"Gather", &Caffe2Backend::CreateGather},
              {"Gemm", &Caffe2Backend::CreateGemm},
              {"Pad", &Caffe2Backend::CreatePad},
              {"Concat", &Caffe2Backend::CreateConcat},
              {"Int8Concat", &Caffe2Backend::CreateConcat},
              {"LogSoftmax", &Caffe2Backend::CreateLogSoftmax},
              {"Slice", &Caffe2Backend::CreateSlice},
              {"Split", &Caffe2Backend::CreateSplit},
              {"Reciprocal", &Caffe2Backend::CreateReciprocal},
              {"BatchNormalization", &Caffe2Backend::CreateBatchNormalization},
              {"MatMul", &Caffe2Backend::CreateMatMul},
              {"Upsample", &Caffe2Backend::CreateUpsample},
              {"Dropout", &Caffe2Backend::CreateDropout},
              {"LRN", &Caffe2Backend::CreateLRN},
              {"DynamicSlice", &Caffe2Backend::CreateDynamicSlice},
              {"RandomNormal", &Caffe2Backend::CreateRandomNormal},
              {"RandomNormalLike", &Caffe2Backend::CreateRandomNormal},
              {"Where", &Caffe2Backend::CreateWhereOp},
              {"NonZero", &Caffe2Backend::CreateNonZeroOp},
              {"Multinomial", &Caffe2Backend::CreateMultinomialOp}};
  return kSpecialOperators;
}

//============================
// Special Operator Converters
//============================

Caffe2Ops Caffe2Backend::CreateArgMaxMin(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  auto& attributes = onnx_node->attributes;
  if (!attributes.HasAttribute("axis")) {
    auto* attr = attributes.AddRewrittenAttribute("axis");
    attr->set_i(0);
  }
  return CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);
}

Caffe2Ops Caffe2Backend::CreateCast(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  auto c2_op = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);

  auto onnx_dtype =
      onnx_node->attributes.get<int64_t>("to", TensorProto::UNDEFINED);
  auto c2_dtype = caffe2::TensorProto::UNDEFINED;
  switch (onnx_dtype) {
    case ::ONNX_NAMESPACE::TensorProto::FLOAT:
      c2_dtype = caffe2::TensorProto::FLOAT;
      break;
    case ::ONNX_NAMESPACE::TensorProto::UINT8:
      c2_dtype = caffe2::TensorProto::UINT8;
      break;
    case ::ONNX_NAMESPACE::TensorProto::INT8:
      c2_dtype = caffe2::TensorProto::INT8;
      break;
    case ::ONNX_NAMESPACE::TensorProto::UINT16:
      c2_dtype = caffe2::TensorProto::UINT16;
      break;
    case ::ONNX_NAMESPACE::TensorProto::INT16:
      c2_dtype = caffe2::TensorProto::INT16;
      break;
    case ::ONNX_NAMESPACE::TensorProto::INT32:
      c2_dtype = caffe2::TensorProto::INT32;
      break;
    case ::ONNX_NAMESPACE::TensorProto::INT64:
      c2_dtype = caffe2::TensorProto::INT64;
      break;
    case ::ONNX_NAMESPACE::TensorProto::STRING:
      c2_dtype = caffe2::TensorProto::STRING;
      break;
    case ::ONNX_NAMESPACE::TensorProto::BOOL:
      c2_dtype = caffe2::TensorProto::BOOL;
      break;
    case ::ONNX_NAMESPACE::TensorProto::FLOAT16:
      c2_dtype = caffe2::TensorProto::FLOAT16;
      break;
    case ::ONNX_NAMESPACE::TensorProto::DOUBLE:
      c2_dtype = caffe2::TensorProto::DOUBLE;
      break;
    case ::ONNX_NAMESPACE::TensorProto::UINT32:
    case ::ONNX_NAMESPACE::TensorProto::UINT64:
    case ::ONNX_NAMESPACE::TensorProto::COMPLEX64:
    case ::ONNX_NAMESPACE::TensorProto::COMPLEX128:
    case ::ONNX_NAMESPACE::TensorProto::UNDEFINED:
      c2_dtype = caffe2::TensorProto::UNDEFINED;
      break;
  };

  CAFFE_ENFORCE_NE(
      c2_dtype,
      caffe2::TensorProto::UNDEFINED,
      "Casting to '",
      onnx_dtype,
      "' dtype is not supported");

  CAFFE_ENFORCE_EQ(
      c2_op.ops.Get(0).arg().size(),
      1,
      "Unexpected number of attributes in 'Cast'");
  c2_op.ops.Mutable(0)->mutable_arg(0)->set_i(c2_dtype);

  return c2_op;
}

Caffe2Ops Caffe2Backend::CreateConstant(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  CAFFE_ENFORCE_EQ(onnx_node->node.output_size(), 1);

  Caffe2Ops ret;
  auto* c2_op = ret.ops.Add();
  const auto* value = onnx_node->attributes.get<const TensorProto*>("value");
  BuildTensorFillingOp(c2_op, *value, onnx_node->node.output(0));

  return ret;
}

Caffe2Ops Caffe2Backend::CreateConstantOfShape(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  CAFFE_ENFORCE_EQ(onnx_node->node.input_size(), 1);
  CAFFE_ENFORCE_EQ(onnx_node->node.output_size(), 1);

  Caffe2Ops ret;
  auto* c2_op = ret.ops.Add();
  const auto* value = onnx_node->attributes.get<const TensorProto*>("value");
  if (value) {
    BuildTensorFillingOp(
        c2_op, *value, onnx_node->node.output(0), onnx_node->node.input(0));
  } else {
    c2_op->set_type("ConstantFill");
    c2_op->add_input(onnx_node->node.input(0));
    c2_op->add_output(onnx_node->node.output(0));
    auto c2_input_as_shape = c2_op->add_arg();
    c2_input_as_shape->set_name("input_as_shape");
    c2_input_as_shape->set_i(1);
  }

  return ret;
}

//  Note [Caffe2 ConvPoolOpBase]
//  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//  To understand what is going on here, we have to talk a little bit about
//  Caffe2's internals.
//
//  First, it's important to know that all of Caffe2's pooling and convolution
//  operators inherit from "ConvPoolOpBase", which is an abstract class that
//  defines all of the attributes (kernels, dilations, strides, etc) which one
//  sees on these operators.  Unfortunately, Caffe2's documentation generator
//  doesn't know how to handle cases like this, so for example, if you look at
//  the docs for MaxPool at
//  <https://caffe2.ai/docs/operators-catalogue.html#maxpool> you won't see any
//  of the attributes.  You have to go source diving to find the information; in
//  particular, you want to look at:
//  https://github.com/caffe2/caffe2/blob/master/caffe2/operators/conv_pool_op_base.h
//  This class handles *global* pooling as well.
//
//  Second, it's important to know what Caffe2 expects for padding, which can
//  be somewhat difficult to understand from the code because Caffe2 handles
//  both singular/pluralized spellings of padding, and there is also legacy
//  padding business.  The short version of the story is that, for NON-legacy
//  padding (which is what we want to output), padding is expected to be
//  *twice* the size of kernels.  So if you have a 2D convolution, Caffe2
//  will accept two values in 'kernels', but FOUR values in 'pads';
//  furthermore, this is *mandatory.*
//
//  Finally, ConvPoolOpBase is not the only class of it's kind; there is
//  be tricked by the fact that Conv and ConvTranspose have similar
//  parameters; they exercise different codepaths and need to be handled
//  differently.
Caffe2Ops Caffe2Backend::CreateConvPoolOpBase(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  const auto& node = onnx_node->node;
  auto& attributes = onnx_node->attributes;
  if (node.op_type().find("Global") == 0) {
    auto* attr = attributes.AddRewrittenAttribute("global_pooling");
    attr->set_i(1);
  }

  if (attributes.HasAttribute("kernel_shape") &&
      attributes.HasAttribute("pads")) {
    auto kernel_shape =
        attributes
            .get<::google::protobuf::RepeatedField<::google::protobuf::int64>>(
                "kernel_shape");
    auto pads =
        attributes
            .get<::google::protobuf::RepeatedField<::google::protobuf::int64>>(
                "pads");
    if (kernel_shape.size() == pads.size()) {
      // Caffe2 requires pads to be twice the size of kernels.
      auto* attr = attributes.AddRewrittenAttribute("pads");
      attr->mutable_ints()->CopyFrom(pads);
      attr->mutable_ints()->MergeFrom(pads);
    }
  }

  return CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);
}

Caffe2Ops Caffe2Backend::CreateReshape(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  auto c2_op = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);
  CAFFE_ENFORCE_EQ(c2_op.ops.size(), 1);
  auto* op = c2_op.ops.Mutable(0);
  op->add_output(dummy_->NewDummyName());

  return c2_op;
}

Caffe2Ops Caffe2Backend::CreateRandomNormal(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  auto& attributes = onnx_node->attributes;

  if (attributes.HasAttribute("seed")) {
    CAFFE_THROW("Caffe2 GaussianFill does not support random seed");
  }

  if (attributes.HasAttribute("dtype")) {
    if (attributes.get<int64_t>("dtype") != TensorProto::FLOAT) {
      CAFFE_THROW("Caffe2 GaussianFill only support FLOAT dtype");
    }
    attributes.remove("dtype");
  }
  if (attributes.HasAttribute("scale")) {
    auto scale = attributes.get<float>("scale");
    auto* attr = attributes.AddRewrittenAttribute("std");
    attr->set_f(scale);
    attributes.remove("scale");
  }
  return CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);
}

Caffe2Ops Caffe2Backend::CreateWhereOp(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  // The native Caffe2 op doesn't support broadcasting, so we defer the handling
  // of this op to the ATen library that does.
  onnx::NodeProto converted;
  converted.CopyFrom(onnx_node->node);
  converted.set_op_type("ATen");
  onnx::AttributeProto* attr = converted.add_attribute();
  attr->set_name("operator");
  attr->set_s("where");
  OnnxNode new_node(converted);
  return CommonOnnxNodeToCaffe2Ops(&new_node, ctx);
}

Caffe2Ops Caffe2Backend::CreateNonZeroOp(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  // Native Caffe2 doesn't support NonZero, fallback to ATen.
  // ATen nonzero is equivalent to Transpose(ONNX::NonZero).
  onnx::NodeProto converted;
  converted.CopyFrom(onnx_node->node);

  auto nonzero_output = dummy_->NewDummyName();
  converted.set_output(0, nonzero_output);
  converted.set_op_type("ATen");
  onnx::AttributeProto* attr = converted.add_attribute();
  attr->set_name("operator");
  attr->set_s("nonzero");
  OnnxNode new_node(converted);
  auto ret = CommonOnnxNodeToCaffe2Ops(&new_node, ctx);

  auto* c2_transpose = ret.ops.Add();
  BuildOperator(
      c2_transpose, "Transpose", {nonzero_output}, {onnx_node->node.output(0)});
  return ret;
}

Caffe2Ops Caffe2Backend::CreateMultinomialOp(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  // Fallback to ATen.
  // ATen::Multinomial takes probabilities as input, ONNX Multinomial expects
  // input to be log probabilities.
  Caffe2Ops ret;
  auto c2_exp_output = dummy_->NewDummyName();
  auto* c2_exp = ret.ops.Add();
  BuildOperator(c2_exp, "Exp", {onnx_node->node.input(0)}, {c2_exp_output});

  auto* c2_multinomial = ret.ops.Add();
  caffe2::Argument c2_arg_op;
  c2_arg_op.set_name("operator");
  c2_arg_op.set_s("multinomial");
  // ONNX Multinomial only supports replacement=True.
  caffe2::Argument c2_arg_rep;
  c2_arg_rep.set_name("replacement");
  c2_arg_rep.set_i(1);
  auto& onnx_attributes = onnx_node->attributes;
  caffe2::Argument c2_arg_num;
  c2_arg_num.set_name("num_samples");
  c2_arg_num.set_i(onnx_attributes.get<int64_t>("sample_size"));

  // ONNX Multinomial has attribute dtype in {int64, int32}, which specifies
  // output datatype. ATen::Multinomial output dtype is always int64.
  auto onnx_dtype =
      onnx_attributes.get<int64_t>("dtype", TensorProto::UNDEFINED);
  if (onnx_dtype == ::ONNX_NAMESPACE::TensorProto::INT64) {
    BuildOperator(
        c2_multinomial,
        "ATen",
        {c2_exp_output},
        {onnx_node->node.output(0)},
        {c2_arg_op, c2_arg_rep, c2_arg_num});
  } else if (onnx_dtype == ::ONNX_NAMESPACE::TensorProto::INT32) {
    auto c2_multinomial_output = dummy_->NewDummyName();
    BuildOperator(
        c2_multinomial,
        "ATen",
        {c2_exp_output},
        {c2_multinomial_output},
        {c2_arg_op, c2_arg_rep, c2_arg_num});

    auto* c2_cast = ret.ops.Add();
    caffe2::Argument to;
    to.set_name("to");
    to.set_i(caffe2::TensorProto::INT32);
    BuildOperator(
        c2_cast,
        "Cast",
        {c2_multinomial_output},
        {onnx_node->node.output(0)},
        {to});
  } else {
    CAFFE_THROW(
        "ONNX does not support dtype other than int32/int64 in Multinomial, but get ",
        onnx_dtype);
  }

  return ret;
}

Caffe2Ops Caffe2Backend::CreateReciprocal(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  const auto& node = onnx_node->node;
  if (node.input_size() != 1 || node.output_size() != 1) {
    CAFFE_THROW("Caffe2 Reciprocal should have 1 input and 1 output");
  }

  Caffe2Ops ret;
  auto* c2_op = ret.ops.Add();

  BuildOperator(c2_op, "Reciprocal", {node.input(0)}, {node.output(0)}, {});
  return ret;
}

Caffe2Ops Caffe2Backend::CreateGather(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  const auto& node = onnx_node->node;
  if (node.input_size() < 2 || node.output_size() < 1) {
    CAFFE_THROW("Caffe2 Gather should have 2 inputs and 1 output");
  }

  Caffe2Ops ret;
  auto* c2_op = ret.ops.Add();

  std::vector<std::string> inputs;
  inputs.emplace_back(node.input(0));
  inputs.emplace_back(node.input(1));
  std::vector<std::string> outputs;
  outputs.emplace_back(node.output(0));

  auto axis = onnx_node->attributes.get<int64_t>("axis", 0L);
  if (axis == 0) {
    BuildOperator(c2_op, "Gather", inputs, outputs);
  } else if (axis == 1) {
    BuildOperator(c2_op, "BatchGather", inputs, outputs);
  } else {
    CAFFE_THROW(
        "Caffe2 only supports Gather with axis being 0 or 1, ",
        "whereas axis is ",
        axis);
  }

  return ret;
}

Caffe2Ops Caffe2Backend::CreateGemm(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  const auto& node = onnx_node->node;
  if (node.input_size() < 3 || node.output_size() < 1) {
    CAFFE_THROW("Caffe2 Gemm should have 3 inputs and 1 output");
  }

  Caffe2Ops ret;
  auto input_a = node.input(0);
  auto input_b = node.input(1);
  auto input_c = node.input(2);
  auto output = node.output(0);

  auto alpha = onnx_node->attributes.get<float>("alpha", 1.0);
  auto beta = onnx_node->attributes.get<float>("beta", 1.0);
  if (!AlmostEqual(alpha, 1)) {
    auto scaled_a = dummy_->NewDummyName();
    caffe2::Argument scale;
    scale.set_name("scale");
    scale.set_f(alpha);

    auto* c2_op = ret.ops.Add();
    BuildOperator(c2_op, "Scale", {input_a}, {scaled_a}, {scale});
    input_a = scaled_a;
  }
  if (!AlmostEqual(beta, 1)) {
    auto scaled_c = dummy_->NewDummyName();
    caffe2::Argument scale;
    scale.set_name("scale");
    scale.set_f(beta);

    auto* c2_op = ret.ops.Add();
    BuildOperator(c2_op, "Scale", {input_c}, {scaled_c}, {scale});
    input_c = scaled_c;
  }

  auto trans_a = onnx_node->attributes.get<int64_t>("transA", 0L);
  auto trans_b = onnx_node->attributes.get<int64_t>("transB", 0L);
  // Support broadcast by default when opset_version > 6.
  auto broadcast = onnx_node->attributes.get<int64_t>(
      "broadcast", (ctx.opset_version() > 6) ? 1L : 0L);

  // If the c's shape information is available and c is a 1d tensor(except
  // c is a scalar), use FC aggressively.
  auto check_fc = [&]() -> bool {
    const auto input_c_vi_iter = ctx.value_infos().find(node.input(2));

    if (input_c_vi_iter == ctx.value_infos().end()) {
      return false;
    }

    const auto input_c_shape =
        input_c_vi_iter->second.type().tensor_type().shape();

    if (input_c_shape.dim_size() != 1) {
      return false;
    }

    // c is a scalar.
    if (input_c_shape.dim(0).dim_value() == 1) {
      const auto input_b_vi_iter = ctx.value_infos().find(node.input(1));

      // If the b's shape is not available, skip FC.
      if (input_b_vi_iter == ctx.value_infos().end()) {
        return false;
      }
      const auto input_b_shape =
          input_b_vi_iter->second.type().tensor_type().shape();
      int input_b_last_dim_index = (trans_b) ? 0 : 1;
      // If b's last dim is not 1, skip FC.
      if (input_b_shape.dim_size() <= input_b_last_dim_index ||
          input_b_shape.dim(input_b_last_dim_index).dim_value() != 1) {
        return false;
      }
    }

    return true;
  };

  if (!trans_a && broadcast && check_fc()) {
    auto* c2_op = ret.ops.Add();
    if (trans_b) {
      BuildOperator(c2_op, "FC", {input_a, input_b, input_c}, {output});
    } else {
      BuildOperator(
          c2_op, "FCTransposed", {input_a, input_b, input_c}, {output});
    }
  } else {
    auto ab = dummy_->NewDummyName();
    caffe2::Argument arg_trans_a;
    arg_trans_a.set_name("trans_a");
    arg_trans_a.set_i(trans_a);
    caffe2::Argument arg_trans_b;
    arg_trans_b.set_name("trans_b");
    arg_trans_b.set_i(trans_b);

    auto* c2_op = ret.ops.Add();
    BuildOperator(
        c2_op, "MatMul", {input_a, input_b}, {ab}, {arg_trans_a, arg_trans_b});
    c2_op = ret.ops.Add();
    if (ctx.opset_version() >= 7) {
      BuildOperator(c2_op, "Add", {ab, input_c}, {output});
    } else {
      caffe2::Argument arg_broadcast;
      arg_broadcast.set_name("broadcast");
      arg_broadcast.set_i(broadcast);
      BuildOperator(c2_op, "Add", {ab, input_c}, {output}, {arg_broadcast});
    }
  }

  return ret;
}

Caffe2Ops Caffe2Backend::CreatePad(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  auto& attributes = onnx_node->attributes;
  ::google::protobuf::RepeatedField<::google::protobuf::int64> pads;
  std::string pad_name = ctx.opset_version() < 2 ? "paddings" : "pads";
  pads = attributes
             .get<::google::protobuf::RepeatedField<::google::protobuf::int64>>(
                 pad_name);
  std::string str;
  std::stringstream ss;
  ss << "[";
  for (const auto& i : pads) {
    ss << i << ", ";
  }
  ss << "]";
  str = ss.str();

  // Guard the invalid (negative) pads attribute.
  for (const auto i : pads) {
    if (i < 0) {
      CAFFE_THROW("ONNX does not support negative pads in Pad, but get ", str);
    }
  }

  // first two dim is for batch and channel. Note that now all the values are
  // non-negative
  if (!(pads.size() == 8 &&
        (pads.Get(0) + pads.Get(1) + pads.Get(4) + pads.Get(5) == 0))) {
    CAFFE_THROW(
        "Caffe2 only supports padding 2D Tensor, whereas padding is ", str);
  }

  // rewrite the padding info
  auto* attr = attributes.AddRewrittenAttribute(pad_name);
  attr->add_ints(pads.Get(2));
  attr->add_ints(pads.Get(3));
  attr->add_ints(pads.Get(6));
  attr->add_ints(pads.Get(7));

  return CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);
}

// TODO: Caffe2 Concat has an extra output. It should be only
// used when doing training, so we should change Caffe2 to allow
// 1 output.
Caffe2Ops Caffe2Backend::CreateConcat(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  auto c2_op = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);
  CAFFE_ENFORCE_EQ(c2_op.ops.size(), 1);
  auto* op = c2_op.ops.Mutable(0);
  op->add_output(dummy_->NewDummyName());

  return c2_op;
}

Caffe2Ops Caffe2Backend::CreateLogSoftmax(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  const auto& node = onnx_node->node;
  if (node.input_size() < 1 || node.output_size() < 1) {
    CAFFE_THROW("LogSoftmax should have 1 input and 1 output");
  }
  auto axis = onnx_node->attributes.get<int64_t>("axis", 1L);
  caffe2::Argument arg_axis;
  arg_axis.set_name("axis");
  arg_axis.set_i(axis);
  auto softmax_a = dummy_->NewDummyName();

  Caffe2Ops ret;
  auto* c2_op = ret.ops.Add();
  BuildOperator(c2_op, "Softmax", {node.input(0)}, {softmax_a}, {arg_axis});
  c2_op = ret.ops.Add();
  BuildOperator(c2_op, "Log", {softmax_a}, {node.output(0)});

  return ret;
}

Caffe2Ops Caffe2Backend::CreateSlice(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  auto op_tmp = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);
  CAFFE_ENFORCE_EQ(op_tmp.ops.size(), 1);
  auto* op = op_tmp.ops.Mutable(0);
  std::unordered_map<std::string, caffe2::Argument*> args;
  for (auto& arg : *op->mutable_arg()) {
    args.emplace(arg.name(), &arg);
  }

  caffe2::Argument starts_vals;
  starts_vals.set_name("values");
  auto pos = args.find("starts");
  if (pos != args.end()) {
    for (auto i : pos->second->ints()) {
      starts_vals.add_ints(i < 0 ? i - 1 : i);
    }
    args.erase(pos);
  }

  caffe2::Argument ends_vals;
  ends_vals.set_name("values");
  pos = args.find("ends");
  if (pos != args.end()) {
    for (auto i : pos->second->ints()) {
      if (i == std::numeric_limits<int64_t>::max()) {
        ends_vals.add_ints(-1);
      } else {
        ends_vals.add_ints(i < 0 ? i - 1 : i);
      }
    }
    args.erase(pos);
  }

  caffe2::Argument axes_vals;
  axes_vals.set_name("values");
  pos = args.find("axes");
  if (pos != args.end()) {
    for (auto i : pos->second->ints()) {
      axes_vals.add_ints(i);
    }
    args.erase(pos);
  } else {
    auto ndim = starts_vals.ints_size();
    for (int64_t i = 0; i < ndim; ++i) {
      axes_vals.add_ints(i);
    }
  }

  CAFFE_ENFORCE_GE(op->input_size(), 1);
  auto data = op->input(0);
  auto shape_tensor = dummy_->NewDummyName();
  Caffe2Ops ret;

  auto* c2_op = ret.ops.Add();
  BuildOperator(c2_op, "Shape", {data}, {shape_tensor});

  auto axes_tensor = dummy_->NewDummyName();
  c2_op = ret.ops.Add();
  {
    caffe2::Argument shape;
    shape.set_name("shape");
    shape.add_ints(axes_vals.ints_size());
    BuildOperator(
        c2_op, "GivenTensorIntFill", {}, {axes_tensor}, {shape, axes_vals});
  }

  auto starts_vals_tensor = dummy_->NewDummyName();
  auto starts_tensor = dummy_->NewDummyName();
  c2_op = ret.ops.Add();
  {
    caffe2::Argument shape_starts;
    shape_starts.set_name("shape");
    shape_starts.add_ints(starts_vals.ints_size());
    BuildOperator(
        c2_op,
        "GivenTensorInt64Fill",
        {},
        {starts_vals_tensor},
        {shape_starts, starts_vals});
  }

  caffe2::Argument dtype;
  dtype.set_name("dtype");
  dtype.set_i(static_cast<int64_t>(caffe2::TensorProto::INT64));
  caffe2::Argument constant;
  constant.set_name("value");
  constant.set_i(0);
  c2_op = ret.ops.Add();
  BuildOperator(
      c2_op,
      "ConstantFill",
      {shape_tensor},
      {starts_tensor},
      {dtype, constant});
  c2_op = ret.ops.Add();
  BuildOperator(
      c2_op,
      "ScatterAssign",
      {starts_tensor, axes_tensor, starts_vals_tensor},
      {starts_tensor});
  // Slice only accepts starts as int
  caffe2::Argument to;
  to.set_name("to");
  to.set_i(static_cast<int64_t>(caffe2::TensorProto::INT32));

  auto ends_vals_tensor = dummy_->NewDummyName();
  auto ends_tensor = dummy_->NewDummyName();
  c2_op = ret.ops.Add();
  {
    caffe2::Argument shape_ends;
    shape_ends.set_name("shape");
    shape_ends.add_ints(ends_vals.ints_size());
    BuildOperator(
        c2_op,
        "GivenTensorInt64Fill",
        {},
        {ends_vals_tensor},
        {shape_ends, ends_vals});
  }

  constant.set_i(-1);
  c2_op = ret.ops.Add();
  BuildOperator(
      c2_op, "ConstantFill", {shape_tensor}, {ends_tensor}, {dtype, constant});
  c2_op = ret.ops.Add();
  BuildOperator(
      c2_op,
      "ScatterAssign",
      {ends_tensor, axes_tensor, ends_vals_tensor},
      {ends_tensor});

  // attach the original op at the end
  c2_op = ret.ops.Add();
  c2_op->CopyFrom(*op);
  c2_op->mutable_input()->Clear();
  c2_op->add_input(data);
  c2_op->add_input(starts_tensor);
  c2_op->add_input(ends_tensor);
  c2_op->mutable_arg()->Clear();
  for (const auto& kv : args) {
    c2_op->add_arg()->CopyFrom(*kv.second);
  }

  return ret;
}

// Do the following:
// for a given index tensor (i.e. `starts` or `ends`):
// 1) Hilariously subtract 1 from the value if it is negative. This due to
//    the behavior of Caffe2's slice operator not matching that of ONNX's slice
// 2) Fully expand the index tensor out to the rank of the data tensor.
//    pseudocode: indices_full = zeros(rank); indices_full[axes] = indices.int()
std::string Caffe2Backend::PreprocessSliceIndexTensor(
    OnnxNode* onnx_node,
    Caffe2Ops& ret,
    std::string indices_tensor,
    std::string axes_tensor,
    std::string rank_tensor,
    std::string zero_tensor,
    std::string one_tensor,
    int default_value) {
  auto indices_tensor_full = dummy_->NewDummyName();

  {
    caffe2::Argument value;
    value.set_name("value");
    value.set_i(default_value);
    caffe2::Argument dtype;
    dtype.set_name("dtype");
    dtype.set_i(static_cast<int64_t>(caffe2::TensorProto::INT64));
    caffe2::Argument input_as_shape;
    input_as_shape.set_name("input_as_shape");
    input_as_shape.set_i(1);
    auto c2_op = ret.ops.Add();
    BuildOperator(
        c2_op,
        "ConstantFill",
        {rank_tensor},
        {indices_tensor_full},
        {value, dtype, input_as_shape});
  }

  // Subtract 1 from each element of the indices tensor that is negative
  auto lt_tensor = dummy_->NewDummyName();
  {
    caffe2::Argument broadcast;
    broadcast.set_name("broadcast");
    broadcast.set_i(1);
    auto c2_op = ret.ops.Add();
    BuildOperator(
        c2_op, "LT", {indices_tensor, zero_tensor}, {lt_tensor}, {broadcast});
  }

  auto sub_one_tensor = dummy_->NewDummyName();
  {
    caffe2::Argument broadcast;
    broadcast.set_name("broadcast");
    broadcast.set_i(1);
    auto c2_op = ret.ops.Add();
    BuildOperator(
        c2_op,
        "Sub",
        {indices_tensor, one_tensor},
        {sub_one_tensor},
        {broadcast});
  }

  auto indices_tensor_adjusted = dummy_->NewDummyName();
  auto c2_op = ret.ops.Add();
  BuildOperator(
      c2_op,
      "Conditional",
      {lt_tensor, sub_one_tensor, indices_tensor},
      {indices_tensor_adjusted},
      {});

  // Fill in values specified from the partially-specified ONNX indices tensor
  c2_op = ret.ops.Add();
  BuildOperator(
      c2_op,
      "ScatterAssign",
      {indices_tensor_full, axes_tensor, indices_tensor_adjusted},
      {indices_tensor_full});

  return indices_tensor_full;
}

Caffe2Ops Caffe2Backend::CreateDynamicSlice(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  auto op_tmp = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);
  CAFFE_ENFORCE_EQ(op_tmp.ops.size(), 1);
  auto* op = op_tmp.ops.Mutable(0);
  std::unordered_map<std::string, caffe2::Argument*> args;
  for (auto& arg : *op->mutable_arg()) {
    args.emplace(arg.name(), &arg);
  }

  CAFFE_ENFORCE_GE(op->input_size(), 1);
  auto data = op->input(0);
  Caffe2Ops ret;

  // First get the shape of the input tensor
  auto* c2_op = ret.ops.Add();
  auto size_tensor = dummy_->NewDummyName();
  BuildOperator(c2_op, "Shape", {data}, {size_tensor});

  // Now get the rank of the tensor by getting the shape of the shape of
  // the input tensor
  c2_op = ret.ops.Add();
  auto rank_tensor = dummy_->NewDummyName();
  BuildOperator(c2_op, "Shape", {size_tensor}, {rank_tensor});

  // Axes tensor will be used to populate the fully-specified starts and ends
  // arguments to the caffe2 Slice operator.
  std::string axes_tensor;
  if (onnx_node->node.input_size() > 3) {
    axes_tensor = onnx_node->node.input(3);
  } else {
    axes_tensor = dummy_->NewDummyName();
    auto* c2_op = ret.ops.Add();
    BuildOperator(c2_op, "Range", {rank_tensor}, {axes_tensor}, {});
  }

  // Useful int tensors
  auto define_integer_constant = [this, &ret](int val) {
    caffe2::Argument value;
    value.set_name("value");
    value.set_i(val);
    caffe2::Argument dtype;
    dtype.set_name("dtype");
    dtype.set_i(static_cast<int64_t>(caffe2::TensorProto::INT64));
    caffe2::Argument shape;
    shape.set_name("shape");
    shape.add_ints(1);
    auto c2_op = ret.ops.Add();
    auto name = dummy_->NewDummyName();
    BuildOperator(c2_op, "ConstantFill", {}, {name}, {value, dtype, shape});
    return name;
  };

  auto zero_tensor = define_integer_constant(0);
  auto one_tensor = define_integer_constant(1);

  auto starts_tensor_full = PreprocessSliceIndexTensor(
      onnx_node,
      ret,
      onnx_node->node.input(1), // starts
      axes_tensor,
      rank_tensor,
      zero_tensor,
      one_tensor,
      0);

  auto ends_tensor_full = PreprocessSliceIndexTensor(
      onnx_node,
      ret,
      onnx_node->node.input(2), // ends
      axes_tensor,
      rank_tensor,
      zero_tensor,
      one_tensor,
      -1);

  // attach the original op at the end
  c2_op = ret.ops.Add();
  c2_op->CopyFrom(*op);
  c2_op->mutable_input()->Clear();
  c2_op->add_input(data);
  c2_op->add_input(starts_tensor_full);
  c2_op->add_input(ends_tensor_full);
  c2_op->mutable_arg()->Clear();
  for (const auto& kv : args) {
    c2_op->add_arg()->CopyFrom(*kv.second);
  }

  return ret;
}

Caffe2Ops Caffe2Backend::CreateBatchNormalization(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  auto& attributes = onnx_node->attributes;

  if (ctx.opset_version() < 6) {
    attributes.remove("consumed_inputs");
  }

  if (ctx.opset_version() >= 7) {
    auto* attr = attributes.AddRewrittenAttribute("is_test");
    attr->set_i(1);
  }

  if (attributes.HasAttribute("spatial") &&
      attributes.get<int64_t>("spatial") == 1) {
    attributes.remove("spatial");
  }

  return CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);
}

Caffe2Ops Caffe2Backend::CreateSplit(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  auto& attributes = onnx_node->attributes;
  if (!attributes.HasAttribute("axis")) {
    auto* attr = attributes.AddRewrittenAttribute("axis");
    attr->set_i(0);
  }

  return CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);
}

Caffe2Ops Caffe2Backend::CreateMatMul(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  const auto& node = onnx_node->node;
  if (node.input_size() != 2) {
    CAFFE_THROW("MatMul should have 2 inputs");
  }

  auto c2_op = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);
  CAFFE_ENFORCE_EQ(c2_op.ops.size(), 1);
  auto* op = c2_op.ops.Mutable(0);
  auto* broadcast_arg = op->add_arg();
  broadcast_arg->set_name("broadcast");
  broadcast_arg->set_i(1);

  return c2_op;
}

Caffe2Ops Caffe2Backend::CreateUpsample(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  auto& attributes = onnx_node->attributes;
  attributes.remove("mode");

  if (ctx.opset_version() >= 7 && ctx.opset_version() < 9) {
    const auto& scales =
        attributes.get<::google::protobuf::RepeatedField<float>>("scales");
    if (scales.size() != 4) {
      CAFFE_THROW("The scales argument should have size 4");
    } else if (
        !AlmostEqual(scales.Get(0), 1) || !AlmostEqual(scales.Get(1), 1)) {
      CAFFE_THROW("The first two elements in the scales argument must be 1");
    }
    attributes.remove("scales");
    auto c2_op = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);
    auto* op = c2_op.ops.Mutable(0);
    auto* c2_height = op->add_arg();
    c2_height->set_name("height_scale");
    c2_height->set_f(scales.Get(2));
    auto* c2_width = op->add_arg();
    c2_width->set_name("width_scale");
    c2_width->set_f(scales.Get(3));
    return c2_op;
  } else if (ctx.opset_version() >= 9) {
    const auto& node = onnx_node->node;
    if (node.input_size() != 2) {
      CAFFE_THROW("Expects 2 input in upsample after onnx version 9");
    }
    Caffe2Ops ret;

    // Slice the input {1, 1, height, width} -> {height, width}
    auto* c2_op = ret.ops.Add();
    auto sliced_input = dummy_->NewDummyName();
    caffe2::Argument arg_starts, arg_ends;
    arg_starts.set_name("starts");
    arg_starts.add_ints(2);
    arg_ends.set_name("ends");
    arg_ends.add_ints(-1);
    BuildOperator(
        c2_op,
        "Slice",
        {node.input(1)},
        {sliced_input},
        {arg_starts, arg_ends});

    // Upsample
    c2_op = ret.ops.Add();
    BuildOperator(
        c2_op,
        "ResizeNearest",
        {node.input(0), sliced_input},
        {node.output(0)},
        {});
    return ret;
  }
  return CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);
}

Caffe2Ops Caffe2Backend::CreateDropout(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  if (ctx.opset_version() >= 7) {
    auto& attributes = onnx_node->attributes;
    auto* attr = attributes.AddRewrittenAttribute("is_test");
    attr->set_i(1);
  }

  return CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);
}

Caffe2Ops Caffe2Backend::CreateLRN(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  auto c2_op = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);
  const auto& attributes = onnx_node->attributes;
  if (!attributes.HasAttribute("alpha")) {
    auto* arg = c2_op.ops.Mutable(0)->add_arg();
    arg->set_name("alpha");
    arg->set_f(1e-4);
  }
  if (!attributes.HasAttribute("beta")) {
    auto* arg = c2_op.ops.Mutable(0)->add_arg();
    arg->set_name("beta");
    arg->set_f(0.75);
  }
  return c2_op;
}

//==============================================
// Rest of the member functions for Caffe2Backend
//==============================================
std::unordered_set<std::string> Caffe2Backend::AllNamesInGraph(
    const GraphProto& graph) {
  std::unordered_set<std::string> names;

  for (const auto& input : graph.input()) {
    names.emplace(input.name());
  }
  for (const auto& output : graph.output()) {
    names.emplace(output.name());
  }
  for (const auto& node : graph.node()) {
    for (const auto& n : node.input()) {
      names.emplace(n);
    }
    for (const auto& n : node.output()) {
      names.emplace(n);
    }
  }

  return names;
}

//  This translator performs the basic translation of ONNX nodes into
//  Caffe2 operators.  Besides doing a straightforward marshalling from
//  one format to another, it also does these extra things:
//
//    - Renames operators based on 'renamed_operators'
//    - Renames attributes based on 'renamed_attrs' and
//      'get_per_op_renamed_attrs'
//
//  If you're writing a custom translator, consider calling this first,
//  and then fixing things up further.
Caffe2Ops Caffe2Backend::CommonOnnxNodeToCaffe2Ops(
    OnnxNode* onnx_node,
    const ConversionContext& ctx) {
  Caffe2Ops ret;
  auto* c2_op = ret.ops.Add();

  const auto& node = onnx_node->node;
  c2_op->mutable_input()->MergeFrom(node.input());
  c2_op->mutable_output()->MergeFrom(node.output());
  c2_op->set_name(node.name());

  // NOLINTNEXTLINE(performance-unnecessary-copy-initialization)
  const auto onnx_op_type = node.op_type();
  auto broken_version = caffe2::get_default(
      get_broken_operators(), onnx_op_type, std::numeric_limits<int>::max());
  if (broken_version <= ctx.opset_version()) {
    CAFFE_THROW(
        "Don't know how to translate op ",
        onnx_op_type,
        " in ONNX operator set v",
        ctx.opset_version(),
        " (I only support prior to v",
        broken_version);
  }
  c2_op->set_type(
      caffe2::get_default(get_renamed_operators(), onnx_op_type, onnx_op_type));
  if (!IsOperator(c2_op->type())) {
    CAFFE_THROW("Don't know how to translate op ", onnx_op_type);
  }

  auto mapper = [&, this](const std::string& k) {
    const auto it = get_per_op_renamed_attrs().find(onnx_op_type);
    if (it != get_per_op_renamed_attrs().end()) {
      const auto it_op = it->second.find(k);
      if (it_op != it->second.end()) {
        return it_op->second;
      }
    }
    const auto it_global = get_renamed_attrs().find(k);
    if (it_global != get_renamed_attrs().end()) {
      return it_global->second;
    }
    return k;
  };
  c2_op->mutable_arg()->MergeFrom(
      onnx_node->attributes.OnnxAttrToCaffe2Arg(mapper));

  return ret;
}

Caffe2Ops Caffe2Backend::ConvertNode(
    const std::string& node_str,
    const ConversionContext& ctx) {
  ::google::protobuf::RepeatedPtrField<NodeProto> nodes;
  auto* n = nodes.Add();
  ParseProtoFromLargeString(node_str, n);
  ModelProto init_model;
  ModelProto pred_model;
  OnnxNode onnx_node = OnnxNode(nodes.Get(0));
  return OnnxNodeToCaffe2Ops(init_model, pred_model, ctx, &onnx_node);
}

void Caffe2Backend::CheckOpSchemaArguments(
    const caffe2::OpSchema& schema,
    const caffe2::OperatorDef& op) {
  const auto& schema_args = schema.args();
  if (schema_args.size() > 0) {
    std::vector<std::string> argnames;
    std::transform(
        schema_args.begin(),
        schema_args.end(),
        std::back_inserter(argnames),
        [](caffe2::OpSchema::Argument elem) { return elem.name(); });

    for (const auto& arg : op.arg()) {
      if (std::count(argnames.begin(), argnames.end(), arg.name()) == 0) {
        CAFFE_THROW(
            "Don't know how to map unexpected argument ",
            arg.name(),
            " (from operator ",
            op.type(),
            ")");
      }
    }
  } else {
    // A number of C2 operators do not declare proper arguments. Let's log the
    // error
    VLOG(2)
        << "Operator " << op.type()
        << " does not declare arguments in its schema. Please file a Caffe2 issue.";
  }
}

Caffe2Ops Caffe2Backend::OnnxNodeToCaffe2Ops(
    const ModelProto& init_model,
    const ModelProto& pred_model,
    const ConversionContext& ctx,
    OnnxNode* onnx_node) {
  Caffe2Ops res;
  if (get_special_operators().count(onnx_node->node.op_type())) {
    res = (this->*get_special_operators().at(onnx_node->node.op_type()))(
        onnx_node, ctx);
  } else {
    res = CommonOnnxNodeToCaffe2Ops(onnx_node, ctx);
  }

  for (const auto& result_op : res.ops) {
    const auto* schema = OpSchemaRegistry::Schema(result_op.type());
    if (schema) {
      CheckOpSchemaArguments(*schema, result_op);
    } else {
      CAFFE_THROW(
          "Caffe2 has no such operator, could not find schema for ",
          result_op.type());
    }
  }
  return res;
}

void Caffe2Backend::OnnxToCaffe2(
    caffe2::NetDef* init_net,
    caffe2::NetDef* pred_net,
    const ModelProto& onnx_model,
    const std::string& device,
    int opset_version,
    bool include_initializers,
    const std::vector<Caffe2Ops>& extras) {
  auto device_option = GetDeviceOption(Device(device));

  ModelProto init_model = ModelProto();
  ModelProto pred_model = onnx_model;
  pred_model.mutable_graph()->mutable_initializer()->Clear();

  init_net->set_name(onnx_model.graph().name() + "_init");
  pred_net->set_name(onnx_model.graph().name() + "_predict");

  // Convert initializer if necessary
  if (include_initializers) {
    for (const auto& tp : onnx_model.graph().initializer()) {
      auto* c2_op = init_net->add_op();
      BuildTensorFillingOp(c2_op, tp);
    }
  }

  auto name_set = AllNamesInGraph(init_model.graph());
  auto name_set_pred = AllNamesInGraph(pred_model.graph());
  name_set.insert(name_set_pred.begin(), name_set_pred.end());
  dummy_->Reset(name_set);

  ValueInfoMap graph_value_infos{};
  for (const auto& vi : pred_model.graph().input()) {
    graph_value_infos[vi.name()].CopyFrom(vi);
  }
  for (const auto& vi : pred_model.graph().output()) {
    graph_value_infos[vi.name()].CopyFrom(vi);
  }
  for (const auto& vi : pred_model.graph().value_info()) {
    graph_value_infos[vi.name()].CopyFrom(vi);
  }

  size_t idx_extra = 0;
  auto converter = [&](const ModelProto& model, caffe2::NetDef* net) mutable {
    net->mutable_device_option()->CopyFrom(device_option);
    for (const auto& node : model.graph().node()) {
      auto* init_net_tmp = include_initializers ? init_net : net;
      // For RNN operators, we rely on Python code to convert them for us, and
      // we simply deserilize the string. This is hack and eventually we want to
      // get rid of this to have one flow. Note that we need to update the dummy
      // name generator to avoid having duplicated names between Python and C++
      // generated dummies
      if (get_rnn_operators().count(node.op_type())) {
        if (idx_extra < extras.size()) {
          const auto& c2ops = extras[idx_extra++];
          for (const auto& op : c2ops.init_ops) {
            UpdateNames(dummy_, op);
          }
          init_net_tmp->mutable_op()->MergeFrom(c2ops.init_ops);
          for (const auto& op : c2ops.ops) {
            UpdateNames(dummy_, op);
          }
          net->mutable_op()->MergeFrom(c2ops.ops);
          for (const auto& input : c2ops.interface_blobs) {
            dummy_->AddName(input);
          }
          net->mutable_external_input()->MergeFrom(c2ops.interface_blobs);
        } else {
          CAFFE_THROW(
              "Don't know how to convert ",
              node.op_type(),
              " without enough extra preconverted string");
        }
      } else {
        ValueInfoMap value_infos{};
        for (const auto& name : node.input()) {
          auto iter = graph_value_infos.find(name);
          if (iter != graph_value_infos.end()) {
            value_infos[name].CopyFrom(iter->second);
          }
        }
        auto onnx_node = OnnxNode(node);
        auto c2ops = OnnxNodeToCaffe2Ops(
            init_model, pred_model, {value_infos, opset_version}, &onnx_node);
        init_net_tmp->mutable_op()->MergeFrom(c2ops.init_ops);
        net->mutable_op()->MergeFrom(c2ops.ops);
        net->mutable_external_input()->MergeFrom(c2ops.interface_blobs);
      }
    }

    for (const auto& value : model.graph().output()) {
      net->add_external_output(value.name());
    }
    for (const auto& value : model.graph().input()) {
      net->add_external_input(value.name());
    }
  };

  converter(init_model, init_net);
  converter(pred_model, pred_net);
}

Caffe2BackendRep* Caffe2Backend::Prepare(
    const std::string& onnx_model_str,
    const std::string& device,
    const std::vector<Caffe2Ops>& extras) {
  Caffe2BackendRep* rep = new Caffe2BackendRep();
  ModelProto onnx_model;
  ParseProtoFromLargeString(onnx_model_str, &onnx_model);

#ifndef C10_MOBILE
  ::ONNX_NAMESPACE::checker::check_model(onnx_model);
#endif

  int opset_version = -1;
  for (const auto& imp : onnx_model.opset_import()) {
    if ((!imp.has_domain()) || imp.domain().empty()) {
      opset_version = imp.version();
      if (opset_version > kKnownOpsetVersion) {
        std::cout
            << "This version of onnx-caffe2 targets ONNX operator set version "
            << kKnownOpsetVersion
            << ", but the model we are trying to import uses version "
            << opset_version << ".  We will try to import it anyway, "
            << "but if the model uses operators which had BC-breaking changes "
               "in the intervening versions, import will fail."
            << std::endl;
      }
    } else {
      std::cout << "Unrecognized operator set " << opset_version << std::endl;
    }
  }
  if (opset_version < 0) {
    if (onnx_model.ir_version() >= 0x00000003) {
      CAFFE_THROW(
          "Model with IR version >= 3 did not specify ONNX operator set "
          "version (onnx-caffe2 requires it)");
    } else {
      opset_version = 1;
    }
  }

  // TODO: avoid extra copy by directly feed initializers to backend blobs
  OnnxToCaffe2(
      &rep->init_net(),
      &rep->pred_net(),
      onnx_model,
      device,
      opset_version,
      true,
      extras);

  // Get a list of uninitialized inputs to help with the inference setup
  auto& uninitialized_inputs = rep->uninitialized_inputs();
  std::unordered_set<std::string> initialized_inputs;
  for (const auto& tp : onnx_model.graph().initializer()) {
    initialized_inputs.emplace(tp.name());
  }
  for (const auto& input : onnx_model.graph().input()) {
    if (!initialized_inputs.count(input.name())) {
      uninitialized_inputs.emplace_back(input.name());
    }
  }

  return rep;
}

template <typename T>
void ConvertIntegralValueToCaffe2(
    caffe2::OperatorDef* c2_op,
    caffe2::Argument* c2_values,
    const TensorProto& onnx_tensor) {
  c2_op->set_type(
      onnx_tensor.data_type() == TensorProto::BOOL ? "GivenTensorBoolFill"
                                                   : "GivenTensorIntFill");
  ::google::protobuf::RepeatedField<T> tmp;
  const ::google::protobuf::RepeatedField<T>* src = &tmp;
  bool converted = TryConvertingTensorRawValues<T>(onnx_tensor, &tmp);
  if (converted) {
    for (const auto i : *src) {
      c2_values->add_ints(i);
    }
  } else {
    const ::google::protobuf::RepeatedField<::google::protobuf::int32>*
        int32_src = &onnx_tensor.int32_data();
    for (const auto i : *int32_src) {
      c2_values->add_ints(i);
    }
  }
}

template <>
void ConvertIntegralValueToCaffe2<::google::protobuf::int64>(
    caffe2::OperatorDef* c2_op,
    caffe2::Argument* c2_values,
    const TensorProto& onnx_tensor) {
  c2_op->set_type("GivenTensorInt64Fill");
  auto* ints = c2_values->mutable_ints();
  if (!TryConvertingTensorRawValues<::google::protobuf::int64>(
          onnx_tensor, ints)) {
    ints->CopyFrom(onnx_tensor.int64_data());
  }
}

template <>
void ConvertIntegralValueToCaffe2<::google::protobuf::uint64>(
    caffe2::OperatorDef* c2_op,
    caffe2::Argument* c2_values,
    const TensorProto& onnx_tensor) {
  c2_op->set_type("GivenTensorInt64Fill");
  ::google::protobuf::RepeatedField<::google::protobuf::uint64> tmp;
  const ::google::protobuf::RepeatedField<::google::protobuf::uint64>* src =
      &tmp;
  if (!TryConvertingTensorRawValues<::google::protobuf::uint64>(
          onnx_tensor, &tmp)) {
    src = &onnx_tensor.uint64_data();
  }
  for (const auto i : *src) {
    c2_values->add_ints(i);
  }
}

void Caffe2Backend::BuildTensorFillingOp(
    caffe2::OperatorDef* c2_op,
    const TensorProto& onnx_tensor,
    const std::string& output_name,
    const std::string& shape_name) {
  auto fill_name = output_name.empty() ? onnx_tensor.name() : output_name;
  CAFFE_ENFORCE(!fill_name.empty());

  if (onnx_tensor.has_segment()) {
    CAFFE_THROW("Currently not supporting loading segments.");
  }

  auto* c2_values = c2_op->add_arg();
  // if shape_name is empty, we generate GivenTensorFill
  // otherwise, we generate ConstantFill, which accept shape as input
  if (shape_name.empty()) {
    // GivenTensor*Fill uses values
    c2_values->set_name("values");
    if (onnx_tensor.data_type() == TensorProto::FLOAT) {
      c2_op->set_type("GivenTensorFill");
      auto* floats = c2_values->mutable_floats();
      if (!TryConvertingTensorRawValues<float>(onnx_tensor, floats)) {
        floats->CopyFrom(onnx_tensor.float_data());
      }
    } else if (onnx_tensor.data_type() == TensorProto::DOUBLE) {
      c2_op->set_type("GivenTensorDoubleFill");
      ::google::protobuf::RepeatedField<double> tmp;
      const ::google::protobuf::RepeatedField<double>* src = &tmp;
      if (!TryConvertingTensorRawValues<double>(onnx_tensor, &tmp)) {
        src = &onnx_tensor.double_data();
      }
      for (const auto i : *src) {
        c2_values->add_floats(i);
      }
    } else if (onnx_tensor.data_type() == TensorProto::INT64) {
      ConvertIntegralValueToCaffe2<::google::protobuf::int64>(
          c2_op, c2_values, onnx_tensor);
    } else if (onnx_tensor.data_type() == TensorProto::UINT32) {
      ConvertIntegralValueToCaffe2<::google::protobuf::uint64>(
          c2_op, c2_values, onnx_tensor);
      // NOLINTNEXTLINE(bugprone-branch-clone)
    } else if (onnx_tensor.data_type() == TensorProto::BOOL) {
      ConvertIntegralValueToCaffe2<::google::protobuf::int8>(
          c2_op, c2_values, onnx_tensor);
    } else if (onnx_tensor.data_type() == TensorProto::UINT8) {
      ConvertIntegralValueToCaffe2<::google::protobuf::uint8>(
          c2_op, c2_values, onnx_tensor);
    } else if (onnx_tensor.data_type() == TensorProto::INT8) {
      ConvertIntegralValueToCaffe2<::google::protobuf::int8>(
          c2_op, c2_values, onnx_tensor);
    } else if (onnx_tensor.data_type() == TensorProto::UINT16) {
      ConvertIntegralValueToCaffe2<::google::protobuf::uint16>(
          c2_op, c2_values, onnx_tensor);
    } else if (onnx_tensor.data_type() == TensorProto::INT16) {
      ConvertIntegralValueToCaffe2<::google::protobuf::int16>(
          c2_op, c2_values, onnx_tensor);
    } else if (onnx_tensor.data_type() == TensorProto::INT32) {
      ConvertIntegralValueToCaffe2<::google::protobuf::int32>(
          c2_op, c2_values, onnx_tensor);
    } else if (onnx_tensor.data_type() == TensorProto::STRING) {
      c2_op->set_type("GivenTensorStringFill");
      auto* strings = c2_values->mutable_strings();
      strings->CopyFrom(onnx_tensor.string_data());
    } else {
      CAFFE_THROW("unrecognized tensor type: ", onnx_tensor.data_type());
    }
    auto* c2_shape = c2_op->add_arg();
    c2_shape->set_name("shape");
    for (const auto d : onnx_tensor.dims()) {
      c2_shape->add_ints(d);
    }
  } else {
    int value_size = 1;
    for (const auto d : onnx_tensor.dims()) {
      value_size *= d;
    }
    CAFFE_ENFORCE(value_size == 1);
    auto c2_input_as_shape = c2_op->add_arg();
    c2_input_as_shape->set_name("input_as_shape");
    c2_input_as_shape->set_i(1);
    c2_values->set_name("value");
    auto* c2_dtype = c2_op->add_arg();
    c2_dtype->set_name("dtype");
    if (onnx_tensor.data_type() == TensorProto::FLOAT) {
      c2_dtype->set_i(caffe2::TensorProto::FLOAT);
      if (onnx_tensor.float_data_size() > 0) {
        c2_values->set_f(onnx_tensor.float_data(0));
      } else {
        CAFFE_ENFORCE(onnx_tensor.raw_data().size() == sizeof(float));
        // NOLINTNEXTLINE(cppcoreguidelines-init-variables)
        float f;
        memcpy(&f, onnx_tensor.raw_data().c_str(), sizeof(float));
        c2_values->set_f(f);
      }
    } else if (onnx_tensor.data_type() == TensorProto::DOUBLE) {
      c2_dtype->set_i(caffe2::TensorProto::DOUBLE);
      if (onnx_tensor.double_data_size() > 0) {
        c2_values->set_f(static_cast<float>(onnx_tensor.double_data(0)));
      } else {
        CAFFE_ENFORCE(onnx_tensor.raw_data().size() == sizeof(double));
        // NOLINTNEXTLINE(cppcoreguidelines-init-variables)
        double d;
        memcpy(&d, onnx_tensor.raw_data().c_str(), sizeof(double));
        c2_values->set_f(static_cast<float>(d));
      }
    } else if (onnx_tensor.data_type() == TensorProto::INT64) {
      c2_dtype->set_i(caffe2::TensorProto::INT64);
      if (onnx_tensor.int64_data_size() > 0) {
        c2_values->set_i(onnx_tensor.int64_data(0));
      } else {
        CAFFE_ENFORCE(onnx_tensor.raw_data().size() == sizeof(int64_t));
        // NOLINTNEXTLINE(cppcoreguidelines-init-variables)
        int64_t i;
        memcpy(&i, onnx_tensor.raw_data().c_str(), sizeof(int64_t));
        c2_values->set_i(i);
      }
    } else if (onnx_tensor.data_type() == TensorProto::INT32) {
      c2_dtype->set_i(caffe2::TensorProto::INT32);
      if (onnx_tensor.int32_data_size() > 0) {
        c2_values->set_i(onnx_tensor.int32_data(0));
      } else {
        CAFFE_ENFORCE(onnx_tensor.raw_data().size() == sizeof(int32_t));
        // NOLINTNEXTLINE(cppcoreguidelines-init-variables)
        int32_t i;
        memcpy(&i, onnx_tensor.raw_data().c_str(), sizeof(int32_t));
        c2_values->set_i(i);
      }
    } else {
      // TODO: to support more data type
      std::stringstream oss;
      oss << "Unsupported dtype: " << onnx_tensor.data_type();
      CAFFE_THROW(oss.str());
    }
    // ConstantFill uses value
    c2_op->set_type("ConstantFill");
    c2_op->add_input(shape_name);
  }

  c2_op->add_output(fill_name);
}

bool Caffe2Backend::SupportOp(const std::string type) const {
  return get_special_operators().count(type);
}

} // namespace onnx
} // namespace caffe2
