#define LUA_LIB

#include "lauxlib.h"

#ifndef LOCAL_LUA
#include "module.h"
#include <string.h>
#include <math.h>
#include <limits.h>
#endif

#include "sjson/json_config.h"
#include "sjson/jsonsl.h"

#define LUA_SJSONLIBNAME "sjson"

#define DEFAULT_DEPTH   20

#define DBG_PRINTF(...)

#define SJSON_FLOAT_FMT   ((sizeof(lua_Float) == 8) ? "%.19g" : "%.9g")

typedef struct {
  jsonsl_t jsn;
  int result_ref;
  int hkey_ref;
  int null_ref;
  int metatable;
  int pos_ref;
  uint8_t complete;
  const char *error;
  lua_State *L;
  size_t min_needed;
  size_t min_available;
  size_t buffer_len;
  const char *buffer; // Points into buffer_ref
  int buffer_ref;
} JSN_DATA;

#define get_parent_object_ref() ((state->level == 1) ? data->result_ref : state[-1].lua_object_ref)
#define get_parent_object_used_count_pre_inc() ((state->level == 1) ? 1 : ++state[-1].used_count)

static const char* get_state_buffer(JSN_DATA *ctx, struct jsonsl_state_st *state)
{
  size_t offset = state->pos_begin - ctx->min_available;
  return ctx->buffer + offset;
}

// The elem data is a ref

static int error_callback(jsonsl_t jsn,
                   jsonsl_error_t err,
                   struct jsonsl_state_st *state,
                   char *at)
{
  JSN_DATA *data = (JSN_DATA *) jsn->data;
  if (!data->complete) {
    data->error = jsonsl_strerror(err);
  }

  //fprintf(stderr, "Got error at pos %lu: %s\n", jsn->pos, jsonsl_strerror(err));
  return 0;
}

static void
create_table(JSN_DATA *data) {
  lua_newtable(data->L);
  if (data->metatable != LUA_NOREF) {
    lua_rawgeti(data->L, LUA_REGISTRYINDEX, data->metatable);
    lua_setmetatable(data->L, -2);
  }
}

static void
create_new_element(jsonsl_t jsn,
                   jsonsl_action_t action,
                   struct jsonsl_state_st *state,
                   const char *buf)
{
  JSN_DATA *data = jsn->data;

  DBG_PRINTF("L%d: new action %d @ %d state->type %s\n", state->level, action, state->pos_begin, jsonsl_strtype(state->type));
  DBG_PRINTF("buf: '%s' ('%.10s')\n", buf, get_state_buffer(data, state));

  state->lua_object_ref = LUA_NOREF;

  switch(state->type) {
    case JSONSL_T_SPECIAL:
    case JSONSL_T_STRING:
    case JSONSL_T_HKEY:
      break;

    case JSONSL_T_LIST:
    case JSONSL_T_OBJECT:
      create_table(data);
      state->lua_object_ref = luaL_ref(data->L, LUA_REGISTRYINDEX);
      state->used_count = 0;

      lua_rawgeti(data->L, LUA_REGISTRYINDEX, get_parent_object_ref());
      if (data->hkey_ref == LUA_NOREF) {
        // list, so append
        lua_pushinteger(data->L, get_parent_object_used_count_pre_inc());
        DBG_PRINTF("Adding array element\n");
      } else {
        // object, so
        lua_rawgeti(data->L, LUA_REGISTRYINDEX, data->hkey_ref);
        luaL_unref(data->L, LUA_REGISTRYINDEX, data->hkey_ref);
        data->hkey_ref = LUA_NOREF;
        DBG_PRINTF("Adding hash element\n");
      }
      if (data->pos_ref != LUA_NOREF && state->level > 1) {
        lua_rawgeti(data->L, LUA_REGISTRYINDEX, data->pos_ref);
        lua_pushinteger(data->L, state->level - 1);
        lua_pushvalue(data->L, -3);     // get the key
        lua_settable(data->L, -3);
        lua_pop(data->L, 1);
      }
      // At this point, the stack:
      // top: index/hash key
      //    : table

      int want_value = 1;
      // Invoke the checkpath method if possible
      if (data->pos_ref != LUA_NOREF) {
        lua_rawgeti(data->L, LUA_REGISTRYINDEX, data->metatable);
        lua_getfield(data->L, -1, "checkpath");
        if (!lua_isnil(data->L, -1)) {
          // Call with the new table and the path as arguments
          lua_rawgeti(data->L, LUA_REGISTRYINDEX, state->lua_object_ref);
          lua_rawgeti(data->L, LUA_REGISTRYINDEX, data->pos_ref);
          lua_call(data->L, 2, 1);
          want_value = lua_toboolean(data->L, -1);
        }
        lua_pop(data->L, 2);    // Discard the metatable and either the getfield result or retval
      }

      if (want_value) {
        lua_rawgeti(data->L, LUA_REGISTRYINDEX, state->lua_object_ref);
        lua_settable(data->L, -3);
        lua_pop(data->L, 1);    // the table
      } else {
        lua_pop(data->L, 2);    // the index and table
      }

      break;

    default:
        DBG_PRINTF("Unhandled type %c\n", state->type);
        luaL_error(data->L, "Unhandled type");
        break;
    }

    data->min_needed = state->pos_begin;
}

static void push_number(JSN_DATA *data, struct jsonsl_state_st *state) {
  lua_pushlstring(data->L, get_state_buffer(data, state), state->pos_cur - state->pos_begin);
#if LUA_VERSION_NUM == 501
  lua_pushnumber(data->L, lua_tonumber(data->L, -1));
#else
  if (!lua_stringtonumber(data->L, lua_tostring(data->L, -1))) {
    // In this case stringtonumber does not push a value
    luaL_error(data->L, "Invalid number");
  }
#endif
  lua_remove(data->L, -2);
}

static int fromhex(char c) {
  if (c <= '9') {
    return c & 0xf;
  }

  return ((c - 'A' + 10) & 0xf);
}

static void output_utf8(luaL_Buffer *buf, int c) {
  char space[4];
  char *b = space;

  if (c<0x80) *b++=c;
  else if (c<0x800) *b++=192+c/64, *b++=128+c%64;
  else if (c-0xd800u<0x800) *b++ = '?';
  else if (c<0x10000) *b++=224+c/4096, *b++=128+c/64%64, *b++=128+c%64;
  else if (c<0x110000) *b++=240+c/262144, *b++=128+c/4096%64, *b++=128+c/64%64, *b++=128+c%64;
  else *b++ = '?';

  luaL_addlstring(buf, space, b - space);
}

static void push_string(JSN_DATA *data, struct jsonsl_state_st *state) {
  luaL_Buffer b;
  luaL_buffinit(data->L, &b);
  int i;
  const char *c = get_state_buffer(data, state) + 1;
  for (i = 0; i < state->pos_cur - state->pos_begin - 1; i++) {
    int nc = c[i];
    if (nc == '\\') {
      i++;
      nc = c[i] & 255;
      switch (c[i]) {
        case 'b':
          nc = '\b';
          break;
        case 'f':
          nc = '\f';
          break;
        case 'n':
          nc = '\n';
          break;
        case 'r':
          nc = '\r';
          break;
        case 't':
          nc = '\t';
          break;
        case 'u':
          nc = fromhex(c[++i]) << 12;
          nc += fromhex(c[++i]) << 8;
          nc += fromhex(c[++i]) << 4;
          nc += fromhex(c[++i])     ;
          output_utf8(&b, nc);
          continue;
      }
    }
    luaL_addchar(&b, nc);
  }
  luaL_pushresult(&b);
}

static void
cleanup_closing_element(jsonsl_t jsn,
                        jsonsl_action_t action,
                        struct jsonsl_state_st *state,
                        const char *at)
{
  JSN_DATA *data = (JSN_DATA *) jsn->data;
  DBG_PRINTF( "L%d: cc action %d state->type %s\n", state->level, action, jsonsl_strtype(state->type));
  DBG_PRINTF( "buf (%d - %d): '%.*s'\n", state->pos_begin, state->pos_cur, state->pos_cur - state->pos_begin, get_state_buffer(data, state));
  DBG_PRINTF( "at: '%s'\n", at);

 switch (state->type) {
   case JSONSL_T_HKEY:
      push_string(data, state);
      data->hkey_ref = luaL_ref(data->L, LUA_REGISTRYINDEX);
      break;

   case JSONSL_T_STRING:
      lua_rawgeti(data->L, LUA_REGISTRYINDEX, get_parent_object_ref());
      if (data->hkey_ref == LUA_NOREF) {
        // list, so append
        lua_pushinteger(data->L, get_parent_object_used_count_pre_inc());
      } else {
        // object, so
        lua_rawgeti(data->L, LUA_REGISTRYINDEX, data->hkey_ref);
        luaL_unref(data->L, LUA_REGISTRYINDEX, data->hkey_ref);
        data->hkey_ref = LUA_NOREF;
      }
      push_string(data, state);
      lua_settable(data->L, -3);
      lua_pop(data->L, 1);
      break;

   case JSONSL_T_SPECIAL:
      DBG_PRINTF("Special flags = 0x%x\n", state->special_flags);
      // need to deal with true/false/null

      if (state->special_flags & (JSONSL_SPECIALf_TRUE|JSONSL_SPECIALf_FALSE|JSONSL_SPECIALf_NUMERIC|JSONSL_SPECIALf_NULL)) {
        if (state->special_flags & JSONSL_SPECIALf_TRUE) {
          lua_pushboolean(data->L, 1);
        } else if (state->special_flags & JSONSL_SPECIALf_FALSE) {
          lua_pushboolean(data->L, 0);
        } else if (state->special_flags & JSONSL_SPECIALf_NULL) {
          DBG_PRINTF("Outputting null\n");
          lua_rawgeti(data->L, LUA_REGISTRYINDEX, data->null_ref);
        } else if (state->special_flags & JSONSL_SPECIALf_NUMERIC) {
          push_number(data, state);
        }

        lua_rawgeti(data->L, LUA_REGISTRYINDEX, get_parent_object_ref());
        if (data->hkey_ref == LUA_NOREF) {
          // list, so append
          lua_pushinteger(data->L, get_parent_object_used_count_pre_inc());
        } else {
          // object, so
          lua_rawgeti(data->L, LUA_REGISTRYINDEX, data->hkey_ref);
          luaL_unref(data->L, LUA_REGISTRYINDEX, data->hkey_ref);
          data->hkey_ref = LUA_NOREF;
        }
        lua_pushvalue(data->L, -3);
        lua_remove(data->L, -4);
        lua_settable(data->L, -3);
        lua_pop(data->L, 1);
      }
      break;
   case JSONSL_T_OBJECT:
   case JSONSL_T_LIST:
      luaL_unref(data->L, LUA_REGISTRYINDEX, state->lua_object_ref);
      state->lua_object_ref = LUA_NOREF;
      if (data->pos_ref != LUA_NOREF) {
        lua_rawgeti(data->L, LUA_REGISTRYINDEX, data->pos_ref);
        lua_pushinteger(data->L, state->level);
        lua_pushnil(data->L);
        lua_settable(data->L, -3);
        lua_pop(data->L, 1);
      }
      if (state->level == 1) {
        data->complete = 1;
      }
      break;
 }
}

static int sjson_decoder_int(lua_State *L, int argno) {
  int nlevels = DEFAULT_DEPTH;

  if (lua_type(L, argno) == LUA_TTABLE) {
    lua_getfield(L, argno, "depth");
    nlevels = lua_tointeger(L, argno);
    if (nlevels == 0) {
      nlevels = DEFAULT_DEPTH;
    }
    if (nlevels < 4) {
      nlevels = 4;
    }
    if (nlevels > 1000) {
      nlevels = 1000;
    }
    lua_pop(L, 1);
  }

  JSN_DATA *data = (JSN_DATA *) lua_newuserdata(L, sizeof(JSN_DATA) + jsonsl_get_size(nlevels));
  //
  // Associate its metatable
  luaL_getmetatable(L, "sjson.decoder");
  lua_setmetatable(L, -2);

  jsonsl_t jsn = jsonsl_init((jsonsl_t) (data + 1), nlevels);
  int i;

  for (i = 0; i < jsn->levels_max; i++) {
    jsn->stack[i].lua_object_ref = LUA_NOREF;
  }
  data->jsn = jsn;
  data->result_ref = LUA_NOREF;

  data->null_ref = LUA_REFNIL;
  data->metatable = LUA_NOREF;
  data->hkey_ref = LUA_NOREF;
  data->pos_ref = LUA_NOREF;
  data->buffer_ref = LUA_NOREF;
  data->complete = 0;
  data->error = NULL;
  data->L = L;
  data->buffer_len = 0;

  data->min_needed = data->min_available = jsn->pos;

  lua_pushlightuserdata(L, 0);
  data->null_ref = luaL_ref(L, LUA_REGISTRYINDEX);

  // This may throw...
  lua_newtable(L);
  data->result_ref = luaL_ref(L, LUA_REGISTRYINDEX);

  if (lua_type(L, argno) == LUA_TTABLE) {
    luaL_unref(L, LUA_REGISTRYINDEX, data->null_ref);
    data->null_ref = LUA_NOREF;
    lua_getfield(L, argno, "null");
    data->null_ref = luaL_ref(L, LUA_REGISTRYINDEX);

    lua_getfield(L, argno, "metatable");
    lua_pushvalue(L, -1);
    data->metatable = luaL_ref(L, LUA_REGISTRYINDEX);

    if (!lua_isnil(L, -1)) {
      lua_getfield(L, -1, "checkpath");
      if (!lua_isnil(L, -1)) {
        lua_newtable(L);
        data->pos_ref = luaL_ref(L, LUA_REGISTRYINDEX);
      }
      lua_pop(L, 1);      // Throw away the checkpath value
    }
    lua_pop(L, 1);      // Throw away the metatable
  }

  jsonsl_enable_all_callbacks(data->jsn);

  jsn->action_callback = NULL;
  jsn->action_callback_PUSH = create_new_element;
  jsn->action_callback_POP = cleanup_closing_element;
  jsn->error_callback = error_callback;
  jsn->data = data;
  jsn->max_callback_level = nlevels;

  return 1;
}

static int sjson_decoder(lua_State *L) {
  return sjson_decoder_int(L, 1);
}

static int sjson_decoder_result_int(lua_State *L, JSN_DATA *data) {
  if (!data->complete) {
    luaL_error(L, "decode not complete");
  }

  lua_rawgeti(L, LUA_REGISTRYINDEX, data->result_ref);
  lua_rawgeti(L, -1, 1);
  lua_remove(L, -2);

  return 1;
}

static int sjson_decoder_result(lua_State *L) {
  JSN_DATA *data = (JSN_DATA *)luaL_checkudata(L, 1, "sjson.decoder");

  return sjson_decoder_result_int(L, data);
}


static void sjson_free_working_data(lua_State *L, JSN_DATA *data) {
  jsonsl_t jsn = data->jsn;
  int i;

  for (i = 0; i < jsn->levels_max; i++) {
    luaL_unref(L, LUA_REGISTRYINDEX, jsn->stack[i].lua_object_ref);
    jsn->stack[i].lua_object_ref = LUA_NOREF;
  }

  luaL_unref(L, LUA_REGISTRYINDEX, data->metatable);
  data->metatable = LUA_NOREF;
  luaL_unref(L, LUA_REGISTRYINDEX, data->hkey_ref);
  data->hkey_ref = LUA_NOREF;
  luaL_unref(L, LUA_REGISTRYINDEX, data->null_ref);
  data->null_ref = LUA_NOREF;
  luaL_unref(L, LUA_REGISTRYINDEX, data->pos_ref);
  data->pos_ref = LUA_NOREF;
  luaL_unref(L, LUA_REGISTRYINDEX, data->buffer_ref);
  data->buffer_ref = LUA_NOREF;
}

static int sjson_decoder_write_int(lua_State *L, int udata_pos, int string_pos) {
  JSN_DATA *data = (JSN_DATA *)luaL_checkudata(L, udata_pos, "sjson.decoder");
  size_t len;

  const char *str = luaL_checklstring(L, string_pos, &len);

  if (data->error) {
    luaL_error(L, "JSON parse error: previous call");
  }

  if (!data->complete) {
    data->L = L;

    // Merge into any existing buffer and deal with discard
    if (data->buffer_ref != LUA_NOREF) {
      luaL_Buffer b;
      luaL_buffinit(L, &b);

      lua_rawgeti(L, LUA_REGISTRYINDEX, data->buffer_ref);
      size_t prev_len;
      const char *prev_buffer = luaL_checklstring(L, -1, &prev_len);
      lua_pop(L, 1);              // But string still referenced so it cannot move
      int discard = data->min_needed - data->min_available;
      prev_buffer += discard;
      prev_len -= discard;
      if (prev_len > 0) {
        luaL_addlstring(&b, prev_buffer, prev_len);
      }
      data->min_available += discard;

      luaL_unref(L, LUA_REGISTRYINDEX, data->buffer_ref);
      data->buffer_ref = LUA_NOREF;

      lua_pushvalue(L, string_pos);
      luaL_addvalue(&b);
      luaL_pushresult(&b);
    } else {
      lua_pushvalue(L, string_pos);
    }

    size_t blen;
    data->buffer = luaL_checklstring(L, -1, &blen);
    data->buffer_len = blen;
    data->buffer_ref = luaL_ref(L, LUA_REGISTRYINDEX);

    jsonsl_feed(data->jsn, str, len);

    if (data->error) {
      luaL_error(L, "JSON parse error: %s", data->error);
    }
  }

  if (data->complete) {
    // We no longer need the buffer
    sjson_free_working_data(L, data);

    return sjson_decoder_result_int(L, data);
  }

  return 0;
}

static int sjson_decoder_write(lua_State *L) {
  return sjson_decoder_write_int(L, 1, 2);
}

static int sjson_decode(lua_State *L) {
  int push_count = sjson_decoder_int(L, 2);
  if (push_count != 1) {
    luaL_error(L, "Internal error in sjson.deocder");
  }

  luaL_checkudata(L, -1, "sjson.decoder");

  push_count = sjson_decoder_write_int(L, -1, 1);

  if (push_count != 1) {
    luaL_error(L, "Incomplete JSON object passed to sjson.decode");
  }

  // Now we have two items on the stack -- the udata and the result
  lua_remove(L, -2);

  return 1;
}

static int sjson_decoder_destructor(lua_State *L) {
  JSN_DATA *data = (JSN_DATA *)luaL_checkudata(L, 1, "sjson.decoder");

  sjson_free_working_data(L, data);

  data->jsn = NULL;

  luaL_unref(L, LUA_REGISTRYINDEX, data->result_ref);
  data->result_ref = LUA_NOREF;

  DBG_PRINTF("Destructor called\n");

  return 0;
}
//
//--------------------------------- ENCODER BELOW
//
//
//
//#undef DBG_PRINTF
//#define DBG_PRINTF printf
typedef struct {
  int lua_object_ref;
  // for arrays
  // 0 -> [
  // 1 -> first element
  // 2 -> ,
  // 3 -> second element
  // 4 -> ]
  // for objects
  // 0 -> { firstkey :
  // 1 -> first value
  // 2 -> , secondkey :
  // 3 -> second value
  // 4 -> }
  short offset;
  // -1 for objects
  // 0 -> n maximum integer key = n
  short size;
  int lua_key_ref;
} ENC_DATA_STATE;

typedef struct {
  ENC_DATA_STATE *stack;
  int nlevels;
  int level;
  int current_str_ref;
  int null_ref;
  int offset;

} ENC_DATA;

static int sjson_encoder_get_table_size(lua_State *L, int argno) {
  // Returns -1 for object, otherwise the maximum integer key value found.
  lua_pushvalue(L, argno);
  // stack now contains: -1 => table
  lua_pushnil(L);
  // stack now contains: -1 => nil; -2 => table
  //
  int maxkey = 0;

  while (lua_next(L, -2)) {
    lua_pop(L, 1);
    // stack now contains: -1 => key; -2 => table
    if (lua_type(L, -1) == LUA_TNUMBER) {
      int val = lua_tointeger(L, -1);
      if (val > maxkey) {
        maxkey = val;
      } else if (val <= 0) {
        maxkey = -1;
        lua_pop(L, 1);
        break;
      }
    } else {
      maxkey = -1;
      lua_pop(L, 1);
      break;
    }
  }

  lua_pop(L, 1);

  return maxkey;
}

static void enc_pop_stack(lua_State *L, ENC_DATA *data) {
  if (data->level < 0) {
    luaL_error(L, "encoder stack underflow");
  }
  ENC_DATA_STATE *state = &data->stack[data->level];

  luaL_unref(L, LUA_REGISTRYINDEX, state->lua_object_ref);
  state->lua_object_ref = LUA_NOREF;
  luaL_unref(L, LUA_REGISTRYINDEX, state->lua_key_ref);
  state->lua_key_ref = LUA_REFNIL;
  data->level--;
}

static void enc_push_stack(lua_State *L, ENC_DATA *data, int argno) {
  if (++data->level >= data->nlevels) {
    luaL_error(L, "encoder stack overflow");
  }
  lua_pushvalue(L, argno);
  ENC_DATA_STATE *state = &data->stack[data->level];
  state->lua_object_ref = luaL_ref(L, LUA_REGISTRYINDEX);
  state->size = sjson_encoder_get_table_size(L, argno);
  state->offset = 0;         // We haven't started on this one yet
}

static int sjson_encoder(lua_State *L) {
  int nlevels = DEFAULT_DEPTH;
  int argno = 1;

  // Validate first arg is a table
  luaL_checktype(L, argno++, LUA_TTABLE);

  if (lua_type(L, argno) == LUA_TTABLE) {
    lua_getfield(L, argno, "depth");
    nlevels = lua_tointeger(L, argno);
    if (nlevels == 0) {
      nlevels = DEFAULT_DEPTH;
    }
    if (nlevels < 4) {
      nlevels = 4;
    }
    if (nlevels > 1000) {
      nlevels = 1000;
    }
    lua_pop(L, 1);
  }

  ENC_DATA *data = (ENC_DATA *) lua_newuserdata(L, sizeof(ENC_DATA) + nlevels * sizeof(ENC_DATA_STATE));

  // Associate its metatable
  luaL_getmetatable(L, "sjson.encoder");
  lua_setmetatable(L, -2);

  data->nlevels = nlevels;
  data->level = -1;
  data->stack = (ENC_DATA_STATE *) (data + 1);
  data->current_str_ref = LUA_NOREF;
  int i;
  for (i = 0; i < nlevels; i++) {
    data->stack[i].lua_object_ref = LUA_NOREF;
    data->stack[i].lua_key_ref = LUA_REFNIL;
  }
  enc_push_stack(L, data, 1);

  data->null_ref = LUA_REFNIL;

  if (lua_type(L, argno) == LUA_TTABLE) {
    luaL_unref(L, LUA_REGISTRYINDEX, data->null_ref);
    data->null_ref = LUA_NOREF;
    lua_getfield(L, argno, "null");
    data->null_ref = luaL_ref(L, LUA_REGISTRYINDEX);
  }

  return 1;
}

static void encode_lua_object(lua_State *L, ENC_DATA *data, int argno, const char *prefix, const char *suffix) {
  luaL_Buffer b;
  luaL_buffinit(L, &b);

  luaL_addstring(&b, prefix);

  int type = lua_type(L, argno);

  if (type == LUA_TSTRING) {
    // Check to see if it is the NULL value
    if (data->null_ref != LUA_REFNIL) {
      lua_rawgeti(L, LUA_REGISTRYINDEX, data->null_ref);
      if (lua_equal(L, -1, -2)) {
        type = LUA_TNIL;
      }
      lua_pop(L, 1);
    }
  }

  switch (type) {
    default:
      luaL_error(L, "Cannot encode type %d", type);
      break;

    case LUA_TLIGHTUSERDATA:
    case LUA_TNIL:
      luaL_addstring(&b, "null");
      break;

    case LUA_TBOOLEAN:
      luaL_addstring(&b, lua_toboolean(L, argno) ? "true" : "false");
      break;

    case LUA_TNUMBER:
    {
      lua_pushvalue(L, argno);
      char value[50];

#if LUA_VERSION_NUM == 501
#ifdef LUA_NUMBER_INTEGRAL
        snprintf(value, sizeof(value), "%d", lua_tointeger(L, -1));
#else
        snprintf(value, sizeof(value), SJSON_FLOAT_FMT, lua_tonumber(L, -1));
#endif
#else
      if (lua_isinteger(L, -1)) {
        snprintf(value, sizeof(value), LUA_INTEGER_FMT, lua_tointeger(L, -1));
      } else {
        snprintf(value, sizeof(value), SJSON_FLOAT_FMT, lua_tonumber(L, -1));
      }
#endif
      lua_pop(L, 1);
      if (strcmp(value, "-inf") == 0 || strcmp(value, "nan") == 0 || strcmp(value, "inf") == 0) {
        luaL_addstring(&b, "null");   // According to ECMA-262 section 24.5.2 Note 4
      } else {
        luaL_addstring(&b, value);
      }
      break;
    }

    case LUA_TSTRING:
    {
      luaL_addchar(&b, '"');
      size_t len;
      const char *str = lua_tolstring(L, argno, &len);
      while (len > 0) {
        if ((*str & 0xff) < 0x20) {
          char value[8];
          value[0] = '\\';

          char *d = value + 1;

          switch(*str) {
            case '\f':
              *d++ = 'f';
              break;
            case '\n':
              *d++ = 'n';
              break;
            case '\t':
              *d++ = 't';
              break;
            case '\r':
              *d++ = 'r';
              break;
            case '\b':
              *d++ = 'b';
              break;

            default:
              *d++ = 'u';
              *d++ = '0';
              *d++ = '0';
              *d++ = "0123456789abcdef"[(*str >> 4) & 0xf];
              *d++ = "0123456789abcdef"[(*str     ) & 0xf];
              break;

          }
          *d = '\0';
          luaL_addstring(&b, value);
        } else if (*str == '"') {
          luaL_addstring(&b, "\\\"");
        } else {
          luaL_addchar(&b, *str);
        }
        str++;
        len--;
      }
      luaL_addchar(&b, '"');
      break;
    }
  }

  luaL_addstring(&b, suffix);
  luaL_pushresult(&b);
}

static int sjson_encoder_next_value_is_table(lua_State *L) {
  int count = 10;

  while ((lua_isfunction(L, -1)
    ) && count-- > 0) {
    // call it and use the return value
    lua_call(L, 0, 1);          // Expecting replacement value
  }

  return (lua_type(L, -1) == LUA_TTABLE);
}

static void sjson_encoder_make_next_chunk(lua_State *L, ENC_DATA *data) {
  if (data->level < 0) {
    return;
  }

  luaL_Buffer b;
  luaL_buffinit(L, &b);

  // Ending condition
  while (data->level >= 0 /* && !b.lvl */) {
    ENC_DATA_STATE *state = &data->stack[data->level];

    int finished = 0;

    if (state->size >= 0) {
      if (state->offset == 0) {
        // start of object or whatever
        luaL_addchar(&b, '[');
      }
      if (state->offset == state->size << 1) {
        luaL_addchar(&b, ']');
        finished = 1;
      } else if ((state->offset & 1) == 0) {
        if (state->offset > 0) {
          luaL_addchar(&b, ',');
        }
      } else {
        // output the value
        lua_rawgeti(L, LUA_REGISTRYINDEX, state->lua_object_ref);
        lua_rawgeti(L, -1, (state->offset >> 1) + 1);
        if (sjson_encoder_next_value_is_table(L)) {
          enc_push_stack(L, data, -1);
          lua_pop(L, 2);
          state->offset++;
          continue;
        }
        encode_lua_object(L, data, -1, "", "");
        lua_remove(L, -2);
        lua_remove(L, -2);
        luaL_addvalue(&b);
      }

      state->offset++;
    } else {
      lua_rawgeti(L, LUA_REGISTRYINDEX, state->lua_object_ref);
      // stack now contains: -1 => table
      lua_rawgeti(L, LUA_REGISTRYINDEX, state->lua_key_ref);
      // stack now contains: -1 => nil or key; -2 => table

      if (lua_next(L, -2)) {
        // save the key
        if (state->offset & 1) {
          luaL_unref(L, LUA_REGISTRYINDEX, state->lua_key_ref);
          state->lua_key_ref = LUA_NOREF;
          // Duplicate the key
          lua_pushvalue(L, -2);
          state->lua_key_ref = luaL_ref(L, LUA_REGISTRYINDEX);
        }

        if ((state->offset & 1) == 0) {
          // copy the key so that lua_tostring does not modify the original
          lua_pushvalue(L, -2);
          // stack now contains: -1 => key; -2 => value; -3 => key; -4 => table
          // key
          lua_tostring(L, -1);
          encode_lua_object(L, data, -1, state->offset ? "," : "{", ":");
          lua_remove(L, -2);
          lua_remove(L, -2);
          lua_remove(L, -2);
          lua_remove(L, -2);
        } else {
          if (sjson_encoder_next_value_is_table(L)) {
            enc_push_stack(L, data, -1);
            lua_pop(L, 3);
            state->offset++;
            continue;
          }
          encode_lua_object(L, data, -1, "", "");
          lua_remove(L, -2);
          lua_remove(L, -2);
          lua_remove(L, -2);
        }
        luaL_addvalue(&b);
      } else {
        lua_pop(L, 1);
        // We have got to the end
        luaL_addchar(&b, '}');
        finished = 1;
      }

      state->offset++;
    }

    if (finished) {
      enc_pop_stack(L, data);
    }
  }
  luaL_pushresult(&b);
  data->current_str_ref = luaL_ref(L, LUA_REGISTRYINDEX);
  data->offset = 0;
}

static int sjson_encoder_read_int(lua_State *L, ENC_DATA *data, int readsize) {
  luaL_Buffer b;
  luaL_buffinit(L, &b);

  size_t len;

  do {
    // Fill the buffer with (up to) readsize characters
    if (data->current_str_ref != LUA_NOREF) {
      // this is not allowed
      lua_rawgeti(L, LUA_REGISTRYINDEX, data->current_str_ref);
      const char *str = lua_tolstring(L, -1, &len);

      lua_pop(L, 1); // Note that we still have the string referenced so it can't go away

      int amnt = len - data->offset;;
      if (amnt > readsize) {
        amnt = readsize;
      }
      luaL_addlstring(&b, str + data->offset, amnt);
      data->offset += amnt;
      readsize -= amnt;

      if (data->offset == len) {
        luaL_unref(L, LUA_REGISTRYINDEX, data->current_str_ref);
        data->current_str_ref = LUA_NOREF;
      }
    }

    if (readsize > 0) {
      // Make the next chunk
      sjson_encoder_make_next_chunk(L, data);
    }

  } while (readsize > 0 && data->current_str_ref != LUA_NOREF);

  luaL_pushresult(&b);

  lua_tolstring(L, -1, &len);

  if (len == 0) {
    // we have got to the end
    lua_pop(L, 1);
    return 0;
  }

  return 1;
}

static int sjson_encoder_read(lua_State *L) {
  ENC_DATA *data = (ENC_DATA *)luaL_checkudata(L, 1, "sjson.encoder");

  int readsize = 1024;
  if (lua_type(L, 2) == LUA_TNUMBER) {
    readsize = lua_tointeger(L, 2);
    if (readsize < 1) {
      readsize = 1;
    }
  }

  return sjson_encoder_read_int(L, data, readsize);
}

static int sjson_encode(lua_State *L) {
  sjson_encoder(L);

  ENC_DATA *data = (ENC_DATA *)luaL_checkudata(L, -1, "sjson.encoder");

  int rc = sjson_encoder_read_int(L, data, 1000000);

  lua_remove(L, -(rc + 1));

  return rc;
}

static int sjson_encoder_destructor(lua_State *L) {
  ENC_DATA *data = (ENC_DATA *)luaL_checkudata(L, 1, "sjson.encoder");

  int i;

  for (i = 0; i < data->nlevels; i++) {
    luaL_unref(L, LUA_REGISTRYINDEX, data->stack[i].lua_object_ref);
    luaL_unref(L, LUA_REGISTRYINDEX, data->stack[i].lua_key_ref);
  }

  luaL_unref(L, LUA_REGISTRYINDEX, data->null_ref);
  luaL_unref(L, LUA_REGISTRYINDEX, data->current_str_ref);

  DBG_PRINTF("Destructor called\n");

  return 0;
}


LROT_BEGIN(sjson_encoder_map, NULL, LROT_MASK_GC_INDEX)
  LROT_FUNCENTRY( __gc, sjson_encoder_destructor )
  LROT_TABENTRY(  __index, sjson_encoder_map )
  LROT_FUNCENTRY( read, sjson_encoder_read )
LROT_END(sjson_encoder_map, NULL, LROT_MASK_GC_INDEX)



LROT_BEGIN(sjson_decoder_map, NULL, LROT_MASK_GC_INDEX)
  LROT_FUNCENTRY( __gc, sjson_decoder_destructor )
  LROT_TABENTRY(  __index, sjson_decoder_map )
  LROT_FUNCENTRY( write, sjson_decoder_write )
  LROT_FUNCENTRY( result, sjson_decoder_result )
LROT_END(sjson_decoder_map, NULL, LROT_MASK_GC_INDEX)


LROT_BEGIN(sjson, NULL, 0)
  LROT_FUNCENTRY( encode, sjson_encode )
  LROT_FUNCENTRY( decode, sjson_decode )
  LROT_FUNCENTRY( encoder, sjson_encoder )
  LROT_FUNCENTRY( decoder, sjson_decoder )
LROT_END(sjson, NULL, 0)

LUALIB_API int luaopen_sjson (lua_State *L) {
  luaL_rometatable(L, "sjson.decoder", LROT_TABLEREF(sjson_decoder_map));
  luaL_rometatable(L, "sjson.encoder", LROT_TABLEREF(sjson_encoder_map));
  return 1;
}

NODEMCU_MODULE(SJSON, "sjson", sjson, luaopen_sjson);