/**
 * @license
 * Copyright 2018 Google LLC. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * =============================================================================
 */

#include "tfjs_backend.h"

#include <algorithm>
#include <cstring>
#include <memory>
#include <set>
#include <string>
#include "napi_auto_ref.h"
#include "tensorflow/c/eager/c_api.h"
#include "tensorflow/c/tf_datatype.h"
#include "tensorflow/c/tf_status.h"
#include "tensorflow/c/tf_tensor.h"
#include "tensorflow/core/platform/ctstring_internal.h"
#include "tf_auto_tensor.h"
#include "tfe_auto_op.h"
#include "utils.h"

namespace tfnodejs {

// Used to hold strings beyond the lifetime of a JS call.
static std::set<std::string> ATTR_NAME_SET;

// Callback to cleanup extra reference count for shared V8/TF tensor memory:
static void DeallocTensor(void *data, size_t len, void *arg) {
  NapiAutoRef *auto_ref = static_cast<NapiAutoRef *>(arg);
  if (!auto_ref) {
#if DEBUG
    fprintf(stderr, "Invalid NapiAutoRef reference passed to V8 cleanup\n");
#endif
    return;
  }
  if (auto_ref->Cleanup() != napi_ok) {
#if DEBUG
    fprintf(stderr, "Exception cleaning up napi_ref instance\n");
#endif
  }
  delete auto_ref;
}

// Creates a TFE_TensorHandle from a JS typed array.
TFE_TensorHandle *CreateTFE_TensorHandleFromTypedArray(napi_env env,
                                                       int64_t *shape,
                                                       uint32_t shape_length,
                                                       TF_DataType dtype,
                                                       napi_value array_value) {
  napi_status nstatus;
  napi_typedarray_type array_type;
  size_t array_length;
  void *array_data;
  nstatus =
      napi_get_typedarray_info(env, array_value, &array_type, &array_length,
                               &array_data, nullptr, nullptr);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  // Double check the underlying TF_Tensor type matches the supplied
  // typed-array.
  size_t width = 0;
  switch (array_type) {
    case napi_float32_array:
      if (dtype != TF_FLOAT) {
        NAPI_THROW_ERROR(env, "Tensor type does not match Float32Array");
        return nullptr;
      }
      width = sizeof(float);
      break;
    case napi_int32_array:
      if (dtype != TF_INT32 && dtype != TF_INT64) {
        // Currently, both int32- and int64-type Tensors are represented
        // as Int32Arrays in JavaScript. See int64_tensors.ts for details
        // about the latter.
        NAPI_THROW_ERROR(env, "Tensor type does not match Int32Array");
        return nullptr;
      }
      width = sizeof(int32_t);
      break;
    case napi_uint8_array:
      if (dtype != TF_BOOL && dtype != TF_UINT8) {
        NAPI_THROW_ERROR(env, "Tensor type does not match Uint8Array");
        return nullptr;
      }
      width = sizeof(uint8_t);
      break;
    default:
      REPORT_UNKNOWN_TYPED_ARRAY_TYPE(env, array_type);
      return nullptr;
  }

  // Double check that width matches TF data type size:
  if (dtype == TF_INT64) {
    // Currently, int64-type Tensors are represented as Int32Arrays. So the
    // logic for comparing the byte size of the typed-array representation and
    // the byte size of the tensor dtype needs to be special-cased for int64.
    if (width * 2 != TF_DataTypeSize(dtype)) {
      NAPI_THROW_ERROR(
          env,
          "Byte size of elements differs between JavaScript VM "
          "(%zu * 2 = %zu) and TensorFlow (%zu) for int64-type tensor",
          width, width * 2, TF_DataTypeSize(dtype));
      return nullptr;
    }
  } else {
    if (width != TF_DataTypeSize(dtype)) {
      NAPI_THROW_ERROR(env,
                       "Byte size of elements differs between JavaScript VM "
                       "(%zu) and TensorFlow (%zu)",
                       width, TF_DataTypeSize(dtype));
      return nullptr;
    }
  }

  // Determine the size of the buffer based on the dimensions.
  size_t num_elements = 1;
  for (size_t i = 0; i < shape_length; i++) {
    num_elements *= shape[i];
  }

  // Ensure the shape matches the length of the passed in typed-array.
  if (dtype == TF_INT64) {
    // Currently, int64-type Tensors are represented as Int32Arrays.
    // To represent a int64-type Tensor of `n` elements, an Int32Array of
    // length `2 * n` is requried. This is why the length-match checking
    // logic is special-cased for int64.
    if (array_length != num_elements * 2) {
      NAPI_THROW_ERROR(
          env,
          "Shape does not match two times typed-array in bindData() "
          "(num_elements * 2 = %zu, array_length=%zu) for int64 data type",
          num_elements * 2, array_length);
      return nullptr;
    }
  } else {
    if (num_elements != array_length) {
      NAPI_THROW_ERROR(env,
                       "Shape does not match typed-array in bindData() "
                       "(num_elements=%zu, array_length=%zu)",
                       num_elements, array_length);
      return nullptr;
    }
  }

  // Sharing V8 memory with the underlying TensorFlow tensor requires adding an
  // additional refcount. When the Tensor is deleted, the refcount will be
  // reduced in the callback helper.
  NapiAutoRef *auto_ref = new NapiAutoRef();
  nstatus = auto_ref->Init(env, array_value);
  if (nstatus != napi_ok) {
    delete auto_ref;
  }
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  // Currently, int64-type Tensors are represented as Int32Arrays.
  // So the logic for comparing the byte size of the typed-array representation
  // and the byte size of the tensor dtype needs to be special-cased for int64.
  const size_t byte_size =
      dtype == TF_INT64 ? num_elements * width * 2 : num_elements * width;

  TF_AutoTensor tensor(TF_NewTensor(dtype, shape, shape_length, array_data,
                                    byte_size, DeallocTensor, auto_ref));

  TF_AutoStatus tf_status;
  TFE_TensorHandle *tfe_tensor_handle =
      TFE_NewTensorHandle(tensor.tensor, tf_status.status);
  if (TF_GetCode(tf_status.status) != TF_OK) {
    delete auto_ref;
    TFE_DeleteTensorHandle(tfe_tensor_handle);
  }
  ENSURE_TF_OK_RETVAL(env, tf_status, nullptr);

  return tfe_tensor_handle;
}

// Creates a TFE_TensorHandle from a JS array of Uint8Array values.
TFE_TensorHandle *CreateTFE_TensorHandleFromStringArray(
    napi_env env, int64_t *shape, uint32_t shape_length, TF_DataType dtype,
    napi_value array_value) {
  napi_status nstatus;

  uint32_t array_length;
  nstatus = napi_get_array_length(env, array_value, &array_length);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  TF_AutoTensor tensor(TF_AllocateTensor(TF_DataType::TF_STRING, shape,
                                         shape_length,
                                         array_length * sizeof(TF_TString)));

  TF_TString *t = reinterpret_cast<TF_TString *>(TF_TensorData(tensor.tensor));

  for (uint32_t i = 0; i < array_length; ++i) {
    napi_value cur_value;
    nstatus = napi_get_element(env, array_value, i, &cur_value);
    ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);
    ENSURE_VALUE_IS_TYPED_ARRAY_RETVAL(env, cur_value, nullptr);

    napi_typedarray_type array_type;
    size_t cur_array_length;
    void *buffer = nullptr;

    nstatus =
        napi_get_typedarray_info(env, cur_value, &array_type, &cur_array_length,
                                 &buffer, nullptr, nullptr);
    ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);
    // Only Uint8 typed arrays are supported.
    if (array_type != napi_uint8_array) {
      NAPI_THROW_ERROR(env, "Unsupported array type - expecting Uint8Array");
      return nullptr;
    }

    TF_TString_Init(t);
    TF_TString_Copy(t, reinterpret_cast<char *>(buffer), cur_array_length);
    t += 1;
  }

  TF_AutoStatus tf_status;

  TFE_TensorHandle *tfe_tensor_handle =
      TFE_NewTensorHandle(tensor.tensor, tf_status.status);
  ENSURE_TF_OK_RETVAL(env, tf_status, nullptr);
  return tfe_tensor_handle;
}

TFE_TensorHandle *CreateTFE_TensorHandleFromJSValues(napi_env env,
                                                     int64_t *shape,
                                                     uint32_t shape_length,
                                                     TF_DataType dtype,
                                                     napi_value array_value) {
  bool is_typed_array;
  napi_status nstatus = napi_is_typedarray(env, array_value, &is_typed_array);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);
  if (is_typed_array) {
    return CreateTFE_TensorHandleFromTypedArray(env, shape, shape_length, dtype,
                                                array_value);
  } else {
    return CreateTFE_TensorHandleFromStringArray(env, shape, shape_length,
                                                 dtype, array_value);
  }
}

TFE_TensorHandle *CopyTFE_TensorHandleToDevice(napi_env env,
                                               const char *device_name,
                                               TFE_TensorHandle *handle,
                                               TFE_Context *tfe_context) {
  TF_AutoStatus tf_status;

  TFE_TensorHandle *new_handle = TFE_TensorHandleCopyToDevice(
      handle, tfe_context, device_name, tf_status.status);
  ENSURE_TF_OK_RETVAL(env, tf_status, nullptr);

  return new_handle;
}

void CopyTFE_TensorHandleDataToTypedArray(napi_env env,
                                          TFE_Context *tfe_context,
                                          TFE_TensorHandle *tfe_tensor_handle,
                                          TF_DataType tensor_data_type,
                                          napi_typedarray_type array_type,
                                          napi_value *result) {
  TF_AutoStatus tf_status;

  TF_AutoTensor tensor(
      TFE_TensorHandleResolve(tfe_tensor_handle, tf_status.status));
  ENSURE_TF_OK(env, tf_status);

  // Determine the length of the array based on the shape of the tensor.
  size_t num_elements = GetTensorNumElements(tensor.tensor);

  if (tensor_data_type == TF_COMPLEX64) {
    // Dimension length will be double for Complex 64.
    num_elements *= 2;
  }

  size_t byte_length = TF_TensorByteSize(tensor.tensor);

  napi_value array_buffer_value;
  void *array_buffer_data;
  napi_status nstatus;
  nstatus = napi_create_arraybuffer(env, byte_length, &array_buffer_data,
                                    &array_buffer_value);
  ENSURE_NAPI_OK(env, nstatus);

  // TFE_TensorHandleResolve can use a shared data pointer, memcpy() the
  // current value to the newly allocated NAPI buffer.
  memcpy(array_buffer_data, TF_TensorData(tensor.tensor), byte_length);

  nstatus = napi_create_typedarray(env, array_type, num_elements,
                                   array_buffer_value, 0, result);
  ENSURE_NAPI_OK(env, nstatus);
}

void CopyTFE_TensorHandleDataToStringArray(napi_env env,
                                           TFE_Context *tfe_context,
                                           TFE_TensorHandle *tfe_tensor_handle,
                                           napi_value *result) {
  TF_AutoStatus tf_status;

  TF_AutoTensor tensor(
      TFE_TensorHandleResolve(tfe_tensor_handle, tf_status.status));
  ENSURE_TF_OK(env, tf_status);

  if (TF_TensorType(tensor.tensor) != TF_STRING) {
    NAPI_THROW_ERROR(env, "Tensor is not of type TF_STRING");
    return;
  }

  size_t num_elements = TF_TensorElementCount(tensor.tensor);

  TF_TString *ts = static_cast<TF_TString *>(TF_TensorData(tensor.tensor));
  TF_AutoStatus status;

  // Create a JS string to stash strings into
  napi_status nstatus;
  nstatus = napi_create_array_with_length(env, num_elements, result);

  for (uint64_t i = 0; i < num_elements; i++) {
    napi_value array_buffer_value;

    const char *raw_string = TF_TString_GetDataPointer(ts);
    size_t raw_string_size = TF_TString_GetSize(ts);
    void *array_buffer_data;
    nstatus = napi_create_arraybuffer(env, raw_string_size, &array_buffer_data,
                                      &array_buffer_value);
    ENSURE_NAPI_OK(env, nstatus);

    memcpy(array_buffer_data, raw_string, raw_string_size);

    napi_value typed_array_value;
    nstatus = napi_create_typedarray(env, napi_uint8_array, raw_string_size,
                                     array_buffer_value, 0, &typed_array_value);
    ENSURE_NAPI_OK(env, nstatus);

    nstatus = napi_set_element(env, *result, i, typed_array_value);
    ENSURE_NAPI_OK(env, nstatus);

    ts += 1;
  }
}

void CopyTFE_TensorHandleDataToResourceArray(
    napi_env env, TFE_Context *tfe_context, TFE_TensorHandle *tfe_tensor_handle,
    napi_value *result) {
  TF_AutoStatus tf_status;

  TF_AutoTensor tensor(
      TFE_TensorHandleResolve(tfe_tensor_handle, tf_status.status));
  ENSURE_TF_OK(env, tf_status);

  if (TF_TensorType(tensor.tensor) != TF_RESOURCE) {
    NAPI_THROW_ERROR(env, "Tensor is not of type TF_RESOURCE");
    return;
  }

  void *tensor_data = TF_TensorData(tensor.tensor);
  ENSURE_VALUE_IS_NOT_NULL(env, tensor_data);

  size_t num_elements = GetTensorNumElements(tensor.tensor);
  if (num_elements != 1) {
    NAPI_THROW_ERROR(env,
                     "For DT_RESOURCE tensors, Node.js binding currently "
                     "supports only exactly 1 element, but encountered "
                     "DT_RESOURCE tensor with %zu elements.",
                     num_elements);
  }

  TF_AutoStatus status;

  // Create a JS string to stash the resouce handle into.
  napi_status nstatus;
  size_t byte_length = TF_TensorByteSize(tensor.tensor);
  nstatus = napi_create_array_with_length(env, byte_length, result);
  ENSURE_NAPI_OK(env, nstatus);

  napi_value array_buffer_value;
  void *array_buffer_data = nullptr;
  nstatus = napi_create_arraybuffer(env, byte_length, &array_buffer_data,
                                    &array_buffer_value);
  ENSURE_NAPI_OK(env, nstatus);

  // TFE_TensorHandleResolve can use a shared data pointer, memcpy() the
  // current value to the newly allocated NAPI buffer.
  memcpy(array_buffer_data, tensor_data, byte_length);

  // This method will only return uint8 arrays.
  nstatus = napi_create_typedarray(env, napi_uint8_array, byte_length,
                                   array_buffer_value, 0, result);
  ENSURE_NAPI_OK(env, nstatus);
}

// Handles converting the stored TF_Tensor data into the correct JS value.
void CopyTFE_TensorHandleDataToJSData(napi_env env, TFE_Context *tfe_context,
                                      TFE_TensorHandle *tfe_tensor_handle,
                                      napi_value *result) {
  if (tfe_context == nullptr) {
    NAPI_THROW_ERROR(env, "Invalid TFE_Context");
    return;
  }
  if (tfe_tensor_handle == nullptr) {
    NAPI_THROW_ERROR(env, "Invalid TFE_TensorHandle");
    return;
  }

  // Determine the type of the array
  napi_typedarray_type typed_array_type;
  bool is_string = false;
  bool is_resource = false;
  TF_DataType tensor_data_type = TFE_TensorHandleDataType(tfe_tensor_handle);
  switch (tensor_data_type) {
    case TF_COMPLEX64:
    case TF_FLOAT:
      typed_array_type = napi_float32_array;
      break;
    case TF_INT32:
      typed_array_type = napi_int32_array;
      break;
    case TF_INT64:
      typed_array_type = napi_bigint64_array;
      break;
    case TF_BOOL:
      typed_array_type = napi_uint8_array;
      break;
    case TF_STRING:
      is_string = true;
      break;
    case TF_RESOURCE:
      // We currently represent a resource handle as an `Uint8Array`.
      typed_array_type = napi_uint8_array;
      is_resource = true;
      break;
    default:
      REPORT_UNKNOWN_TF_DATA_TYPE(env,
                                  TFE_TensorHandleDataType(tfe_tensor_handle));
      return;
  }

  if (is_string) {
    CopyTFE_TensorHandleDataToStringArray(env, tfe_context, tfe_tensor_handle,
                                          result);
  } else if (is_resource) {
    CopyTFE_TensorHandleDataToResourceArray(env, tfe_context, tfe_tensor_handle,
                                            result);
  } else {
    CopyTFE_TensorHandleDataToTypedArray(env, tfe_context, tfe_tensor_handle,
                                         tensor_data_type, typed_array_type,
                                         result);
  }
}

void GetTFE_TensorHandleShape(napi_env env, TFE_TensorHandle *handle,
                              napi_value *result) {
  napi_status nstatus;

  TF_AutoStatus tf_status;
  uint32_t num_dims = TFE_TensorHandleNumDims(handle, tf_status.status);
  ENSURE_TF_OK(env, tf_status);

  if (num_dims == 0) {
    nstatus = napi_create_array_with_length(env, 0, result);
    ENSURE_NAPI_OK(env, nstatus);
  } else {
    nstatus = napi_create_array_with_length(env, num_dims, result);
    ENSURE_NAPI_OK(env, nstatus);

    for (uint32_t i = 0; i < num_dims; i++) {
      napi_value cur_dim;
      nstatus = napi_create_int64(
          env, TFE_TensorHandleDim(handle, i, tf_status.status), &cur_dim);
      ENSURE_TF_OK(env, tf_status);
      ENSURE_NAPI_OK(env, nstatus);

      nstatus = napi_set_element(env, *result, i, cur_dim);
      ENSURE_NAPI_OK(env, nstatus);
    }
  }
}

inline bool IsArray(napi_env env, napi_status &nstatus, napi_value *val) {
  bool is_array;
  nstatus = napi_is_array(env, *val, &is_array);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, false);
  return is_array;
}

void GetTFE_TensorHandleType(napi_env env, TFE_TensorHandle *handle,
                             napi_value *result) {
  napi_status nstatus;

  TF_DataType dtype = TFE_TensorHandleDataType(handle);
  nstatus = napi_create_int32(env, dtype, result);
  ENSURE_NAPI_OK(env, nstatus);
}

void AssignOpAttr(napi_env env, TFE_Op *tfe_op, napi_value attr_value) {
  napi_status nstatus;

  napi_value attr_name_value;
  nstatus = napi_get_named_property(env, attr_value, "name", &attr_name_value);
  ENSURE_NAPI_OK(env, nstatus);

  std::string attr_name_string;
  nstatus = GetStringParam(env, attr_name_value, attr_name_string);
  ENSURE_NAPI_OK(env, nstatus);

  // OpAttr will be used beyond the scope of this function call. Stash ops in
  // a set for re-use instead of dynamically reallocating strings for
  // operations.
  const char *attr_name =
      ATTR_NAME_SET.insert(attr_name_string.c_str()).first->c_str();

  napi_value attr_type_value;
  nstatus = napi_get_named_property(env, attr_value, "type", &attr_type_value);
  ENSURE_NAPI_OK(env, nstatus);

  TF_AttrType tf_attr_type;
  nstatus = napi_get_value_int32(env, attr_type_value,
                                 reinterpret_cast<int32_t *>(&tf_attr_type));
  ENSURE_NAPI_OK(env, nstatus);

  napi_value js_value;
  nstatus = napi_get_named_property(env, attr_value, "value", &js_value);
  ENSURE_NAPI_OK(env, nstatus);

  switch (tf_attr_type) {
    case TF_ATTR_STRING: {
      // NOTE: String attribute values do not have to be utf8 encoded strings
      // (could be arbitrary byte sequences).
      std::string str_value;
      nstatus = GetStringParam(env, js_value, str_value);
      ENSURE_NAPI_OK(env, nstatus);

      TFE_OpSetAttrString(tfe_op, attr_name, str_value.c_str(),
                          str_value.size());
      break;
    }

    case TF_ATTR_INT: {
      if (IsArray(env, nstatus, &js_value)) {
        uint32_t length;
        nstatus = napi_get_array_length(env, js_value, &length);
        ENSURE_NAPI_OK(env, nstatus);
        std::unique_ptr<int64_t[]> data(new int64_t[length]);
        for (uint32_t i = 0; i < length; ++i) {
          napi_value element;
          nstatus = napi_get_element(env, js_value, i, &element);
          ENSURE_NAPI_OK(env, nstatus);
          int32_t value;
          nstatus = napi_get_value_int32(env, element, &value);
          ENSURE_NAPI_OK(env, nstatus);
          data[i] = value;
        }
        TFE_OpSetAttrIntList(tfe_op, attr_name, data.get(),
                             static_cast<int>(length));
      } else {
        int64_t value;
        nstatus = napi_get_value_int64(env, js_value, &value);
        ENSURE_NAPI_OK(env, nstatus);

        TFE_OpSetAttrInt(tfe_op, attr_name, value);
      }
      break;
    }

    case TF_ATTR_FLOAT: {
      if (IsArray(env, nstatus, &js_value)) {
        uint32_t length;
        nstatus = napi_get_array_length(env, js_value, &length);
        ENSURE_NAPI_OK(env, nstatus);
        std::unique_ptr<float[]> data(new float[length]);
        for (uint32_t i = 0; i < length; ++i) {
          napi_value element;
          nstatus = napi_get_element(env, js_value, i, &element);
          ENSURE_NAPI_OK(env, nstatus);
          double value;
          nstatus = napi_get_value_double(env, element, &value);
          ENSURE_NAPI_OK(env, nstatus);
          data[i] = static_cast<float>(value);
        }
        TFE_OpSetAttrFloatList(tfe_op, attr_name, data.get(),
                               static_cast<int>(length));
      } else {
        double value;
        nstatus = napi_get_value_double(env, js_value, &value);
        ENSURE_NAPI_OK(env, nstatus);
        TFE_OpSetAttrFloat(tfe_op, attr_name, static_cast<float>(value));
      }
      break;
    }

    case TF_ATTR_BOOL: {
      if (IsArray(env, nstatus, &js_value)) {
        uint32_t length;
        nstatus = napi_get_array_length(env, js_value, &length);
        ENSURE_NAPI_OK(env, nstatus);
        std::unique_ptr<unsigned char[]> data(new unsigned char[length]);
        for (uint32_t i = 0; i < length; ++i) {
          napi_value element;
          nstatus = napi_get_element(env, js_value, i, &element);
          ENSURE_NAPI_OK(env, nstatus);
          bool value;
          nstatus = napi_get_value_bool(env, element, &value);
          ENSURE_NAPI_OK(env, nstatus);
          data[i] = value ? 1 : 0;
        }
        TFE_OpSetAttrBoolList(tfe_op, attr_name, data.get(),
                              static_cast<int>(length));
      } else {
        bool value;
        nstatus = napi_get_value_bool(env, js_value, &value);
        ENSURE_NAPI_OK(env, nstatus);
        TFE_OpSetAttrBool(tfe_op, attr_name, value ? 1 : 0);
      }
      break;
    }

    case TF_ATTR_TYPE: {
      TF_DataType tf_data_type;
      nstatus = napi_get_value_int32(
          env, js_value, reinterpret_cast<int32_t *>(&tf_data_type));
      ENSURE_NAPI_OK(env, nstatus);

      TFE_OpSetAttrType(tfe_op, attr_name, tf_data_type);
      break;
    }

    case TF_ATTR_SHAPE: {
      std::vector<int64_t> shape_vector;
      ExtractArrayShape(env, js_value, &shape_vector);

      TF_AutoStatus tf_status;
      TFE_OpSetAttrShape(tfe_op, attr_name, shape_vector.data(),
                         shape_vector.size(), tf_status.status);
      ENSURE_TF_OK(env, tf_status);
      break;
    }

    default:
      REPORT_UNKNOWN_TF_ATTR_TYPE(env, tf_attr_type);
      break;
  }
}

TFJSBackend::TFJSBackend(napi_env env)
    : next_tensor_id_(0), next_savedmodel_id_(0) {
  TF_AutoStatus tf_status;
  TFE_ContextOptions *tfe_options = TFE_NewContextOptions();
  tfe_context_ = TFE_NewContext(tfe_options, tf_status.status);
  if (TF_GetCode(tf_status.status) != TF_OK) {
    NAPI_THROW_ERROR(env, "Exception creating TFE_Context");
  }

  TFE_DeleteContextOptions(tfe_options);

  TF_DeviceList *device_list =
      TFE_ContextListDevices(tfe_context_, tf_status.status);
  if (TF_GetCode(tf_status.status) != TF_OK) {
    NAPI_THROW_ERROR(env, "Exception creating TFE_Context");
  }

  // TODO(kreeger): Add better support for this in the future through the JS
  // API. https://github.com/tensorflow/tfjs/issues/320
  std::string cpu_device_name;
  const int num_devices = TF_DeviceListCount(device_list);
  for (int i = 0; i < num_devices; i++) {
    const char *device_type =
        TF_DeviceListType(device_list, i, tf_status.status);
    ENSURE_TF_OK(env, tf_status);

    // Keep a reference to the host CPU device:
    if (strcmp(device_type, "CPU") == 0) {
      cpu_device_name =
          std::string(TF_DeviceListName(device_list, i, tf_status.status));
      ENSURE_TF_OK(env, tf_status);
    } else if (strcmp(device_type, "GPU") == 0) {
      device_name =
          std::string(TF_DeviceListName(device_list, i, tf_status.status));
      ENSURE_TF_OK(env, tf_status);
    }
  }

  // If no GPU devices found, fallback to host CPU:
  if (device_name.empty()) {
    device_name = cpu_device_name;
    is_gpu_device = false;
  } else {
    is_gpu_device = true;
  }
  TF_DeleteDeviceList(device_list);
}

TFJSBackend::~TFJSBackend() {
  for (auto &kv : tfe_handle_map_) {
    TFE_DeleteTensorHandle(kv.second);
  }
  for (auto &kv : tf_savedmodel_map_) {
    TF_AutoStatus tf_status;
    TF_DeleteSession(kv.second.first, tf_status.status);
    TF_DeleteGraph(kv.second.second);
  }
  if (tfe_context_ != nullptr) {
    TFE_DeleteContext(tfe_context_);
  }
}

TFJSBackend *TFJSBackend::Create(napi_env env) { return new TFJSBackend(env); }

int32_t TFJSBackend::InsertHandle(TFE_TensorHandle *tfe_handle) {
  return tfe_handle_map_.insert(std::make_pair(next_tensor_id_++, tfe_handle))
      .first->first;
}

int32_t TFJSBackend::InsertSavedModel(TF_Session *tf_session,
                                      TF_Graph *tf_graph) {
  // Both TF_Session and TF_Graph are required when executing SavedModel.
  // TF_Graph is used to find input/output operation from string name.
  return tf_savedmodel_map_
      .insert(std::make_pair(next_savedmodel_id_++,
                             std::make_pair(tf_session, tf_graph)))
      .first->first;
}

napi_value TFJSBackend::CreateTensor(napi_env env, napi_value shape_value,
                                     napi_value dtype_value,
                                     napi_value array_value) {
  napi_status nstatus;

  std::vector<int64_t> shape_vector;
  ExtractArrayShape(env, shape_value, &shape_vector);
  // Check to see if an exception exists, if so return a failure.
  if (IsExceptionPending(env)) {
    return nullptr;
  }

  int32_t dtype_int32;
  nstatus = napi_get_value_int32(env, dtype_value, &dtype_int32);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);
  TFE_TensorHandle *tfe_handle = CreateTFE_TensorHandleFromJSValues(
      env, shape_vector.data(), shape_vector.size(),
      static_cast<TF_DataType>(dtype_int32), array_value);

  // Check to see if an exception exists, if so return a failure.
  if (IsExceptionPending(env)) {
    return nullptr;
  }

  // Copy non-int32 and non-string tensors to a device. Most GPU kernels expect
  // to have int32 tensors in host memory.
  if (dtype_int32 != TF_INT32 && dtype_int32 != TF_STRING) {
    // Note that this is a shallow copy and will share the underlying buffer
    // if copying to the same device.
    TFE_TensorHandle *new_handle = CopyTFE_TensorHandleToDevice(
        env, device_name.c_str(), tfe_handle, tfe_context_);

    TFE_DeleteTensorHandle(tfe_handle);
    tfe_handle = new_handle;
  }

  napi_value output_tensor_id;
  nstatus = napi_create_int32(env, InsertHandle(tfe_handle), &output_tensor_id);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);
  return output_tensor_id;
}

void TFJSBackend::DeleteTensor(napi_env env, napi_value tensor_id_value) {
  int32_t tensor_id;
  ENSURE_NAPI_OK(env, napi_get_value_int32(env, tensor_id_value, &tensor_id));

  auto tensor_entry = tfe_handle_map_.find(tensor_id);
  if (tensor_entry == tfe_handle_map_.end()) {
    NAPI_THROW_ERROR(env,
                     "Delete called on a Tensor not referenced (tensor_id: %d)",
                     tensor_id);
    return;
  }

  TFE_TensorHandle *tensor_handle = tensor_entry->second;
  if (TFE_TensorHandleDataType(tensor_handle) == TF_STRING) {
    TF_AutoStatus tf_status;
    TF_AutoTensor tensor(
        TFE_TensorHandleResolve(tensor_handle, tf_status.status));
    ENSURE_TF_OK(env, tf_status);
    size_t num_elements = GetTensorNumElements(tensor.tensor);
    TF_TString *data = reinterpret_cast<TF_TString *>(TF_TensorData(tensor.tensor));

    // Deallocate each string
    for (size_t i = 0; i < num_elements; i++) {
      TF_TString_Dealloc(data + i);
    }
  };
  TFE_DeleteTensorHandle(tensor_entry->second);
  tfe_handle_map_.erase(tensor_entry);
}

napi_value TFJSBackend::GetNumOfTensors(napi_env env) {
  napi_status nstatus;
  napi_value num_of_tensors;
  nstatus =
      napi_create_int32(env, tfe_handle_map_.size(), &num_of_tensors);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);
  return num_of_tensors;
}

napi_value TFJSBackend::GetTensorData(napi_env env,
                                      napi_value tensor_id_value) {
  int32_t tensor_id;
  ENSURE_NAPI_OK_RETVAL(
      env, napi_get_value_int32(env, tensor_id_value, &tensor_id), nullptr);

  auto tensor_entry = tfe_handle_map_.find(tensor_id);
  if (tensor_entry == tfe_handle_map_.end()) {
    NAPI_THROW_ERROR(
        env, "Get data called on a Tensor not referenced (tensor_id: %d)",
        tensor_id);
    return nullptr;
  }

  napi_value js_value;
  CopyTFE_TensorHandleDataToJSData(env, tfe_context_, tensor_entry->second,
                                   &js_value);
  return js_value;
}

napi_value TFJSBackend::ExecuteOp(napi_env env, napi_value op_name_value,
                                  napi_value op_attr_inputs,
                                  napi_value input_tensor_ids,
                                  napi_value num_output_values) {
  napi_status nstatus;

  std::string op_name;
  nstatus = GetStringParam(env, op_name_value, op_name);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  TF_AutoStatus tf_status;
  TFE_AutoOp tfe_op(TFE_NewOp(tfe_context_, op_name.c_str(), tf_status.status));
  ENSURE_TF_OK_RETVAL(env, tf_status, nullptr);

  uint32_t num_input_ids;
  nstatus = napi_get_array_length(env, input_tensor_ids, &num_input_ids);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  for (uint32_t i = 0; i < num_input_ids; i++) {
    napi_value cur_input_id;
    nstatus = napi_get_element(env, input_tensor_ids, i, &cur_input_id);
    ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

    int32_t cur_input_tensor_id;
    nstatus = napi_get_value_int32(env, cur_input_id, &cur_input_tensor_id);
    ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

    auto input_tensor_entry = tfe_handle_map_.find(cur_input_tensor_id);
    if (input_tensor_entry == tfe_handle_map_.end()) {
      NAPI_THROW_ERROR(env, "Input Tensor ID not referenced (tensor_id: %d)",
                       cur_input_tensor_id);
      return nullptr;
    }

    TFE_OpAddInput(tfe_op.op, input_tensor_entry->second, tf_status.status);
    ENSURE_TF_OK_RETVAL(env, tf_status, nullptr);
  }

  uint32_t op_attrs_length;
  nstatus = napi_get_array_length(env, op_attr_inputs, &op_attrs_length);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  for (uint32_t i = 0; i < op_attrs_length; i++) {
    napi_value cur_op_attr;
    nstatus = napi_get_element(env, op_attr_inputs, i, &cur_op_attr);
    ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

    AssignOpAttr(env, tfe_op.op, cur_op_attr);

    // Check to see if an exception exists, if so return a failure.
    if (IsExceptionPending(env)) {
      return nullptr;
    }
  }

  int32_t num_outputs;
  nstatus = napi_get_value_int32(env, num_output_values, &num_outputs);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  // Push `nullptr` to get a valid pointer in the call to `TFE_Execute()`
  // below.
  std::vector<TFE_TensorHandle *> result_handles(num_outputs, nullptr);

  int size = result_handles.size();
  TFE_Execute(tfe_op.op, result_handles.data(), &size, tf_status.status);
  ENSURE_TF_OK_RETVAL(env, tf_status, nullptr);

  napi_value output_tensor_infos;
  nstatus = napi_create_array_with_length(env, size, &output_tensor_infos);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  for (int32_t i = 0; i < num_outputs; i++) {
    TFE_TensorHandle *handle = result_handles[i];
    napi_value tensor_info_value = GenerateOutputTensorInfo(env, handle);
    // Push into output array
    nstatus = napi_set_element(env, output_tensor_infos, i, tensor_info_value);
    ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);
  }

  return output_tensor_infos;
}

/* Helper function to generate TensorInfo(used for JavaScript) from
 * TFE_TensorHandle. This helper function is used by ExecuteOp() and
 * RunSavedModel().
 */
napi_value TFJSBackend::GenerateOutputTensorInfo(napi_env env,
                                                 TFE_TensorHandle *handle) {
  napi_status nstatus;

  // Output tensor info object:
  napi_value tensor_info_value;
  nstatus = napi_create_object(env, &tensor_info_value);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  // Output tensor ID:
  napi_value output_tensor_id_value;
  nstatus =
      napi_create_int32(env, InsertHandle(handle), &output_tensor_id_value);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  nstatus = napi_set_named_property(env, tensor_info_value, "id",
                                    output_tensor_id_value);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  // Output tensor shape:
  napi_value shape_value;
  GetTFE_TensorHandleShape(env, handle, &shape_value);

  nstatus =
      napi_set_named_property(env, tensor_info_value, "shape", shape_value);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  // Output tensor dtype:
  napi_value type_value;
  GetTFE_TensorHandleType(env, handle, &type_value);

  nstatus =
      napi_set_named_property(env, tensor_info_value, "dtype", type_value);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  return tensor_info_value;
}

napi_value TFJSBackend::LoadSavedModel(napi_env env,
                                       napi_value export_dir_value,
                                       napi_value tags_value) {
  TF_SessionOptions *session_options = TF_NewSessionOptions();

  TF_Buffer *run_options = TF_NewBufferFromString("", 0);

  std::string export_dir_string;
  napi_status nstatus;
  nstatus = GetStringParam(env, export_dir_value, export_dir_string);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);
  const char *export_dir = export_dir_string.c_str();

  std::string tags;
  nstatus = GetStringParam(env, tags_value, tags);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  std::vector<const char *> tags_ptrs = splitStringByComma(tags);

  TF_Graph *graph = TF_NewGraph();

  TF_Buffer *metagraph = TF_NewBuffer();

  TF_AutoStatus tf_status;

  TF_Session *session = TF_LoadSessionFromSavedModel(
      session_options, run_options, export_dir, tags_ptrs.data(),
      tags_ptrs.size(), graph, metagraph, tf_status.status);
  // Delete objects that are necessary when loading the SavedModel but not gonna
  // be used later.
  TF_DeleteSessionOptions(session_options);
  TF_DeleteBuffer(run_options);
  TF_DeleteBuffer(metagraph);

  if (TF_GetCode(tf_status.status) != TF_OK) {
    NAPI_THROW_ERROR(env, "Failed to load SavedModel: %s",
                     TF_Message(tf_status.status));
    return nullptr;
  }

  napi_value output_session_id;
  nstatus = napi_create_int32(env, InsertSavedModel(session, graph),
                              &output_session_id);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  // clean up the tags strings.
  for (uint32_t i = 0; i < tags_ptrs.size(); i++) {
    delete tags_ptrs[i];
  }
  return output_session_id;
}

void TFJSBackend::DeleteSavedModel(napi_env env,
                                   napi_value savedmodel_id_value) {
  int32_t savedmodel_id;
  ENSURE_NAPI_OK(
      env, napi_get_value_int32(env, savedmodel_id_value, &savedmodel_id));

  auto savedmodel_entry = tf_savedmodel_map_.find(savedmodel_id);
  if (savedmodel_entry == tf_savedmodel_map_.end()) {
    NAPI_THROW_ERROR(
        env, "Delete called on a SavedModel not found (savedmodel_id: %d)",
        savedmodel_id);
    return;
  }

  TF_AutoStatus tf_status;
  TF_DeleteSession(savedmodel_entry->second.first, tf_status.status);
  if (TF_GetCode(tf_status.status) != TF_OK) {
    NAPI_THROW_ERROR(env, "Failed to delete SavedModel: %s",
                     TF_Message(tf_status.status));
    return;
  }
  // TODO(kangyizhang): Add tests to validate TF_Session and TF_Graph are
  // deleted.
  TF_DeleteGraph(savedmodel_entry->second.second);
  tf_savedmodel_map_.erase(savedmodel_entry);
}

napi_value TFJSBackend::RunSavedModel(napi_env env,
                                      napi_value savedmodel_id_value,
                                      napi_value input_tensor_ids,
                                      napi_value input_op_names_value,
                                      napi_value output_op_names_value) {
  napi_status nstatus;
  TF_AutoStatus tf_status;

  int32_t savedmodel_id;
  nstatus = napi_get_value_int32(env, savedmodel_id_value, &savedmodel_id);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  // Get corresponding SavedModel session and graph.
  auto savedmodel_entry = tf_savedmodel_map_.find(savedmodel_id);
  if (savedmodel_entry == tf_savedmodel_map_.end()) {
    NAPI_THROW_ERROR(env, "SavedModel ID not found (savedmodel_id: %d)",
                     savedmodel_id);
    return nullptr;
  }

  std::string input_op_names;
  nstatus = GetStringParam(env, input_op_names_value, input_op_names);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);
  std::string output_op_names;
  nstatus = GetStringParam(env, output_op_names_value, output_op_names);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  // Get input/output op names as vector
  std::vector<const char *> input_op_name_array =
      splitStringByComma(input_op_names);
  std::vector<const char *> output_op_name_array =
      splitStringByComma(output_op_names);

  std::vector<TF_Output> inputs;
  std::vector<TF_Output> outputs;

  uint32_t num_input_ids;
  nstatus = napi_get_array_length(env, input_tensor_ids, &num_input_ids);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  if (input_op_name_array.size() != num_input_ids) {
    NAPI_THROW_ERROR(env,
                     "Length of input op names (%d) does not match the length "
                     "of input tensors (%d).",
                     input_op_name_array.size(), num_input_ids);
    return nullptr;
  }

  std::vector<TF_Tensor *> input_values;

  for (uint32_t i = 0; i < num_input_ids; i++) {
    napi_value cur_input_id;
    nstatus = napi_get_element(env, input_tensor_ids, i, &cur_input_id);
    ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

    int32_t cur_input_tensor_id;
    nstatus = napi_get_value_int32(env, cur_input_id, &cur_input_tensor_id);
    ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

    // Find input tensor based on tensor id.
    auto tensor_entry = tfe_handle_map_.find(cur_input_tensor_id);
    if (tensor_entry == tfe_handle_map_.end()) {
      NAPI_THROW_ERROR(env, "Input Tensor ID not found (tensor_id: %d)",
                       cur_input_tensor_id);
      return nullptr;
    }
    TF_Tensor *inputTensor =
        TFE_TensorHandleResolve(tensor_entry->second, tf_status.status);

    if (TF_GetCode(tf_status.status) != TF_OK) {
      NAPI_THROW_ERROR(
          env, "Failed to get input tensor (tensor_id: %d) for session.",
          cur_input_tensor_id);
      return nullptr;
    }

    // Add input tensor into input values list.
    input_values.push_back(inputTensor);

    // The item in input_op_name_array is something like "serving_default_x:0".
    // Parse it into input op name and index for provided tensor.
    std::string name(input_op_name_array[i]);
    int index = name.find(":");
    std::string input_op_name = name.substr(0, index);
    std::string input_op_index = name.substr(index + 1);
    int input_tensor_index;
    if (input_op_index.length() == 0) {
      input_tensor_index = 0;
    } else {
      input_tensor_index = atoi(input_op_index.c_str());
    }

    // Add input op into input ops list.
    // TODO(kangyizhang): Store these TF_Operations somewhere so they don't need
    // to be generated  every time.
    TF_Operation *input_op = TF_GraphOperationByName(
        savedmodel_entry->second.second, input_op_name.c_str());
    if (input_op == nullptr) {
      NAPI_THROW_ERROR(env, "Input op name can not be found in the graph.");
      return nullptr;
    }
    TF_Output in = {input_op, input_tensor_index};
    inputs.push_back(in);
  }

  // Add output op into output ops list.
  for (uint32_t i = 0; i < output_op_name_array.size(); i++) {
    // The item in output_op_name_array is something like
    // "StatefulPartitionedCall:0". Parse it into output op name and index.
    std::string name(output_op_name_array[i]);
    int index = name.find(":");
    std::string output_op_name = name.substr(0, index);
    std::string output_op_index = name.substr(index + 1);
    int output_tensor_index;
    if (output_op_index.length() == 0) {
      output_tensor_index = 0;
    } else {
      output_tensor_index = atoi(output_op_index.c_str());
    }

    TF_Operation *output_op = TF_GraphOperationByName(
        savedmodel_entry->second.second, output_op_name.c_str());
    if (output_op == nullptr) {
      NAPI_THROW_ERROR(env, "Output op name can not be found in the graph.");
      return nullptr;
    }
    TF_Output out = {output_op, output_tensor_index};
    outputs.push_back(out);
  }

  std::vector<TF_Tensor *> output_values(outputs.size(), nullptr);

  TF_SessionRun(savedmodel_entry->second.first, nullptr, inputs.data(),
                input_values.data(), num_input_ids, outputs.data(),
                output_values.data(), output_op_name_array.size(), nullptr, 0,
                nullptr, tf_status.status);

  if (TF_GetCode(tf_status.status) != TF_OK) {
    NAPI_THROW_ERROR(env, "Session fail to run with error: %s",
                     TF_Message(tf_status.status));
    return nullptr;
  }

  napi_value output_tensor_infos;
  nstatus = napi_create_array_with_length(env, 1, &output_tensor_infos);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);

  // Generate output tensors for JS.
  for (uint32_t i = 0; i < output_op_name_array.size(); i++) {
    TFE_TensorHandle *tfe_handle =
        TFE_NewTensorHandle(output_values[i], tf_status.status);
    // Deallocate output TF_Tensor in C++.
    TF_DeleteTensor(output_values[i]);

    napi_value tensor_info_value = GenerateOutputTensorInfo(env, tfe_handle);
    // Push into output array
    nstatus = napi_set_element(env, output_tensor_infos, i, tensor_info_value);
    ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);
    // delete output op name string
    delete output_op_name_array[i];
  }

  for (uint32_t i = 0; i < num_input_ids; i++) {
    // Deallocate input TF_Tensor in C++.
    TF_DeleteTensor(input_values[i]);
    // delete input op name string
    delete input_op_name_array[i];
  }

  return output_tensor_infos;
}

napi_value TFJSBackend::GetNumOfSavedModels(napi_env env) {
  napi_status nstatus;
  napi_value num_saved_models;
  nstatus =
      napi_create_int32(env, tf_savedmodel_map_.size(), &num_saved_models);
  ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr);
  return num_saved_models;
}

}  // namespace tfnodejs
