-----------------------------------------------------------------------------
-- JSON4Lua: JSON encoding / decoding support for the Lua language.
-- json Module.
-- Author: Craig Mason-Jones
-- Homepage: http://json.luaforge.net/
-- Version: 0.9.50
-- This module is released under the MIT License (MIT).
-- Please see LICENCE.txt for details.
--
-- USAGE:
-- This module exposes two functions:
--   encode(o)
--     Returns the table / string / boolean / number / nil / json.null value as a JSON-encoded string.
--   decode(json_string)
--     Returns a Lua object populated with the data encoded in the JSON string json_string.
--
-- REQUIREMENTS:
--   compat-5.1 if using Lua 5.0
--
-- CHANGELOG
-- 			en inConcert agregamos parametro 'pretty' al encode()
--          en inConcert agregamos que en el encode de los strings escapee los codepoints utf-8
--          en inConcert agregamos el manejo de secuencias unicode en los strings (ej. \uxxxx)
--          en inConcert agregamos el soporte para sandboxing (_G es limitado)
--          en inConcert agregamos la funcion append
--	 0.9.50 Radical performance improvement on decode from Eike Decker. Many thanks!
--	 0.9.40 Changed licence to MIT License (MIT)
--   0.9.20 Introduction of local Lua functions for private functions (removed _ function prefix). 
--          Fixed Lua 5.1 compatibility issues.
--   		Introduced json.null to have null values in associative arrays.
--          encode() performance improvement (more than 50%) through table.concat rather than ..
--          Introduced decode ability to ignore /**/ comments in the JSON string.
--   0.9.10 Fix to array encoding / decoding to correctly manage nil/null values in arrays.
-----------------------------------------------------------------------------

-----------------------------------------------------------------------------
-- Imports and dependencies
-----------------------------------------------------------------------------
local math = require('math')
local string = require("string")
local table = require("table")
local lpeg = require("lpeg")

local assert, type, tostring, tonumber, pairs = assert, type, tostring, tonumber, pairs
local getmetatable, loadstring = getmetatable, loadstring
local error = error

local print = print

-----------------------------------------------------------------------------
-- Module declaration
-----------------------------------------------------------------------------
module("json")

-- Public functions

-- Private functions
local decode_scanArray
local decode_scanComment
local decode_scanConstant
local decode_scanNumber
local decode_scanObject
local decode_scanString
local decode_scanWhitespace
local encodeString
local isArray
local isEncodable

-----------------------------------------------------------------------------
-- PUBLIC FUNCTIONS
-----------------------------------------------------------------------------
--- Encodes an arbitrary Lua object / variable.
-- @param v The Lua object / variable to be JSON encoded.
-- @return String containing the JSON encoding in internal Lua string format (i.e. not unicode)
function encode (v, pretty, level)
  -- Handle nil values
  if v==nil then
    return "null"
  end
  level = level or 0
  
  local vtype = type(v)  

  -- Handle strings
  if vtype=='string' then    
    return '"' .. encodeString(v) .. '"'	    -- Need to handle encoding in string
  end
  
  -- Handle booleans
  if vtype=='number' or vtype=='boolean' then
    return tostring(v)
  end
  
  local linePrefix = pretty and string.rep("\t",level) or ''
  local linePrefixPlus = pretty and linePrefix.."\t" or ''

  -- Handle tables
  if vtype=='table' then
    local mt = getmetatable(v)
    if mt and type(mt.__toJson) == 'function' then
        return string.gsub(string.gsub(mt.__toJson(v), "^", linePrefix), "\n", "\n"..linePrefix) 
    end
    local rval = {}
    -- Consider arrays separately
    local bArray, maxCount = isArray(v)
    if bArray then
      for i = 1,maxCount do
        table.insert(rval, linePrefixPlus..encode(v[i], pretty, level+1))
      end
    else	-- An object, not an array
      local iterator
      if mt and type(mt.__json_pairs) == 'function' then
        iterator = mt.__json_pairs
      else
        iterator = pairs
      end
      for i,j in iterator(v) do
        if isEncodable(i) and isEncodable(j) then
          table.insert(rval, linePrefixPlus .. '"' .. encodeString(i) .. '":' .. (pretty and ' ' or '') .. encode(j, pretty, level+1))
        end
      end
    end
    local terminator = pretty and '\n' or ''
	local concatenated = table.concat(rval,','..terminator)
	if concatenated ~= '' then
		concatenated = terminator..concatenated..terminator..linePrefix
	end
    if bArray then
      return '[' .. concatenated .. ']'
    else
      return '{' .. concatenated .. '}'
    end
  end
  
  -- Handle null values
  if vtype=='function' and v==null then
    return 'null'
  end
  
  assert(false,'encode attempt to encode unsupported type ' .. vtype .. ':' .. tostring(v))
end


function append(jsonData, newData, id)
	local suffix = string.sub(jsonData, -1)
	local data = string.sub(jsonData, 1, #jsonData - 1)
	local t = {}
	if suffix == "]" then
		-- si es un array, puedo agregar cualquier cosa
		table.insert(t, data)
		if #data > 1 then
			table.insert(t, ",")
		end
		table.insert(t, newData)
	elseif suffix == "}" then
		-- si es un hash, necesito un id
		assert(type(id) == "string", "an Id is required")
		table.insert(t, data)
		if #data > 1 then
			table.insert(t, ",")
		end
		table.insert(t, encode(id) .. ":")
		table.insert(t, newData)
	end
	table.insert(t, suffix)
	return table.concat(t)
end

--- Decodes a JSON string and returns the decoded value as a Lua data structure / value.
-- @param s The string to scan.
-- @return Lua objectthat was scanned, as a Lua table / string / number / boolean or nil.
function decode(s)
	-- Function is re-defined below after token and other items are created.
	-- Just defined here for code neatness.
	return null
end

--- The null function allows one to specify a null value in an associative array (which is otherwise
-- discarded if you set the value with 'nil' in Lua. Simply set t = { first=json.null }
function null()
  return null -- so json.null() will also return null ;-)
end

-----------------------------------------------------------------------------
-- Internal, PRIVATE functions.
-----------------------------------------------------------------------------

--
-- Lo siguiente fue sacado de https://github.com/harningt/luajson
-- Gracias mil a Thomas Harning Jr. <harningt@gmail.com>

local function encode_utf(codepoint)
	if codepoint > 0x10FFFF then
		error("Codepoint > 10FFFF cannot be encoded")
	elseif codepoint > 0xFFFF then
		-- Surrogate pair needed
		codepoint = codepoint - 0x10000
		local first, second = codepoint / 0x0400 + 0xD800, codepoint % 0x0400 + 0xDC00
		return ("\\u%.4X\\u%.4X"):format(first, second)
	else
		return ("\\u%.4X"):format(codepoint)
	end
end

-- decode a two-byte UTF-8 sequence
local function f2 (s)
	local c1, c2 = string.byte(s, 1, 2)
	return encode_utf(c1 * 64 + c2 - 12416)
end

-- decode a three-byte UTF-8 sequence
local function f3 (s)
	local c1, c2, c3 = string.byte(s, 1, 3)
	return encode_utf((c1 * 64 + c2) * 64 + c3 - 925824)
end

-- decode a four-byte UTF-8 sequence
local function f4 (s)
	local c1, c2, c3, c4 = string.byte(s, 1, 4)
	return encode_utf(((c1 * 64 + c2) * 64 + c3) * 64 + c4 - 63447168)
end

local cont = lpeg.R("\128\191")   -- continuation byte

local utf8 = lpeg.R("\0\127") -- Do nothing here
	+ lpeg.R("\194\223") * cont / f2
	+ lpeg.R("\224\239") * cont * cont / f3
	+ lpeg.R("\240\244") * cont * cont * cont / f4

local utf8_decode_pattern = lpeg.Cs(utf8^0) * -1

--- Encodes a string to be JSON-compatible.
-- This just involves back-quoting inverted commas, back-quotes and newlines, I think ;-)
-- Also escape control characters.
-- @param s The string to return as a JSON encoded (i.e. backquoted string)
-- @return The string appropriately escaped.
local qrep = {["\\"]="\\\\", ['"']='\\"',['\n']='\\n',['\r']='\\r',['\t']='\\t'}
function encodeString(s)
  s = tostring(s):gsub('["\\\n\r\t]',qrep)
  s = utf8_decode_pattern:match(s)
  --  escape codepoints (U+0000 through U+001F)
  s = s:gsub("%c", function(char)
	return ("\\u00%02x"):format( string.byte(char) )
  end)
  return s
end

-- Determines whether the given Lua type is an array or a table / dictionary.
-- We consider any table an array if it has indexes 1..n for its n items, and no
-- other data in the table.
-- I think this method is currently a little 'flaky', but can't think of a good way around it yet...
-- @param t The table to evaluate as an array
-- @return boolean, number True if the table can be represented as an array, false otherwise. If true,
-- the second returned value is the maximum
-- number of indexed elements in the array. 
function isArray(t)
  -- Next we count all the elements, ensuring that any non-indexed elements are not-encodable 
  -- (with the possible exception of 'n')
  local maxIndex = 0
  for k,v in pairs(t) do
    if (type(k)=='number' and math.floor(k)==k and 1<=k) then	-- k,v is an indexed pair
      if (not isEncodable(v)) then return false end	-- All array elements must be encodable
      maxIndex = math.max(maxIndex,k)
    else
      if (k=='n') then
        if v ~= table.getn(t) then return false end  -- False if n does not hold the number of elements
      else -- Else of (k=='n')
        if isEncodable(v) then return false end
      end  -- End of (k~='n')
    end -- End of k,v not an indexed pair
  end  -- End of loop across all pairs
  return true, maxIndex
end

--- Determines whether the given Lua object / table / variable can be JSON encoded. The only
-- types that are JSON encodable are: string, boolean, number, nil, table and json.null.
-- In this implementation, all other types are ignored.
-- @param o The object to examine.
-- @return boolean True if the object should be JSON encoded, false if it should be ignored.
function isEncodable(o)
  local t = type(o)
  return (t=='string' or t=='boolean' or t=='number' or t=='nil' or t=='table') or (t=='function' and o==null) 
end

--
-- The following function was taken from: http://github.com/harningt/luajson/blob/master/lua/json/decode/strings.lua
-- according to the table at http://da.wikipedia.org/wiki/UTF-8
local function utf8DecodeUnicode(code1, code2)
	if code1 == 0 and code2 < 0x80 then
		return string.char(code2)
	end
	if code1 < 0x08 then
		return string.char(
			0xC0 + code1 * 4 + math.floor(code2 / 64),
			0x80 + code2 % 64)
	end
	return string.char(
		0xE0 + math.floor(code1 / 16),
		0x80 + (code1 % 16) * 4 + math.floor(code2 / 64),
		0x80 + code2 % 64)
end

-- Radical performance improvement for decode from Eike Decker!
do
	local type = type
	local error = error
	local assert = assert
	local print = print
	local tonumber = tonumber
	-- initialize some values to be used in decoding function
	
	-- initializes a table to contain a byte=>table mapping
	-- the table contains tokens (byte values) as keys and maps them on other
	-- token tables (mostly, the boolean value 'true' is used to indicate termination
	-- of a token sequence)
	-- the token table's purpose is, that it allows scanning a sequence of bytes
	-- until something interesting has been found (e.g. a token that is not expected)
	-- name is a descriptor for the table to be printed in error messages
	local function init_token_table (tt)
		local struct = {}
		local value
		function struct:link(other_tt)
			value = other_tt
			return struct
		end
		function struct:to(chars)
			for i=1,#chars do 
				tt[chars:byte(i)] = value
			end
			return struct
		end
		return function (name)
			tt.name = name
			return struct
		end
	end
	
	-- keep "named" byte values at hands
	local 
		c_esc,
		c_e,
		c_l,
		c_r,
		c_u,
		c_f,
		c_a,
		c_s,
		c_slash = ("\\elrufas/"):byte(1,9)
	
	-- token tables - tt_doublequote_string = strDoubleQuot, tt_singlequote_string = strSingleQuot
	local 
		tt_object_key,
		tt_object_colon,
		tt_object_value,
		tt_doublequote_string,
		tt_singlequote_string,
		tt_array_value,
		tt_array_seperator,
		tt_numeric,
		tt_boolean,
		tt_null,
		tt_comment_start,
		tt_comment_middle,
		tt_ignore --< tt_ignore is special - marked tokens will be tt_ignored
			= {},{},{},{},{},{},{},{},{},{},{},{},{}
	
	-- strings to be used in certain token tables
	local strchars = "" -- all valid string characters (all except newlines)
	local allchars = "" -- all characters that are valid in comments
	--local escapechar = {}
	for i=0,0xff do 
		local c = string.char(i)
		if c~="\n" and c~="\r" then strchars = strchars .. c end
		allchars = allchars .. c
		--escapechar[i] = "\\" .. string.char(i)
	end
	
--[[	
	charstounescape = "\"\'\\bfnrt/";
	unescapechars = "\"'\\\b\f\n\r\t\/";
	for i=1,#charstounescape do
		escapechar[ charstounescape:byte(i) ] = unescapechars:sub(i,i)
	end
--]]

	-- obj key reader, expects the end of the object or a quoted string as key
	init_token_table (tt_object_key) "object (' or \" or } or , expected)" 
		:link(tt_singlequote_string) :to "'"
		:link(tt_doublequote_string) :to '"'
		:link(true)                  :to "}"
		:link(tt_object_key)         :to ","
		:link(tt_comment_start)      :to "/"
		:link(tt_ignore)             :to " \t\r\n"
	
	
	-- after the key, a colon is expected (or comment)
	init_token_table (tt_object_colon) "object (: expected)" 
		:link(tt_object_value)       :to ":"  
		:link(tt_comment_start)      :to "/" 
		:link(tt_ignore)             :to" \t\r\n"
		
	-- as values, anything is possible, numbers, arrays, objects, boolean, null, strings
	init_token_table (tt_object_value) "object ({ or [ or ' or \" or number or boolean or null expected)"
		:link(tt_object_key)         :to "{" 
		:link(tt_array_seperator)    :to "[" 
		:link(tt_singlequote_string) :to "'" 
		:link(tt_doublequote_string) :to '"' 
		:link(tt_numeric)            :to "0123456789.-" 
		:link(tt_boolean)            :to "tf" 
		:link(tt_null)               :to "n" 
		:link(tt_comment_start)      :to "/" 
		:link(tt_ignore)             :to " \t\r\n"
		
	-- token tables for reading strings
	init_token_table (tt_doublequote_string) "double quoted string"
		:link(tt_ignore)             :to (strchars)
		:link(c_esc)                 :to "\\"
		:link(true)                  :to '"'
		
	init_token_table (tt_singlequote_string) "single quoted string"
		:link(tt_ignore)             :to (strchars)
		:link(c_esc)                 :to "\\" 
		:link(true)                  :to "'"
		
	-- array reader that expects termination of the array or a comma that indicates the next value
	init_token_table (tt_array_value) "array (, or ] expected)"
		:link(tt_array_seperator)    :to "," 
		:link(true)                  :to "]"
		:link(tt_comment_start)      :to "/" 
		:link(tt_ignore)             :to " \t\r\n"
	
	-- a value, pretty similar to tt_object_value
	init_token_table (tt_array_seperator) "array ({ or [ or ' or \" or number or boolean or null expected)"
		:link(tt_object_key)         :to "{" 
		:link(tt_array_seperator)    :to "[" 
		:link(true)    				 :to "]" 
		:link(tt_singlequote_string) :to "'" 
		:link(tt_doublequote_string) :to '"'  
		:link(tt_comment_start)      :to "/" 
		:link(tt_numeric)            :to "0123456789.-" 
		:link(tt_boolean)            :to "tf" 
		:link(tt_null)               :to "n" 
		:link(tt_ignore)             :to " \t\r\n"
	
	-- valid number tokens
	init_token_table (tt_numeric) "number"
		:link(tt_ignore)             :to "0123456789.-Ee+"
		
	-- once a comment has been started with /, a * is expected
	init_token_table (tt_comment_start) "comment start (* expected)"
		:link(tt_comment_middle)     :to "*"
		
	-- now everything is allowed, watch out for * though. The next char is then checked manually
	init_token_table (tt_comment_middle) "comment end"
		:link(tt_ignore)             :to (allchars)
		:link(true)                  :to "*"
		
	function decode (js_string)
		local pos = 1 -- position in the string
		
		-- read the next byte value
		local function next_byte () pos = pos + 1 return js_string:byte(pos-1) end
		
		-- in case of error, report the location using line numbers
		local function location () 
			local n = ("\n"):byte()
			local line,lpos = 1,0
			for i=1,pos do 
				if js_string:byte(i) == n then
					line,lpos = line + 1,1
				else
					lpos = lpos + 1
				end
			end
			return "Line "..line.." character "..lpos
		end
		
		-- debug func
		--local function status (str)
		--	print(str.." ("..s:sub(math.max(1,p-10),p+10)..")")
		--end
		
		-- read the next token, according to the passed token table
		local function next_token (tok)
			while pos <= #js_string do
				local b = js_string:byte(pos) 
				local t = tok[b]
				if not t then 
					error("Unexpected character at "..location()..": "..
						string.char(b).." ("..b..") when reading "..tok.name.."\nContext: \n"..
						js_string:sub(math.max(1,pos-30),pos+30).."\n"..(" "):rep(pos+math.min(-1,30-pos)).."^")
				end
				pos = pos + 1
				if t~=tt_ignore then return t end
			end
			error("unexpected termination of JSON while looking for "..tok.name)
		end
		
		-- read a string, double and single quoted ones
		local function read_string (tok)
			local start = pos
			--local returnString = {}
			repeat
				local t = next_token(tok)
				if t == c_esc then 
					--table.insert(returnString, js_string:sub(start, pos-2))
					--table.insert(returnString, escapechar[ js_string:byte(pos) ])
					pos = pos + 1
					--start = pos
				end -- jump over escaped chars, no matter what
			until t == true
			local stringValue = js_string:sub(start-1, pos-1)
			stringValue = string.gsub(stringValue, "([\\]+)u(%x%x)(%x%x)", function(escape, hex_value_high, hex_value_low)
				if #escape > 1 then
					return [[\\u]]..hex_value_high..hex_value_low	-- double escaped since it will be 'eval'd later
				end
				local hex_value_high, hex_value_low = tonumber(hex_value_high, 16), tonumber(hex_value_low, 16)
				local converted
				if hex_value_high == 0 then
					converted = (hex_value_low == 10 and [[\n]]) or (hex_value_low == 13 and [[\r]]) or (hex_value_low == 9 and [[\t]])
				end
				return converted or utf8DecodeUnicode(hex_value_high, hex_value_low)
			end)
			return (loadstring("return " .. stringValue ) ())

			-- We consider the situation where no escaped chars were encountered separately,
			-- and use the fastest possible return in this case.
			
			--if 0 == #returnString then
			--	return js_string:sub(start,pos-2)
			--else
			--	table.insert(returnString, js_string:sub(start,pos-2))
			--	return table.concat(returnString,"");
			--end
			--return js_string:sub(start,pos-2)
		end
		
		local function read_num ()
			local start = pos
			while pos <= #js_string do
				local b = js_string:byte(pos)
				if not tt_numeric[b] then break end
				pos = pos + 1
			end
			return tonumber(js_string:sub(start-1,pos-1))
		end
		
		-- read_bool and read_null are both making an assumption that I have not tested:
		-- I would expect that the string extraction is more expensive than actually 
		-- making manual comparision of the byte values
		local function read_bool () 
			pos = pos + 3
			local a,b,c,d = js_string:byte(pos-3,pos)
			if a == c_r and b == c_u and c == c_e then return true end
			pos = pos + 1
			if a ~= c_a or b ~= c_l or c ~= c_s or d ~= c_e then 
				error("Invalid boolean: "..js_string:sub(math.max(1,pos-5),pos+5)) 
			end
			return false
		end
		
		-- same as read_bool: only last 
		local function read_null ()
			pos = pos + 3
			local u,l1,l2 = js_string:byte(pos-3,pos-1)
			if u == c_u and l1 == c_l and l2 == c_l then return nil end
			error("Invalid value (expected null):"..js_string:sub(pos-4,pos-1)..
				" ("..js_string:byte(pos-1).."="..js_string:sub(pos-1,pos-1).." / "..c_l..")")
		end
		
		local read_object_value,read_object_key,read_array,read_value,read_comment
	
		-- read a value depending on what token was returned, might require info what was used (in case of comments)
		function read_value (t,fromt)
			if t == tt_object_key         then return read_object_key({}) end
			if t == tt_array_seperator    then return read_array({}) end
			if t == tt_singlequote_string or 
			   t == tt_doublequote_string then return read_string(t) end
			if t == tt_numeric            then return read_num() end
			if t == tt_boolean            then return read_bool() end
			if t == tt_null               then return read_null() end
			if t == tt_comment_start      then return read_value(read_comment(fromt)) end
			error("unexpected termination - "..js_string:sub(math.max(1,pos-10),pos+10))
		end
		
		-- read comments until something noncomment like surfaces, using the token reader which was 
		-- used when stumbling over this comment
		function read_comment (fromt)
			while true do
				next_token(tt_comment_start)
				while true do
					local t = next_token(tt_comment_middle)
					if next_byte() == c_slash then
						local t = next_token(fromt)
						if t~= tt_comment_start then return t end
						break
					end
				end
			end
		end
		
		-- read arrays, empty array expected as o arg
		function read_array (o,i)
			--if not i then status "arr open" end
			i = i or 1
			-- loop until ...
			while true do
				local t = next_token(tt_array_seperator)
				if t == true then	-- we found a terminator token right away (an empty array)
					-- status "arr close"
					return o
				end
				o[i] = read_value(t,tt_array_seperator)
				t = next_token(tt_array_value)
				if t == tt_comment_start then
					t = read_comment(tt_array_value)
				end
				if t == true then  -- ... we found a terminator token
					--status "arr close"
					return o 
				end
				i = i + 1
			end
		end
		
		-- object value reading
		function read_object_value (o)
			local t = next_token(tt_object_value)
			return read_value(t,tt_object_value)
		end
		
		-- object key reading, might also terminate the object
		function read_object_key (o)
			while true do
				local t = next_token(tt_object_key)
				if t == tt_comment_start then
					t = read_comment(tt_object_key)
				end
				if t == true then return o end
				if t == tt_object_key then return read_object_key(o) end
				local k = read_string(t)
				
				if next_token(tt_object_colon) == tt_comment_start then
					t = read_comment(tt_object_colon)
				end
				
				local v = read_object_value(o)
				o[k] = v
			end
		end
		
		-- now let's read data from our string and pretend it's an object value
		local r = read_object_value()
		if pos<=#js_string then
			-- not sure about what to do with dangling characters
			--error("Dangling characters in JSON code ("..location()..")")
		end
		
		return r
	end
end
