--- Text processing utilities.
--
-- 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.
--
-- See  @{03-strings.md.String_Templates|the Guide}.
--
-- Calling `text.format_operator()` 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.
--
--  > require 'tagen.text'.format_operator()
--  > = '%s = %5.3f' % {'PI',math.pi}
--  PI = 3.142
--  > = '$name = $value' % {name='dog',value='Pluto'}
--  dog = Pluto
--
-- Dependencies: `tagen.core`
-- @module tagen.text

local gsub = string.gsub
local concat,append = table.concat,table.insert
local tagen = require 'tagen.core'
local bind1,usplit,assert_arg,is_callable = tagen.bind1,tagen.split,tagen.assert_arg,tagen.is_callable

local function lstrip(str)  return (str:gsub('^%s+',''))  end
local function strip(str)  return (lstrip(str):gsub('%s+$','')) end
local function make_list(l)  return setmetatable(l,tagen.stdmt.List) end
local function split(s,delim)  return make_list(usplit(s,delim)) end

local function imap(f,t,...)
  local res = {}
  for i = 1,#t do res[i] = f(t[i],...) end
  return res
end

--[[
module ('tagen.text',tagen._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 = imap(string.sub,sl,i2+1)
  return concat(sl,'\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,line = {}
  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
    append(lines,strip(line))
  end
  return make_list(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 concat(text.wrap(s,width),'\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 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 <a href="http://lua-users.org/wiki/StringInterpolation">the lua-users wiki</a>) --

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

  local function basic_subst(s,t)
    return (s:gsub('%$([%w_]+)',t))
  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
