#include "caffe2/core/context.h"
#include "caffe2/core/logging.h"
#include "caffe2/core/operator.h"
#include "caffe2/core/timer.h"
#include "snpe_ffi.h"
#include <dlfcn.h>

namespace caffe2 {

template <typename T>
using deleted_unique_ptr = std::unique_ptr<T, std::function<void(T*)>>;

class SNPEOp final : public Operator<CPUContext> {
 public:
  SNPEOp(const OperatorDef& def, Workspace* ws) : Operator<CPUContext>(def, ws),
        model_buffer_(OperatorBase::GetSingleArgument<string>("model_buffer", "")),
        input_name_(OperatorBase::GetSingleArgument<string>("input_name", "data"))
  {
    CAFFE_ENFORCE(gSNPELocation() != "", "SNPE library \"", gSNPELocation(), "\" does not exist.");
    std::ostringstream snpe_ffi;
    snpe_ffi << gSNPELocation() << "/" << snpe_ffi_so;
    handle_ = deleted_unique_ptr<void>(dlopen(snpe_ffi.str().c_str(), RTLD_LAZY), [](void* handle) {
      if (handle) {
        dlclose(handle);
      }
    });
    if (!handle_.get()) {
      std::cerr << dlerror() << std::endl;
    }

    OPERATOR_NEEDS_FEATURE(handle_.get(), "Couldn't find ", snpe_ffi.str());

#define X(n)                                    \
  dlerror();                                    \
  auto* n##_f = (decltype(&n))dlsym(handle_.get(), #n); \
  OPERATOR_NEEDS_FEATURE(n##_f, dlerror());

    {
      X(snpe_has_gpu);
      X(snpe_create);
      X(snpe_destroy);
      X(snpe_get_input_dims);
      X(snpe_run);
      X(snpe_copy_output_to);
    }

    X(snpe_has_gpu);
    OPERATOR_NEEDS_FEATURE(snpe_has_gpu_f(), "No GPU found, cannot use SNPE.");

    X(snpe_create)
#undef X

// Redefine to use CAFFE_ENFORCE instead of OPERATOR_NEEDS_FEATURE.

#define X(n)                                              \
      dlerror();                                          \
      auto* n##_f = (decltype(&n))dlsym(handle_.get(), #n); \
      CAFFE_ENFORCE(n##_f, dlerror());

    CAFFE_ENFORCE(def.input_size(), "No inputs.");
    if (input_name_ == "") {
      input_name_ = def.input().Get(0);
		}
    ctx_ = deleted_unique_ptr<void>(snpe_create_f(reinterpret_cast<const unsigned char *>(model_buffer_.data()),
          model_buffer_.length(), input_name_.c_str()), [this](void* ctx) {
      if (ctx) {
        X(snpe_destroy);
        snpe_destroy_f(ctx);
      }
    });
  }

  bool RunOnDevice() override {
    CAFFE_ENFORCE(gSNPELocation() != "", "SNPE library was never loaded.");

    X(snpe_get_input_dims);
    size_t const* dims;
    size_t dimSize;
    snpe_get_input_dims_f(ctx_.get(), &dims, &dimSize);
    if (Input(0).ndim() != dimSize) {
      if (dimSize == 3 && dimSize == Input(0).ndim() - 1 && Input(0).dim32(0) == 1) {
        const int C = Input(0).dim32(1);
        const int H = Input(0).dim32(2);
        const int W = Input(0).dim32(3);
        if (dims[0] != C ||
            dims[1] != H ||
            dims[2] != W) {
          CAFFE_THROW("Input size must match what SNPE expects, which in this case is: ",
              dims[0], " ", dims[1], " ", dims[2]);
        }
      } else {
        CAFFE_THROW("SNPE input dimensions are not compatible.");
      }
    } else {
      for (auto i = 0; i < Input(0).ndim(); ++i) {
        CAFFE_ENFORCE_EQ(dims[i], Input(0).dim32(i), "SNPE input dimension is not compatible.");
      }
		}

    X(snpe_run);
    CAFFE_ENFORCE(ctx_.get(), "SNPE context doesn't exist.");
    snpe_run_f(ctx_.get(), Input(0).data<float>(), Input(0).size(), &dims, &dimSize);

    std::vector<int64_t> outputDims(dimSize + 1);
    outputDims[0] = 1;
    for (auto i = 0; i < dimSize; ++i) {
      outputDims[i+1] = dims[i];
    };

    Output(0)->Resize(outputDims);
    X(snpe_copy_output_to);
    snpe_copy_output_to_f(ctx_.get(), Output(0)->mutable_data<float>());

    CAFFE_ENFORCE(
        Output(0)->data<float>(), "nullptr where output should be!\n");
    return true;
  }

 private:
  string model_buffer_;
  string input_name_;
  deleted_unique_ptr<void> handle_;
  // needs to be destroyed *before* handle_
  deleted_unique_ptr<void> ctx_;
};

REGISTER_CPU_OPERATOR(SNPE, SNPEOp);
}

#undef X
