/**
 * Gets the number of dimensions at index 0, including tuple
 * and struct as dimensions.
 */
static intptr_t get_leading_dim_count(const dynd::ndt::type &tp)
{
  intptr_t ndim = tp.get_ndim();
  if (ndim) {
    return ndim + get_leading_dim_count(tp.get_dtype());
  }
  else if (tp.get_kind() == expr_kind) {
    return get_leading_dim_count(tp.value_type());
  }
  else if (tp.get_kind() == tuple_kind || tp.get_kind() == struct_kind) {
    if (tp.extended<ndt::tuple_type>()->get_field_count() == 0) {
      return 1;
    }
    else {
      return 1 + get_leading_dim_count(
                     tp.extended<ndt::tuple_type>()->get_field_type(0));
    }
  }
  else {
    return 0;
  }
}
intptr_t pydynd::nd::copy_from_numpy_kernel::instantiate(
    char *DYND_UNUSED(static_data), size_t DYND_UNUSED(data_size),
    char *DYND_UNUSED(data), void *ckb, intptr_t ckb_offset,
    const dynd::ndt::type &dst_tp, const char *dst_arrmeta,
    intptr_t DYND_UNUSED(nsrc), const dynd::ndt::type *src_tp,
    const char *const *src_arrmeta, dynd::kernel_request_t kernreq,
    const dynd::eval::eval_context *ectx, intptr_t nkwd,
    const dynd::nd::array *kwds,
    const std::map<std::string, dynd::ndt::type> &tp_vars)
{
  if (src_tp[0].get_type_id() != dynd::void_type_id) {
    stringstream ss;
    ss << "Cannot instantiate dynd::nd::callable copy_from_numpy with "
          "signature (";
    ss << src_tp[0] << ") -> " << dst_tp;
    throw dynd::type_error(ss.str());
  }

  PyArray_Descr *dtype =
      *reinterpret_cast<PyArray_Descr *const *>(src_arrmeta[0]);
  uintptr_t src_alignment =
      reinterpret_cast<const uintptr_t *>(src_arrmeta[0])[1];

  if (!PyDataType_FLAGCHK(dtype, NPY_ITEM_HASOBJECT)) {
    // If there is no object type in the numpy type, get the dynd equivalent
    // type and use it to do the copying
    dynd::ndt::type src_view_tp = _type_from_numpy_dtype(dtype, src_alignment);
    return dynd::make_assignment_kernel(ckb, ckb_offset, dst_tp, dst_arrmeta,
                                        src_view_tp, NULL, kernreq, ectx);
  } else if (PyDataType_ISOBJECT(dtype)) {
    dynd::callable_type_data *af = copy_from_pyobject::get().get();
    return af->instantiate(af->static_data, 0, NULL, ckb, ckb_offset, dst_tp,
                           dst_arrmeta, 1, src_tp, src_arrmeta, kernreq, ectx,
                           nkwd, kwds, tp_vars);
  } else if (PyDataType_HASFIELDS(dtype)) {
    if (dst_tp.get_kind() != dynd::struct_kind &&
        dst_tp.get_kind() != dynd::tuple_kind) {
      stringstream ss;
      ss << "Cannot assign from numpy type " << pyobject_repr((PyObject *)dtype)
         << " to dynd type " << dst_tp;
      throw invalid_argument(ss.str());
    }

    // Get the fields out of the numpy dtype
    vector<PyArray_Descr *> field_dtypes_orig;
    vector<std::string> field_names_orig;
    vector<size_t> field_offsets_orig;
    pydynd::extract_fields_from_numpy_struct(
        dtype, field_dtypes_orig, field_names_orig, field_offsets_orig);
    intptr_t field_count = field_dtypes_orig.size();
    if (field_count !=
        dst_tp.extended<dynd::ndt::base_tuple_type>()->get_field_count()) {
      stringstream ss;
      ss << "Cannot assign from numpy type " << pyobject_repr((PyObject *)dtype)
         << " to dynd type " << dst_tp;
      throw invalid_argument(ss.str());
    }

    // Permute the numpy fields to match with the dynd fields
    vector<PyArray_Descr *> field_dtypes;
    vector<size_t> field_offsets;
    if (dst_tp.get_kind() == dynd::struct_kind) {
      field_dtypes.resize(field_count);
      field_offsets.resize(field_count);
      for (intptr_t i = 0; i < field_count; ++i) {
        intptr_t src_i =
            dst_tp.extended<dynd::ndt::base_struct_type>()->get_field_index(
                field_names_orig[i]);
        if (src_i >= 0) {
          field_dtypes[src_i] = field_dtypes_orig[i];
          field_offsets[src_i] = field_offsets_orig[i];
        } else {
          stringstream ss;
          ss << "Cannot assign from numpy type "
             << pyobject_repr((PyObject *)dtype) << " to dynd type " << dst_tp;
          throw invalid_argument(ss.str());
        }
      }
    } else {
      // In the tuple case, use position instead of name
      field_dtypes.swap(field_dtypes_orig);
      field_offsets.swap(field_offsets_orig);
    }

    vector<dynd::ndt::type> src_fields_tp(field_count,
                                          dynd::ndt::type::make<void>());
    vector<copy_from_numpy_arrmeta> src_arrmeta_values(field_count);
    vector<const char *> src_fields_arrmeta(field_count);
    for (intptr_t i = 0; i < field_count; ++i) {
      src_arrmeta_values[i].src_dtype = field_dtypes[i];
      src_arrmeta_values[i].src_alignment = src_alignment | field_offsets[i];
      src_fields_arrmeta[i] =
          reinterpret_cast<const char *>(&src_arrmeta_values[i]);
    }

    const uintptr_t *dst_arrmeta_offsets =
        dst_tp.extended<dynd::ndt::base_tuple_type>()
            ->get_arrmeta_offsets_raw();
    dynd::shortvector<const char *> dst_fields_arrmeta(field_count);
    for (intptr_t i = 0; i != field_count; ++i) {
      dst_fields_arrmeta[i] = dst_arrmeta + dst_arrmeta_offsets[i];
    }

    // Todo: Remove this line
    dynd::nd::callable af = dynd::nd::callable::make<copy_from_numpy_kernel>(
        dynd::ndt::type("(void, broadcast: bool) -> T"), 0);

    return make_tuple_unary_op_ckernel(
        af.get(), af.get_type(), ckb, ckb_offset, field_count,
        dst_tp.extended<dynd::ndt::base_tuple_type>()->get_data_offsets(
            dst_arrmeta),
        dst_tp.extended<dynd::ndt::base_tuple_type>()->get_field_types_raw(),
        dst_fields_arrmeta.get(), &field_offsets[0], &src_fields_tp[0],
        &src_fields_arrmeta[0], kernreq, ectx);
  } else {
    stringstream ss;
    ss << "TODO: implement assign from numpy type "
       << pyobject_repr((PyObject *)dtype) << " to dynd type " << dst_tp;
    throw invalid_argument(ss.str());
  }
}