if vim.g.loaded_parinfer ~= nil then
  return
end

vim.g.loaded_parinfer = true

local ok, parinfer_lib = pcall(require, "parinfer_lib")
if not ok then
  return vim.notify("parinfer.nvim requires libparinfer_nvim.so, cd into the plugin and run `make all`")
end
---@cast parinfer_lib parinfer_lib

---@nodiscard
---@param ft string
---@return parinfer_request_options
local parinfer_options = function(ft)
  local commentstring = vim.filetype.get_option(ft, "commentstring") --[[@as string ]]
  local options = {
    comment_char = commentstring:format(""):match("^."),
    force_balance = false,
    hy_bracket_strings = false,
    guile_block_comments = false,
    janet_long_strings = false,
    lisp_block_comments = false,
    lisp_vline_symbols = false,
    prev_text = "",
    scheme_sexp_comments = false,
    string_delimiters = { '"' },
    cursor_line = 0,
    cursor_x = 0,
    prev_cursor_line = 0,
    prev_cursor_x = 0,
  }
  -- shamelessly copypasta, mostly...
  -- from https://github.com/eraserhd/parinfer-rust/blob/afe6b1176cd805c000713e23b654fbf4b9f4b156/src/cli_options.rs
  -- and https://github.com/eraserhd/parinfer-rust/blob/afe6b1176cd805c000713e23b654fbf4b9f4b156/plugin/parinfer.vim
  if ft == "racket" or ft == "scheme" or ft == "chicken" then
    -- yes, there's filetype `chicken` for the chicken scheme implementation
    options.lisp_vline_symbols = true
    options.lisp_block_comments = true
    options.scheme_sexp_comments = true
    options.guile_block_comments = true
  elseif ft == "yuck" then
    options.string_delimiters = { '"', "'", "`" }
  elseif ft:find("lisp", 1, true) then
    options.lisp_vline_symbols = true
    options.lisp_block_comments = true
  elseif ft == "janet" then
    options.janet_long_strings = true
  elseif ft == "hy" then
    options.hy_bracket_strings = true
  end
  return options
end

---@type table<number, parinfer_request?>
local parinfer_requests = {}
---@type table<number, parinfer_response?>
local parinfer_responses = {}

---@param buf number
local parinfer_run = function(buf)
  if not vim.api.nvim_buf_is_loaded(buf) then
    return
  end
  local req = parinfer_requests[buf]
  if req == nil then
    req = {
      mode = "paren",
      text = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, false), "\n"),
      options = parinfer_options(vim.bo[buf].filetype),
    }
  else
    req.options.prev_text = req.text
    req.options.prev_cursor_line = req.options.cursor_line
    req.options.prev_cursor_x = req.options.cursor_x
    local lnum, col = unpack(vim.api.nvim_win_get_cursor(0), 1, 2)
    local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
    local line = lines[lnum]
    req.options.cursor_line = lnum - 1
    req.options.cursor_x = parinfer_lib.to_charpos(line, col)
    req.mode = "smart"
    req.text = table.concat(lines, "\n")
  end
  local res = assert(parinfer_lib.run(req))
  req.options.prev_text = req.text
  req.options.prev_cursor_line = req.options.cursor_line
  req.options.prev_cursor_x = req.options.cursor_x
  if res.success == true then
    req.text = res.text
    req.options.cursor_line = res.cursor_line
    req.options.cursor_x = res.cursor_x
    if req.options.prev_text ~= req.text then
      local options = req.options
      local lines = vim.split(req.text, "\n")
      local lnum = options.cursor_line + 1
      local line = lines[lnum]
      local col = parinfer_lib.to_bytepos(line, options.cursor_x)
      local cursor = { lnum, col }
      ---@diagnostic disable-next-line: deprecated # until Nvim 1.0 as vim.text.diff is only an alias
      local hunks = vim.diff(options.prev_text, req.text, { result_type = "indices" }) --[[@as integer[][] ]]
      local first, last = hunks[1], hunks[#hunks]
      local from = first[1] - 1
      local to = last[1] + last[2] - 1
      local new_lines = table.move(lines, first[3], last[3] + last[4] - 1, 1, {})
      ---@diagnostic disable-next-line: missing-fields
      vim.api.nvim_cmd({ cmd = "undojoin", mods = { emsg_silent = true, silent = true } }, { output = false })
      vim.api.nvim_buf_set_lines(buf, from, to, false, new_lines)
      vim.api.nvim_win_set_cursor(0, cursor)
    end
  end
  parinfer_requests[buf] = req
  parinfer_responses[buf] = res
end

local parinfer_on_change = function(ctx)
  if not vim._getvar("b", ctx.buf, "parinfer_disabled")
    and vim.bo.modifiable
    and not vim.bo.readonly then
    parinfer_run(ctx.buf)
  end
end

local parinfer_on_filetype = function(ctx)
  if vim.wo.previewwindow or not vim.bo.modifiable or vim.bo.readonly then return end

  parinfer_run(ctx.buf)
  vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI", "TextChangedP", "TextChangedI", "TextChanged" }, {
    group = ctx.group,
    buffer = ctx.buf,
    callback = parinfer_on_change,
  })

  vim.api.nvim_buf_set_keymap(ctx.buf, "i", "<c-t>", "<plug>(parinfer-indent)", { noremap = true })
  vim.api.nvim_buf_set_keymap(ctx.buf, "i", "<c-d>", "<plug>(parinfer-dedent)", { noremap = true })
end

vim.api.nvim_create_autocmd("FileType", {
  -- stylua: ignore
  pattern = { "clojure", "scheme", "lisp", "racket", "hy", "fennel", "janet", "carp", "wast", "yuck", "dune", "chicken", "query" },
  group = vim.api.nvim_create_augroup("parinfer", { clear = true }),
  -- stylua: ignore
  callback = parinfer_on_filetype,
})


local parinfer_indent = function(dx)
  local buf = vim.api.nvim_get_current_buf()
  local response = parinfer_responses[buf]
  if response == nil then return end

  local line = vim.api.nvim_get_current_line()
  local indent = select(2, line:find("^%s*"))
  if dx < 0 and indent == 0 then return end

  local tab
  if response.tab_stops ~= nil and #response.tab_stops > 0 then
    local stops = {}
    for _, tab_stop in ipairs(response.tab_stops) do
      table.insert(stops, tab_stop.x)
      table.insert(stops, tab_stop.x + (tab_stop.ch == "(" and 2 or 1))
      if tab_stop.arg_x then
        table.insert(stops, tab_stop.arg_x)
      end
    end

    local start, finish = 1, #stops
    if dx < 0 then start, finish = finish, start end
    for i = start, finish, dx do
      local stop = stops[i]
      local a, b = indent * dx, stop * dx
      if a < b then
        tab = stop; break
      end
    end
  end
  tab = tab or math.max(0, indent + (dx * 2))
  local new_line = string.gsub(line, "^%s*", string.rep(" ", tab))
  vim.api.nvim_set_current_line(new_line)
  local cursor = vim.api.nvim_win_get_cursor(0)
  cursor[2] = tab
  vim.api.nvim_win_set_cursor(0, cursor)
  -- sometimes, some crazy fuck will just hold down them keys to repeat the keymap
  -- if by any chance that results in changing more the a couple of lines, neovim
  -- may lag behind. Depending on the size of that change and the amount of
  -- decorations active for and the buffer in question, the editor may stutter.
  -- So go for broke and redraw it on EVERY call to this function
  vim.api.nvim__redraw({ buf = buf, cursor = true })
end

vim.api.nvim_set_keymap("i", "<plug>(parinfer-indent)", "", { noremap = true, callback = function() parinfer_indent(1) end })
vim.api.nvim_set_keymap("i", "<plug>(parinfer-dedent)", "", { noremap = true, callback = function() parinfer_indent(-1) end })

do
  local decorations = require("parinfer.decorations")
  if vim.g.parinfer_decorations == nil then vim.g.parinfer_decorations = false end
  vim.api.nvim_create_user_command("ParinferDecorations", function()
    local enabled = vim.g.parinfer_decorations
    if enabled then
      decorations.detach()
    else
      decorations.attach(parinfer_responses)
    end
    vim.g.parinfer_decorations = not enabled
  end, { force = true, nargs = "?" })
end

---@class (exact) parinfer_request
---@field options parinfer_request_options
---@field mode 'smart'|'paren'|'indent'
---@field text string
---@class (exact) parinfer_request_options
---@field comment_char string
---@field cursor_line? integer
---@field cursor_x?             integer
---@field selection_start_line? integer
---@field force_balance         boolean
---@field guile_block_comments  boolean
---@field hy_bracket_strings    boolean
---@field janet_long_strings    boolean
---@field lisp_block_comments   boolean
---@field lisp_vline_symbols    boolean
---@field prev_cursor_line?     integer
---@field prev_cursor_x?        integer
---@field prev_text?            string
---@field scheme_sexp_comments  boolean
---@field string_delimiters     string[]
---@alias ErrorName
---|"quote-danger"
---|"eol-backslash"
---|"unclosed-quote"
---|"unclosed-paren"
---|"unmatched-close-paren"
---|"unmatched-open-paren"
---|"leading-close-paren"
---|"utf8-error"
---|"json-error"
---|"panic"
---@class (exact) parinfer_error
---@field name          ErrorName,
---@field message       string
---@field x             number
---@field line_no       number
---@field input_x       number
---@field input_line_no number
---@class (exact) parinfer.tabstop
---@field ch     '('|'['|'{'
---@field line_no integer
---@field x      integer
---@field arg_x?  integer
---@class (exact) parinfer_response
---@field success        boolean
---@field text           string
---@field cursor_x       integer
---@field cursor_line    integer
---@field tab_stops?     parinfer.tabstop[]
---@field paren_trails?  parinfer.paren_trail[]
---@field private parens?       any[]
---@field error?         parinfer_error
---@class (exact) parinfer.change
---@field line_no  integer
---@field x        integer
---@field old_text string
---@field new_text string
---@class (exact) parinfer.paren_trail
---@field end_x   integer
---@field line_no integer
---@field start_x integer
---@class parinfer_lib
---@field run fun(request: parinfer_request): parinfer_response
---@field to_charpos fun(line: string, bytepos: number): number
---@field to_bytepos fun(line: string, charpos: number): number
