Quantcast
--- LUA companion library.
-- @class file
-- @name LUA
-- @author Steven Jackson (2014)
-- @release 1.0.0
local _G            = getfenv(0)
local select        = _G.select;
local assert        = _G.assert;
local type          = _G.type;
local error         = _G.error;
local pairs         = _G.pairs;
local next          = _G.next;
local ipairs        = _G.ipairs;
local loadstring    = _G.loadstring;
local setmetatable  = _G.setmetatable;
local getmetatable  = _G.getmetatable;
local rawset        = _G.rawset;
local rawget        = _G.rawget;
local tostring      = _G.tostring;
local tonumber      = _G.tonumber;
local xpcall        = _G.xpcall;
local pcall         = _G.pcall;
local table         = _G.table;
local tconcat       = table.concat;
local tremove       = table.remove;
local tinsert       = table.insert;
local table_sort    = table.sort;
local string        = _G.string;
local match         = string.match;
local gmatch 				= string.gmatch;
local gsub          = string.gsub;
local rep           = string.rep;
local char 					= string.char;
local strmatch      = _G.strmatch;
local bit           = _G.bit;
local band          = bit.band;
local math          = _G.math;
local floor         = math.floor;
local huge          = math.huge;

---------------------------------------------------------------------
-- Math
-- @section MATH UTILITIES
---------------------------------------------------------------------

---------------------------------------------------------------------
-- Integer float utility for lua.
-- @return floating point integer
-- @param value The integer amount to be adjusted.
-- @param decimal Number of decimal places allowed.
---------------------------------------------------------------------

function math.parsefloat(value, decimal)
	value = value or 0
	if(decimal and decimal > 0) then
		local calc1 = 10 ^ decimal;
		local calc2 = (value * calc1) + 0.5;
		return floor(calc2) / calc1
	end
	return floor(value + 0.5)
end

---------------------------------------------------------------------
-- Pickle
-- @section SERIALIZE UTILITIES
---------------------------------------------------------------------

---------------------------------------------------------------------
-- Global class used by pickle/unpickle functions.
-- @todo Does this need to be global?
---------------------------------------------------------------------

Pickle = {
  clone = function (t) local nt={}; for i, v in pairs(t) do nt[i]=v end return nt end
}

---------------------------------------------------------------------
-- A table serialization utility for lua.
-- @return serialized table data
-- @param t A table to be serialized.
-- @author Steve Dekorte, http://www.dekorte.com, Apr 2000
---------------------------------------------------------------------

function pickle(t)
  return Pickle:clone():pickle_(t)
end

function Pickle:pickle_(root)
  if type(root) ~= "table" then
    error("can only pickle tables, not ".. type(root).."s")
  end
  self._tableToRef = {}
  self._refToTable = {}
  local savecount = 0
  self:ref_(root)
  local s = ""

  while table.getn(self._refToTable) > savecount do
    savecount = savecount + 1
    local t = self._refToTable[savecount]
    s = s.."{\n"
    for i, v in pairs(t) do
        s = string.format("%s[%s]=%s,\n", s, self:value_(i), self:value_(v))
    end
    s = s.."},\n"
  end

  return string.format("{%s}", s)
end

function Pickle:value_(v)
  local vtype = type(v)
  if     vtype == "string" then return string.format("%q", v)
  elseif vtype == "number" then return v
  elseif vtype == "boolean" then return tostring(v)
  elseif vtype == "table" then return "{"..self:ref_(v).."}"
  else --error("pickle a "..type(v).." is not supported")
  end
end

function Pickle:ref_(t)
  local ref = self._tableToRef[t]
  if not ref then
    if t == self then error("can't pickle the pickle class") end
    table.insert(self._refToTable, t)
    ref = table.getn(self._refToTable)
    self._tableToRef[t] = ref
  end
  return ref
end

---------------------------------------------------------------------
-- Un-serialization tool (pretty sure thats not a word).
-- @return serialized table data
-- @param s A serialized table to be reversed.
-- @author Steve Dekorte, http://www.dekorte.com, Apr 2000
---------------------------------------------------------------------

function unpickle(s)
  if type(s) ~= "string" then
    error("can't unpickle a "..type(s)..", only strings")
  end
  local gentables = loadstring("return "..s)
  local tables = gentables()

  for tnum = 1, table.getn(tables) do
    local t = tables[tnum]
    local tcopy = {}; for i, v in pairs(t) do tcopy[i] = v end
    for i, v in pairs(tcopy) do
      local ni, nv
      if type(i) == "table" then ni = tables[i[1]] else ni = i end
      if type(v) == "table" then nv = tables[v[1]] else nv = v end
      t[i] = nil
      t[ni] = nv
    end
  end
  return tables[1]
end

---------------------------------------------------------------------
-- String
-- @section STRING UTILITIES
---------------------------------------------------------------------

local char_table='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

---------------------------------------------------------------------
-- Base64 encoding tool.
-- @return encoded string
-- @param data string data to be encoded.
---------------------------------------------------------------------

function string.encode(data)
    return ((data:gsub('.', function(x)
        local r,b='',x:byte()
        for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end
        return r;
    end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x)
        if (#x < 6) then return '' end
        local c=0
        for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end
        return char_table:sub(c+1,c+1)
    end)..({ '', '==', '=' })[#data%3+1])
end

---------------------------------------------------------------------
-- Base64 decoding tool.
-- @return decoded string
-- @param data encoded string to be decoded.
---------------------------------------------------------------------

function string.decode(data)
    data = gsub(data, '[^'..char_table..'=]', '')
    return (data:gsub('.', function(x)
        if (x == '=') then return '' end
        local r,f='',(char_table:find(x)-1)
        for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end
        return r;
    end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
        if (#x ~= 8) then return '' end
        local c=0
        for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
        return char(c)
    end))
end

---------------------------------------------------------------------
-- String to array utility.
-- @return table data
-- @param data string to be converted to table data.
-- @param delim Character delimiter to separate the string by.
---------------------------------------------------------------------

function string.explode(data, delim)
    local pattern = format("([^%s]+)", delim);
    local res = {};
		local count = 1;
    for line in gmatch(data, pattern) do
				res[count] = line;
				count = count + 1;
    end
    return res
end

function string.loadtable(data)
   local t = {}
   local f = assert(loadstring(data))
   setfenv(f, t)
   f()
   return t
end

-------------------------------------------------------------------
--PRETTY PRINT FOR TABLES

local prettify = {}
prettify.KEY       = setmetatable({}, {__tostring = function() return 'prettify.KEY' end})
prettify.METATABLE = setmetatable({}, {__tostring = function() return 'prettify.METATABLE' end})

-- Apostrophizes the string if it has quotes, but not aphostrophes
-- Otherwise, it returns a regular quoted string
local function smartQuote(str)
  if str:match('"') and not str:match("'") then
	return "'" .. str .. "'"
  end
  return '"' .. str:gsub('"', '\\"') .. '"'
end

local controlCharsTranslation = {
  ["\a"] = "\\a",  ["\b"] = "\\b", ["\f"] = "\\f",  ["\n"] = "\\n",
  ["\r"] = "\\r",  ["\t"] = "\\t", ["\v"] = "\\v"
}

local function escape(str)
  local result = str:gsub("\\", "\\\\"):gsub("(%c)", controlCharsTranslation)
  return result
end

local function isIdentifier(str)
  return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" )
end

local function isSequenceKey(k, length)
  return type(k) == 'number'
	 and 1 <= k
	 and k <= length
	 and floor(k) == k
end

local defaultTypeOrders = {
  ['number']   = 1, ['boolean']  = 2, ['string'] = 3, ['table'] = 4,
  ['function'] = 5, ['userdata'] = 6, ['thread'] = 7
}

local function sortKeys(a, b)
  local ta, tb = type(a), type(b)

  -- strings and numbers are sorted numerically/alphabetically
  if ta == tb and (ta == 'string' or ta == 'number') then return a < b end

  local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb]
  -- Two default types are compared according to the defaultTypeOrders table
  if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb]
  elseif dta     then return true  -- default types before custom ones
  elseif dtb     then return false -- custom types after default ones
  end

  -- custom types are sorted out alphabetically
  return ta < tb
end

local function getNonSequentialKeys(t)
  local keys, length = {}, #t
  for k,_ in pairs(t) do
	if not isSequenceKey(k, length) then table.insert(keys, k) end
  end
  table.sort(keys, sortKeys)
  return keys
end

local function getToStringResultSafely(t, mt)
  local __tostring = type(mt) == 'table' and rawget(mt, '__tostring')
  local str, ok
  if type(__tostring) == 'function' then
	ok, str = pcall(__tostring, t)
	str = ok and str or 'error: ' .. tostring(str)
  end
  if type(str) == 'string' and #str > 0 then return str end
end

local maxIdsMetaTable = {
  __index = function(self, typeName)
	rawset(self, typeName, 0)
	return 0
  end
}

local idsMetaTable = {
  __index = function (self, typeName)
	local col = setmetatable({}, {__mode = "kv"})
	rawset(self, typeName, col)
	return col
  end
}

local function countTableAppearances(t, tableAppearances)
  tableAppearances = tableAppearances or setmetatable({}, {__mode = "k"})

  if type(t) == 'table' then
	if not tableAppearances[t] then
	  tableAppearances[t] = 1
	  for k,v in pairs(t) do
		countTableAppearances(k, tableAppearances)
		countTableAppearances(v, tableAppearances)
	  end
	  countTableAppearances(getmetatable(t), tableAppearances)
	else
	  tableAppearances[t] = tableAppearances[t] + 1
	end
  end

  return tableAppearances
end

local copySequence = function(s)
  local copy, len = {}, #s
  for i=1, len do copy[i] = s[i] end
  return copy, len
end

local function makePath(path, ...)
  local keys = {...}
  local newPath, len = copySequence(path)
  for i=1, #keys do
	newPath[len + i] = keys[i]
  end
  return newPath
end

local function processRecursive(process, item, path)
  if item == nil then return nil end

  local processed = process(item, path)
  if type(processed) == 'table' then
	local processedCopy = {}
	local processedKey

	for k,v in pairs(processed) do
	  processedKey = processRecursive(process, k, makePath(path, k, prettify.KEY))
	  if processedKey ~= nil then
		processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey))
	  end
	end

	local mt  = processRecursive(process, getmetatable(processed), makePath(path, prettify.METATABLE))
	setmetatable(processedCopy, mt)
	processed = processedCopy
  end
  return processed
end


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

local PrettifyTable = {}
local PrettifyTable_mt = {__index = PrettifyTable}

function PrettifyTable:puts(...)
  local args   = {...}
  local buffer = self.buffer
  local len    = #buffer
  for i=1, #args do
	len = len + 1
	buffer[len] = tostring(args[i])
  end
end

function PrettifyTable:down(f)
  self.level = self.level + 1
  f()
  self.level = self.level - 1
end

function PrettifyTable:tabify()
  self:puts(self.newline, rep(self.indent, self.level))
end

function PrettifyTable:alreadyVisited(v)
  return self.ids[type(v)][v] ~= nil
end

function PrettifyTable:getId(v)
  local tv = type(v)
  local id = self.ids[tv][v]
  if not id then
	id              = self.maxIds[tv] + 1
	self.maxIds[tv] = id
	self.ids[tv][v] = id
  end
  return id
end

function PrettifyTable:putKey(k)
  if isIdentifier(k) then return self:puts(k) end
  self:puts("[")
  self:putValue(k)
  self:puts("]")
end

function PrettifyTable:putTable(t)
  if t == prettify.KEY or t == prettify.METATABLE then
	self:puts(tostring(t))
  elseif self:alreadyVisited(t) then
	self:puts('<table ', self:getId(t), '>')
  elseif self.level >= self.depth then
	self:puts('{...}')
  else
	if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end

	local nonSequentialKeys = getNonSequentialKeys(t)
	local length            = #t
	local mt                = getmetatable(t)
	local toStringResult    = getToStringResultSafely(t, mt)

	self:puts('{')
	self:down(function()
	  if toStringResult then
		self:puts(' -- ', escape(toStringResult))
		if length >= 1 then self:tabify() end
	  end

	  local count = 0
	  for i=1, length do
		if count > 0 then self:puts(',') end
		self:puts(' ')
		self:putValue(t[i])
		count = count + 1
	  end

	  for _,k in ipairs(nonSequentialKeys) do
		if count > 0 then self:puts(',') end
		self:tabify()
		self:putKey(k)
		self:puts(' = ')
		self:putValue(t[k])
		count = count + 1
	  end

	  if mt then
		if count > 0 then self:puts(',') end
		self:tabify()
		self:puts('<metatable> = ')
		self:putValue(mt)
	  end
	end)

	if #nonSequentialKeys > 0 or mt then -- result is multi-lined. Justify closing }
	  self:tabify()
	elseif length > 0 then -- array tables have one extra space before closing }
	  self:puts(' ')
	end

	self:puts('}')
  end
end

function PrettifyTable:putValue(v)
  local tv = type(v)

  if tv == 'string' then
	self:puts(smartQuote(escape(v)))
  elseif tv == 'number' or tv == 'boolean' or tv == 'nil' then
	self:puts(tostring(v))
  elseif tv == 'table' then
	self:putTable(v)
  else
	self:puts('<',tv,' ',self:getId(v),'>')
  end
end


function prettify.prettify(root, options)
  options       = options or {}

  local depth   = options.depth   or huge
  local newline = options.newline or '\n'
  local indent  = options.indent  or '  '
  local process = options.process

  if process then
	root = processRecursive(process, root, {})
  end

  local prettify_table = setmetatable({
	depth            = depth,
	buffer           = {},
	level            = 0,
	ids              = setmetatable({}, idsMetaTable),
	maxIds           = setmetatable({}, maxIdsMetaTable),
	newline          = newline,
	indent           = indent,
	tableAppearances = countTableAppearances(root)
  }, PrettifyTable_mt)

  prettify_table:putValue(root)

  return tconcat(prettify_table.buffer)
end

setmetatable(prettify, { __call = function(_, ...) return prettify.prettify(...) end })

---------------------------------------------------------------------
-- Table
-- @section TABLE UTILITIES
---------------------------------------------------------------------

function table.val_to_str(v)
  	if "string" == type(v) then
		v = gsub(v, "\n", "\\n")
		if match( gsub(v,"[^'\"]",""), '^"+$') then
			return "'" .. v .. "'"
		end
	  	return '"' .. gsub(v,'"', '\\"') .. '"'
	else
		return "table" == type(v) and table.tostring(v) or
		tostring(v)
	end
end

function table.key_to_str(k)
	if "string" == type(k) and match(k, "^[_%a][_%a%d]*$") then
		return k
	else
		return "[" .. table.val_to_str(k) .. "]"
	end
end

---------------------------------------------------------------------
-- Dump table contents to string
-- @return string value
-- @param tbl A table to be stringified.
-- @param pretty Flag to syntactically format the result.
---------------------------------------------------------------------

function table.tostring(tbl, pretty)
	if(pretty) then
		return prettify(tbl)
	else
		local result, done = {}, {}
		for k, v in ipairs(tbl) do
			tinsert(result, table.val_to_str(v))
			done[k] = true
		end
		for k, v in pairs(tbl) do
			if not done[k] then
			  	tinsert(result, table.key_to_str(k) .. "=" .. table.val_to_str(v))
		  	end
		end
		return "{" .. tconcat( result, "," ) .. "}"
	end
end

---------------------------------------------------------------------
-- Copy all table data from a source to another table
-- @return copied data
-- @param targetTable The recipient of the copied data.
-- @param deepCopy Flag the use of DEEP copying.
-- @param mergeTable The origin of the copied data.
---------------------------------------------------------------------

function table.copy(targetTable, deepCopy, mergeTable)
	mergeTable = mergeTable or {};
	if(targetTable == nil) then return nil end
	if(mergeTable[targetTable]) then return mergeTable[targetTable] end
	local replacementTable = {}
	for key,value in pairs(targetTable)do
		if deepCopy and type(value) == "table" then
			replacementTable[key] = table.copy(value, deepCopy, mergeTable)
		else
			replacementTable[key] = value
		end
	end
	setmetatable(replacementTable, table.copy(getmetatable(targetTable), deepCopy, mergeTable))
	mergeTable[targetTable] = replacementTable;
	return replacementTable
end