--[[------------------------------------------------------

  # Inspector

  The Inspector 'knows' about the functions and classes
  and can answer queries.

--]]------------------------------------------------------
local lub     = require 'lub'
local dub     = require 'dub'
local lib     = lub.class 'dub.Inspector'
local private = {}


-- Create a new inspector by providing sources to scan. All upercase fields in
-- `opts` correspond to options used by Doxygen. The `opts` table can
-- contain the following keys (keys in parenthesis are optional):
--
-- + INPUT         : List of source files separated by spaces (doxygen format). The
--                   sources can also be passed as a table.
-- + (html)        : Generate html documentation along the way (implies `keep_xml`).
-- + (keep_xml)    : Keep xml generated by Doxygen (used for debugging purposes).
-- + (Doxyfile)    : Custom Doxyfile to pass to Doxygen parser.
-- + (PREDEFINED)  : Defines to use by Doxygen during parsing (usually to remove
--                   unwanted clutter from "ATTRIBUTE_ALIGNED16" or
--                   "SIMD_FORCE_INLINE" kind of macros.
-- + (EXCLUDE_PATTERNS) : Doxygen option to exclude files and/or directories.
-- + (FILE_PATTERNS) : Doxygen option for header file name patterns such as `*.h *.hpp`.
-- + (doc_dir)     : Directory to store generated xml and html.
-- + (ignore)      : List of root level classes or functions to ignore.
function lib.new(opts)
  local self = {db = dub.MemoryStorage()}
  setmetatable(self, lib)
  private.parse(self, opts)
  return self
end

-- The command used to call doxygen.
lib.DOXYGEN_CMD = 'doxygen'


-- Return a class or function from its name. A class in a namespace is
-- queried with 'foo::Bar'. Uses dub.MemoryStorage.findByFullname.
function lib:find(name)
  -- A 'child' of the Inspector can be anything so we
  -- have to walk through the files to find what we
  -- are asked for.
  -- Object lives at the root of the name space.
  return self.db:findByFullname(name)
end

-- Return an interator on all children known to the inspector (can be a function
-- or class). Uses dub.MemoryStorage.children.
function lib:children()
  return self.db:children()
end

-- Try to follow typedefs to resolve a type. Uses dub.MemoryStorage.resolveType
-- internally.
function lib:resolveType(name)
  return self.db:resolveType(self.db, name)
end


--=============================================== PRIVATE

-- Add xml headers to the database. If `not_lazy` is set, parse everything
-- directly (not when needed).
function private:parseXml(xml_dir, not_lazy, ignore_list)
  self.db:parse(xml_dir, not_lazy, ignore_list)
end

function private:parse(opts)
  if type(opts) == 'string' then
    opts = {INPUT = opts}
  end
  assert(opts and opts.INPUT, "Missing 'INPUT' field")

  if not opts.FILE_PATTERNS then
    opts.FILE_PATTERNS = [[
      *.c \
      *.cc \
      *.cxx \
      *.cpp \
      *.c++ \
      *.d \
      *.java \
      *.ii \
      *.ixx \
      *.ipp \
      *.i++ \
      *.inl \
      *.h \
      *.H \
      *.hh \
      *.hxx \
      *.hpp \
      *.h++ \
      *.idl \
      *.odl \
      *.cs \
      *.php \
      *.php3 \
      *.inc \
      *.m \
      *.mm \
      *.dox \
      *.py \
      *.f90 \
      *.f \
      *.for \
      *.vhd \
      *.vhdl
    ]]
  end

  if opts.html then
    opts.GENERATE_HTML = 'YES'
    opts.keep_xml = true
  end

  local doc_dir = opts.doc_dir
  if opts.keep_xml and not doc_dir then
    doc_dir = 'dub-doc'
  elseif not doc_dir then
    doc_dir = 'dub-tmp'
    local i = 0
    while true do
      if lub.exist(doc_dir) then
        i = i + 1
        doc_dir = string.format('dub-tmp-%i', i)
      else
        break
      end
    end
  end

  private.execute('mkdir -p ' .. doc_dir)

  local doxypath = opts.Doxyfile
  if not doxypath then
    doxypath = doc_dir .. '/Doxyfile'
    local doxyfile = io.open(doxypath, 'w')

    local doxytemplate = lub.Template {path = lub.path('|assets/Doxyfile')}
    if type(opts.INPUT) == 'table' then
      opts.INPUT = lub.join(opts.INPUT, ' ')
    end
    if not opts.EXCLUDE_PATTERNS then
      opts.EXCLUDE_PATTERNS = ''
    elseif type(opts.EXCLUDE_PATTERNS) == 'table' then
      opts.EXCLUDE_PATTERNS = lub.join(opts.EXCLUDE_PATTERNS, ' ')
    end
    if type(opts.FILE_PATTERNS) == 'table' then
      opts.FILE_PATTERNS = lub.join(opts.FILE_PATTERNS, ' ')
    end
    if type(opts.PREDEFINED) == 'table' then
      opts.PREDEFINED = lub.join(opts.PREDEFINED, ' \\\n                         ')
    end

    -- Generate Doxyfile
    doxyfile:write(doxytemplate:run({doc_dir = doc_dir, opts = opts}))
    doxyfile:close()
  end

  -- Generate xml
  private.execute(self.DOXYGEN_CMD .. ' ' .. doxypath)
  -- Parse xml
  private.parseXml(self, doc_dir .. '/xml', true, opts.ignore)
  if not opts.keep_xml then
    if not opts.doc_dir then
      lub.rmTree(doc_dir, true)
    else
      lub.rmTree(doc_dir .. '/xml', true)
    end
  end
end

function private.execute(cmd)
  os.execute(cmd)
end

return lib
