// Note(jiayq): the import_array function is done inside
// caffe2_python.cc. Read
// http://docs.scipy.org/doc/numpy-1.10.1/reference/c-api.array.html#miscellaneous
// for more details.
#define NO_IMPORT_ARRAY

#include "pybind_state.h"

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

#include "caffe2/ideep/operators/operator_fallback_ideep.h"
#include <caffe2/ideep/ideep_utils.h>

namespace caffe2 {
namespace python {

USE_IDEEP_DEF_ALIASES();

class IDeepFetcher;
class IDeepFeeder;

REGISTER_IDEEP_OPERATOR(Python, IDEEPFallbackOp<PythonOp<CPUContext, false>>);

REGISTER_BLOB_FETCHER((TypeMeta::Id<itensor>()), IDeepFetcher);
REGISTER_BLOB_FEEDER(IDEEP, IDeepFeeder);

class IDeepFetcher : public BlobFetcherBase {
  TypeMeta type_transform(const itensor &atensor) {
    switch (atensor.get_data_type()) {
    case itensor::data_type::f32:
      return TypeMeta::Make<float>();
    case itensor::data_type::s32:
      return TypeMeta::Make<int>();
    case itensor::data_type::s8:
      return TypeMeta::Make<int8_t>();
    case itensor::data_type::u8:
      return TypeMeta::Make<uint8_t>();
    default:
      // Should we throw exception?
      return TypeMeta();
    }
  }

public:
  pybind11::object Fetch(const Blob &blob) override {
    try {
      return FetchTensor(blob.Get<itensor>(), true).obj;
    } catch (ideep::error &e) {
      LOG(ERROR) << "IDEEP error: " << e.message;
      throw;
    }
  }

  FetchedBlob FetchTensor(const itensor &atensor, bool force_copy) {
#ifdef USE_NUMPY
    FetchedBlob result;
    CAFFE_ENFORCE((atensor.ndims() != 0) &&
                  (atensor.get_nelems() == 0 ||
                   atensor.get_data_handle() != nullptr),
                  "Trying to fetch uninitialized tensor");
    // NOTE: Only support float so far.
    const int numpy_type = NPY_FLOAT;
    CAFFE_ENFORCE(
        numpy_type != -1,
        "Unsupported ideep memory data type? This usually should not happen "
        "since ideep memory usually only do float and double.");
    itensor::dims dims = atensor.get_public_format_dims();
    std::vector<npy_intp> npy_dims(dims.begin(), dims.end());

    result.copied = force_copy || atensor.need_reorder();
    void *outPtr;
    if (result.copied) {
      result.obj = py::reinterpret_steal<py::object>(
          PyArray_SimpleNew(atensor.ndims(), npy_dims.data(), numpy_type));
      outPtr = static_cast<void *>(
          PyArray_DATA(reinterpret_cast<PyArrayObject *>(result.obj.ptr())));
    } else {
      outPtr = atensor.get_data_handle();
      result.obj = py::reinterpret_steal<py::object>(PyArray_SimpleNewFromData(
          atensor.ndims(), npy_dims.data(), numpy_type, outPtr));
    }

    if (numpy_type == NPY_OBJECT) {
      CAFFE_THROW("We don't support strings.");
    }

    if (result.copied) {
      atensor.to_public(outPtr);
    }

    return result;
#else
    CAFFE_THROW("Caffe2 was compiled without NumPy support.");
#endif // USE_NUMPY
  }
};

class IDeepFeeder : public BlobFeederBase {
  itensor::data_type type_transform(const TypeMeta &meta) {
    if (meta == TypeMeta::Make<float>())
      return itensor::data_type::f32;
    else if (meta == TypeMeta::Make<int>())
      return itensor::data_type::s32;
    else if (meta == TypeMeta::Make<int8_t>())
      return itensor::data_type::s8;
    else if (meta == TypeMeta::Make<uint8_t>())
      return itensor::data_type::u8;
    else
      return itensor::data_type::data_undef;
  }

public:
  void FeedTensor(
      const DeviceOption &option,
      PyArrayObject *original_array,
      itensor *tensor) {
#ifdef USE_NUMPY
    PyArrayObject *array = PyArray_GETCONTIGUOUS(original_array);
    auto g = MakeGuard([&]() { Py_XDECREF(array); });
    const auto npy_type = PyArray_TYPE(array);
    const TypeMeta &meta = NumpyTypeToCaffe(npy_type);
    CAFFE_ENFORCE_NE(
        meta.id(),
        TypeIdentifier::uninitialized(),
        "This numpy data type is not supported: ",
        PyArray_TYPE(array), ".");

    int ndim = PyArray_NDIM(array);
    npy_intp *npy_dims = PyArray_DIMS(array);

    itensor::dims adims;
    for (int i = 0; i < ndim; i++) {
      adims.push_back(static_cast<itensor::dims::value_type>(npy_dims[i]));
    }

    switch (npy_type) {
      case NPY_OBJECT:
      case NPY_UNICODE:
        CAFFE_THROW("IDeep doesn't support string");
        break;
      default:
        auto type = type_transform(meta);
        if (tensor->get_dims() != adims || type != tensor->get_data_type()) {
          tensor->resize(adims, type);
        }
        tensor->feed_from(adims, type,
                             static_cast<void *>(PyArray_DATA(array)));
    }
#else
    CAFFE_THROW("Caffe2 was compiled without NumPy support.");
#endif // USE_NUMPY
  }

  bool ZeroDim(PyArrayObject *array) {
#ifdef USE_NUMPY
    int ndim = PyArray_NDIM(array);
    return ndim == 0;
#else
    CAFFE_THROW("Caffe2 was compiled without NumPy support.");
#endif
  }

  void Feed(
      const DeviceOption& option,
      PyArrayObject* original_array,
      Blob* blob,
      bool in_place) override {
#ifdef USE_NUMPY
    try {
      PyArrayObject *array = PyArray_GETCONTIGUOUS(original_array);
      auto g = MakeGuard([&]() { Py_XDECREF(array); });

      const auto npy_type = PyArray_TYPE(array);
      const TypeMeta &meta = NumpyTypeToCaffe(npy_type);

      // TODO: if necessary, use dispatcher.
      if ((in_place && blob->IsType<itensor>())
          || (meta.Match<float>() && !ZeroDim(original_array))) {
        FeedTensor(option, original_array, blob->GetMutable<itensor>());
      } else {
        DeviceOption cpu_option(option);
        cpu_option.set_device_type(DeviceTypeProto::PROTO_CPU);
        TensorFeeder<CPUContext> cpu_tensor_feeder;
        if (in_place) {
          cpu_tensor_feeder.FeedTensor(
              cpu_option,
              original_array,
              BlobGetMutableTensor(blob, OptionToDevice(cpu_option).type()),
              true);
        } else {
          blob->Reset<Tensor>(new Tensor(
                                  cpu_tensor_feeder.FeedTensor(cpu_option, original_array)));
        }
      }
    } catch (ideep::error &e) {
      LOG(ERROR) << "IDEEP error: " << e.message;
      throw;
    }
#else
    CAFFE_THROW("Caffe2 was compiled without NumPy support.");
#endif
  }
};

} // namespace python
} // namespace caffe2
