-- Copyright 2007-2013 Mitchell mitchell.att.foicica.com. See LICENSE.

-- Adeptsense doclet for LuaDoc.
-- This module is used by LuaDoc to create an adeptsense for Lua with a fake
-- ctags file and an api file.
-- To preserve formatting, the included *luadoc.patch* file must be applied to
-- your instance of LuaDoc. It will not affect the look of HTML web pages, only
-- the look of plain-text Adeptsense api files.
-- Since LuaDoc does not recognize module fields, this doclet parses the Lua
-- modules for comments of the form "-- * `field_name`" to generate a field tag
-- and apidoc. Multiple line comments for fields must be indented flush with
-- `field_name` (3 spaces).
-- @usage luadoc -d [output_path] -doclet path/to/adeptsensedoc [file(s)]
local M = {}

local CTAGS_FMT = '%s\t_\t0;"\t%s\t%s'
local string_format = string.format

-- Writes a ctag.
-- @param file The file to write to.
-- @param name The name of the tag.
-- @param k The kind of ctag. Lua adeptsense uses 4 kinds: m Module, f Function,
--   t Table, and F Field.
-- @param ext_fields The ext_fields for the ctag.
local function write_tag(file, name, k, ext_fields)
  if type(ext_fields) == 'table' then
    ext_fields = table.concat(ext_fields, '\t')
  end
  file[#file + 1] = string_format(CTAGS_FMT, name, k, ext_fields)
end

-- Sanitizes Markdown from the given documentation string by stripping links and
-- replacing HTML entities.
-- @param s String to sanitize Markdown from.
-- @return string
local function sanitize_markdown(s)
  return s:gsub('%[([^%]\r\n]+)%]%b[]', '%1') -- [foo][]
          :gsub('%[([^%]\r\n]+)%]%b()', '%1') -- [foo](bar)
          :gsub('\r?\n\r?\n%[([^%]\r\n]+)%]:[^\r\n]+', '') -- [foo]: bar
          :gsub('\r?\n%[([^%]\r\n]+)%]:[^\r\n]+', '') -- [foo]: bar
          :gsub('&([%a]+);', {quot = '"', apos = "'"})
end

-- Writes a function or field apidoc.
-- @param file The file to write to.
-- @param m The LuaDoc module object.
-- @param b The LuaDoc block object.
local function write_apidoc(file, m, b)
  -- Function or field name.
  local name = b.name
  if not name:find('[%.:]') then name = m.name..'.'..name end
  -- Block documentation for the function or field.
  local doc = {}
  -- Function arguments or field type.
  local class = b.class
  local header = name
  if class == 'function' then
    header = header..(b.param and '('..table.concat(b.param, ', ')..')' or '')
  elseif class == 'field' and b.description:find('^%s*%b()') then
    header = header..' '..b.description:match('^%s*(%b())')
  elseif class == 'module' or class == 'table' then
    header = header..' ('..class..')'
  end
  doc[#doc + 1] = header
  -- Function or field description.
  local description = b.description
  if class == 'module' then
    -- Modules usually have additional Markdown documentation so just grab the
    -- documentation before a Markdown header.
    description = description:match('^(.-)[\r\n]+#') or description
  elseif class == 'field' then
    -- Type information is already in the header; discard it in the description.
    description = description:match('^%s*%b()[\t ]*[\r\n]*(.+)$') or description
    -- Strip consistent leading whitespace.
    local indent
    indent, description = description:match('^(%s*)(.*)$')
    if indent ~= '' then description = description:gsub('\n'..indent, '\n') end
  end
  doc[#doc + 1] = sanitize_markdown(description)
  -- Function parameters (@param).
  if class == 'function' and b.param then
    for _, p in ipairs(b.param) do
      if b.param[p] and #b.param[p] > 0 then
        doc[#doc + 1] = '@param '..p..' '..sanitize_markdown(b.param[p])
      end
    end
  end
  -- Function usage (@usage).
  if class == 'function' and b.usage then
    if type(b.usage) == 'string' then
      doc[#doc + 1] = '@usage '..b.usage
    else
      for _, u in ipairs(b.usage) do doc[#doc + 1] = '@usage '..u end
    end
  end
  -- Function returns (@return).
  if class == 'function' and b.ret then
    if type(b.ret) == 'string' then
      doc[#doc + 1] = '@return '..b.ret
    else
      for _, u in ipairs(b.ret) do doc[#doc + 1] = '@return '..u end
    end
  end
  -- See also (@see).
  if b.see then
    if type(b.see) == 'string' then
      doc[#doc + 1] = '@see '..b.see
    else
      for _, s in ipairs(b.see) do doc[#doc + 1] = '@see '..s end
    end
  end
  -- Format the block documentation.
  doc = table.concat(doc, '\n'):gsub('\n', '\\n')
  file[#file + 1] = name:match('[^%.:]+$')..' '..doc
end

-- Called by LuaDoc to process a doc object.
-- @param doc The LuaDoc doc object.
function M.start(doc)
--  require('luarocks.require')
--  local profiler = require('profiler')
--  profiler.start()

  local modules, files = doc.modules, doc.files

  -- Convert module functions in the Lua luadoc into LuaDoc modules.
  local lua_luadoc = files['../modules/lua/lua.luadoc']
  if lua_luadoc then
    for _, f in ipairs(lua_luadoc.functions) do
      f = lua_luadoc.functions[f]
      local module = f.name:match('^([^%.:]+)[%.:]') or '_G'
      if not modules[module] then
        modules[#modules + 1] = module
        modules[module] = {name = module, functions = {}}
        -- For functions like file:read(), 'file' is not a module; fake it.
        if f.name:find(':') then modules[module].fake = true end
      end
      local module = modules[module]
      module.description = 'Lua '..module.name..' module.'
      module.functions[#module.functions + 1] = f.name
      module.functions[f.name] = f
    end
  end

  -- Create a map of file names to doc objects so their module names can be
  -- determined.
  local filedocs = {}
  for _, name in ipairs(files) do filedocs[name] = files[name].doc end

  -- Parse out module fields (-- * `FIELD`: doc) and insert them into the
  -- module's LuaDoc.
  for _, file in ipairs(files) do
    local module_name, field, docs
    local module_doc = filedocs[file][1]
    if module_doc and module_doc.class == 'module' then
      module_name = module_doc.name
      modules[module_name].fields = module_doc.field
    elseif module_doc then
      print('[WARN] '..file..' has no module declaration')
    end
    -- Adds the field to its module's LuaDoc.
    local function add_field()
      local doc = table.concat(docs, '\n')
      field.description = doc
      local m = modules[field.module]
      if not m then
        local name = field.module
        print('[INFO] module `'..name..'\' does not exist. Faking...')
        m = {name = name, functions = {}, fake = true}
        modules[#modules + 1] = name
        modules[name] = m
      end
      if not m.fields then m.fields = {} end
      m.fields[#m.fields + 1] = field.name
      m.fields[field.name] = field.description
      field = nil
    end
    local f = io.open(file, 'rb')
    for line in f:lines() do
      if line:find('^%-%- %* `') then
        -- New field; if another field was parsed right before this one, add
        -- the former field to its module's LuaDoc.
        if field then add_field() end
        field, docs = {}, {}
        local name, doc = line:match('^%-%- %* `([^`]+)`%s*([^\r\n]*)')
        field.module = name:match('^_G%.(.-)%.[^%.]+$') or module_name or
                       name:match('^[^%.]+')
        field.name = name:match('[^%.]+$')
        if doc ~= '' then docs[#docs + 1] = doc end
      elseif field and line:find('^%-%-%s+[^\r\n]+') then
        docs[#docs + 1] = line:match('^%-%-%s%s%s(%s*[^\r\n]+)')
      elseif field and
             (line:find('^%-%-[\r\n]*$') or line:find('^[\r\n]*$')) then
        -- End of field documentation. Add it to its module's LuaDoc.
        add_field()
      end
    end
    f:close()
  end

  -- Process LuaDoc and write the ctags and api file.
  local ctags, apidoc = {}, {}
  for _, m in ipairs(modules) do
    m = modules[m]
    local module = m.name
    if not m.fake then
      -- Tag the module and write the apidoc.
      write_tag(ctags, module, 'm', '')
      if module:find('%.') then
        -- Tag the last part of the module as a table of the first part.
        local parent, child = module:match('^(.-)%.([^%.]+)$')
        write_tag(ctags, child, 't', 'class:'..parent)
      elseif module ~= '_G' then
        -- Tag the module as a global table.
        write_tag(ctags, module, 't', '')
      end
      m.class = 'module'
      write_apidoc(apidoc, {name = '_G'}, m)
    end
    -- Tag the functions and write the apidoc.
    for _, f in ipairs(m.functions) do
      local func = f:match('[^%.:]+$')
      local ext_fields = module == '_G' and '' or 'class:'..module
      write_tag(ctags, func, 'f', ext_fields)
      write_apidoc(apidoc, m, m.functions[f])
    end
    -- Tag the tables and write the apidoc.
    for _, t in ipairs(m.tables or {}) do
      local table = m.tables[t]
      local module = module -- define locally so any modification stays local
      if t:find('^_G%.') then module, t = t:match('^_G%.(.-)%.?([^%.]+)$') end
      if not module then print(table.name) end
      local ext_fields = module == '_G' and '' or 'class:'..module
      write_tag(ctags, t, 't', ext_fields)
      write_apidoc(apidoc, m, table)
      -- Tag the fields of the tables.
      t = module..'.'..t
      for _, f in ipairs(table.field or {}) do
        write_tag(ctags, f, 'F', 'class:'..t)
        write_apidoc(apidoc, {name = t}, {
                       name = f,
                       description = table.field[f],
                       class = 'table'
                     })
      end
    end
    -- Tag the fields.
    for _, f in ipairs(m.fields or {}) do
      local field = m.fields[f]
      local module = module -- define locally so any modification stays local
      if f:find('^_G%.') then module, f = f:match('^_G%.(.-)%.?([^%.]+)$') end
      if not module then print(field.name) end
      local ext_fields = module == '_G' and '' or 'class:'..module
      write_tag(ctags, f, 'F', ext_fields)
      write_apidoc(apidoc, {name = f}, {
                     name = module..'.'..f,
                     description = field,
                     class = 'field'
                   })
    end
  end
  table.sort(ctags)
  table.sort(apidoc)
  local f = io.open(M.options.output_dir..'/tags', 'wb')
  f:write(table.concat(ctags, '\n'))
  f:close()
  f = io.open(M.options.output_dir..'api', 'wb')
  f:write(table.concat(apidoc, '\n'))
  f:close()

--  profiler.stop()
end

return M
