--- Text processing utilities. <p>
-- This provides a Template class (modeled after the same from the Python 
-- libraries, see string.Template). It also provides similar functions to those
-- found in the textwrap module.
-- <p>
-- Calling <code>text.format_operator()</code> overloads the % operator for strings to give Python/Ruby style formated output. 
-- This is extended to also do template-like substitution for map-like data.
-- <pre class=example>
-- > require 'pl.text'.format_operator()
-- > = '%s = %5.3f' % {'PI',math.pi}
-- PI = 3.142
-- > = '$name = $value' % {name='dog',value='Pluto'}
-- dog = Pluto
-- </pre>
-- @class module
-- @name pl.text

local gsub = string.gsub
local stringx = require 'pl.stringx'
local concat = table.concat
local imap = require 'pl.tablex'.imap
local utils = require 'pl.utils'
local bind1 = utils.bind1
local split = stringx.split
local List = require 'pl.list'.List
local lstrip,strip = stringx.lstrip,stringx.strip
local assert_arg = utils.assert_arg

--[[
module ('pl.text',utils._module)
]]

local text = {}

local function _indent (s,sp)
    local sl = split(s,'\n')
    return concat(imap(bind1('..',sp),sl),'\n')..'\n'
end

--- indent a multiline string.
-- @param s the string
-- @param n the size of the indent
-- @param ch the character to use when indenting (default ' ')
-- @return indented string
function text.indent (s,n,ch)
    assert_arg(1,s,'string')
    assert_arg(2,s,'number')
    return _indent(s,string.rep(ch or ' ',n))
end

--- dedent a multiline string by removing any initial indent.
-- useful when working with [[..]] strings.
-- @param s the string
-- @return a string with initial indent zero.
function text.dedent (s)
    assert_arg(1,s,'string')
    local sl = split(s,'\n')
    local i1,i2 = sl[1]:find('^%s*')
    sl = sl:map(string.sub,i2+1)
    return sl:concat('\n')..'\n'
end

--- format a paragraph into lines so that they fit into a line width.
-- It will not break long words, so lines can be over the length
-- to that extent.
-- @param s the string
-- @param width the margin width, default 70
-- @return a list of lines
function text.wrap (s,width)
    assert_arg(1,s,'string')
    width = width or 70
    s = s:gsub('\n',' ')
    local i,nxt = 1
    local lines = List()
    while i < #s do
        nxt = i+width
        if s:find("[%w']",nxt) then -- inside a word
            nxt = s:find('%W',nxt+1) -- so find word boundary
        end
        line = s:sub(i,nxt)
        i = i + #line
        lines:append(strip(line))
    end
    return lines
end

--- format a paragraph so that it fits into a line width.
-- @param s the string
-- @param width the margin width, default 70
-- @return a string
-- @see wrap
function text.fill (s,width)
    return text.wrap(s,width):concat '\n' .. '\n'
end

local Template = {}
text.Template = Template
Template.__index = Template
setmetatable(Template, {
    __call = function(obj,tmpl)
        return Template.new(tmpl)
    end})

function Template.new(tmpl)
    assert_arg(1,tmpl,'string')
    local res = {}
    res.tmpl = tmpl
    setmetatable(res,Template)
    return res
end

local function _substitute(s,tbl,safe)
    local subst
    if utils.is_callable(tbl) then
        subst = tbl
    else
        function subst(f)
            local s = tbl[f]
            if not s then
                if safe then
                    return f
                else
                    error("not present in table "..f)
                end
            else
                return s
            end
        end
    end
    local res = gsub(s,'%${([%w_]+)}',subst)
    return (gsub(res,'%$([%w_]+)',subst))
end

--- substitute values into a template, throwing an error.
-- This will throw an error if no name is found.
-- @param tbl a table of name-value pairs.
function Template:substitute(tbl)
    assert_arg(1,tbl,'table')
    return _substitute(self.tmpl,tbl,false)
end

--- substitute values into a template.
-- This version just passes unknown names through.
-- @param tbl a table of name-value pairs.
function Template:safe_substitute(tbl)
    assert_arg(1,tbl,'table')
    return _substitute(self.tmpl,tbl,true)
end

--- substitute values into a template, preserving indentation. <br>
-- If the value is a multiline string _or_ a template, it will insert
-- the lines at the correct indentation. <br>
-- Furthermore, if a template, then that template will be subsituted
-- using the same table.
-- @param tbl a table of name-value pairs.
function Template:indent_substitute(tbl)
    assert_arg(1,tbl,'table')
    if not self.strings then
        self.strings = split(self.tmpl,'\n')
    end
    -- the idea is to substitute line by line, grabbing any spaces as
    -- well as the $var. If the value to be substituted contains newlines,
    -- then we split that into lines and adjust the indent before inserting.
    local function subst(line)
        return line:gsub('(%s*)%$([%w_]+)',function(sp,f)
			local subtmpl
            local s = tbl[f]
            if not s then error("not present in table "..f) end
			if getmetatable(s) == Template then
				subtmpl = s
				s = s.tmpl
			else
				s = tostring(s)
			end
            if s:find '\n' then
                s = _indent(s,sp)
            end
			if subtmpl then return _substitute(s,tbl)
			else return s
			end
        end)
    end
    local lines = imap(subst,self.strings)
    return concat(lines,'\n')..'\n'
end

------- Python-style formatting operator ------
-- (see http://lua-users.org/wiki/StringInterpolation) --

function text.format_operator()

    local format = string.format

    -- a more forgiving version of string.format, which applies
    -- tostring() to any value with a %s format.
    local function formatx (fmt,...)
        local args = {...}
        local i = 1
        for p in fmt:gmatch('%%.') do
            if p == '%s' and type(args[i]) ~= 'string' then
                args[i] = tostring(args[i])
            end
            i = i + 1
        end
        return format(fmt,unpack(args))
    end

    -- Note this goes further than the original, and will allow these cases:
    -- 1. a single value
    -- 2. a list of values
    -- 3. a map of var=value pairs
    -- 4. a function, as in gsub
    -- For the second two cases, it uses $-variable substituion.
    getmetatable("").__mod = function(a, b)
        if b == nil then
            return a
        elseif type(b) == "table" and getmetatable(b) == nil then
            if #b == 0 then -- assume a map-like table
                return _substitute(a,b,true)
            else
                return formatx(a,unpack(b))
            end
        elseif type(b) == 'function' then
            return basic_subst(a,b)
        else
            return formatx(a,b)
        end
    end
end


return text
