local ls = require("luasnip")
-- auto_pairs  {{{
local get_visual = function(args, parent)
  if #parent.snippet.env.SELECT_RAW > 0 then
    return sn(nil, i(1, parent.snippet.env.SELECT_RAW))
  else
    return sn(nil, i(1, ""))
  end
end
local function char_count_same(c1, c2)
  local line = vim.api.nvim_get_current_line()
  -- '%'-escape chars to force explicit match (gsub accepts patterns).
  -- second return value is number of substitutions.
  local _, ct1 = string.gsub(line, "%" .. c1, "")
  local _, ct2 = string.gsub(line, "%" .. c2, "")
  return ct1 == ct2
end

local function even_count(c, ...)
  local line = vim.api.nvim_get_current_line()
  local _, ct = string.gsub(line, c, "")
  return ct % 2 == 0
end

-- This makes creation of pair-type snippets easier.
local function pair(pair_begin, pair_end, file_types, condition_function)
  -- FIXME(@Soispha): This only works if file_types == nil, otherwise the snippet does not expand.
  -- It would be nice, if it would support both an empty array (`{}`) and nil <2023-08-27>
  -- file_types = file_types or {};

  return s(
    { trig = pair_begin, wordTrig = false, snippetType = "autosnippet" },
    { t({ pair_begin }), d(1, get_visual), t({ pair_end }) },
    {
      condition = function()
        local filetype_check = true
        if file_types ~= nil then
          filetype_check = file_types[vim.bo.filetype] or false
        end
        return (not condition_function(pair_begin, pair_end)) and filetype_check
      end,
    }
  )
end

local auto_pairs = {
  pair("(", ")", nil, char_count_same),
  pair("{", "}", nil, char_count_same),
  pair("[", "]", nil, char_count_same),
  pair("<", ">", { ["rust"] = true, ["tex"] = true }, char_count_same),
  pair("'", "'", nil, even_count),
  pair('"', '"', nil, even_count),
  pair("`", "`", nil, even_count),
}

ls.add_snippets("all", auto_pairs, { type = "snippets", key = "auto_pairs" })
-- }}}

-- todo_comments {{{
local calculate_comment_string = require("Comment.ft").calculate
local utils = require("Comment.utils")

--- Get the comment string {beg,end} table
---@param ctype integer 1 for `line`-comment and 2 for `block`-comment
---@return table comment_strings {begcstring, endcstring}
local get_cstring = function(ctype)
  -- use the `Comments.nvim` API to fetch the comment string for the region (eq. '--%s' or '--[[%s]]' for `lua`)
  local cstring = calculate_comment_string({ ctype = ctype, range = utils.get_region() }) or vim.bo.commentstring
  -- as we want only the strings themselves and not strings ready for using `format` we want to split the left and right side
  local left, right = utils.unwrap_cstr(cstring)
  -- create a `{left, right}` table for it
  return { left, right }
end
_G.luasnip = {}
_G.luasnip.vars = {
  username = "@soispha",
  email = "soispha@vhack.eu",
}

--- Options for marks to be used in a TODO comment
---@return table,table: The first table contains a node for the date, the second for the signature
local marks = {
  signature = function()
    return t("(" .. _G.luasnip.vars.username .. ")"), t("")
  end,
  date_signature = function()
    return t("<" .. os.date("%Y-%m-%d") .. ">"), t("(" .. _G.luasnip.vars.username .. ")")
  end,
  date = function()
    return t("<" .. os.date("%Y-%m-%d") .. ">"), t("")
  end,
  empty = function()
    return t(""), t("")
  end,
}

---@param alias string
---@param opts table
---@param mark_function function: This function should return two nodes
---@return table: Returns the comment node
local todo_snippet_nodes = function(alias, opts, mark_function)
  local date_node, signature_node = mark_function()
  -- format them into the actual snippet
  local comment_node = fmta("<> <><>: <> <> <>", {
    f(function()
      return get_cstring(opts.ctype)[1] -- get <comment-string[1]>
    end),
    t(alias), -- [name-of-comment]
    signature_node,
    i(0), -- {comment-text}
    date_node,
    f(function()
      return get_cstring(opts.ctype)[2] -- get <comment-string[2]>
    end),
  })
  return comment_node
end

--- Generate a TODO comment snippet with an automatic description and docstring
---@param context table merged with the generated context table `trig` must be specified
---@param alias string of aliases for the todo comment (ex.: {FIX, ISSUE, FIXIT, BUG})
---@param opts table merged with the snippet opts table
---@param mark_function function: The function used to get the marks
local todo_snippet = function(context, alias, opts, mark_function)
  opts = opts or {}
  context = context or {}
  if not context.trig then
    return error("context doesn't include a `trig` key which is mandatory", 2) -- all we need from the context is the trigger
  end
  opts.ctype = opts.ctype or 1 -- comment type can be passed in the `opts` table, but if it is not, we have to ensure, it is defined
  local alias_string = alias -- `choice_node` documentation
  context.name = context.name or (alias_string .. " comment") -- generate the `name` of the snippet if not defined
  context.dscr = context.dscr or (alias_string .. " comment with a signature-mark") -- generate the `dscr` if not defined
  context.docstring = context.docstring or (" {1:" .. alias_string .. "}: {3} <{2:mark}>{0} ") -- generate the `docstring` if not defined
  local comment_node = todo_snippet_nodes(alias, opts, mark_function)
  return s(context, comment_node, opts) -- the final todo-snippet constructed from our parameters
end

---@param context table: The luasnip context
---@param opts table: The luasnip opts table, needs to have a ctype set
---@param aliases string: All aliases for a name
---@param marks table: Possible marks to account in snipped generation
---@return table: All possible snippets build from the marks
local process_marks = function(context, aliases, opts, marks)
  local output = {}
  for mark_name, mark_function in pairs(marks) do
    local contex_trig_local = context.trig
    context.trig = context.trig .. "-" .. mark_name
    output[#output + 1] = todo_snippet(context, aliases, opts, mark_function)
    context.trig = contex_trig_local
  end
  return output
end

local todo_snippet_specs = {
  { { trig = "todo" }, { "TODO" }, { ctype = 1 } },
  { { trig = "fix" }, { "FIXME", "ISSUE" }, { ctype = 1 } },
  { { trig = "hack" }, { "HACK" }, { ctype = 1 } },
  { { trig = "warn" }, { "WARNING" }, { ctype = 1 } },
  { { trig = "perf" }, { "PERFORMANCE", "OPTIMIZE" }, { ctype = 1 } },
  { { trig = "note" }, { "NOTE", "INFO" }, { ctype = 1 } },

  -- NOTE: Block commented todo-comments
  { { trig = "todob" }, { "TODO" }, { ctype = 2 } },
  { { trig = "fixb" }, { "FIXME", "ISSUE" }, { ctype = 2 } },
  { { trig = "hackb" }, { "HACK" }, { ctype = 2 } },
  { { trig = "warnb" }, { "WARNING" }, { ctype = 2 } },
  { { trig = "perfb" }, { "PERF", "PERFORMANCE", "OPTIM", "OPTIMIZE" }, { ctype = 2 } },
  { { trig = "noteb" }, { "NOTE", "INFO" }, { ctype = 2 } },
}

local todo_comment_snippets = {}
for _, v in ipairs(todo_snippet_specs) do
  local snippets = process_marks(v[1], v[2][1], v[3], marks)
  for _, value in pairs(snippets) do
    table.insert(todo_comment_snippets, value)
  end
end

ls.add_snippets("all", todo_comment_snippets, { type = "snippets", key = "todo_comments" })

-- }}}