/// @file encode.c
///
/// File containing functions for encoding and decoding Vimscript values.
///
/// Split out from eval.c.

#include <assert.h>
#include <inttypes.h>
#include <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>

#include "klib/kvec.h"
#include "nvim/api/private/helpers.h"
#include "nvim/ascii_defs.h"
#include "nvim/eval.h"
#include "nvim/eval/encode.h"
#include "nvim/eval/typval.h"
#include "nvim/eval/typval_encode.h"
#include "nvim/garray.h"
#include "nvim/gettext_defs.h"
#include "nvim/globals.h"
#include "nvim/hashtab.h"
#include "nvim/macros_defs.h"
#include "nvim/math.h"
#include "nvim/mbyte.h"
#include "nvim/memory.h"
#include "nvim/message.h"
#include "nvim/msgpack_rpc/packer.h"
#include "nvim/strings.h"
#include "nvim/types_defs.h"
#include "nvim/vim_defs.h"  // For _()

const char *const encode_bool_var_names[] = {
  [kBoolVarTrue] = "v:true",
  [kBoolVarFalse] = "v:false",
};

const char *const encode_special_var_names[] = {
  [kSpecialVarNull] = "v:null",
};

#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "eval/encode.c.generated.h"
#endif

/// Msgpack callback for writing to a Blob
int encode_blob_write(void *const data, const char *const buf, const size_t len)
  FUNC_ATTR_NONNULL_ARG(1)
{
  ga_concat_len(&((blob_T *)data)->bv_ga, buf, len);
  return (int)len;
}

/// Msgpack callback for writing to readfile()-style list
void encode_list_write(void *const data, const char *const buf, const size_t len)
  FUNC_ATTR_NONNULL_ARG(1)
{
  if (len == 0) {
    return;
  }
  list_T *const list = (list_T *)data;
  const char *const end = buf + len;
  const char *line_end = buf;
  listitem_T *li = tv_list_last(list);

  // Continue the last list element
  if (li != NULL) {
    line_end = xmemscan(buf, NL, len);
    if (line_end != buf) {
      const size_t line_length = (size_t)(line_end - buf);
      char *str = TV_LIST_ITEM_TV(li)->vval.v_string;
      const size_t li_len = (str == NULL ? 0 : strlen(str));
      TV_LIST_ITEM_TV(li)->vval.v_string = xrealloc(str, li_len + line_length + 1);
      str = TV_LIST_ITEM_TV(li)->vval.v_string + li_len;
      memcpy(str, buf, line_length);
      str[line_length] = 0;
      memchrsub(str, NUL, NL, line_length);
    }
    line_end++;
  }

  while (line_end < end) {
    const char *line_start = line_end;
    line_end = xmemscan(line_start, NL, (size_t)(end - line_start));
    char *str = NULL;
    if (line_end != line_start) {
      const size_t line_length = (size_t)(line_end - line_start);
      str = xmemdupz(line_start, line_length);
      memchrsub(str, NUL, NL, line_length);
    }
    tv_list_append_allocated_string(list, str);
    line_end++;
  }
  if (line_end == end) {
    tv_list_append_allocated_string(list, NULL);
  }
}

/// Abort conversion to string after a recursion error.
static bool did_echo_string_emsg = false;

/// Show a error message when converting to msgpack value
///
/// @param[in]  msg  Error message to dump. Must contain exactly two %s that
///                  will be replaced with what was being dumped: first with
///                  something like “F” or “function argument”, second with path
///                  to the failed value.
/// @param[in]  mpstack  Path to the failed value.
/// @param[in]  objname  Dumped object name.
///
/// @return FAIL.
static int conv_error(const char *const msg, const MPConvStack *const mpstack,
                      const char *const objname)
  FUNC_ATTR_NONNULL_ALL
{
  garray_T msg_ga;
  ga_init(&msg_ga, (int)sizeof(char), 80);
  const char *const key_msg = _("key %s");
  const char *const key_pair_msg = _("key %s at index %i from special map");
  const char *const idx_msg = _("index %i");
  const char *const partial_arg_msg = _("partial");
  const char *const partial_arg_i_msg = _("argument %i");
  const char *const partial_self_msg = _("partial self dictionary");
  for (size_t i = 0; i < kv_size(*mpstack); i++) {
    if (i != 0) {
      ga_concat(&msg_ga, ", ");
    }
    MPConvStackVal v = kv_A(*mpstack, i);
    switch (v.type) {
    case kMPConvDict: {
      typval_T key_tv = {
        .v_type = VAR_STRING,
        .vval = { .v_string =
                    (v.data.d.hi ==
                     NULL ? v.data.d.dict->dv_hashtab.ht_array : (v.data.d.hi -
                                                                  1))->hi_key },
      };
      char *const key = encode_tv2string(&key_tv, NULL);
      vim_snprintf(IObuff, IOSIZE, key_msg, key);
      xfree(key);
      ga_concat(&msg_ga, IObuff);
      break;
    }
    case kMPConvPairs:
    case kMPConvList: {
      const int idx = (v.data.l.li == tv_list_first(v.data.l.list)
                       ? 0
                       : (v.data.l.li == NULL
                          ? tv_list_len(v.data.l.list) - 1
                          : tv_list_idx_of_item(v.data.l.list,
                                                TV_LIST_ITEM_PREV(v.data.l.list,
                                                                  v.data.l.li))));
      const listitem_T *const li = (v.data.l.li == NULL
                                    ? tv_list_last(v.data.l.list)
                                    : TV_LIST_ITEM_PREV(v.data.l.list,
                                                        v.data.l.li));
      if (v.type == kMPConvList
          || li == NULL
          || (TV_LIST_ITEM_TV(li)->v_type != VAR_LIST
              && tv_list_len(TV_LIST_ITEM_TV(li)->vval.v_list) <= 0)) {
        vim_snprintf(IObuff, IOSIZE, idx_msg, idx);
        ga_concat(&msg_ga, IObuff);
      } else {
        assert(li != NULL);
        listitem_T *const first_item =
          tv_list_first(TV_LIST_ITEM_TV(li)->vval.v_list);
        assert(first_item != NULL);
        typval_T key_tv = *TV_LIST_ITEM_TV(first_item);
        char *const key = encode_tv2echo(&key_tv, NULL);
        vim_snprintf(IObuff, IOSIZE, key_pair_msg, key, idx);
        xfree(key);
        ga_concat(&msg_ga, IObuff);
      }
      break;
    }
    case kMPConvPartial:
      switch (v.data.p.stage) {
      case kMPConvPartialArgs:
        abort();
        break;
      case kMPConvPartialSelf:
        ga_concat(&msg_ga, partial_arg_msg);
        break;
      case kMPConvPartialEnd:
        ga_concat(&msg_ga, partial_self_msg);
        break;
      }
      break;
    case kMPConvPartialList: {
      const int idx = (int)(v.data.a.arg - v.data.a.argv) - 1;
      vim_snprintf(IObuff, IOSIZE, partial_arg_i_msg, idx);
      ga_concat(&msg_ga, IObuff);
      break;
    }
    }
  }
  semsg(msg, _(objname), (kv_size(*mpstack) == 0
                          ? _("itself")
                          : (char *)msg_ga.ga_data));
  ga_clear(&msg_ga);
  return FAIL;
}

/// Convert readfile()-style list to a char * buffer with length
///
/// @param[in]  list  Converted list.
/// @param[out]  ret_len  Resulting buffer length.
/// @param[out]  ret_buf  Allocated buffer with the result or NULL if ret_len is
///                       zero.
///
/// @return true in case of success, false in case of failure.
bool encode_vim_list_to_buf(const list_T *const list, size_t *const ret_len, char **const ret_buf)
  FUNC_ATTR_NONNULL_ARG(2, 3) FUNC_ATTR_WARN_UNUSED_RESULT
{
  size_t len = 0;
  TV_LIST_ITER_CONST(list, li, {
    if (TV_LIST_ITEM_TV(li)->v_type != VAR_STRING) {
      return false;
    }
    len++;
    if (TV_LIST_ITEM_TV(li)->vval.v_string != NULL) {
      len += strlen(TV_LIST_ITEM_TV(li)->vval.v_string);
    }
  });
  if (len) {
    len--;
  }
  *ret_len = len;
  if (len == 0) {
    *ret_buf = NULL;
    return true;
  }
  ListReaderState lrstate = encode_init_lrstate(list);
  char *const buf = xmalloc(len);
  size_t read_bytes;
  if (encode_read_from_list(&lrstate, buf, len, &read_bytes) != OK) {
    abort();
  }
  assert(len == read_bytes);
  *ret_buf = buf;
  return true;
}

/// Read bytes from list
///
/// @param[in,out]  state  Structure describing position in list from which
///                        reading should start. Is updated to reflect position
///                        at which reading ended.
/// @param[out]  buf  Buffer to write to.
/// @param[in]  nbuf  Buffer length.
/// @param[out]  read_bytes  Is set to amount of bytes read.
///
/// @return OK when reading was finished, FAIL in case of error (i.e. list item
///         was not a string), NOTDONE if reading was successful, but there are
///         more bytes to read.
int encode_read_from_list(ListReaderState *const state, char *const buf, const size_t nbuf,
                          size_t *const read_bytes)
  FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
{
  char *const buf_end = buf + nbuf;
  char *p = buf;
  while (p < buf_end) {
    assert(state->li_length == 0
           || TV_LIST_ITEM_TV(state->li)->vval.v_string != NULL);
    for (size_t i = state->offset; i < state->li_length && p < buf_end; i++) {
      assert(TV_LIST_ITEM_TV(state->li)->vval.v_string != NULL);
      const char ch = TV_LIST_ITEM_TV(state->li)->vval.v_string[state->offset++];
      *p++ = (char)(ch == (char)NL ? (char)NUL : ch);
    }
    if (p < buf_end) {
      state->li = TV_LIST_ITEM_NEXT(state->list, state->li);
      if (state->li == NULL) {
        *read_bytes = (size_t)(p - buf);
        return OK;
      }
      *p++ = NL;
      if (TV_LIST_ITEM_TV(state->li)->v_type != VAR_STRING) {
        *read_bytes = (size_t)(p - buf);
        return FAIL;
      }
      state->offset = 0;
      state->li_length = (TV_LIST_ITEM_TV(state->li)->vval.v_string == NULL
                          ? 0
                          : strlen(TV_LIST_ITEM_TV(state->li)->vval.v_string));
    }
  }
  *read_bytes = nbuf;
  return ((state->offset < state->li_length
           || TV_LIST_ITEM_NEXT(state->list, state->li) != NULL)
          ? NOTDONE
          : OK);
}

#define TYPVAL_ENCODE_CONV_STRING(tv, buf, len) \
  do { \
    const char *const buf_ = (buf); \
    if ((buf) == NULL) { \
      ga_concat(gap, "''"); \
    } else { \
      const size_t len_ = (len); \
      ga_grow(gap, (int)(2 + len_ + memcnt(buf_, '\'', len_))); \
      ga_append(gap, '\''); \
      for (size_t i_ = 0; i_ < len_; i_++) { \
        if (buf_[i_] == '\'') { \
          ga_append(gap, '\''); \
        } \
        ga_append(gap, (uint8_t)buf_[i_]); \
      } \
      ga_append(gap, '\''); \
    } \
  } while (0)

#define TYPVAL_ENCODE_CONV_STR_STRING(tv, buf, len) \
  TYPVAL_ENCODE_CONV_STRING(tv, buf, len)

#define TYPVAL_ENCODE_CONV_EXT_STRING(tv, buf, len, type)

#define TYPVAL_ENCODE_CONV_BLOB(tv, blob, len) \
  do { \
    const blob_T *const blob_ = (blob); \
    const int len_ = (len); \
    if (len_ == 0) { \
      ga_concat(gap, "0z"); \
    } else { \
      /* Allocate space for "0z", the two hex chars per byte, and a */ \
      /* "." separator after every eight hex chars. */ \
      /* Example: "0z00112233.44556677.8899" */ \
      ga_grow(gap, 2 + 2 * len_ + (len_ - 1) / 4); \
      ga_concat(gap, "0z"); \
      char numbuf[NUMBUFLEN]; \
      for (int i_ = 0; i_ < len_; i_++) { \
        if (i_ > 0 && (i_ & 3) == 0) { \
          ga_append(gap, '.'); \
        } \
        vim_snprintf((char *)numbuf, ARRAY_SIZE(numbuf), "%02X", \
                     (int)tv_blob_get(blob_, i_)); \
        ga_concat(gap, numbuf); \
      } \
    } \
  } while (0)

#define TYPVAL_ENCODE_CONV_NUMBER(tv, num) \
  do { \
    char numbuf[NUMBUFLEN]; \
    vim_snprintf(numbuf, ARRAY_SIZE(numbuf), "%" PRId64, (int64_t)(num)); \
    ga_concat(gap, numbuf); \
  } while (0)

#define TYPVAL_ENCODE_CONV_FLOAT(tv, flt) \
  do { \
    const float_T flt_ = (flt); \
    switch (xfpclassify(flt_)) { \
    case FP_NAN: { \
        ga_concat(gap, "str2float('nan')"); \
        break; \
    } \
    case FP_INFINITE: { \
        if (flt_ < 0) { \
          ga_append(gap, '-'); \
        } \
        ga_concat(gap, "str2float('inf')"); \
        break; \
    } \
    default: { \
        char numbuf[NUMBUFLEN]; \
        vim_snprintf(numbuf, ARRAY_SIZE(numbuf), "%g", flt_); \
        ga_concat(gap, numbuf); \
    } \
    } \
  } while (0)

#define TYPVAL_ENCODE_CONV_FUNC_START(tv, fun) \
  do { \
    const char *const fun_ = (fun); \
    if (fun_ == NULL) { \
      internal_error("string(): NULL function name"); \
      ga_concat(gap, "function(NULL"); \
    } else { \
      ga_concat(gap, "function("); \
      TYPVAL_ENCODE_CONV_STRING(tv, fun_, strlen(fun_)); \
    } \
  } while (0)

#define TYPVAL_ENCODE_CONV_FUNC_BEFORE_ARGS(tv, len) \
  do { \
    if ((len) != 0) { \
      ga_concat(gap, ", "); \
    } \
  } while (0)

#define TYPVAL_ENCODE_CONV_FUNC_BEFORE_SELF(tv, len) \
  do { \
    if ((ptrdiff_t)(len) != -1) { \
      ga_concat(gap, ", "); \
    } \
  } while (0)

#define TYPVAL_ENCODE_CONV_FUNC_END(tv) \
  ga_append(gap, ')')

#define TYPVAL_ENCODE_CONV_EMPTY_LIST(tv) \
  ga_concat(gap, "[]")

#define TYPVAL_ENCODE_CONV_LIST_START(tv, len) \
  ga_append(gap, '[')

#define TYPVAL_ENCODE_CONV_REAL_LIST_AFTER_START(tv, mpsv)

#define TYPVAL_ENCODE_CONV_EMPTY_DICT(tv, dict) \
  ga_concat(gap, "{}")

#define TYPVAL_ENCODE_CHECK_BEFORE

#define TYPVAL_ENCODE_CONV_NIL(tv) \
  ga_concat(gap, "v:null")

#define TYPVAL_ENCODE_CONV_BOOL(tv, num) \
  ga_concat(gap, ((num) ? "v:true" : "v:false"))

#define TYPVAL_ENCODE_CONV_UNSIGNED_NUMBER(tv, num)

#define TYPVAL_ENCODE_CONV_DICT_START(tv, dict, len) \
  ga_append(gap, '{')

#define TYPVAL_ENCODE_CONV_REAL_DICT_AFTER_START(tv, dict, mpsv)

#define TYPVAL_ENCODE_CONV_DICT_END(tv, dict) \
  ga_append(gap, '}')

#define TYPVAL_ENCODE_CONV_DICT_AFTER_KEY(tv, dict) \
  ga_concat(gap, ": ")

#define TYPVAL_ENCODE_CONV_DICT_BETWEEN_ITEMS(tv, dict) \
  ga_concat(gap, ", ")

#define TYPVAL_ENCODE_SPECIAL_DICT_KEY_CHECK(label, key)

#define TYPVAL_ENCODE_CONV_LIST_END(tv) \
  ga_append(gap, ']')

#define TYPVAL_ENCODE_CONV_LIST_BETWEEN_ITEMS(tv) \
  TYPVAL_ENCODE_CONV_DICT_BETWEEN_ITEMS(tv, NULL)

#define TYPVAL_ENCODE_CONV_RECURSE(val, conv_type) \
  do { \
    if (!did_echo_string_emsg) { \
      /* Only give this message once for a recursive call to avoid */ \
      /* flooding the user with errors. */ \
      did_echo_string_emsg = true; \
      emsg(_("E724: unable to correctly dump variable " \
             "with self-referencing container")); \
    } \
    char ebuf[NUMBUFLEN + 7]; \
    size_t backref = 0; \
    for (; backref < kv_size(*mpstack); backref++) { \
      const MPConvStackVal mpval = kv_A(*mpstack, backref); \
      if (mpval.type == (conv_type)) { \
        if ((conv_type) == kMPConvDict) { \
          if ((void *)mpval.data.d.dict == (void *)(val)) { \
            break; \
          } \
        } else if ((conv_type) == kMPConvList) { \
          if ((void *)mpval.data.l.list == (void *)(val)) { \
            break; \
          } \
        } \
      } \
    } \
    vim_snprintf(ebuf, ARRAY_SIZE(ebuf), "{E724@%zu}", backref); \
    ga_concat(gap, &ebuf[0]); \
  } while (0)

#define TYPVAL_ENCODE_ALLOW_SPECIALS false

#define TYPVAL_ENCODE_SCOPE static
#define TYPVAL_ENCODE_NAME string
#define TYPVAL_ENCODE_FIRST_ARG_TYPE garray_T *const
#define TYPVAL_ENCODE_FIRST_ARG_NAME gap
#include "nvim/eval/typval_encode.c.h"
#undef TYPVAL_ENCODE_SCOPE
#undef TYPVAL_ENCODE_NAME
#undef TYPVAL_ENCODE_FIRST_ARG_TYPE
#undef TYPVAL_ENCODE_FIRST_ARG_NAME

#undef TYPVAL_ENCODE_CONV_RECURSE
#define TYPVAL_ENCODE_CONV_RECURSE(val, conv_type) \
  do { \
    char ebuf[NUMBUFLEN + 7]; \
    size_t backref = 0; \
    for (; backref < kv_size(*mpstack); backref++) { \
      const MPConvStackVal mpval = kv_A(*mpstack, backref); \
      if (mpval.type == (conv_type)) { \
        if ((conv_type) == kMPConvDict) { \
          if ((void *)mpval.data.d.dict == (void *)(val)) { \
            break; \
          } \
        } else if ((conv_type) == kMPConvList) { \
          if ((void *)mpval.data.l.list == (void *)(val)) { \
            break; \
          } \
        } \
      } \
    } \
    if ((conv_type) == kMPConvDict) { \
      vim_snprintf(ebuf, ARRAY_SIZE(ebuf), "{...@%zu}", backref); \
    } else { \
      vim_snprintf(ebuf, ARRAY_SIZE(ebuf), "[...@%zu]", backref); \
    } \
    ga_concat(gap, &ebuf[0]); \
    return OK; \
  } while (0)

#define TYPVAL_ENCODE_SCOPE
#define TYPVAL_ENCODE_NAME echo
#define TYPVAL_ENCODE_FIRST_ARG_TYPE garray_T *const
#define TYPVAL_ENCODE_FIRST_ARG_NAME gap
#include "nvim/eval/typval_encode.c.h"
#undef TYPVAL_ENCODE_SCOPE
#undef TYPVAL_ENCODE_NAME
#undef TYPVAL_ENCODE_FIRST_ARG_TYPE
#undef TYPVAL_ENCODE_FIRST_ARG_NAME

#undef TYPVAL_ENCODE_CONV_RECURSE
#define TYPVAL_ENCODE_CONV_RECURSE(val, conv_type) \
  do { \
    if (!did_echo_string_emsg) { \
      /* Only give this message once for a recursive call to avoid */ \
      /* flooding the user with errors. */ \
      did_echo_string_emsg = true; \
      emsg(_("E724: unable to correctly dump variable " \
             "with self-referencing container")); \
    } \
  } while (0)

#undef TYPVAL_ENCODE_ALLOW_SPECIALS
#define TYPVAL_ENCODE_ALLOW_SPECIALS true

#define TYPVAL_ENCODE_CHECK_BEFORE

#undef TYPVAL_ENCODE_CONV_NIL
#define TYPVAL_ENCODE_CONV_NIL(tv) \
  ga_concat(gap, "null")

#undef TYPVAL_ENCODE_CONV_BOOL
#define TYPVAL_ENCODE_CONV_BOOL(tv, num) \
  ga_concat(gap, ((num) ? "true" : "false"))

#undef TYPVAL_ENCODE_CONV_UNSIGNED_NUMBER
#define TYPVAL_ENCODE_CONV_UNSIGNED_NUMBER(tv, num) \
  do { \
    char numbuf[NUMBUFLEN]; \
    vim_snprintf(numbuf, ARRAY_SIZE(numbuf), "%" PRIu64, (num)); \
    ga_concat(gap, numbuf); \
  } while (0)

#undef TYPVAL_ENCODE_CONV_FLOAT
#define TYPVAL_ENCODE_CONV_FLOAT(tv, flt) \
  do { \
    const float_T flt_ = (flt); \
    switch (xfpclassify(flt_)) { \
    case FP_NAN: { \
        emsg(_("E474: Unable to represent NaN value in JSON")); \
        return FAIL; \
    } \
    case FP_INFINITE: { \
        emsg(_("E474: Unable to represent infinity in JSON")); \
        return FAIL; \
    } \
    default: { \
        char numbuf[NUMBUFLEN]; \
        vim_snprintf(numbuf, ARRAY_SIZE(numbuf), "%g", flt_); \
        ga_concat(gap, numbuf); \
        break; \
    } \
    } \
  } while (0)

/// Escape sequences used in JSON
static const char escapes[][3] = {
  [BS] = "\\b",
  [TAB] = "\\t",
  [NL] = "\\n",
  [CAR] = "\\r",
  ['"'] = "\\\"",
  ['\\'] = "\\\\",
  [FF] = "\\f",
};

static const char xdigits[] = "0123456789ABCDEF";

/// Convert given string to JSON string
///
/// @param[out]  gap  Garray where result will be saved.
/// @param[in]  buf  Converted string.
/// @param[in]  len  Converted string length.
///
/// @return OK in case of success, FAIL otherwise.
static inline int convert_to_json_string(garray_T *const gap, const char *const buf,
                                         const size_t len)
  FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_ALWAYS_INLINE
{
  const char *utf_buf = buf;
  if (utf_buf == NULL) {
    ga_concat(gap, "\"\"");
  } else {
    size_t utf_len = len;
    char *tofree = NULL;
    size_t str_len = 0;
    // Encode character as \uNNNN if
    // 1. It is an ASCII control character (0x0 .. 0x1F; 0x7F not
    //    utf_printable and thus not checked specially).
    // 2. Code point is not printable according to utf_printable().
    // This is done to make resulting values displayable on screen also not from
    // Neovim.
#define ENCODE_RAW(ch) \
  ((ch) >= 0x20 && utf_printable(ch))
    for (size_t i = 0; i < utf_len;) {
      const int ch = utf_ptr2char(utf_buf + i);
      const size_t shift = (ch == 0 ? 1 : ((size_t)utf_ptr2len(utf_buf + i)));
      assert(shift > 0);
      i += shift;
      switch (ch) {
      case BS:
      case TAB:
      case NL:
      case FF:
      case CAR:
      case '"':
      case '\\':
        str_len += 2;
        break;
      default:
        if (ch > 0x7F && shift == 1) {
          semsg(_("E474: String \"%.*s\" contains byte that does not start "
                  "any UTF-8 character"),
                (int)(utf_len - (i - shift)), utf_buf + i - shift);
          xfree(tofree);
          return FAIL;
        } else if ((SURROGATE_HI_START <= ch && ch <= SURROGATE_HI_END)
                   || (SURROGATE_LO_START <= ch && ch <= SURROGATE_LO_END)) {
          semsg(_("E474: UTF-8 string contains code point which belongs "
                  "to a surrogate pair: %.*s"),
                (int)(utf_len - (i - shift)), utf_buf + i - shift);
          xfree(tofree);
          return FAIL;
        } else if (ENCODE_RAW(ch)) {
          str_len += shift;
        } else {
          str_len += ((sizeof("\\u1234") - 1)
                      * (size_t)(1 + (ch >= SURROGATE_FIRST_CHAR)));
        }
        break;
      }
    }
    ga_append(gap, '"');
    ga_grow(gap, (int)str_len);
    for (size_t i = 0; i < utf_len;) {
      const int ch = utf_ptr2char(utf_buf + i);
      const size_t shift = (ch == 0 ? 1 : ((size_t)utf_char2len(ch)));
      assert(shift > 0);
      // Is false on invalid unicode, but this should already be handled.
      assert(ch == 0 || shift == ((size_t)utf_ptr2len(utf_buf + i)));
      switch (ch) {
      case BS:
      case TAB:
      case NL:
      case FF:
      case CAR:
      case '"':
      case '\\':
        ga_concat_len(gap, escapes[ch], 2);
        break;
      default:
        if (ENCODE_RAW(ch)) {
          ga_concat_len(gap, utf_buf + i, shift);
        } else if (ch < SURROGATE_FIRST_CHAR) {
          ga_concat_len(gap, ((const char[]) {
            '\\', 'u',
            xdigits[(ch >> (4 * 3)) & 0xF],
            xdigits[(ch >> (4 * 2)) & 0xF],
            xdigits[(ch >> (4 * 1)) & 0xF],
            xdigits[(ch >> (4 * 0)) & 0xF],
          }), sizeof("\\u1234") - 1);
        } else {
          const int tmp = ch - SURROGATE_FIRST_CHAR;
          const int hi = SURROGATE_HI_START + ((tmp >> 10) & ((1 << 10) - 1));
          const int lo = SURROGATE_LO_END + ((tmp >>  0) & ((1 << 10) - 1));
          ga_concat_len(gap, ((const char[]) {
            '\\', 'u',
            xdigits[(hi >> (4 * 3)) & 0xF],
            xdigits[(hi >> (4 * 2)) & 0xF],
            xdigits[(hi >> (4 * 1)) & 0xF],
            xdigits[(hi >> (4 * 0)) & 0xF],
            '\\', 'u',
            xdigits[(lo >> (4 * 3)) & 0xF],
            xdigits[(lo >> (4 * 2)) & 0xF],
            xdigits[(lo >> (4 * 1)) & 0xF],
            xdigits[(lo >> (4 * 0)) & 0xF],
          }), (sizeof("\\u1234") - 1) * 2);
        }
        break;
      }
      i += shift;
    }
    ga_append(gap, '"');
    xfree(tofree);
  }
  return OK;
}

#undef TYPVAL_ENCODE_CONV_STRING
#define TYPVAL_ENCODE_CONV_STRING(tv, buf, len) \
  do { \
    if (convert_to_json_string(gap, (buf), (len)) != OK) { \
      return FAIL; \
    } \
  } while (0)

#undef TYPVAL_ENCODE_CONV_EXT_STRING
#define TYPVAL_ENCODE_CONV_EXT_STRING(tv, buf, len, type) \
  do { \
    xfree(buf); \
    emsg(_("E474: Unable to convert EXT string to JSON")); \
    return FAIL; \
  } while (0)

#undef TYPVAL_ENCODE_CONV_BLOB
#define TYPVAL_ENCODE_CONV_BLOB(tv, blob, len) \
  do { \
    const blob_T *const blob_ = (blob); \
    const int len_ = (len); \
    if (len_ == 0) { \
      ga_concat(gap, "[]"); \
    } else { \
      ga_append(gap, '['); \
      char numbuf[NUMBUFLEN]; \
      for (int i_ = 0; i_ < len_; i_++) { \
        if (i_ > 0) { \
          ga_concat(gap, ", "); \
        } \
        vim_snprintf((char *)numbuf, ARRAY_SIZE(numbuf), "%d", \
                     (int)tv_blob_get(blob_, i_)); \
        ga_concat(gap, numbuf); \
      } \
      ga_append(gap, ']'); \
    } \
  } while (0)

#undef TYPVAL_ENCODE_CONV_FUNC_START
#define TYPVAL_ENCODE_CONV_FUNC_START(tv, fun) \
  return conv_error(_("E474: Error while dumping %s, %s: " \
                      "attempt to dump function reference"), \
                    mpstack, objname)

/// Check whether given key can be used in json_encode()
///
/// @param[in]  tv  Key to check.
bool encode_check_json_key(const typval_T *const tv)
  FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE
{
  if (tv->v_type == VAR_STRING) {
    return true;
  }
  if (tv->v_type != VAR_DICT) {
    return false;
  }
  const dict_T *const spdict = tv->vval.v_dict;
  if (spdict->dv_hashtab.ht_used != 2) {
    return false;
  }
  const dictitem_T *type_di;
  const dictitem_T *val_di;
  if ((type_di = tv_dict_find(spdict, S_LEN("_TYPE"))) == NULL
      || type_di->di_tv.v_type != VAR_LIST
      || type_di->di_tv.vval.v_list != eval_msgpack_type_lists[kMPString]
      || (val_di = tv_dict_find(spdict, S_LEN("_VAL"))) == NULL
      || val_di->di_tv.v_type != VAR_LIST) {
    return false;
  }
  if (val_di->di_tv.vval.v_list == NULL) {
    return true;
  }
  TV_LIST_ITER_CONST(val_di->di_tv.vval.v_list, li, {
    if (TV_LIST_ITEM_TV(li)->v_type != VAR_STRING) {
      return false;
    }
  });
  return true;
}

#undef TYPVAL_ENCODE_SPECIAL_DICT_KEY_CHECK
#define TYPVAL_ENCODE_SPECIAL_DICT_KEY_CHECK(label, key) \
  do { \
    if (!encode_check_json_key(&(key))) { \
      emsg(_("E474: Invalid key in special dictionary")); \
      goto label; \
    } \
  } while (0)

#define TYPVAL_ENCODE_SCOPE static
#define TYPVAL_ENCODE_NAME json
#define TYPVAL_ENCODE_FIRST_ARG_TYPE garray_T *const
#define TYPVAL_ENCODE_FIRST_ARG_NAME gap
#include "nvim/eval/typval_encode.c.h"
#undef TYPVAL_ENCODE_SCOPE
#undef TYPVAL_ENCODE_NAME
#undef TYPVAL_ENCODE_FIRST_ARG_TYPE
#undef TYPVAL_ENCODE_FIRST_ARG_NAME

#undef TYPVAL_ENCODE_CONV_STRING
#undef TYPVAL_ENCODE_CONV_STR_STRING
#undef TYPVAL_ENCODE_CONV_EXT_STRING
#undef TYPVAL_ENCODE_CONV_BLOB
#undef TYPVAL_ENCODE_CONV_NUMBER
#undef TYPVAL_ENCODE_CONV_FLOAT
#undef TYPVAL_ENCODE_CONV_FUNC_START
#undef TYPVAL_ENCODE_CONV_FUNC_BEFORE_ARGS
#undef TYPVAL_ENCODE_CONV_FUNC_BEFORE_SELF
#undef TYPVAL_ENCODE_CONV_FUNC_END
#undef TYPVAL_ENCODE_CONV_EMPTY_LIST
#undef TYPVAL_ENCODE_CONV_LIST_START
#undef TYPVAL_ENCODE_CONV_REAL_LIST_AFTER_START
#undef TYPVAL_ENCODE_CONV_EMPTY_DICT
#undef TYPVAL_ENCODE_CHECK_BEFORE
#undef TYPVAL_ENCODE_CONV_NIL
#undef TYPVAL_ENCODE_CONV_BOOL
#undef TYPVAL_ENCODE_CONV_UNSIGNED_NUMBER
#undef TYPVAL_ENCODE_CONV_DICT_START
#undef TYPVAL_ENCODE_CONV_REAL_DICT_AFTER_START
#undef TYPVAL_ENCODE_CONV_DICT_END
#undef TYPVAL_ENCODE_CONV_DICT_AFTER_KEY
#undef TYPVAL_ENCODE_CONV_DICT_BETWEEN_ITEMS
#undef TYPVAL_ENCODE_SPECIAL_DICT_KEY_CHECK
#undef TYPVAL_ENCODE_CONV_LIST_END
#undef TYPVAL_ENCODE_CONV_LIST_BETWEEN_ITEMS
#undef TYPVAL_ENCODE_CONV_RECURSE
#undef TYPVAL_ENCODE_ALLOW_SPECIALS

/// Return a string with the string representation of a variable.
/// Puts quotes around strings, so that they can be parsed back by eval().
///
/// @param[in]  tv  typval_T to convert.
/// @param[out]  len  Location where length of the result will be saved.
///
/// @return String representation of the variable or NULL.
char *encode_tv2string(typval_T *tv, size_t *len)
  FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_MALLOC
{
  garray_T ga;
  ga_init(&ga, (int)sizeof(char), 80);
  const int evs_ret = encode_vim_to_string(&ga, tv,
                                           N_("encode_tv2string() argument"));
  (void)evs_ret;
  assert(evs_ret == OK);
  did_echo_string_emsg = false;
  if (len != NULL) {
    *len = (size_t)ga.ga_len;
  }
  ga_append(&ga, NUL);
  return (char *)ga.ga_data;
}

/// Return a string with the string representation of a variable.
/// Does not put quotes around strings, as ":echo" displays values.
///
/// @param[in]  tv  typval_T to convert.
/// @param[out]  len  Location where length of the result will be saved.
///
/// @return String representation of the variable or NULL.
char *encode_tv2echo(typval_T *tv, size_t *len)
  FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_MALLOC
{
  garray_T ga;
  ga_init(&ga, (int)sizeof(char), 80);
  if (tv->v_type == VAR_STRING || tv->v_type == VAR_FUNC) {
    if (tv->vval.v_string != NULL) {
      ga_concat(&ga, tv->vval.v_string);
    }
  } else {
    const int eve_ret = encode_vim_to_echo(&ga, tv, N_(":echo argument"));
    (void)eve_ret;
    assert(eve_ret == OK);
  }
  if (len != NULL) {
    *len = (size_t)ga.ga_len;
  }
  ga_append(&ga, NUL);
  return (char *)ga.ga_data;
}

/// Return a string with the string representation of a variable.
/// Puts quotes around strings, so that they can be parsed back by eval().
///
/// @param[in]  tv  typval_T to convert.
/// @param[out]  len  Location where length of the result will be saved.
///
/// @return String representation of the variable or NULL.
char *encode_tv2json(typval_T *tv, size_t *len)
  FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_MALLOC
{
  garray_T ga;
  ga_init(&ga, (int)sizeof(char), 80);
  const int evj_ret = encode_vim_to_json(&ga, tv,
                                         N_("encode_tv2json() argument"));
  if (!evj_ret) {
    ga_clear(&ga);
  }
  did_echo_string_emsg = false;
  if (len != NULL) {
    *len = (size_t)ga.ga_len;
  }
  ga_append(&ga, NUL);
  return (char *)ga.ga_data;
}

#define TYPVAL_ENCODE_CONV_STRING(tv, buf, len) \
  mpack_bin(cbuf_as_string(buf, (len)), packer); \

#define TYPVAL_ENCODE_CONV_STR_STRING(tv, buf, len) \
  mpack_str(cbuf_as_string(buf, (len)), packer); \

#define TYPVAL_ENCODE_CONV_EXT_STRING(tv, buf, len, type) \
  mpack_ext(buf, (len), (int8_t)(type), packer); \

#define TYPVAL_ENCODE_CONV_BLOB(tv, blob, len) \
  mpack_bin(cbuf_as_string((blob) ? (blob)->bv_ga.ga_data : NULL, (size_t)(len)), packer);

#define TYPVAL_ENCODE_CONV_NUMBER(tv, num) \
  mpack_integer(&packer->ptr, (Integer)(num))

#define TYPVAL_ENCODE_CONV_FLOAT(tv, flt) \
  mpack_float8(&packer->ptr, (double)(flt))

#define TYPVAL_ENCODE_CONV_FUNC_START(tv, fun) \
  return conv_error(_("E5004: Error while dumping %s, %s: " \
                      "attempt to dump function reference"), \
                    mpstack, objname)

#define TYPVAL_ENCODE_CONV_FUNC_BEFORE_ARGS(tv, len)
#define TYPVAL_ENCODE_CONV_FUNC_BEFORE_SELF(tv, len)
#define TYPVAL_ENCODE_CONV_FUNC_END(tv)

#define TYPVAL_ENCODE_CONV_EMPTY_LIST(tv) \
  mpack_array(&packer->ptr, 0)

#define TYPVAL_ENCODE_CONV_LIST_START(tv, len) \
  mpack_array(&packer->ptr, (uint32_t)(len))

#define TYPVAL_ENCODE_CONV_REAL_LIST_AFTER_START(tv, mpsv)

#define TYPVAL_ENCODE_CONV_EMPTY_DICT(tv, dict) \
  mpack_map(&packer->ptr, 0)

#define TYPVAL_ENCODE_CHECK_BEFORE \
  mpack_check_buffer(packer)

#define TYPVAL_ENCODE_CONV_NIL(tv) \
  mpack_nil(&packer->ptr)

#define TYPVAL_ENCODE_CONV_BOOL(tv, num) \
  mpack_bool(&packer->ptr, (bool)num); \

#define TYPVAL_ENCODE_CONV_UNSIGNED_NUMBER(tv, num) \
  mpack_uint64(&packer->ptr, (num))

#define TYPVAL_ENCODE_CONV_DICT_START(tv, dict, len) \
  mpack_map(&packer->ptr, (uint32_t)(len))

#define TYPVAL_ENCODE_CONV_REAL_DICT_AFTER_START(tv, dict, mpsv)

#define TYPVAL_ENCODE_CONV_DICT_END(tv, dict)

#define TYPVAL_ENCODE_CONV_DICT_AFTER_KEY(tv, dict)

#define TYPVAL_ENCODE_CONV_DICT_BETWEEN_ITEMS(tv, dict)

#define TYPVAL_ENCODE_SPECIAL_DICT_KEY_CHECK(label, key)

#define TYPVAL_ENCODE_CONV_LIST_END(tv)

#define TYPVAL_ENCODE_CONV_LIST_BETWEEN_ITEMS(tv)

#define TYPVAL_ENCODE_CONV_RECURSE(val, conv_type) \
  return conv_error(_("E5005: Unable to dump %s: " \
                      "container references itself in %s"), \
                    mpstack, objname)

#define TYPVAL_ENCODE_ALLOW_SPECIALS true

#define TYPVAL_ENCODE_SCOPE
#define TYPVAL_ENCODE_NAME msgpack
#define TYPVAL_ENCODE_FIRST_ARG_TYPE PackerBuffer *const
#define TYPVAL_ENCODE_FIRST_ARG_NAME packer
#include "nvim/eval/typval_encode.c.h"
#undef TYPVAL_ENCODE_SCOPE
#undef TYPVAL_ENCODE_NAME
#undef TYPVAL_ENCODE_FIRST_ARG_TYPE
#undef TYPVAL_ENCODE_FIRST_ARG_NAME

#undef TYPVAL_ENCODE_CONV_STRING
#undef TYPVAL_ENCODE_CONV_STR_STRING
#undef TYPVAL_ENCODE_CONV_EXT_STRING
#undef TYPVAL_ENCODE_CONV_BLOB
#undef TYPVAL_ENCODE_CONV_NUMBER
#undef TYPVAL_ENCODE_CONV_FLOAT
#undef TYPVAL_ENCODE_CONV_FUNC_START
#undef TYPVAL_ENCODE_CONV_FUNC_BEFORE_ARGS
#undef TYPVAL_ENCODE_CONV_FUNC_BEFORE_SELF
#undef TYPVAL_ENCODE_CONV_FUNC_END
#undef TYPVAL_ENCODE_CONV_EMPTY_LIST
#undef TYPVAL_ENCODE_CONV_LIST_START
#undef TYPVAL_ENCODE_CONV_REAL_LIST_AFTER_START
#undef TYPVAL_ENCODE_CONV_EMPTY_DICT
#undef TYPVAL_ENCODE_CHECK_BEFORE
#undef TYPVAL_ENCODE_CONV_NIL
#undef TYPVAL_ENCODE_CONV_BOOL
#undef TYPVAL_ENCODE_CONV_UNSIGNED_NUMBER
#undef TYPVAL_ENCODE_CONV_DICT_START
#undef TYPVAL_ENCODE_CONV_REAL_DICT_AFTER_START
#undef TYPVAL_ENCODE_CONV_DICT_END
#undef TYPVAL_ENCODE_CONV_DICT_AFTER_KEY
#undef TYPVAL_ENCODE_CONV_DICT_BETWEEN_ITEMS
#undef TYPVAL_ENCODE_SPECIAL_DICT_KEY_CHECK
#undef TYPVAL_ENCODE_CONV_LIST_END
#undef TYPVAL_ENCODE_CONV_LIST_BETWEEN_ITEMS
#undef TYPVAL_ENCODE_CONV_RECURSE
#undef TYPVAL_ENCODE_ALLOW_SPECIALS

/// Initialize ListReaderState structure
ListReaderState encode_init_lrstate(const list_T *const list)
  FUNC_ATTR_NONNULL_ALL
{
  return (ListReaderState) {
    .list = list,
    .li = tv_list_first(list),
    .offset = 0,
    .li_length = (TV_LIST_ITEM_TV(tv_list_first(list))->vval.v_string == NULL
                  ? 0
                  : strlen(TV_LIST_ITEM_TV(tv_list_first(list))->vval.v_string)),
  };
}
