local Snipe = {}
local Buffer = require("snipe.buffer")
local Config = require("snipe.config")
local Highlights = require("snipe.highlights")

Snipe.setup = function(config)
  Config.setup(config)
  --- @deprecated Snipe.config is deprecated, use require('snipe.config') instead
  Snipe.config = Config

  local SnipeMenu = require("snipe.menu")
  Snipe.global_menu = SnipeMenu:new({
    map_tags = Snipe.default_map_tags,
    set_window_local_options = Snipe.set_window_local_options,
    persist_tags_key = Config.ui.persist_tags and function (item) return item.id end or nil,
  })

  -- Setup autocmd to prune persistent_tags
  vim.api.nvim_create_autocmd("BufDelete", {
    group = Snipe.global_menu.augroup,
    callback = function(ev)
      Snipe.global_menu:free_tag(ev.buf)
    end,
    desc = "Free up tags from closed buffers",
  })
end

function Snipe.set_window_local_options(wid)
  vim.wo[wid].foldenable = false
  vim.wo[wid].wrap = false
  vim.wo[wid].cursorline = true
end

Snipe.index_to_tag = {}

function Snipe.default_map_tags(tags)
  for _, index_and_tag in ipairs(Snipe.index_to_tag) do
    tags[index_and_tag.index] = index_and_tag.tag
  end
  return tags
end

---@param m snipe.Menu
function Snipe.default_keymaps(m)
  m:default_keymaps({
    under_cursor = function()
      local hovered = m:hovered()
      m:close()
      vim.api.nvim_set_current_buf(m.items[hovered].id)
    end,
  })

  local opts = { nowait = true, buffer = m.buf }

  -- Specific keymaps

  if Config.navigate.close_buffer then
    vim.keymap.set("n", Config.navigate.close_buffer, function()
      Snipe.close_buf(m, m:hovered())
    end, opts)
  end

  if Config.navigate.open_vsplit then
    vim.keymap.set("n", Config.navigate.open_vsplit, function()
      Snipe.open_vsplit(m, m:hovered())
    end, opts)
  end

  if Config.navigate.open_split then
    vim.keymap.set("n", Config.navigate.open_split, function()
      Snipe.open_split(m, m:hovered())
    end, opts)
  end

  if Config.navigate.change_tag then
    vim.keymap.set("n", Config.navigate.change_tag, function()
      Snipe.change_tag(m, m:hovered())
    end, opts)
  end
end

---create "format" function based on Config.ui options
---@param buffers snipe.Buffer[]
---@return function
function Snipe.create_buffer_formatter(buffers)
  if #buffers == 0 or Config.ui.buffer_format ~= nil then -- custom buffer_format takes precedence
    return Snipe.default_fmt(Config.ui.buffer_format)
  elseif Config.ui.text_align == "file-first" then -- pre-format basename if text_align is "file-first"
    local buffers_basenames = vim.tbl_map(function(buf)
      return vim.fs.basename(buf.name)
    end, buffers)
    local max_name_length = #buffers_basenames[1]
    for _, basename in ipairs(buffers_basenames) do
      if #basename > max_name_length then
        max_name_length = #basename
      end
    end
    return Snipe.default_fmt({
      "icon",
      " ",
      function(buffer)
        local basename = vim.fs.basename(buffer.name)
        return basename .. string.rep(" ", max_name_length - #basename)
      end,
      " ",
      "directory",
    })
  else -- return "directory/filename" if text_align is "left"|"right", actual alignment will be done by `Menu`
    return Snipe.default_fmt({
      "directory",
      function()
        return "/", "SnipeDirname"
      end,
      "filename",
    })
  end
end

---create format function for `Menu`
---@param line_format (string|function)[]
---@return fun(buf: snipe.Buffer): string, {first: integer, last: integer, hlgroup: string}[] - format function
function Snipe.default_fmt(line_format)
  return function(item)
    ---@type { first: integer, last: integer, hlgroup: string}[]
    local highlights = {}
    local result = ""
    local hl_start_index = 1

    for _, format in ipairs(line_format) do
      if format == "filename" then
        local basename = vim.fs.basename(item.name)
        result = result .. basename
        table.insert(highlights, {
          first = hl_start_index,
          last = hl_start_index + #basename,
          hlgroup = Highlights.highlight_groups.filename.name,
        })
        hl_start_index = hl_start_index + #basename
      elseif format == "directory" then
        local full_path = vim.api.nvim_buf_get_name(item.id)
        local cwd = vim.fn.getcwd()
        local relative_to_cwd = item.name -- fallback
        local _, cwd_end = full_path:find(cwd, 1, true)
        if cwd_end then
          relative_to_cwd = full_path:sub(cwd_end + 2) -- remove cwd and '/'
        end
        local dirname = vim.fs.dirname(relative_to_cwd)
        result = result .. dirname
        table.insert(highlights, {
          first = hl_start_index,
          last = hl_start_index + #dirname,
          hlgroup = Highlights.highlight_groups.dirname.name,
        })
        hl_start_index = hl_start_index + #dirname
      elseif format == "icon" then
        local icon, hl = nil, nil
        -- try mini.icons
        if _G.MiniIcons then
          icon, hl = MiniIcons.get("file", item.name)
        end
        -- try nvim-web-devicons
        local has_devicons, devicons = pcall(require, "nvim-web-devicons")
        if not icon and has_devicons then
          icon, hl = devicons.get_icon(item.name)
        end
        if icon then
          result = result .. icon
          table.insert(highlights, {
            first = hl_start_index,
            last = hl_start_index + #icon,
            hlgroup = hl,
          })
          hl_start_index = hl_start_index + #icon
        else
          -- ignore "icon" altogether if no supported icon provider found
        end
      else
        local text, hl = nil, nil
        if type(format) == "string" then
          text = format
        elseif type(format) == "function" then
          text, hl = format(item)
        else
          -- invalid format passed, ignore
        end
        if text then
          result = result .. text
          table.insert(highlights, {
            first = hl_start_index,
            last = hl_start_index + #text,
            hlgroup = hl or Highlights.highlight_groups.text.name,
          })
          hl_start_index = hl_start_index + #text
        end
      end
    end
    return result, highlights
  end
end

function Snipe.default_select(m, i, leader)
  if leader then
    pcall(Config.navigate.leader_map[vim.fn.getcharstr()], m, i)
  else
    Snipe.global_menu:close()
    m.opened_from_wid = m:open_over()
    vim.api.nvim_set_current_win(m.opened_from_wid)
    vim.api.nvim_set_current_buf(m.items[i].id)
  end
end

function Snipe.preselect_by_classifier(classifier)
  return function(bs)
    for i, b in ipairs(bs) do
      -- Check if the classifier is anywhere in the classifier string
      for j = 1, #b.classifiers do
        if b.classifiers:sub(j, j) == classifier then
          return i
        end
      end
    end
    return 1 -- default select the first item if classifier not found
  end
end

---Get the buffer list sorted in the order of the `sort` config options
---@return snipe.Buffer[]
function Snipe.get_sorted_buffer_list()
  local cmd = Config.sort == "last" and "ls t" or "ls"
  local buffers = Buffer.get_buffers(cmd)
  if type(Config.sort) == "function" then
    return Config.sort(buffers)
  end
  return buffers
end

function Snipe.open_buffer_menu()
  local buffers = Snipe.get_sorted_buffer_list()
  local format_buffer = Snipe.create_buffer_formatter(buffers)
  Snipe.global_menu:add_new_buffer_callback(Snipe.default_keymaps)

  if Config.ui.preselect then
    local i = Config.ui.preselect(buffers)
    Snipe.global_menu:open(buffers, Snipe.default_select, format_buffer, i)
  elseif Config.ui.preselect_current then
    local opened = false
    for i, b in ipairs(buffers) do
      if b.classifiers:sub(2, 2) == "%" then
        Snipe.global_menu:open(buffers, Snipe.default_select, format_buffer, i)
        opened = true
      end
    end
    if not opened then
      Snipe.global_menu:open(buffers, Snipe.default_select, format_buffer)
    end
  else
    Snipe.global_menu:open(buffers, Snipe.default_select, format_buffer)
  end
end

Snipe.ui_select_menu = nil

-- Can be set as your `vim.ui.select`
---@param items any[] Arbitrary items
---@param opts table Additional options
---@param on_choice fun(item: any|nil, idx: integer|nil)
function Snipe.ui_select(items, opts, on_choice)
  if Snipe.ui_select_menu == nil then
    vim.notify("Must instanciate `require('snipe').ui_select_menu' before using `ui_select'", vim.log.levels.ERROR)
    return
  end

  local format_item = function (item)
    if not opts.format_item then return item end
    local fmtd = opts.format_item(item) -- Force take only the first return value
                                        -- This is because snipe format function
                                        -- looks at the second return value and
                                        -- expects it to have highlight
                                        -- information
    return fmtd
  end

  if opts.prompt ~= nil then
    Snipe.ui_select_menu.config.open_win_override.title = opts.prompt
  end
  Snipe.ui_select_menu:open(items, function(m, i)
    on_choice(m.items[i], i)
    m:close()
  end, format_item)
  Snipe.ui_select_menu.config.open_win_override.title = Config.ui.open_win_override.title
end

function Snipe.close_buf(m, i)
  local bufnr = m.items[i].id
  -- I have to hack switch back to main window, otherwise currently background focused
  -- window cannot be deleted when focused on a floating window
  m.opened_from_wid = m:open_over()
  vim.api.nvim_set_current_win(m.opened_from_wid)

  local ok, snacks = pcall(require, "snacks")
  if ok then
    snacks.bufdelete(bufnr)
  else
    vim.api.nvim_buf_delete(bufnr, { force = false })
  end

  vim.api.nvim_set_current_win(m.win)
  table.remove(m.items, i)
  m:reopen()
end

function Snipe.open_vsplit(m, i)
  local bufnr = m.items[i].id
  m:close() -- make sure to call first !
  vim.api.nvim_open_win(bufnr, true, { vertical = true, win = 0 })
end

function Snipe.open_split(m, i)
  local split_direction = vim.opt.splitbelow:get() and "below" or "above"
  local bufnr = m.items[i].id
  m:close() -- make sure to call first !
  vim.api.nvim_open_win(bufnr, true, { split = split_direction, win = 0 })
end

function Snipe.change_tag(m, i)
  vim.ui.input({ prompt = "Enter custom tag: " }, function(input)
    table.insert(Snipe.index_to_tag, { index = i, tag = input })
    m:reopen()
  end)
end

return Snipe
