------------
-- Language-dependent parsing of code.
-- This encapsulates the different strategies needed for parsing C and Lua
-- source code.

local class = require 'pl.class'
local utils = require 'pl.utils'
local List =  require 'pl.List'
local tools = require 'ldoc.tools'
local lexer = require 'ldoc.lexer'
local quit = utils.quit
local tnext = lexer.skipws


local Lang = class()

function Lang:trim_comment (s)
   return s:gsub(self.line_comment,'')
end

function Lang:start_comment (v)
   local line = v:match (self.start_comment_)
   if line and self.end_comment_ and v:match (self.end_comment_) then
      return nil
   end
   local block = v:match(self.block_comment)
   return line or block, block
end

function Lang:empty_comment (v)
   return v:match(self.empty_comment_)
end

function Lang:grab_block_comment(v,tok)
   v = v:gsub(self.block_comment,'')
   return tools.grab_block_comment(v,tok,self.end_comment)
end

function Lang:find_module(tok,t,v)
   return '...',t,v
end

function Lang:item_follows(t,v)
   return false
end

function Lang:finalize()
   self.empty_comment_ = self.start_comment_..'%s*$'
end

function Lang:search_for_token (tok,type,value,t,v)
   while t and not (t == type and v == value) do
      if t == 'comment' and self:start_comment(v) then return nil,t,v end
      t,v = tnext(tok)
   end
   return t ~= nil,t,v
end

function Lang:parse_extra (tags,tok)
end

function Lang:is_module_modifier ()
   return false
end

function Lang:parse_module_modifier (tags, tok)
   return nil, "@usage or @exports deduction not implemented for this language"
end


local Lua = class(Lang)

function Lua:_init()
   self.line_comment = '^%-%-+' -- used for stripping
   self.start_comment_ = '^%-%-%-+'     -- used for doc comment line start
   self.block_comment = '^%-%-%[=*%[%-+' -- used for block doc comments
   self.end_comment_ = '[^%-]%-%-+[^-]*\n$' ---- exclude --- this kind of comment ---
   self.method_call = ':'
   self:finalize()
end

function Lua.lexer(fname)
   local f,e = io.open(fname)
   if not f then quit(e) end
   return lexer.lua(f,{}),f
end

function Lua:grab_block_comment(v,tok)
   local equals = v:match('^%-%-%[(=*)%[')
   if not equals then return v end
   v = v:gsub(self.block_comment,'')
   return tools.grab_block_comment(v,tok,'%]'..equals..'%]')
end


-- luacheck: push ignore 312
function Lua:parse_module_call(tok,t,v)
   t,v = tnext(tok)
   if t == '(' then t,v = tnext(tok) end
   if t == 'string' then -- explicit name, cool
      return v,t,v
   elseif t == '...' then -- we have to guess!
      return '...',t,v
   end
end
-- luacheck: pop

-- If a module name was not provided, then we look for an explicit module()
-- call. However, we should not try too hard; if we hit a doc comment then
-- we should go back and process it. Likewise, module(...) also means
-- that we must infer the module name.
function Lua:find_module(tok,t,v)
   local res
   res,t,v = self:search_for_token(tok,'iden','module',t,v)
   if not res then return nil,t,v end
   return self:parse_module_call(tok,t,v)
end

local function parse_lua_parameters (tags,tok)
   tags.formal_args = tools.get_parameters(tok)
   tags:add('class','function')
end

local function parse_lua_function_header (tags,tok)
   if not tags.name then
      tags:add('name',tools.get_fun_name(tok))
   end
   if not tags.name then return 'function has no name' end
   parse_lua_parameters(tags,tok)
end

local function parse_lua_table (tags,tok)
   tags.formal_args = tools.get_parameters(tok,'}',function(s)
      return s == ',' or s == ';'
   end)
end

--------------- function and variable inference -----------
-- After a doc comment, there may be a local followed by:
-- [1] (l)function: function NAME
-- [2] (l)function: NAME = function
-- [3] table: NAME = {
-- [4] field: NAME = <anything else>  (this is a module-level field)
--
-- Depending on the case successfully detected, returns a function which
-- will be called later to fill in inferred item tags
function Lua:item_follows(t,v,tok)
   local parser, case
   local is_local = t == 'keyword' and v == 'local'
   if is_local then t,v = tnext(tok) end
   if t == 'keyword' and v == 'function' then -- case [1]
      case = 1
      parser = parse_lua_function_header
   elseif t == 'iden' then
      local name,t,_ = tools.get_fun_name(tok,v)
      if t ~= '=' then return nil,"not  'name = function,table or value'" end
      t,v = tnext(tok)
      if t == 'keyword' and v == 'function' then -- case [2]
         tnext(tok) -- skip '('
         case = 2
         parser = function(tags,tok)
            tags:add('name',name)
            parse_lua_parameters(tags,tok)
         end
      elseif t == '{' then -- case [3]
         case = 3
         parser = function(tags,tok)
            tags:add('class','table')
            tags:add('name',name)
            parse_lua_table (tags,tok)
         end
      else -- case [4]
         case = 4
         parser = function(tags)
            tags:add('class','field')
            tags:add('name',name)
         end
      end
   elseif t == 'keyword' and v == 'return' then
      t, v = tnext(tok)
      if t == 'keyword' and v == 'function' then
         -- return function(a, b, c)
         tnext(tok) -- skip '('
         case = 2
         parser = parse_lua_parameters
      elseif t ==  '{' then
         -- return {...}
         case = 5
         parser = function(tags,tok)
            tags:add('class','table')
            parse_lua_table(tags,tok)
         end
      else
         return nil,'not returning function or table'
      end
   else
      return nil,"not 'name=value' or 'return value'"
   end
   return parser, is_local, case
end


-- we only call the function returned by the item_follows above if there
-- is not already a name and a type.
-- Otherwise, this is called. Currently only tries to fill in the fields
-- of a table from a table definition as identified above
function Lua:parse_extra (tags,tok,case)
   if tags.class == 'table' and not tags.field and case == 3 then
      parse_lua_table(tags,tok)
   end
end

-- For Lua, a --- @usage comment means that a long
-- string containing the usage follows, which we
-- use to update the module usage tag. Likewise, the @export
-- tag alone in a doc comment refers to the following returned
-- Lua table of functions


function Lua:is_module_modifier (tags)
   return tags.summary == '' and (tags.usage or tags.export)
end

--  Allow for private name convention.
function Lua:is_private_var (name)
   return name:match '^_' or name:match '_$'
end

function Lua:parse_module_modifier (tags, tok, F)
   if tags.usage then
      if tags.class ~= 'field' then return nil,"cannot deduce @usage" end
      local t1= tnext(tok)
      if t1 ~= '[' then return nil, t1..' '..': not a long string' end
      local _, v = tools.grab_block_comment('',tok,'%]%]')
      return true, v, 'usage'
   elseif tags.export then
      if tags.class ~= 'table' then return nil, "cannot deduce @export" end
      for f in tags.formal_args:iter() do
         if not self:is_private_var(f) then
            F:export_item(f)
         end
      end
      return true
   end
end


-- note a difference here: we scan C/C++ code in full-text mode, not line by line.
-- This is because we can't detect multiline comments in line mode.
-- Note: this applies to C/C++ code used to generate _Lua_ documentation!

local CC = class(Lang)

function CC:_init()
   self.line_comment = '^//+'
   self.start_comment_ = '^///+'
   self.block_comment = '^/%*%*+'
   self.method_call = ':'
   self:finalize()
end

function CC.lexer(f)
   local err
   f,err = utils.readfile(f)
   if not f then quit(err) end
   return lexer.cpp(f,{},nil,true)
end

function CC:grab_block_comment(v,tok)
   v = v:gsub(self.block_comment,''):gsub('\n%s*%*','\n')
   return 'comment',v:sub(1,-3)
end

--- here the argument name is always last, and the type is composed of any tokens before
function CC:extract_arg (tl,idx)
   idx = idx or 1
   local res = List()
   for i = idx,#tl-1 do
      res:append(tl[i][2])
   end
   local type = res:join ' '
   return tl[#tl][2], type
end

function CC:item_follows (t,v,tok)
   if not self.extra.C then
      return false
   end
   if t == 'iden' or t == 'keyword' then --
      local _
      if v == self.extra.export then -- this is not part of the return type!
         _,v = tnext(tok)
      end
      -- types may have multiple tokens: example, const char *bonzo(...)
      local return_type, name = v
      _,v = tnext(tok)
      name = v
      t,v = tnext(tok)
      while t ~= '(' do
         return_type = return_type .. ' ' .. name
         name = v
         t,v = tnext(tok)
      end
      return function(tags,tok)
         if not tags.name then
            tags:add('name',name)
         end
         tags:add('class','function')
         if t == '(' then
            tags.formal_args,t,_ = tools.get_parameters(tok,')',',',self)
            if return_type ~= 'void' then
               tags.formal_args.return_type = return_type
            end
         end
      end
   end
   return false
end

local Moon = class(Lua)

function Moon:_init()
   self.line_comment = '^%-%-+' -- used for stripping
   self.start_comment_ = '^%s*%-%-%-+'     -- used for doc comment line start
   self.block_comment = '^%-%-%[=*%[%-+' -- used for block doc comments
   self.end_comment_ = '[^%-]%-%-+\n$' ---- exclude --- this kind of comment ---
   self.method_call = '\\'
   self:finalize()
end

--- much like Lua, BUT auto-assign parameters start with @
function Moon:extract_arg (tl,idx)
   idx = idx or 1
   local auto_assign = tl[idx][1] == '@'
   if auto_assign then idx = idx + 1 end
   local res = tl[idx][2]
   return res
end

function Moon:item_follows (t,v,tok)
   local classmethod = false
   if t == '.' then -- enclosed in with statement
      t,v = tnext(tok)
   end
   if t == '@' then -- static member declaration
      classmethod = true
      t,v = tnext(tok)
   end
   if t == 'iden' then
      local name,t,v = tools.get_fun_name(tok,v,'')
      if name == 'class' then
         local _
         name,_,_ = tools.get_fun_name(tok,v,'')
         -- class!
         return function(tags,tok)
            tags:add('class','type')
            tags:add('name',name)
         end
      elseif t == '=' or t == ':' then -- function/method
         local _
         t, _ = tnext(tok)
         return function(tags,tok)
            if not tags.name then
               tags:add('name',name)
            end
            if t == '(' then
               tags.formal_args,t, _ = tools.get_parameters(tok,')',',',self)
            else
               tags.formal_args = List()
            end
            if t == '-' then
               tags:add('static')
            end
            if classmethod then
               tags:add('classmethod')
            end
            t, _ = tnext(tok)
            tags:add('class','function')
            if t ~= '>' then
               tags:add('static')
            end
         end
      else
         return nil, "expecting '=' or ':'"
      end
   end
end

return { lua = Lua(), cc = CC(), moon = Moon() }
