local ipairs, load, pairs, type = ipairs, load, pairs, type
local debug, string, table = debug, string, table
local _G = _G

_ENV = pandoc

local stringify = utils.stringify

local get = function (fieldname)
  return function (obj) return obj[fieldname] end
end

local function read_blocks (txt)
  return read(txt, 'commonmark').blocks
end

local function read_inlines (txt)
  return utils.blocks_to_inlines(read_blocks(txt))
end

local function argslist (parameters)
  local required = List{}
  local optional = List{}
  for i, param in ipairs(parameters) do
    if param.optional then
      optional:insert(param.name)
    else
      required:extend(optional)
      required:insert(param.name)
      optional = List{}
    end
  end
  if #optional == 0 then
    return table.concat(required, ', ')
  end
  return table.concat(required, ', ')
    .. '[, ' .. table.concat(optional, '[, ') .. string.rep(']', #optional)
end

local function render_results (results)
  if type(results) == 'string' then
    return read_blocks(results)
  elseif type(results) == 'table' then
    return {BulletList(
      List(results):map(
        function (res)
          -- Types starting with a capital letter are pandoc types, so we can
          -- link them.
          local type_ = res.type:match'^[A-Z]'
            and Link(res.type, '#type-' .. res.type:lower())
            or Str(res.type)
          return Para(
            read_inlines(res.description)
            .. {Space()}
            .. Inlines '(' .. Inlines{type_} .. Inlines ')'
          )
        end
      )
    )}
  else
    return Blocks{}
  end
end

local function render_function (doc, level, modulename)
  local name = doc.name
  level = level or 1
  local id = modulename and modulename .. '.' .. doc.name or ''
  local args = argslist(doc.parameters)
  local paramlist = DefinitionList(
    List(doc.parameters):map(
      function (p)
        return {
          {Code(p.name)},
          {read_blocks(p.description .. ' (' .. p.type .. ')')}
        }
      end
    )
  )
  return Blocks{
    Header(level, name, {id}),
    Plain{Code(string.format('%s (%s)', name, args))},
  } .. read_blocks(doc.description)
    .. List(#doc.parameters > 0 and {Para 'Parameters'} or {})
    .. List{paramlist}
    .. List(#doc.results > 0 and {Para 'Returns'} or {})
    .. render_results(doc.results)
end

local function render_field (field, level, modulename)
  local id = modulename and modulename .. '.' .. field.name or ''
  return {Header(level, field.name, {id})} .. read_blocks(field.description)
end

local function render_module (doc)
  local fields = Blocks{}
  if #doc.fields then
    fields:insert(Header(2, 'Fields', {doc.name .. '-' .. 'fields'}))
    for i, fld in ipairs(doc.fields) do
      fields:extend(render_field(fld, 3, doc.name))
    end
  end

  local functions = Blocks{}
  if #doc.functions > 0 then
    functions:insert(Header(2, 'Functions', {doc.name .. '-' .. 'functions'}))
    for i, fun in ipairs(doc.functions) do
      functions:extend(render_function(fun, 3, doc.name))
    end
  end

  return Blocks{
    Header(1, Inlines('Module ' .. doc.name), {'module-' .. doc.name})
  } .. read_blocks(doc.description) .. fields .. functions
end

--- Retrieves the documentation object for the given value.
local function documentation (value)
  return debug.getregistry()['HsLua docs'][value]
end

local function get_module_name(header)
  return stringify(header):match 'Module pandoc%.([%w]*)'
end

--- Set of modules for which documentation should be generated.
local handled_modules = {
  layout = true
}

return {{
    Pandoc = function (doc)
      local blocks = List{}
      local in_module_docs = false
      for i, blk in ipairs(doc.blocks) do
        if blk.t == 'Header' and blk.level == 1 then
          local module_name = get_module_name(blk)
          if module_name and handled_modules[module_name] then
            local object = _ENV[module_name]
            blocks:extend(render_module(documentation(object)))
            in_module_docs = true
          else
            blocks:insert(blk)
            in_module_docs = false
          end
        elseif not in_module_docs then
          blocks:insert(blk)
        end
      end
      return Pandoc(blocks, doc.meta)
    end
}}