-- Import

local ffi = require 'ffi'
local bit = require 'bit'

-- Constants

--------------------------------------------------------------------------------
-- Util

local HEX_TO_BIN = {
    ['0'] = '0000', ['1'] = '0001', ['2'] = '0010', ['3'] = '0011',
    ['4'] = '0100', ['5'] = '0101', ['6'] = '0110', ['7'] = '0111',
	['8'] = '1000', ['9'] = '1001', ['A'] = '1010', ['B'] = '1011',
	['C'] = '1100', ['D'] = '1101', ['E'] = '1110', ['F'] = '1111',
}

local function to_hex (val, nr_elements, element_size)
	local l = {}
	for i = 0, nr_elements - 1 do
		local v = val[i]
		l[#l+1] = bit.tohex(v, -2*element_size)
		l[#l+1] = ' '
	end
	l[#l] = nil
	return table.concat(l, '')
end

local function to_bin (val, nr_elements, element_size)
	return to_hex(val, nr_elements, element_size):gsub('[0-9A-F]', HEX_TO_BIN)
end

local function is_nice_unicode_string (str)
	-- TODO... Maybe also look into a purely binary oriented representation.
	return false
end

local function is_nice_ascii_string (str)
	for i = 1, #str do
		local byte = str:byte(i)
		if not (32 <= byte and byte <= 126) then  return false  end
	end
	return true
end

local function get_type_and_size_of_singular ( ctype )
	local nr_elements, layers  =  1, 0
	while true do
		local etype, elements  =  ctype:match('(.+)%[(%d*)%]$')
		if not elements then  break  end
		ctype, nr_elements  =  etype, nr_elements * elements
		layers = layers + 1
	end
	return ctype, nr_elements, layers
end

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

local CDATA_REPR_MATCHER = 'cdata<(.+)>: (0x%w+)'


local function format_cdata (value, display, l, format_value)

	-- Error check
	assert(type(value)        == 'cdata'   )
	assert(type(display)      == 'number'  )
	assert(type(l)            == 'table'   )
	assert(type(format_value) == 'function')

	-- Do stuff
	local native_repr  =  tostring(value)
	local data_length  =  ffi.sizeof(value)
	local ctype, addr  =  native_repr:match(CDATA_REPR_MATCHER)

	-- Is void pointer?
	if ctype == 'void *' then
		local address_pointing_at = tonumber(ffi.cast('int', value))
		l[#l+1] = 'void pointer to ' .. addr
		return ;
	end

	-- Is normal pointer?
	if ctype:match('%*$') then
		if type(value[0]) ~= 'cdata' then
			-- Data presentable in Lua, refered to by pointers?
			l[#l+1] = 'pointer to '
			return format_value(value[0], display, l.options, l)
		else
			l[#l+1] = '* '
			return format_cdata(value[0], display, l.options, l, format_value)
		end
	end

	l[#l+1] = 'cdata {'
	--l[#l+1] = '\n\tnative = \''   .. native_repr .. '\','
	l[#l+1] = '\n\ttype   = '   .. ctype .. ','
	l[#l+1] = '\n\taddr   = '   .. addr .. ','
	if data_length then
		-- Size
		local str = ffi.string(value, data_length)
		l[#l+1] = '\n\tsize   = '   .. data_length .. ','

		-- Element size and type
		local element_type, nr_elements, nr_layers  =  get_type_and_size_of_singular(ctype)
		local element_size = data_length / nr_elements
		l[#l+1] = '\n\tnr_e   = ' .. nr_elements .. ','
		l[#l+1] = '\n\ttype_e = '   .. element_type .. ','
		l[#l+1] = '\n\tsize_e = '   .. element_size .. ','

		-- If can be expressed as string, express it as string.
		if is_nice_ascii_string(str) or is_nice_unicode_string(str) then
			local string_or_unicode = is_nice_ascii_string(str) and 'ascii' or 'utf8 '
			l[#l+1] = '\n\t'..string_or_unicode..'    = ' .. str .. ','
		end
		--
		if nr_layers == 1 then
			-- Only a single level of arrays
			l[#l+1] = '\n\thex    = ' .. to_hex(value, nr_elements, element_size) .. ','
			l[#l+1] = '\n\tbin    = ' .. to_bin(value, nr_elements, element_size) .. ','
		end

	end
	l[#l+1] = '\n}'
end

return format_cdata