-- pretty.function
-- The function formatting module for pretty.

--[=[ Thoughts on displaying functions in an informative way.

How is one supposed to pretty print functions? Well, there are many different
formats, and no "best" one, only "best for the purpose". Lets start at the
simplest, and move towards abstraction.

1.	The default Lua format: "function: 0x41f71c60"
	This is the default, and by far the easiest. Also, very uninformative. We
	only get a unique id for the function.
2.	Include the arguments: "function (t, str) ... end"
	This is slightly more advanced, as it requires using the debug library to
	discover the names of the arguments. In addition it comes closer to a Lua
	parsable formatting. We can know the argument names, and if these are
	descriptive, we can learn some things about the function.
3.	Include some documentation: "function (x) --[[math.cosh: Returns the hyperbolic cosine of x.]] ... end"
	We retain the arguments and almost parsable formatting from above, and add
	documentation taken from elsewhere - Lua Reference Manual, LuaJIT webpage -
	as comments. This is great for explorative programming, as we can read about
	the language by just fiddling around with it.
4.	Short names: "math.min"
	We can assume that an experienced Lua user might already know how any given
	library function works, and would thus prefer a shorthand, rather than the
	documentation. To that aim, we can use the short name/access path instead.
	This is also ideal in places where we don't have a lot of space.
	This representation is completely parsable, but won't necessarily work for
	custom enviroment and user-defined functions.
5.	Include source code: "function (a, b) return (a + b)/2 end"
	Now we find the source code somehow, and use it as the representation. This
	is powerful because we can directly inspect the code. But it makes the
	function representation a lot more bussy. It won't work with builtins,
	closured functions, or when fucking around with the source files. It also
	hits limits when functions are nested, as the debug library does not give
	enough precision.
	This also works nicely with 3. due to being able to read function
	documentation from the source file.
6.	Include closures: "(function () local i = 5; return function () i = i + 1; return i end end)()"
	In cases where a function has a closure, we can use debug.getinfo to get
	the names and values of the upvalues. We can then represent the closure, by
	creating the closure itself. Iterators like the example above works nicely,
	but more complex chains of closures break down. For example:

		local val_a  = 1
		local func_a = function () val_a = val_a + 1; return val_a end
		local val_a  = val_a
		local func_c = function () return func_a() + val_a end

	Here we have two functions, both with their own upvalue, both named "val_a",
	yet those names refer to two different "slots". Successive calls to
	`func_c` should produce the list: 2, 3, 4, 5, 6, 7, ...
	To break through this barrier, we need to parse the Lua AST, and that is
	beyond this project.
	Closures also create extremely bussy output.
--]=]

-- Import

local LIBRARY =  require((... and select('1', ...):match('.+%.') or '')..'library') or {}
local DISPLAY =  assert(require((... and select('1', ...):match('.+%.') or '')..'common'), '[pretty]: Could not load vital library: common') . DISPLAY
local utf8_string_length =  assert(require((... and select('1', ...):match('.+%.') or '')..'common'), '[pretty]: Could not load vital library: common') . utf8_string_length
local width_of_strings_in_l =  assert(require((... and select('1', ...):match('.+%.') or '')..'common'), '[pretty]: Could not load vital library: common') . width_of_strings_in_l

-- Constants

-- FUNCTION_DEFINITION_MATCH is a lua pattern, for finding a function definition.
-- NOTE: It will match malformed unicode sequences, and thus assumes that the
-- string checked against have been checked by the lua interpreter.
local FUNCTION_KEYWORD_MATCH =  '%f[%a_]function%f[^%a_]'
local FUNCTION_DEFINITION_MATCH = '.-'   .. -- Look for stuff before the function
    FUNCTION_KEYWORD_MATCH .. '%s*'      .. -- Look for the function keyword
	'([a-zA-Z0-9\128-\255_.:]*)%s*'      .. -- Look for the function name, if any
	'(%([a-zA-Z0-9\128-\255_,. \t]*%))'  .. -- Look for the function parameter list.
	'[ \t]*(.+)[ \t]*'                   .. -- Look for the function body
	'end'                                   -- Look for the end keyword

local LUA_FILE_PATTERN = '^%s*[%w_]+.lua%s*$'

local NR_CHARS_IN_LONG_FUNCTION_BODY = 30

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

local function get_function_info (f)
    -- NOTE: Works best in LuaJIT or Lua 5.2+
	-- Regarding get-info:
    -- *  No need to includ 'f'. Function is already known
    -- *  No need to include 'L' (active lines) option. Ignored
    -- *  No need to include 'n' (name and namewhat). Won't work.

	assert(type(f) == 'function')

    local info = debug.getinfo(f, 'Su')
          info.params = {}
          info.ups = {}
          info.env = debug.getfenv and debug.getfenv(f)
          info.builtin = (info.source == '=[C]')
    for i = 1, info.nparams or 0 do  info.params[i] = debug.getlocal(f, i)  end
    if info.isvararg or not info.nparams then  info.params[#info.params+1] = '...'  end
    -- Get upvalues
    for i = 1, info.nups do
        local k, v = debug.getupvalue(f, i)
        if k == '_ENV' and not debug.getfenv then
            info.env = v
        else
            info.ups[k] = v
        end
    end

	if    	info.source:sub(1,1) == '='       	then	info.defined_how = 'C'
	elseif	info.source:sub(1,1) == '@'       	then	info.defined_how = 'file'
	elseif	info.source:find(LUA_FILE_PATTERN)	then	info.defined_how = 'file'    -- Fix for when someone has misunderstood the source format is for.
	else  	                                  	    	info.defined_how = 'string'
	end

    if info.builtin and LIBRARY[f] then
        info.name       = LIBRARY[f].name
        info.params[1]  = LIBRARY[f].para
        info.docs       = LIBRARY[f].docs
    end

    return info
end

local function get_line_index (str, line_nr)
	assert(type(str) == 'string')
	assert(type(line_nr) == 'number')

    local index = 0
    for _ = 2, line_nr do
        index = str:find('\n', index, true)
        if not index then  return utf8_string_length(str)  end
        index = index + 1
    end
    return index
end

local function get_docs_from_function_body (body, max_index)
	-- Finds the documentation lines of a function.
	-- Also returns the remaining non-documentation lines.

	assert(type(body) == 'string')

	local doc_lines  =  {}
	for line in body:sub(1, max_index):gmatch('[^\n]+', true) do
		if not line:match '^%s*$' then
			local line_text = line:match '^%s*%-%-%s*(.*)%s*$'
			doc_lines[#doc_lines+1] = line_text
			if not line_text then  break  end
		end
	end
	return table.concat(doc_lines, '\n'):match '^%s*(.-)%s*$'
end

local function get_docs_split_index (body)
	local index = 1
	while index <= #body do
		local next_newline  =  body:find('\n', index + 1) or -1
		if body:sub(index, next_newline):match('^%s*$') or body:sub(index, next_newline):match('^%s*%-%-%s*(.*)%s*$') then
			index = next_newline
		else
			return index
		end
	end
	return -1
end

local function get_function_body_info (info)
	-- Will attempt to expand `info` with extra info found by looking at the
	-- source code. This could require opening a file.
	-- There is no guarentee that the function body it finds is correct, or even
	-- that it finds any.

	-- Error check
	assert(type(info) == 'table')

	if info.defined_how == 'C' then
		error('[pretty.function/internal]: Cannot find source-code for C functions.', 2)
	end

	-- First find the string to search through.
	local str  =  info.source
	if info.defined_how == 'file' then
		-- Read file
		local file = io.open(info.short_src, 'r')
		if file then
			str = file:read '*all'
			file:close()
		else
			str = nil
		end
	end

	if not str then  return info  end

	-- Now find some info about the function.
	-- NOTE: function_params is currently not used for anything.
	local function_name, function_params, function_body

	if info.linedefined == 0 then
		-- A function is a "chunk" when linedefined is equal to 0.
		-- This is a "toplevel" function. One without a function
		-- definition. The entire string is it's function body.
		function_name	= ''
		function_body	= str
	else
		-- This is not a chunk. Look for function definition.

		-- Calculate indices of the lines the function should be defined at.
		local start_line_index	=  get_line_index(str, info.linedefined)
		local end_line_index	=  get_line_index(str, info.lastlinedefined + 1)

		function_name, function_params, function_body  =  str:sub(start_line_index, end_line_index):match(FUNCTION_DEFINITION_MATCH)

		-- Throw an error if we can't find anything.
		if type(function_body) ~= 'string' then
			error(('[pretty.function/internal]: Could not find the function defined on lines %i-%i (indices %i-%i) for string:\n\n%s\n'):format(info.linedefined, info.lastlinedefined, start_line_index, end_line_index, str))
		end
	end

	local function_body	=  function_body:match '^%s*(.-)%s*$'
	local pivot_index	=  get_docs_split_index (function_body)

	info.name	= info.name or function_name:match '^%s*(.-)%s*$'
	info.docs	= info.docs or get_docs_from_function_body(function_body, pivot_index)
	info.body	= info.body or function_body:sub(pivot_index, -1):match '^%s*(.-)%s*$'

	if info.name == '' then	info.name = nil	end
	if info.docs == '' then	info.docs = nil	end

	return info
end

--------------------------------------------------------------------------------
-- Text handling

local function add_indent_to_string (str, indent)
	-- Indents `str` by `indent`.

	assert(type(str)    == 'string')
	assert(type(indent) == 'string')

	return indent .. str:gsub('\n', '\n'..indent)
end

local function wrap_text (text, max_width)
	-- Creates a wrapped version of given `text`, with no lines longer than
	-- `max_width`. Splits only at whitespace.

	-- TODO: Fix this.

	local l, i, last_i  =  {}, max_width, 1
	repeat
		if text:sub(i, i) == ' ' then
			l[#l+1], last_i, i  =  text:sub(last_i, i - 1), i + 1, i + max_width
		elseif i <= last_i then
			i = text:find(' ', last_i) or #text
		else
			i = i - 1
		end
	until i >= #text
	l[#l+1] = text:sub(last_i)
	return table.concat(l, '\n')
end

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

return function (value, display, l, format_value)
	assert(type(value) == 'function')
	assert(type(display) == 'number' and type(l) == 'table' and type(format_value) == 'function')

    local info = get_function_info(value)

	local function_params, function_body  =  nil, '...'

	if not info.docs and info.defined_how ~= 'C' and (display == DISPLAY.EXPAND or info.defined_how == 'string') then
		-- Only look for documentation, when at display = DISPLAY.EXPAND, or when defined in
		-- string. We don't want to open a ton of files constantly when
		-- formatting a table.
		info = get_function_body_info(info)

		if info.body and #info.body <= NR_CHARS_IN_LONG_FUNCTION_BODY and not info.body:find '\n' and not info.body:find(FUNCTION_KEYWORD_MATCH) then
			if info.defined_how == 'string' then  function_body  =  info.body  end
		end
	end

	if info.builtin and display == DISPLAY.HIDE then
		assert(info.name)
		return l(info.name);
	end

    -- Include function modifier, and alignment info.
    l[#l+1] = info.builtin and 'builtin ' or ''
    l[#l+1] = { 'align', 'func_mod', #l[#l]}

    -- Build rest of function signature
	l[#l+1] = 'function '
	local top_before = #l
	if function_params then
		l[#l+1] = function_params
	else
		l[#l+1] = '('
	    for _, param in ipairs(info.params) do  l[#l+1], l[#l+2] = param, ', '  end
	    if l[#l] == ', ' then  l[#l] = nil  end
	    l[#l+1] = ')'
	end
    l[#l+1] = { 'align', 'func_def', width_of_strings_in_l(l, top_before) }

    -- Cleanup and finish
    if display ~= DISPLAY.EXPAND  then
		l[#l+1] = (function_body:sub(1,1) == '\n') and '' or ' '
		l[#l+1] = function_body
		l[#l+1] = { 'align', 'func_end', utf8_string_length(function_body) }
		l[#l+1] = (function_body:sub(-1) == '\n' or function_body == '') and '' or ' '
		return l 'end'
    end

	-- More info! --

	local indent = '\n' .. l.options.indent

	-- Name
    if info.name then
        l[#l+1] = indent
        l[#l+1] = '-- '
        l[#l+1] = info.name
    end

	-- Doc
    if info.docs then
		l[#l+1] = '\n'
		local indent  =  l.options.indent .. '-- '
		local docs    =  not info.builtin and info.docs or wrap_text(info.docs, 80 - #indent)
		l[#l+1] = add_indent_to_string(docs, indent)
    end

    -- source
	if info.docs or info.name then -- Do nothing
	elseif info.defined_how == 'string' then
		l[#l+1] = indent
		l[#l+1] = '-- Loaded from string'
    elseif not info.builtin then
        l[#l+1] = indent
        l[#l+1] = ('-- Source file: \'%s\' '):format(info.short_src)
        if info.linedefined == info.lastlinedefined then
			l[#l+1] = ('[Line: %i]'):format(info.linedefined)
        else
			l[#l+1] = ('[Lines: %i - %i]'):format(info.linedefined, info.lastlinedefined)
        end
    end

    -- upvalues
    if info.nups > 0 and (not info.builtin and not info.docs) then
        l[#l+1] = indent
        l[#l+1] = '-- Up values:'
        for up_name, up_value in pairs(info.ups) do
	        l[#l+1] = indent
	        l[#l+1] = '--   '
	        l[#l+1] = up_name
	        l[#l+1] = ' = '
			format_value(up_value, DISPLAY.HIDE, l)
		end
    end

	-- Ignore spacing and function body if it's a Λ string.
	if function_body ~= '' then
	    l[#l+1] = '\n'
		l[#l+1] = indent
		l[#l+1] = function_body
	end
    l[#l+1] = '\nend'
end