local grammar = require('gen.c_grammar').grammar

--- @param fname string
--- @return string?
local function read_file(fname)
  local f = io.open(fname, 'r')
  if not f then
    return
  end
  local contents = f:read('*a')
  f:close()
  return contents
end

--- @param fname string
--- @param contents string[]
local function write_file(fname, contents)
  local contents_s = table.concat(contents, '\n') .. '\n'
  local fcontents = read_file(fname)
  if fcontents == contents_s then
    return
  end
  local f = assert(io.open(fname, 'w'))
  f:write(contents_s)
  f:close()
end

--- @param fname string
--- @param non_static_fname string
--- @return string? non_static
local function add_iwyu_non_static(fname, non_static_fname)
  if fname:find('.*/src/nvim/.*%.c$') then
    -- Add an IWYU pragma comment if the corresponding .h file exists.
    local header_fname = fname:sub(1, -3) .. '.h'
    local header_f = io.open(header_fname, 'r')
    if header_f then
      header_f:close()
      return (header_fname:gsub('.*/src/nvim/', 'nvim/'))
    end
  elseif non_static_fname:find('/include/api/private/dispatch_wrappers%.h%.generated%.h$') then
    return 'nvim/api/private/dispatch.h'
  elseif non_static_fname:find('/include/ui_events_call%.h%.generated%.h$') then
    return 'nvim/ui.h'
  elseif non_static_fname:find('/include/ui_events_client%.h%.generated%.h$') then
    return 'nvim/ui_client.h'
  elseif non_static_fname:find('/include/ui_events_remote%.h%.generated%.h$') then
    return 'nvim/api/ui.h'
  end
end

--- @param d string
local function process_decl(d)
  -- Comments are really handled by preprocessor, so the following is not
  -- needed
  d = d:gsub('/%*.-%*/', '')
  d = d:gsub('//.-\n', '\n')
  d = d:gsub('# .-\n', '')
  d = d:gsub('\n', ' ')
  d = d:gsub('%s+', ' ')
  d = d:gsub(' ?%( ?', '(')
  d = d:gsub(' ?, ?', ', ')
  d = d:gsub(' ?(%*+) ?', ' %1')
  d = d:gsub(' ?(FUNC_ATTR_)', ' %1')
  d = d:gsub(' $', '')
  d = d:gsub('^ ', '')
  return d .. ';'
end

--- @param fname string
--- @param text string
--- @return string[] static
--- @return string[] non_static
--- @return boolean any_static
local function gen_declarations(text)
  local non_static = {} --- @type string[]
  local static = {} --- @type string[]

  local any_static = false
  for _, node in ipairs(grammar:match(text)) do
    if node[1] == 'proto' then
      local node_text = text:sub(node.pos, node.endpos - 1)
      local declaration = process_decl(node_text)

      if node.static then
        if not any_static and declaration:find('FUNC_ATTR_') then
          any_static = true
        end
        static[#static + 1] = declaration
      else
        non_static[#non_static + 1] = 'DLLEXPORT ' .. declaration
      end
    end
  end

  return static, non_static, any_static
end

local usage = [[
Usage:

    gen_declarations.lua definitions.c static.h non-static.h

Generates declarations for a C file definitions.c, putting declarations for
static functions into static.h and declarations for non-static functions into
non-static.h. Also generate an IWYU comment.
]]

local function main()
  local fname = arg[1]
  local static_fname = arg[2]
  local non_static_fname = arg[3]
  local static_basename = arg[4]

  if fname == '--help' or #arg < 4 then
    print(usage)
    os.exit()
  end

  local text = assert(read_file(fname))

  local static_decls, non_static_decls, any_static = gen_declarations(text)

  local static = {} --- @type string[]
  if fname:find('.*/src/nvim/.*%.h$') then
    static[#static + 1] = ('// IWYU pragma: private, include "%s"'):format(
      fname:gsub('.*/src/nvim/', 'nvim/')
    )
  end
  vim.list_extend(static, {
    '#define DEFINE_FUNC_ATTRIBUTES',
    '#include "nvim/func_attr.h"',
    '#undef DEFINE_FUNC_ATTRIBUTES',
  })
  vim.list_extend(static, static_decls)
  vim.list_extend(static, {
    '#define DEFINE_EMPTY_ATTRIBUTES',
    '#include "nvim/func_attr.h"  // IWYU pragma: export',
    '',
  })

  write_file(static_fname, static)

  if any_static then
    local orig_text = assert(read_file(fname))
    local pat = '\n#%s?include%s+"' .. static_basename .. '"\n'
    local pat_comment = '\n#%s?include%s+"' .. static_basename .. '"%s*//'
    if not orig_text:find(pat) and not orig_text:find(pat_comment) then
      error(('fail: missing include for %s in %s'):format(static_basename, fname))
    end
  end

  if non_static_fname ~= 'SKIP' then
    local non_static = {} --- @type string[]
    local iwyu_non_static = add_iwyu_non_static(fname, non_static_fname)
    if iwyu_non_static then
      non_static[#non_static + 1] = ('// IWYU pragma: private, include "%s"'):format(
        iwyu_non_static
      )
    end
    vim.list_extend(non_static, {
      '#define DEFINE_FUNC_ATTRIBUTES',
      '#include "nvim/func_attr.h"',
      '#undef DEFINE_FUNC_ATTRIBUTES',
      '#ifndef DLLEXPORT',
      '#  ifdef MSWIN',
      '#    define DLLEXPORT __declspec(dllexport)',
      '#  else',
      '#    define DLLEXPORT',
      '#  endif',
      '#endif',
    })
    vim.list_extend(non_static, non_static_decls)
    non_static[#non_static + 1] = '#include "nvim/func_attr.h"'
    write_file(non_static_fname, non_static)
  end
end

return main()
