#!/usr/bin/env texlua
local LIBRARY_MODE = (... == "comment2tex")

local M = {}

M.styles = {
  bash = { comment = "##",  language = "bash" },
  lua  = { comment = "---", language = "{[5.3]Lua}" },
}

M.wrappers = {
  lstlisting = {
    begin  = "\\begin{lstlisting}[language=@LANG@,@CONT@numbers=left]",
    finish = "\\end{lstlisting}",
  },
  plain = {
    begin  = "\\ctxlisting%",
    finish = "\\endctxlisting",
  },
}

M.defaults = {
  style    = "bash",
  wrapper  = "lstlisting",
  comment  = nil,
  language = nil,
  begin    = nil,
  finish   = nil,
}

function M.new_opts(over)
  local o = {}
  for k, v in pairs(M.defaults) do o[k] = v end
  if over then
    for k, v in pairs(over) do
      if v ~= nil then o[k] = v end
    end
  end
  return o
end

function M.resolve(o)
  local style = M.styles[o.style]
  if not style then
    error("comment2tex: unknown style: " .. tostring(o.style) .. " (expected bash or lua)")
  end
  local wrapper = M.wrappers[o.wrapper]
  if not wrapper then
    error("comment2tex: unknown wrapper: " .. tostring(o.wrapper) .. " (expected lstlisting or plain)")
  end
  o.comment  = o.comment  or style.comment
  o.language = o.language or style.language
  o.begin    = o.begin    or wrapper.begin
  o.finish   = o.finish   or wrapper.finish
  return o
end

function M.convert(o, lines, emit)
  local prefix = o.comment
  local plen = #prefix
  local in_code = false
  local block_count = 0

  local function open_code()
    if not in_code then
      block_count = block_count + 1
      local cont = block_count == 1 and "" or "firstnumber=last,"
      local line = o.begin:gsub("@LANG@", o.language):gsub("@CONT@", cont)
      emit(line)
      in_code = true
    end
  end

  local function close_code()
    if in_code then
      emit(o.finish)
      in_code = false
    end
  end

  for _, line in ipairs(lines) do
    if line:sub(1, plen) == prefix then
      close_code()
      emit((line:sub(plen + 1):gsub("^ ", "")))
    else
      open_code()
      emit(line)
    end
  end
  close_code()
end

function M.read_lines(path)
  local fh, err = io.open(path, "r")
  if not fh then error("comment2tex: cannot open input: " .. tostring(err)) end
  local data = fh:read("*a")
  fh:close()
  local lines = {}
  for line in (data .. "\n"):gmatch("(.-)\n") do
    lines[#lines + 1] = line
  end
  if data:sub(-1) == "\n" then
    lines[#lines] = nil
  end
  return lines
end

function M.convert_file(path, o)
  o = M.resolve(o or M.new_opts())
  local out = {}
  M.convert(o, M.read_lines(path), function(line) out[#out + 1] = line end)
  return table.concat(out, "\n") .. "\n"
end

function M.write_file(infile, outfile, o)
  local text = M.convert_file(infile, o)
  local fh, err = io.open(outfile, "w")
  if not fh then error("comment2tex: cannot open output: " .. tostring(err)) end
  fh:write(text)
  fh:close()
  return outfile
end

function M.write(style, wrapper, infile, outfile)
  return M.write_file(infile, outfile, M.new_opts{ style = style, wrapper = wrapper })
end

local function usage(stream)
  stream:write([[
Usage: comment2tex.lua [options] <input>

Convert a source file with embedded LaTeX doc-comments to LaTeX.

Options:
  -s, --style NAME       preset: bash (##) or lua (---)        [default: bash]
  -w, --wrapper NAME     listing wrapper: lstlisting or plain  [default: lstlisting]
  -c, --comment PREFIX   doc-comment prefix marking a doc line
  -l, --language LANG    listing language for code blocks
  -b, --begin TEMPLATE   listing begin template (@LANG@, @CONT@)
  -e, --end TEMPLATE     listing end template
  -o, --output FILE      write LaTeX here instead of stdout
  -h, --help             show this help

Templates substitute @LANG@ with the language and @CONT@ with
"firstnumber=last," on continuation blocks (empty on the first).
]])
end

local function die(msg)
  io.stderr:write("comment2tex: " .. msg .. "\n")
  os.exit(1)
end

function M.main(argv)
  local over = {}
  local input
  local i = 1
  local function value(flag)
    i = i + 1
    local v = argv[i]
    if v == nil then die("missing value for " .. flag) end
    return v
  end
  while i <= #argv do
    local a = argv[i]
    if a == "-h" or a == "--help" then
      usage(io.stdout); return 0
    elseif a == "-s" or a == "--style" then
      over.style = value(a)
    elseif a == "-w" or a == "--wrapper" then
      over.wrapper = value(a)
    elseif a == "-c" or a == "--comment" then
      over.comment = value(a)
    elseif a == "-l" or a == "--language" then
      over.language = value(a)
    elseif a == "-b" or a == "--begin" then
      over.begin = value(a)
    elseif a == "-e" or a == "--end" then
      over.finish = value(a)
    elseif a == "-o" or a == "--output" then
      over.output = value(a)
    elseif a == "--" then
      input = argv[i + 1]; break
    elseif a:sub(1, 1) == "-" and a ~= "-" then
      die("unknown option: " .. a)
    elseif input == nil then
      input = a
    else
      die("unexpected argument: " .. a)
    end
    i = i + 1
  end

  if not input then
    usage(io.stderr); return 1
  end

  local ok, err = pcall(function()
    local o = M.resolve(M.new_opts(over))
    local text = M.convert_file(input, o)
    if over.output then
      M.write_file(input, over.output, o)
    else
      io.stdout:write(text)
    end
  end)
  if not ok then die(tostring(err):gsub("^comment2tex: ", "")) end
  return 0
end

if not LIBRARY_MODE then
  os.exit(M.main(arg))
end

return M
