/*
 * json: Reading and Writing JSON
 */

#include "gap_all.h" // GAP headers

#include "gap-functions.h"

#include "picojson/picojson.h"
#include "picojson/gap-traits.h"

static Obj _GapToJsonStreamInternal;

typedef picojson::value_t<gap_type_traits> gmp_value;

Obj JsonToGap(const gmp_value& v)
{
    if (v.is<picojson::null>()) {
      return Fail;
    } else if (v.is<bool>()) {
        if(v.get<bool>())
            return True;
        else
            return False;
    } else if (v.is<gap_val>()) {
        return v.get<gap_val>().obj;
    } else if (v.is<std::string>()) {
        Obj str;
        const char* c_str = v.get<std::string>().c_str();
        Int len = v.get<std::string>().size();
        str = NEW_STRING(len);
        memcpy(CHARS_STRING(str), c_str, len);
        return str;
    } else if (v.is<gmp_value::array>()) {
        const gmp_value::array& a = v.get<gmp_value::array>();
        Obj list = NEW_PLIST(T_PLIST_DENSE, a.size());
        SET_LEN_PLIST(list, a.size());
        for(int i = 1; i <= a.size(); ++i)
        {
            Obj val = JsonToGap(a[i-1]);
            SET_ELM_PLIST(list, i, val);
            CHANGED_BAG(list);
        }
        return list;
    } else if (v.is<gmp_value::object>()) {
        const gmp_value::object& o = v.get<gmp_value::object>();
        Obj rec = NEW_PREC(0);
        for (gmp_value::object::const_iterator i = o.begin(); i != o.end(); ++i) {
            Obj res = JsonToGap(i->second);
            AssPRec(rec, RNamName(i->first.c_str()), res);
            CHANGED_BAG(rec);
        }
        return rec;
    }
    return Fail;
}

// Add function prototype missing from string.h
extern Obj CopyToStringRep(Obj string);

static Int numberOfBytes(UInt c)
{
    if (c < 128)
        return 1;
    if (c < 224)
        return 2;
    if (c < 240)
        return 3;
    return 4;
}

static Int getChar(Obj list, Int pos)
{
    Obj c = ELM_LIST(list, pos);
    if (c == NULL)
        return 0;
    else
        return *((UChar *)ADDR_OBJ(c));
}

static Int getUTF8Char(Obj list, Int * basepos)
{
    Int  pos = *basepos;
    UInt val = getChar(list, pos);
    UInt singlebyte_val = val;
    Int  len = numberOfBytes(val);
    pos++;

    if (len == 1) {
        *basepos = pos;
        return val;
    }

    switch (len) {
    case 2:
        val = val & 0x3F;
        if (val & 0x20)
            goto invalid;
        break;
    case 3:
        val = val & 0x1F;
        if (val & 0x10)
            goto invalid;
        break;
    case 4:
        val = val & 0x0F;
        if (val & 0x08)
            goto invalid;
        break;
    default:
        abort();
    }
    val = val & 0x3F;

    for (Int i = 1; i < len; ++i) {
        UInt c = getChar(list, pos);
        if ((c & 0xC0) != 0x80)
            goto invalid;
        val = (val << 6) | (c & 0x3F);
        pos++;
    }

    // Too high
    if (val > 0x10ffff)
        goto invalid;

    // UTF-16 Surrogate pair
    if (val >= 0xd800 && val <= 0xdfff)
        goto invalid;

    *basepos = pos;
    return val;


// Hope this is Latin-1
invalid:
    *basepos = *basepos + 1;
    return singlebyte_val;
}

static UChar * outputUnicodeChar(UChar * s, UInt val)
{
    if (val <= 0x7f)
        *(s++) = val;
    else if (val <= 0x7ff) {
        *(s++) = (0xc0 | ((val >> 6) & 0x1f));
        *(s++) = (0x80 | (val & 0x3f));
    }
    else if (val <= 0xffff) {
        *(s++) = (0xe0 | ((val >> 12) & 0x0f));
        *(s++) = (0x80 | ((val >> 6) & 0x3f));
        *(s++) = (0x80 | (val & 0x3f));
    }
    else {
        *(s++) = (0xf0 | ((val >> 18) & 0x07));
        *(s++) = (0x80 | ((val >> 12) & 0x3f));
        *(s++) = (0x80 | ((val >> 6) & 0x3f));
        *(s++) = (0x80 | (val & 0x3f));
    }
    return s;
}

static Obj FuncJSON_ESCAPE_STRING(Obj self, Obj param)
{
    if(!IS_STRING(param))
    {
        ErrorQuit("Input to JsonEscapeString must be a string", 0, 0);
    }

    Int needEscaping = 0;
    Int lenString = LEN_LIST(param);
    for (Int i = 1; i <= lenString && needEscaping == 0; ++i) {
        Obj gapchar = ELMW_LIST(param, i);
        UChar u = *((UChar*)ADDR_OBJ(gapchar));
        switch(u)
        {
            case '\\': case '"': case '/': case '\b':
            case '\t': case '\n': case '\f': case '\r':
                needEscaping = 1;
                break;
            default:
                if (u < ' ' || u >= 128)
                    needEscaping = 1;
        }
    }

    if (needEscaping == 0)
        return param;

    // Massively over-long string
    Obj     copy = NEW_STRING(lenString * 6 + 7);
    UChar * base = CHARS_STRING(copy);
    UChar * out = base;
    Int     i = 1;
    while (i <= lenString) {
        Int u = getUTF8Char(param, &i);
        switch(u)
        {
            case '\\': case '"': case '/':
                out[0] = '\\';
                out[1] = u;
                out += 2;
                break;
#define ESCAPE_CASE(x,y) case x: out[0] = '\\'; out[1] = y; out += 2; break;
            ESCAPE_CASE('\b', 'b');
            ESCAPE_CASE('\t', 't');
            ESCAPE_CASE('\n', 'n');
            ESCAPE_CASE('\f', 'f');
            ESCAPE_CASE('\r', 'r');
#undef ESCAPE_CASE
            default:
                if(u < ' ')
                {
                    snprintf((char*)out,7, "\\u%04X",(unsigned)u);
                    out += 6;
                }
                else
                {
                    out = outputUnicodeChar(out, u);
                }
        }
    }

    SET_LEN_STRING(copy, out - base);
    ResizeBag(copy, SIZEBAG_STRINGLEN(out - base));
    return copy;
}

static Obj FuncGAP_LIST_TO_JSON_STRING(Obj self, Obj string, Obj stream, Obj list) {
    RequireDenseList("list", list);
    Int len = LEN_LIST(list);
    char buf[50] = {};

    // Call this at the start
    ConvString(string);
    AppendCStr(string, "[", 1);
    for(int i = 1; i <= len; ++i) {
        if(i != 1) {
            AppendCStr(string, ",", 1);
        }
        Obj val = ELM_LIST(list, i);
        if(IS_INTOBJ(val)) {
            snprintf(buf, sizeof(buf), "%ld", INT_INTOBJ(val));
            AppendCStr(string, buf, strlen(buf));
        } else if(IS_LIST(val) && !(IS_STRING(val))) {
            FuncGAP_LIST_TO_JSON_STRING(self, string, stream, val);
        } else {
            CALL_2ARGS(_GapToJsonStreamInternal, stream, val);
            // Convert back in case this call modified the string
            ConvString(string);
        }
    }
    AppendCStr(string, "]", 1);

    return 0;
}

// WARNING: This class is only complete enough to work with
// picojson's iterator support.
struct GapStreamToInputIterator
{
  Obj stream;
  enum State {
    notread, failed, cached
  };
  State state;
  char store;
  
  GapStreamToInputIterator()
  : stream(0), state(notread), store(0)
  { }
  
  GapStreamToInputIterator(Obj s)
  : stream(s), state(notread), store(0)
  { }
  
  GapStreamToInputIterator(const GapStreamToInputIterator& gstii)
  : stream(gstii.stream), state(gstii.state), store(gstii.store)
  { } 

  char operator*()
  {
    if(state == cached)
      return store;
    
    if(state == failed)
      return 0;

    Obj val = callGAPFunction(ReadByteFunction, stream);
    if(val == Fail)
    {
      state = failed;
      return 0;
    }
    else
    {
      state = cached;
      store = INT_INTOBJ(val);
      return store;
    }
  }
  
  void operator++()
  {
    if(state == failed)
      return;
    
    // skip character
    if(state == notread)
    {
      *(*this);
    }
    
    state = notread;
  }
  
  friend bool operator==(GapStreamToInputIterator& lhs, GapStreamToInputIterator& rhs)
  { return (lhs.state == failed) == (rhs.state == failed); }
  
};


static GapStreamToInputIterator endGapStreamIterator()
{
  GapStreamToInputIterator g;
  g.state = GapStreamToInputIterator::failed;
  return g;
}

// making an object of this type will ensure when the scope is exited
// we clean up any GAP objects we cached
struct CleanupCacheGuard
{
  ~CleanupCacheGuard()
    { callGAPFunction(ClearGAPCacheFunction); }
};

static Obj FuncJSON_STREAM_TO_GAP(Obj self, Obj stream)
{
  JSON_setupGAPFunctions();
  CleanupCacheGuard ccg;
  
  typedef picojson::value_t<gap_type_traits> gmp_value;
  
  gmp_value v;
  
  std::string err;
  bool ungotc_check = false;
  picojson::parse(v, GapStreamToInputIterator(stream), endGapStreamIterator(), &err, &ungotc_check);
  if (! err.empty()) {
    ErrorQuit(err.c_str(), 0, 0);
    return Fail;
  }

  return JsonToGap(v);
}

// WARNING: This class is only complete enough to work with
// picojson's iterator support.
struct GapStringToInputIterator {
    Obj    obj;
    size_t pos;

    GapStringToInputIterator() : obj(0), pos(0)
    {
    }

    GapStringToInputIterator(Obj s, size_t startpos = 0)
        : obj(s), pos(startpos)
    {
    }

    GapStringToInputIterator(const GapStringToInputIterator & gstii)
        : obj(gstii.obj), pos(gstii.pos)
    {
    }

    char operator*()
    {
        return CSTR_STRING(obj)[pos];
    }

    void operator++()
    {
        pos++;
    }

    friend bool operator==(GapStringToInputIterator & lhs,
                           GapStringToInputIterator & rhs)
    {
        return (lhs.pos == rhs.pos);
    }
};


static GapStringToInputIterator endGapStringIterator(Obj obj)
{
    GapStringToInputIterator g(obj, GET_LEN_STRING(obj));
    return g;
}

static Obj FuncJSON_STRING_TO_GAP(Obj self, Obj param)
{
    JSON_setupGAPFunctions();
    CleanupCacheGuard ccg;

    if(!IS_STRING(param))
    {
        ErrorQuit("Input to JsonToGap must be a string", 0, 0);
    }
    
    Obj real_string = param;
    
    if(!IS_STRING_REP(param))
    {
        real_string = CopyToStringRep(param);
    }
    
    
    typedef picojson::value_t<gap_type_traits> gmp_value;
    
    gmp_value v;
    
    std::string err;
    bool ungotc_check = false;
    GapStringToInputIterator endparse = picojson::parse(
        v, GapStringToInputIterator(real_string),
        endGapStringIterator(real_string), &err, &ungotc_check);

    //    char* res = picojson::parse(v, ptr, ptrend, &err, &ungotc_check);
    if (! err.empty()) {
      ErrorQuit(err.c_str(), 0, 0);
      return Fail;
    }

    // Check end of string
    const char * ptr = CSTR_STRING(real_string);
    const char * ptrend = ptr + strlen(ptr);

    // Extra position in the string
    const char * res = ptr + endparse.pos;

    // Woo, this is horrible. The parser steps one character too far
    // if the only thing parsed is a number. So step back.
    if(ungotc_check) {
      res--;
    }

    for(; res != ptrend; res++)
    {
      if(!(isspace(*res)) && *res)
      {
        ErrorMayQuit("Failed to parse end of string: '%s'",(Int)res,0);
        return Fail;
      }
    }
    
    return JsonToGap(v);
}

// Table of functions to export
static StructGVarFunc GVarFuncs [] = {
    GVAR_FUNC_1ARGS(JSON_STRING_TO_GAP, string),
    GVAR_FUNC_1ARGS(JSON_ESCAPE_STRING, string),
    GVAR_FUNC_1ARGS(JSON_STREAM_TO_GAP, string),
    GVAR_FUNC_3ARGS(GAP_LIST_TO_JSON_STRING, string, stream, list),
    
	{ 0 } /* Finish with an empty entry */

};

/******************************************************************************
*F  InitKernel( <module> )  . . . . . . . . initialise kernel data structures
*/
static Int InitKernel( StructInitInfo *module )
{
    /* init filters and functions                                          */
    InitHdlrFuncsFromTable( GVarFuncs );

    ImportGVarFromLibrary("_GapToJsonStreamInternal", &_GapToJsonStreamInternal);

    /* return success                                                      */
    return 0;
}

/******************************************************************************
*F  InitLibrary( <module> ) . . . . . . .  initialise library data structures
*/
static Int InitLibrary( StructInitInfo *module )
{
    /* init filters and functions */
    InitGVarFuncsFromTable( GVarFuncs );
    
    /* return success                                                      */
    return 0;
}

/******************************************************************************
*F  InitInfopl()  . . . . . . . . . . . . . . . . . table of init functions
*/
static StructInitInfo module = {
 /* type        = */ MODULE_DYNAMIC,
 /* name        = */ "json",
 /* revision_c  = */ 0,
 /* revision_h  = */ 0,
 /* version     = */ 0,
 /* crc         = */ 0,
 /* initKernel  = */ InitKernel,
 /* initLibrary = */ InitLibrary,
 /* checkInit   = */ 0,
 /* preSave     = */ 0,
 /* postSave    = */ 0,
 /* postRestore = */ 0
};

extern "C"
StructInitInfo * Init__Dynamic ( void )
{
  return &module;
}
