local FileCache = require("quicker.filecache")
local util = require("quicker.util")

local M = {}

---@class (exact) quicker.QFContext
---@field num_before integer
---@field num_after integer

---@class (exact) quicker.ExpandOpts
---@field before? integer Number of lines of context to show before the line (default 2)
---@field after? integer Number of lines of context to show after the line (default 2)
---@field add_to_existing? boolean
---@field loclist_win? integer

---@param item QuickFixItem
---@param new_text string
local function update_item_text_keep_diagnostics(item, new_text)
  -- If this is an "error" item, replace the text with the source line and store that text
  -- in the user data so we can add it as virtual text later
  if item.type ~= "" and not vim.endswith(new_text, item.text) then
    local user_data = util.get_user_data(item)
    if not user_data.error_text then
      user_data.error_text = item.text
      item.user_data = user_data
    end
  end
  item.text = new_text
end

---@param opts? quicker.ExpandOpts
function M.expand(opts)
  opts = opts or {}
  if not opts.loclist_win and util.get_win_type(0) == "l" then
    opts.loclist_win = vim.api.nvim_get_current_win()
  end
  local qf_list
  if opts.loclist_win then
    qf_list = vim.fn.getloclist(opts.loclist_win, { all = 0 })
  else
    qf_list = vim.fn.getqflist({ all = 0 })
  end
  local winid = qf_list.winid
  if not winid then
    vim.notify("Cannot find quickfix window", vim.log.levels.ERROR)
    return
  end
  local ctx = qf_list.context or {}
  if type(ctx) ~= "table" then
    -- If the quickfix had a non-table context, we're going to have to overwrite it
    ctx = {}
  end
  ---@type quicker.QFContext
  local quicker_ctx = ctx.quicker
  if not quicker_ctx then
    quicker_ctx = { num_before = 0, num_after = 0 }
    ctx.quicker = quicker_ctx
  end
  local curpos = vim.api.nvim_win_get_cursor(winid)[1]
  local cur_item = qf_list.items[curpos]
  local newpos

  -- calculate the number of lines to show before and after the current line
  local num_before = opts.before or 2
  if opts.add_to_existing then
    num_before = num_before + quicker_ctx.num_before
  end
  num_before = math.max(0, num_before)
  quicker_ctx.num_before = num_before
  local num_after = opts.after or 2
  if opts.add_to_existing then
    num_after = num_after + quicker_ctx.num_after
  end
  num_after = math.max(0, num_after)
  quicker_ctx.num_after = num_after

  local items = {}
  ---@type nil|QuickFixItem
  local prev_item
  ---@param i integer
  ---@return nil|QuickFixItem
  local function get_next_item(i)
    local item = qf_list.items[i]
    for j = i + 1, #qf_list.items do
      local next_item = qf_list.items[j]
      -- Next valid item that is on a different line (since we dedupe same-line items)
      if
        next_item.valid == 1 and (item.bufnr ~= next_item.bufnr or item.lnum ~= next_item.lnum)
      then
        return next_item
      end
    end
  end

  for i, item in ipairs(qf_list.items) do
    (function()
      ---@cast item QuickFixItem
      if item.valid == 0 or item.bufnr == 0 then
        return
      end

      if not vim.api.nvim_buf_is_loaded(item.bufnr) then
        vim.fn.bufload(item.bufnr)
      end

      local overlaps_previous = false
      local header_type = "hard"
      local low = math.max(0, item.lnum - 1 - num_before)
      if prev_item then
        if prev_item.bufnr == item.bufnr then
          -- If this is the second match on the same line, skip this item
          if prev_item.lnum == item.lnum then
            return
          end
          header_type = "soft"
          if prev_item.lnum + num_after >= low then
            low = math.min(item.lnum - 1, prev_item.lnum + num_after)
            overlaps_previous = true
          end
        end
      end

      local high = item.lnum + num_after
      local next_item = get_next_item(i)
      if next_item then
        if next_item.bufnr == item.bufnr and next_item.lnum <= high then
          high = next_item.lnum - 1
        end
      end

      local item_start_idx = #items
      local lines = vim.api.nvim_buf_get_lines(item.bufnr, low, high, false)
      for j, line in ipairs(lines) do
        if j + low == item.lnum then
          update_item_text_keep_diagnostics(item, line)
          table.insert(items, item)
        else
          table.insert(items, {
            bufnr = item.bufnr,
            lnum = low + j,
            text = line,
            valid = 0,
            user_data = { lnum = low + j },
          })
        end
        if cur_item.bufnr == item.bufnr and cur_item.lnum == low + j then
          newpos = #items
        end
      end

      -- Add the header to the first item in this sequence, if one is needed
      if prev_item and not overlaps_previous then
        local first_item = items[item_start_idx + 1]
        if first_item then
          first_item.user_data = first_item.user_data or {}
          first_item.user_data.header = header_type
        end
      end

      prev_item = item
    end)()

    if i == curpos and not newpos then
      newpos = #items
    end
  end

  if opts.loclist_win then
    vim.fn.setloclist(
      opts.loclist_win,
      {},
      "r",
      { items = items, title = qf_list.title, context = ctx }
    )
  else
    vim.fn.setqflist({}, "r", { items = items, title = qf_list.title, context = ctx })
  end

  pcall(vim.api.nvim_win_set_cursor, qf_list.winid, { newpos, 0 })
end

---@class (exact) quicker.CollapseArgs
---@field loclist_win? integer
---
function M.collapse(opts)
  opts = opts or {}
  if not opts.loclist_win and util.get_win_type(0) == "l" then
    opts.loclist_win = vim.api.nvim_get_current_win()
  end
  local curpos = vim.api.nvim_win_get_cursor(0)[1]
  local qf_list
  if opts.loclist_win then
    qf_list = vim.fn.getloclist(opts.loclist_win, { all = 0 })
  else
    qf_list = vim.fn.getqflist({ all = 0 })
  end
  local items = {}
  local last_item
  for i, item in ipairs(qf_list.items) do
    if item.valid == 1 then
      if item.user_data then
        -- Clear the header, if present
        item.user_data.header = nil
      end
      table.insert(items, item)
      if i <= curpos then
        last_item = #items
      end
    end
  end

  vim.tbl_filter(function(item)
    return item.valid == 1
  end, qf_list.items)

  local ctx = qf_list.context or {}
  if type(ctx) == "table" then
    local quicker_ctx = ctx.quicker
    if quicker_ctx then
      quicker_ctx = { num_before = 0, num_after = 0 }
      ctx.quicker = quicker_ctx
    end
  end

  if opts.loclist_win then
    vim.fn.setloclist(
      opts.loclist_win,
      {},
      "r",
      { items = items, title = qf_list.title, context = qf_list.context }
    )
  else
    vim.fn.setqflist({}, "r", { items = items, title = qf_list.title, context = qf_list.context })
  end
  if qf_list.winid then
    if last_item then
      vim.api.nvim_win_set_cursor(qf_list.winid, { last_item, 0 })
    end
  end
end

---@param opts? quicker.ExpandOpts
function M.toggle(opts)
  opts = opts or {}
  local ctx
  if opts.loclist_win then
    ctx = vim.fn.getloclist(opts.loclist_win, { context = 0 }).context
  else
    ctx = vim.fn.getqflist({ context = 0 }).context
  end

  if
    type(ctx) == "table"
    and ctx.quicker
    and (ctx.quicker.num_before > 0 or ctx.quicker.num_after > 0)
  then
    M.collapse()
  else
    M.expand(opts)
  end
end

---@class (exact) quicker.RefreshOpts
---@field keep_diagnostics? boolean If a line has a diagnostic type, keep the original text and display it as virtual text after refreshing from source.

---@param loclist_win? integer
---@param opts? quicker.RefreshOpts
function M.refresh(loclist_win, opts)
  opts = vim.tbl_extend("keep", opts or {}, { keep_diagnostics = true })
  if not loclist_win then
    local ok, qf = pcall(vim.fn.getloclist, 0, { filewinid = 0 })
    if ok and qf.filewinid and qf.filewinid ~= 0 then
      loclist_win = qf.filewinid
    end
  end

  local qf_list
  if loclist_win then
    qf_list = vim.fn.getloclist(loclist_win, { all = 0 })
  else
    qf_list = vim.fn.getqflist({ all = 0 })
  end

  local items = {}
  local filecache = FileCache.new(qf_list.items)
  for _, item in ipairs(qf_list.items) do
    local line = filecache:get_line(item.bufnr, item.lnum)
    if line then
      if opts.keep_diagnostics then
        update_item_text_keep_diagnostics(item, line)
      else
        item.text = line
      end
      table.insert(items, item)
    else
      table.insert(items, item)
    end
  end

  if loclist_win then
    vim.fn.setloclist(
      loclist_win,
      {},
      "r",
      { items = items, title = qf_list.title, context = qf_list.context }
    )
  else
    vim.fn.setqflist({}, "r", { items = items, title = qf_list.title, context = qf_list.context })
  end
end

return M
