--[[----------------------------------------------------------------------------

json.lua
Json encode/decode implemented entirely in Lua.

--------------------------------------------------------------------------------

ADOBE SYSTEMS INCORPORATED
 Copyright 2007 Adobe Systems Incorporated
 All Rights Reserved.

NOTICE: Adobe permits you to use, modify, and distribute this file in accordance
with the terms of the Adobe license agreement accompanying it. If you have received
this file from a source other than Adobe, then your use, modification, or distribution
of it requires the prior written permission of Adobe.

------------------------------------------------------------------------------]]

local JSON = {
   VERSION = 20100507.1
}

local string_byte = string.byte
local table_insert = table.insert
local string_format = string.format
local string_gfind = string.gfind
local table_concat = table.concat

--
-- This provides simple JSON encoding and decoding in pure Lua.
--
--   JSON = (loadfile "JSON.lua")() -- load the routines
--   local val = JSON.decode(raw_json_text)
--   local raw_json_text = JSON.encode(lua_table_or_value)
--
--
-- If the JSON text is for an object or an array, e.g.
--   { "what": "books", "count": 3 }
-- or
--   [ "Larry", "Curly", "Moe" ]
--
-- the result is a Lua table, e.g.
--   { what = "books", count = 3 }
-- or
--   { "Larry", "Curly", "Moe" }
--
--
-- On error during decodeing, the code calls
--
--    JSON.onError(message, [text, [location]])
--
-- with a message about the error, and if known, the JSON text being parsed
-- and the byte count where the problem was discovered. You can replace the
-- default JSON.onError() with your own function. The default merely aguments
-- the message with data about the text and the location, if known, and then
-- calls JSON.assert(), which itself defaults to Lua's built-in assert(.
--
--

---------------------------------------------------------------------------

--
-- http://www.json.org/
--
local function unicode_codepoint_as_utf8(codepoint)
   --
   -- codepoint is a number
   --
   if codepoint <= 127 then
      return string.char(codepoint)

   elseif codepoint <= 2047 then
      --
      -- 110yyyxx 10xxxxxx         <-- useful notation from http://en.wikipedia.org/wiki/Utf8
      --
      local highpart = math.floor(codepoint / 0x40)
      local lowpart  = codepoint - (0x40 * highpart)
      return string.char(0xC0 + highpart,
                         0x80 + lowpart)

   elseif codepoint <= 65535 then
      --
      -- 1110yyyy 10yyyyxx 10xxxxxx
      --
      local highpart  = math.floor(codepoint / 0x1000)
      local remainder = codepoint - 0x1000 * highpart
      local midpart   = math.floor(remainder / 0x40)
      local lowpart   = remainder - 0x40 * midpart

      highpart = 0xE0 + highpart
      midpart = 0x80 + midpart
      lowpart = 0x80 + lowpart
        
      -- [ arahn ] Replace invalid chars with placeholder
      if
            ( highpart == 0xE0 and midpart < 0xA0 ) or
            ( highpart == 0xED and midpart > 0x9F ) or
            ( highpart == 0xF0 and midpart < 0x90 ) or
            ( highpart == 0xF4 and midpart > 0x8F )    then
        
         return "?"
            
      end
        
      return string.char( highpart,
                          midpart,
                          lowpart)

   else
      -- Not actually used for JSON, but here for completeness.
      --
      -- 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx
      --
      local highpart  = math.floor(codepoint / 0x40000)
      local remainder = codepoint - 0x40000 * highpart
      local midA      = math.floor(remainder / 0x1000)
      remainder       = remainder - 0x1000 * midA
      local midB      = math.floor(remainder / 0x40)
      local lowpart   = remainder - 0x40 * midB

      return string.char(0xF0 + highpart,
                         0x80 + midA,
                         0x80 + midB,
                         0x80 + lowpart)
   end
end


function JSON.onError(message, text, location)
   local assert = JSON.assert or assert

   if text then
      if location then
         message = string.format("%s at char %d of: %s", message, location, text)
      else
         message = string.format("%s: %s", message, text)
      end
   end
   assert(false, message)
end

local function grok_number(text, start)
   --
   -- Grab the integer part
   --
   local integer_part = text:match('^-?[1-9]%d*', start)
                     or text:match("^-?0",        start)

   if not integer_part then
      JSON.onError("expected number", text, start)
   end

   local i = start + integer_part:len()

   --
   -- Grab an optional decimal part
   --
   local decimal_part = text:match('^%.%d+', i) or ""

   i = i + decimal_part:len()

   --
   -- Grab an optional exponential part
   --
   local exponent_part = text:match('^[eE][-+]?%d+', i) or ""

   i = i + exponent_part:len()

   local full_number_text = integer_part .. decimal_part .. exponent_part
   local as_number = tonumber(full_number_text)

   if not as_number then
      JSON.onError("bad number", text, start)
   end

   return as_number, i
end

local function grok_string(text, start)
   if text:sub(start,start) ~= '"' then --" calm down IDE text coloring
      JSON.onError("expected string's opening quote", text, start)
   end

   local i = start + 1 -- +1 to bypass the initial quote
   local text_len = text:len()
   local VALUE = ""
   while i <= text_len do
      local c = text:sub(i,i)
      if c == '"' then --" calm down IDE text coloring
         return VALUE, i + 1
      end
      if c ~= '\\' then
         VALUE = VALUE .. c
         i = i + 1
      elseif text:match('^\\b', i) then
         VALUE = VALUE .. "\b"
         i = i + 2
      elseif text:match('^\\f', i) then
         VALUE = VALUE .. "\f"
         i = i + 2
      elseif text:match('^\\n', i) then
         VALUE = VALUE .. "\n"
         i = i + 2
      elseif text:match('^\\r', i) then
         VALUE = VALUE .. "\r"
         i = i + 2
      elseif text:match('^\\t', i) then
         VALUE = VALUE .. "\t"
         i = i + 2
      elseif text:match('^\\u', i) then
         local hex = text:match('^\\u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i)
         VALUE = VALUE .. unicode_codepoint_as_utf8(tonumber(hex, 16))
         i = i + 6
      else
         -- just pass through what's escaped
         VALUE = VALUE .. text:match('^\\(.)', i)
         i = i + 2
      end
   end

   JSON.onError("unclosed string", text, start)
end

local function skip_whitespace(text, start)
   local _, match_end = text:find("^%s+", start)
   if match_end then
      return match_end + 1
   else
      return start
   end
end

local grok_one -- assigned later

local function grok_object(text, start)
   if not (text:sub(start,start) == '{') then
      JSON.onError("expected '{'", text, start)
   end

   local i = skip_whitespace(text, start + 1) -- +1 to skip the '{'

   local VALUE = { }
   if text:sub(i,i) == '}' then
      return VALUE, i + 1
   end
   local text_len = text:len()
   while i <= text_len do
      local key, new_i = grok_string(text, i)

      i = skip_whitespace(text, new_i)

      if text:sub(i, i) ~= ':' then
         JSON.onError("expected colon", text, i)
      end

      i = skip_whitespace(text, i + 1)

      local val
      val, new_i = grok_one(text, i)

      VALUE[key] = val

      --
      -- Expect now either '}' to end things, or a ',' to allow us to continue.
      --
      i = skip_whitespace(text, new_i)

      local c = text:sub(i,i)

      if c == '}' then
         return VALUE, i + 1
      end

      if text:sub(i, i) ~= ',' then
         JSON.onError("expected comma or '}'", text, i)
      end

      i = skip_whitespace(text, i + 1)
   end

   JSON.onError("unclosed '{'", text, start)
end

local function grok_array(text, start)
   if not (text:sub(start,start) == '[') then
      JSON.onError("expected '['", text, start)
   end

   local i = skip_whitespace(text, start + 1) -- +1 to skip the '['
   local VALUE = { }
   if text:sub(i,i) == ']' then
      return VALUE, i + 1
   end

   local text_len = text:len()
   while i <= text_len do
      local val, new_i = grok_one(text, i)

      table.insert(VALUE, val)

      i = skip_whitespace(text, new_i)

      --
      -- Expect now either ']' to end things, or a ',' to allow us to continue.
      --
      local c = text:sub(i,i)
      if c == ']' then
         return VALUE, i + 1
      end
      if text:sub(i, i) ~= ',' then
         JSON.onError("expected comma or '['", text, i)
      end
      i = skip_whitespace(text, i + 1)
   end
   JSON.onError("unclosed '['", text, start)
end


grok_one = function(text, start)
   -- Skip any whitespace
   start = skip_whitespace(text, start)

   if start > text:len() then
      JSON.onError("unexpected end of string", text)
   end

   if text:find('^"', start) then --" calm down IDE text coloring
      return grok_string(text, start)
   elseif text:find('^[-0123456789 ]', start) then
      return grok_number(text, start)
   elseif text:find('^%{', start) then
      return grok_object(text, start)
   elseif text:find('^%[', start) then
      return grok_array(text, start)
   elseif text:find('^true', start) then
      return true, start + 4
   elseif text:find('^false', start) then
      return false, start + 5
   elseif text:find('^null', start) then
      return nil, start + 4
   else
      JSON.onError("can't parse JSON", text, start)
   end
end

function JSON.decode(text)
   if type(text) ~= 'string' then
      JSON.onError("expected string argument to JSON.decode()")
   end

   if text:match('^%s*$') then
      return nil
   end

   --
   -- Ensure that it's not UTF-32 or UTF-16
   -- Those are perfectly valid encodings for JSON (as per RFC 4627 section 3),
   -- but this package can't handle them.
   --
   if text:sub(1,1):byte() == 0 or (text:len() >= 2 and text:sub(2,2):byte() == 0) then
      JSON.onError("JSON package groks only UTF-8, sorry", text)
   end

   local success, value = pcall(grok_one, text, 1)
   if success then
      return value
   else
      assert(false, value)
      return nil
   end
end

--------------------------------------------------------------------------------
-- SPAM [arahn 2012-02-01]: Copied from AgStringUtils.lua

--[[
    Create a function that will process UTF8 bytes and escape multi
    byte characters using your provided escape function.
    
    This is the minimal implementation of a routine that would consume the
    stateMachine function.  multiByteFunc is some function that will convert
    a integer value for a unicode character into a string representation.
    
    This function handles lower ascii characters itself (so you couldn't use
    this to create a UTF-8 to UTF-16 translator).
    
    local result = {}
    local mode = 1
    local temp = 0
    local process = AgStringUtils.makeUTF8Processor( multiByteFunc )

    for x in string_gfind( str, '.' ) do

        local b =  string_byte( x )
        mode, temp = process( result, x, mode, temp )
 
    end
    
    result = table_concat( result, "" )
    
     Given a single byte from a UTF8 string and a mode flag describing what
    we saw in the previous byte, return a string fragment describing that
    character in XML. This will either be the byte itself (if it's 7-bit
    ASCII) or a numeric XML entity reference for other Unicode characters.
]]--


local function makeUTF8Processor( multiByteFunc )

    return function( result, x, mode, temp )
    
        local b =  string_byte( x )
    
        if mode == 1 then    -- first byte of a UTF-8 character sequence
    
            --if b < 32 then
            --    temp = string_format( "&#x%4X;", b )
            -- else ...
            if b <= 127 then
                table_insert( result, x )
            elseif b >= 192 and b <= 223 then
                mode = 2
                temp = ( b - 192 ) * 64
            elseif b >= 224 and b <= 239 then
                mode = 3
                temp = ( b - 224 ) * 4096
            else
                -- skip corrupt byte
            end
    
        elseif mode == 2 then    -- second byte of a UTF-8 character sequence
    
            if b >= 128 then
                temp = temp + ( b - 128 )
            else
                -- skip corrupt byte
            end
            mode = 1
            temp = multiByteFunc( temp )
            table_insert( result, temp )
            temp = 0
    
        elseif mode == 3 then    -- third byte of a UTF-8 character sequence
    
            if b >= 128 then
                temp = temp + ( b - 128 ) * 64
            else
                -- skip corrupt byte
            end
            mode = 2
    
        end
    
        return mode, temp
    
    end
    
end

--------------------------------------------------------------------------------
--[[
    Escape UTF-8 text for JSON.
]]--

local processUTF8Byte = makeUTF8Processor(
    function( multiByte ) return string_format( "{\\u%0.3d}", multiByte ) end ) -- private

local kSpecialChars = {
    [ '\\' ] = '\\\\',
    [ '\n' ] = '\\n',
    [ '"' ] = '\\"',
--    [ '&' ] = '\\&',
    [ '\r' ] = '\\r',
    [ '\t' ] = '\\t',
    [ '\b' ] = '\\b',
    [ '\f' ] = '\\f',
    [ '\0' ] = '\\u000',
}

local function escapeUTF8ForJSON( str )

    local result = {}
    local mode = 1
    local temp = 0

    for x in string_gfind( str, '.' ) do

        if mode == 1 and kSpecialChars[ x ] then
            table_insert( result, kSpecialChars[ x ] )
        else
            mode, temp = processUTF8Byte( result, x, mode, temp )
        end
 
    end
    
    return table_concat( result, "" )

end

--------------------------------------------------------------------------------

function JSON.encode(value)
   if type(value) == 'string' then
      local json_string_guts = escapeUTF8ForJSON( value )
      return '"' .. json_string_guts .. '"'

   elseif type(value) == 'number' then
      return tostring(value)

   elseif type(value) == 'boolean' then
      return tostring(value)

   elseif type(value) == 'nil' then
      return 'null'

   elseif type(value) == 'table' then
      local T = value
      -- could be an object or an array
      if T[1] then
         -- an array
         local ITEMS = { }
         for _, val in ipairs(T) do
            table.insert(ITEMS, JSON.encode(val))
         end
         return "[" .. table.concat(ITEMS, ",") .. "]"
      else
         -- an object
         local PARTS = { }
         for key, val in pairs(T) do
            table.insert(PARTS, string.format("%s:%s",
                                              JSON.encode(tostring(key)),
                                             JSON.encode(val)))
         end
         return "{" .. table.concat(PARTS, ",") .. "}"
      end
   else
      assert(false, "can't convert to JSON: " .. type(value))
   end
end

return JSON
