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

--- @class nvim.cdoc.parser.param
--- @field name string
--- @field type string
--- @field desc string

--- @class nvim.cdoc.parser.return
--- @field name string
--- @field type string
--- @field desc string

--- @class nvim.cdoc.parser.note
--- @field desc string

--- @class nvim.cdoc.parser.brief
--- @field kind 'brief'
--- @field desc string

--- @class nvim.cdoc.parser.fun
--- @field name string
--- @field params nvim.cdoc.parser.param[]
--- @field returns nvim.cdoc.parser.return[]
--- @field desc string
--- @field deprecated? true
--- @field since? string
--- @field attrs? string[]
--- @field nodoc? true
--- @field notes? nvim.cdoc.parser.note[]
--- @field see? nvim.cdoc.parser.note[]

--- @class nvim.cdoc.parser.State
--- @field doc_lines? string[]
--- @field cur_obj? nvim.cdoc.parser.obj
--- @field last_doc_item? nvim.cdoc.parser.param|nvim.cdoc.parser.return|nvim.cdoc.parser.note
--- @field last_doc_item_indent? integer

--- @alias nvim.cdoc.parser.obj
--- | nvim.cdoc.parser.fun
--- | nvim.cdoc.parser.brief

--- If we collected any `---` lines. Add them to the existing (or new) object
--- Used for function/class descriptions and multiline param descriptions.
--- @param state nvim.cdoc.parser.State
local function add_doc_lines_to_obj(state)
  if state.doc_lines then
    state.cur_obj = state.cur_obj or {}
    local cur_obj = assert(state.cur_obj)
    local txt = table.concat(state.doc_lines, '\n')
    if cur_obj.desc then
      cur_obj.desc = cur_obj.desc .. '\n' .. txt
    else
      cur_obj.desc = txt
    end
    state.doc_lines = nil
  end
end

--- @param line string
--- @param state nvim.cdoc.parser.State
local function process_doc_line(line, state)
  line = line:gsub('^%s+@', '@')

  local parsed = cdoc_grammar:match(line)

  if not parsed then
    if line:match('^ ') then
      line = line:sub(2)
    end

    if state.last_doc_item then
      if not state.last_doc_item_indent then
        state.last_doc_item_indent = #line:match('^%s*') + 1
      end
      state.last_doc_item.desc = (state.last_doc_item.desc or '')
        .. '\n'
        .. line:sub(state.last_doc_item_indent or 1)
    else
      state.doc_lines = state.doc_lines or {}
      table.insert(state.doc_lines, line)
    end
    return
  end

  state.last_doc_item_indent = nil
  state.last_doc_item = nil

  local kind = parsed.kind

  state.cur_obj = state.cur_obj or {}
  local cur_obj = assert(state.cur_obj)

  if kind == 'brief' then
    state.cur_obj = {
      kind = 'brief',
      desc = parsed.desc,
    }
  elseif kind == 'param' then
    state.last_doc_item_indent = nil
    cur_obj.params = cur_obj.params or {}
    state.last_doc_item = {
      name = parsed.name,
      desc = parsed.desc,
    }
    table.insert(cur_obj.params, state.last_doc_item)
  elseif kind == 'return' then
    cur_obj.returns = { {
      desc = parsed.desc,
    } }
    state.last_doc_item_indent = nil
    state.last_doc_item = cur_obj.returns[1]
  elseif kind == 'deprecated' then
    cur_obj.deprecated = true
  elseif kind == 'nodoc' then
    cur_obj.nodoc = true
  elseif kind == 'since' then
    cur_obj.since = parsed.desc
  elseif kind == 'see' then
    cur_obj.see = cur_obj.see or {}
    table.insert(cur_obj.see, { desc = parsed.desc })
  elseif kind == 'note' then
    state.last_doc_item_indent = nil
    state.last_doc_item = {
      desc = parsed.desc,
    }
    cur_obj.notes = cur_obj.notes or {}
    table.insert(cur_obj.notes, state.last_doc_item)
  else
    error('Unhandled' .. vim.inspect(parsed))
  end
end

--- @param item table
--- @param state nvim.cdoc.parser.State
local function process_proto(item, state)
  state.cur_obj = state.cur_obj or {}
  local cur_obj = assert(state.cur_obj)
  cur_obj.name = item.name
  cur_obj.params = cur_obj.params or {}

  for _, p in ipairs(item.parameters) do
    local param = { name = p[2], type = p[1] }
    local added = false
    for _, cp in ipairs(cur_obj.params) do
      if cp.name == param.name then
        cp.type = param.type
        added = true
        break
      end
    end

    if not added then
      table.insert(cur_obj.params, param)
    end
  end

  cur_obj.returns = cur_obj.returns or { {} }
  cur_obj.returns[1].type = item.return_type

  for _, a in ipairs({
    'fast',
    'remote_only',
    'lua_only',
    'textlock',
    'textlock_allow_cmdwin',
  }) do
    if item[a] then
      cur_obj.attrs = cur_obj.attrs or {}
      table.insert(cur_obj.attrs, a)
    end
  end

  cur_obj.since = item.since
  cur_obj.deprecated_since = item.deprecated_since

  -- Remove some arguments
  for i = #cur_obj.params, 1, -1 do
    local p = cur_obj.params[i]
    if p.name == 'channel_id' or vim.tbl_contains({ 'lstate', 'arena', 'error' }, p.type) then
      table.remove(cur_obj.params, i)
    end
  end
end

local M = {}

--- @param filename string
--- @return {} classes
--- @return nvim.cdoc.parser.fun[] funs
--- @return string[] briefs
function M.parse(filename)
  local funs = {} --- @type nvim.cdoc.parser.fun[]
  local briefs = {} --- @type string[]
  local state = {} --- @type nvim.cdoc.parser.State

  local txt = assert(io.open(filename, 'r')):read('*all')

  local parsed = c_grammar.grammar:match(txt)
  for _, item in ipairs(parsed) do
    if item.comment then
      process_doc_line(item.comment, state)
    else
      add_doc_lines_to_obj(state)
      if item[1] == 'proto' then
        process_proto(item, state)
        table.insert(funs, state.cur_obj)
      end
      local cur_obj = state.cur_obj
      if cur_obj and not item.static then
        if cur_obj.kind == 'brief' then
          table.insert(briefs, cur_obj.desc)
        end
      end
      state = {}
    end
  end

  return {}, funs, briefs
end

-- M.parse('src/nvim/api/vim.c')

return M
