-- (c) 2009-2011 John MacFarlane.  Released under MIT license.
-- See the file LICENSE in the source for details.

--- Utility functions for lunamark.

local M = {}
local cosmo = require("cosmo")
local rep  = string.rep
local lpeg = require("lpeg")
local Cs, P, S, lpegmatch = lpeg.Cs, lpeg.P, lpeg.S, lpeg.match
local any = lpeg.P(1)

--- Find a template and return its contents (or `false` if
-- not found). The template is sought first in the
-- working directory, then in `templates`, then in
-- `$HOME/.lunamark/templates`, then in the Windows
-- `APPDATA` directory.
function M.find_template(name)
  local base, ext = name:match("([^%.]*)(.*)")
  local fname = base .. ext
  local file = io.open(fname, "read")
  if not file then
    file = io.open("templates/" .. fname, "read")
  end
  if not file then
    local home = os.getenv("HOME")
    if home then
      file = io.open(home .. "/.lunamark/templates/" .. fname, "read")
    end
  end
  if not file then
    local appdata = os.getenv("APPDATA")
    if appdata then
      file = io.open(appdata .. "/lunamark/templates/" .. fname, "read")
    end
  end
  if file then
    local data = file:read("*all")
    file:close()
    return data
  else
    return false, "Could not find template '" .. fname .. "'"
  end
end

--- Implements a `sepby` directive for cosmo templates.
-- `$sepby{ myarray }[[$it]][[, ]]` will render the elements
-- of `myarray` separated by commas. If `myarray` is a string,
-- it will be treated as an array with one element.  If it is
-- `nil`, it will be treated as an empty array.
function M.sepby(arg)
  local a = arg[1]
  if not a then
    a = {}
  elseif type(a) ~= "table" then
    a = {a}
  end
  for i in ipairs(a) do
     if i > 1 then cosmo.yield{_template=2} end
     cosmo.yield{it = a[i], _template=1}
  end
end

--[[
-- extend(t) returns a table that falls back to t for non-found values
function M.extend(prototype)
  local newt = {}
  local metat = { __index = function(t,key)
                              return prototype[key]
                            end }
  setmetatable(newt, metat)
  return newt
end
--]]

--- Print error message and exit.
function M.err(msg, exit_code)
  io.stderr:write("lunamark: " .. msg .. "\n")
  os.exit(exit_code or 1)
end

--- Shallow table copy including metatables.
function M.table_copy(t)
  local u = { }
  for k, v in pairs(t) do u[k] = v end
  return setmetatable(u, getmetatable(t))
end

--- Expand tabs in a line of text.
-- `s` is assumed to be a single line of text.
-- From *Programming Lua*.
function M.expand_tabs_in_line(s, tabstop)
  local tab = tabstop or 4
  local corr = 0
  return (s:gsub("()\t", function(p)
          local sp = tab - (p - 1 + corr)%tab
          corr = corr - 1 + sp
          return rep(" ",sp)
        end))
end

--- Walk a rope `t`, applying a function `f` to each leaf element in order.
-- A rope is an array whose elements may be ropes, strings, numbers,
-- or functions. If a leaf element is a function, call it and get the return value
-- before proceeding.
local function walk(t, f)
    local typ = type(t)
    if typ == "string" then
      f(t)
    elseif typ == "table" then
      local i = 1
      local n
      n = t[i]
      while n do
        walk(n, f)
        i = i + 1
        n = t[i]
      end
    elseif typ == "function" then
      local ok, val = pcall(t)
      if ok then
        walk(val,f)
      end
    else
      f(tostring(t))
    end
end

M.walk = walk

--- Flatten an array `ary`.
local function flatten(ary)
  local new = {}
  for _,v in ipairs(ary) do
    if type(v) == "table" then
      for _,w in ipairs(flatten(v)) do
        new[#new + 1] = w
      end
    else
      new[#new + 1] = v
    end
  end
  return new
end

M.flatten = flatten

--- Convert a rope to a string.
local function rope_to_string(rope)
  local buffer = {}
  walk(rope, function(x) buffer[#buffer + 1] = x end)
  return table.concat(buffer)
end

M.rope_to_string = rope_to_string

-- assert(rope_to_string{"one","two"} == "onetwo")
-- assert(rope_to_string{"one",{"1","2"},"three"} == "one12three")

--- Return the last item in a rope.
local function rope_last(rope)
  if #rope == 0 then
    return nil
  else
    local l = rope[#rope]
    if type(l) == "table" then
      return rope_last(l)
    else
      return l
    end
  end
end

-- assert(rope_last{"one","two"} == "two")
-- assert(rope_last{} == nil)
-- assert(rope_last{"one",{"2",{"3","4"}}} == "4")

M.rope_last = rope_last

--- Given an array `ary`, return a new array with `x`
-- interspersed between elements of `ary`.
function M.intersperse(ary, x)
  local new = {}
  local l = #ary
  for i,v in ipairs(ary) do
    local n = #new
    new[n + 1] = v
    if i ~= l then
      new[n + 2] = x
    end
  end
  return new
end

--- Given an array `ary`, return a new array with each
-- element `x` of `ary` replaced by `f(x)`.
function M.map(ary, f)
  local new = {}
  for i,v in ipairs(ary) do
    new[i] = f(v)
  end
  return new
end

--- Given a table `char_escapes` mapping escapable characters onto
-- their escaped versions and optionally `string_escapes` mapping
-- escapable strings (or multibyte UTF-8 characters) onto their
-- escaped versions, returns a function that escapes a string.
-- This function uses lpeg and is faster than gsub.
function M.escaper(char_escapes, string_escapes)
  local char_escapes_list = ""
  for i,_ in pairs(char_escapes) do
    char_escapes_list = char_escapes_list .. i
  end
  local escapable = S(char_escapes_list) / char_escapes
  if string_escapes then
    for k,v in pairs(string_escapes) do
      escapable = P(k) / v + escapable
    end
  end
  local escape_string = Cs((escapable + any)^0)
  return function(s)
    return lpegmatch(escape_string, s)
  end
end

--- Given a Roman number in uppercase, return its decimal value.
function M.roman_to_number(roman)
  -- Logic from https://rosettacode.org/wiki/Roman_numerals/Decode#Lua
  -- Assuming we don't need to go so far.
  local romans = { ["L"] = 50, ["X"] = 10, ["V"] = 5, ["I"] = 1 }
  local numeral = 0

  local i = 1
  local len = string.len(roman)
  while i < len do
    local z1, z2 = romans[ string.sub(roman, i, i) ], romans[ string.sub(roman, i+1, i+1) ]
    if z1 < z2 then
        numeral = numeral + (z2 - z1)
        i = i + 2
    else
        numeral = numeral + z1
        i = i + 1
    end
  end
  if i <= len then numeral = numeral + romans[ string.sub(roman,i,i) ] end
  return numeral
end

--- Given an ordered list prefix (e.g. "1."", "iv."", "a)", return the
-- the value converted to decimal, the numbering scheme and the end delimiter.
-- The two latter are mapped to symbolic names loosely taken from Pandoc.
function M.order_to_numberstyle(listprefix)
  local numstr, delimend = listprefix:match("^([A-Za-z0-9]*)([.)]*)")
  local numdelim
  if delimend == ")" then
    numdelim = "OneParen"
  elseif delimend == "." then
    numdelim = "Period"
  else
    numdelim = "Default"
  end
  numstr = numstr or listprefix -- Just for robustness if we got a weird input...

  local num
  num = numstr:match("^([IVXL]+)")
  if num then
    return M.roman_to_number(num), "UpperRoman", numdelim
  end
  num = numstr:match("^([ivxl]+)")
  if num then
    return M.roman_to_number(string.upper(num)), "LowerRoman", numdelim
  end
  num = numstr:match("^([A-Z])") -- One is mad to need more that 26 items here.
  if num then
    return string.byte(num) - string.byte("A") + 1, "UpperAlpha", numdelim
  end
  num = numstr:match("^([a-z])") -- Idem.
  if num then
    return string.byte(num) - string.byte("a") + 1, "LowerAlpha", numdelim
  end
  return math.floor(tonumber(numstr) or 1), "Decimal", numdelim
end

return M
