local uv = vim.uv or vim.loop

---@class snacks.scratch
---@overload fun(opts?: snacks.scratch.Config): snacks.win
local M = setmetatable({}, {
  __call = function(M, ...)
    return M.open(...)
  end,
})

M.meta = {
  desc = "Scratch buffers with a persistent file",
}

---@class snacks.scratch.File
---@field file string full path to the scratch buffer
---@field stat uv.fs_stat.result File stat result
---@field name string name of the scratch buffer
---@field ft string file type
---@field icon? string icon for the file type
---@field cwd? string current working directory
---@field branch? string Git branch
---@field count? number vim.v.count1 used to open the buffer

---@class snacks.scratch.Config
---@field win? snacks.win.Config scratch window
---@field template? string template for new buffers
---@field file? string scratch file path. You probably don't need to set this.
---@field ft? string|fun():string the filetype of the scratch buffer
local defaults = {
  name = "Scratch",
  ft = function()
    if vim.bo.buftype == "" and vim.bo.filetype ~= "" then
      return vim.bo.filetype
    end
    return "markdown"
  end,
  ---@type string|string[]?
  icon = nil, -- `icon|{icon, icon_hl}`. defaults to the filetype icon
  root = vim.fn.stdpath("data") .. "/scratch",
  autowrite = true, -- automatically write when the buffer is hidden
  -- unique key for the scratch file is based on:
  -- * name
  -- * ft
  -- * vim.v.count1 (useful for keymaps)
  -- * cwd (optional)
  -- * branch (optional)
  filekey = {
    cwd = true, -- use current working directory
    branch = true, -- use current branch name
    count = true, -- use vim.v.count1
  },
  win = { style = "scratch" },
  ---@type table<string, snacks.win.Config>
  win_by_ft = {
    lua = {
      keys = {
        ["source"] = {
          "<cr>",
          function(self)
            local name = "scratch." .. vim.fn.fnamemodify(vim.api.nvim_buf_get_name(self.buf), ":e")
            Snacks.debug.run({ buf = self.buf, name = name })
          end,
          desc = "Source buffer",
          mode = { "n", "x" },
        },
      },
    },
  },
}

Snacks.util.set_hl({
  Title = "FloatTitle",
  Footer = "FloatFooter",
  Key = "DiagnosticVirtualTextInfo",
  Desc = "DiagnosticInfo",
}, { prefix = "SnacksScratch", default = true })

Snacks.config.style("scratch", {
  width = 100,
  height = 30,
  bo = { buftype = "", buflisted = false, bufhidden = "hide", swapfile = false },
  minimal = false,
  noautocmd = false,
  -- position = "right",
  zindex = 20,
  wo = { winhighlight = "NormalFloat:Normal" },
  border = "rounded",
  title_pos = "center",
  footer_pos = "center",
})

--- Return a list of scratch buffers sorted by mtime.
---@return snacks.scratch.File[]
function M.list()
  local root = Snacks.config.get("scratch", defaults).root
  ---@type snacks.scratch.File[]
  local ret = {}
  for file, t in vim.fs.dir(root) do
    if t == "file" then
      local decoded = Snacks.util.file_decode(file)
      local count, icon, name, cwd, branch, ft = decoded:match("^(%d*)|([^|]*)|([^|]*)|([^|]*)|([^|]*)%.([^|]*)$")
      if count and icon and name and cwd and branch and ft then
        file = svim.fs.normalize(root .. "/" .. file)
        table.insert(ret, {
          file = file,
          stat = uv.fs_stat(file),
          count = count ~= "" and tonumber(count) or nil,
          icon = icon ~= "" and icon or nil,
          name = name,
          cwd = cwd ~= "" and cwd or nil,
          branch = branch ~= "" and branch or nil,
          ft = ft,
        })
      end
    end
  end
  table.sort(ret, function(a, b)
    return a.stat.mtime.sec > b.stat.mtime.sec
  end)
  return ret
end

--- Select a scratch buffer from a list of scratch buffers.
function M.select()
  local widths = { 0, 0, 0, 0 }
  local items = M.list()
  for _, item in ipairs(items) do
    item.icon = item.icon or Snacks.util.icon(item.ft, "filetype")
    item.branch = item.branch and ("branch:%s"):format(item.branch) or ""
    item.cwd = item.cwd and vim.fn.fnamemodify(item.cwd, ":p:~") or ""
    widths[1] = math.max(widths[1], vim.api.nvim_strwidth(item.cwd))
    widths[2] = math.max(widths[2], vim.api.nvim_strwidth(item.icon))
    widths[3] = math.max(widths[3], vim.api.nvim_strwidth(item.name))
    widths[4] = math.max(widths[4], vim.api.nvim_strwidth(item.branch))
  end
  vim.ui.select(items, {
    prompt = "Select Scratch Buffer",
    ---@param item snacks.scratch.File
    format_item = function(item)
      local parts = { item.cwd, item.icon, item.name, item.branch }
      for i, part in ipairs(parts) do
        parts[i] = part .. string.rep(" ", widths[i] - vim.api.nvim_strwidth(part))
      end
      return table.concat(parts, " ")
    end,
  }, function(selected)
    if selected then
      M.open({ icon = selected.icon, file = selected.file, name = selected.name, ft = selected.ft })
    end
  end)
end

--- Open a scratch buffer with the given options.
--- If a window is already open with the same buffer,
--- it will be closed instead.
---@param opts? snacks.scratch.Config
function M.open(opts)
  opts = Snacks.config.get("scratch", defaults, opts)
  local ft = "markdown"
  if type(opts.ft) == "function" then
    ft = opts.ft()
  elseif type(opts.ft) == "string" then
    ft = opts.ft --[[@as string]]
  end

  opts.win = Snacks.win.resolve("scratch", opts.win_by_ft[ft], opts.win, { show = false })
  opts.win.bo = opts.win.bo or {}
  opts.win.bo.filetype = ft

  local file = opts.file
  if not file then
    local branch = ""
    if opts.filekey.branch and uv.fs_stat(".git") then
      local ret = vim.fn.systemlist("git branch --show-current")[1]
      if vim.v.shell_error == 0 then
        branch = ret
      end
    end

    local filekey = {
      opts.filekey.count and tostring(vim.v.count1) or "",
      opts.icon or "",
      opts.name:gsub("|", " "),
      opts.filekey.cwd and svim.fs.normalize(assert(uv.cwd())) or "",
      branch,
    }

    vim.fn.mkdir(opts.root, "p")
    local fname = Snacks.util.file_encode(table.concat(filekey, "|") .. "." .. ft)
    file = opts.root .. "/" .. fname
  end
  file = svim.fs.normalize(file)

  local icon, icon_hl = unpack(type(opts.icon) == "table" and opts.icon or { opts.icon, nil })
  ---@cast icon string
  if not icon then
    icon, icon_hl = Snacks.util.icon(ft, "filetype")
  end
  opts.win.title = {
    { " " },
    { icon .. string.rep(" ", 2 - vim.api.nvim_strwidth(icon)), icon_hl },
    { " " },
    { opts.name .. (vim.v.count1 > 1 and " " .. vim.v.count1 or "") },
    { " " },
  }
  for _, t in ipairs(opts.win.title) do
    t[2] = t[2] or "SnacksScratchTitle"
  end

  local is_new = not uv.fs_stat(file)
  local buf = vim.fn.bufadd(file)

  local closed = false
  for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
    if vim.api.nvim_win_get_buf(win) == buf then
      vim.schedule(function()
        vim.api.nvim_win_call(win, function()
          vim.cmd([[close]])
        end)
      end)
      closed = true
    end
  end
  if closed then
    return
  end
  is_new = is_new
    and vim.api.nvim_buf_line_count(buf) == 0
    and #(vim.api.nvim_buf_get_lines(buf, 0, 1, false)[1] or "") == 0

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

  if opts.template then
    local function reset()
      vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(opts.template, "\n"))
    end
    opts.win.keys = opts.win.keys or {}
    opts.win.keys.reset = { "R", reset, desc = "Reset buffer" }
    if is_new then
      reset()
    end
  end

  opts.win.buf = buf
  local ret = Snacks.win(opts.win)
  ret.opts.footer = {}
  table.sort(ret.keys, function(a, b)
    return a[1] < b[1]
  end)
  for _, key in ipairs(ret.keys) do
    local keymap = vim.fn.keytrans(Snacks.util.keycode(key[1]))
    table.insert(ret.opts.footer, { " " })
    table.insert(ret.opts.footer, { " " .. keymap .. " ", "SnacksScratchKey" })
    table.insert(ret.opts.footer, { " " .. (key.desc or keymap) .. " ", "SnacksScratchDesc" })
  end
  table.insert(ret.opts.footer, { " " })
  for _, t in ipairs(ret.opts.footer) do
    t[2] = t[2] or "SnacksScratchFooter"
  end
  if opts.autowrite then
    vim.api.nvim_create_autocmd("BufHidden", {
      group = vim.api.nvim_create_augroup("snacks_scratch_autowrite_" .. buf, { clear = true }),
      buffer = buf,
      callback = function(ev)
        vim.api.nvim_buf_call(ev.buf, function()
          vim.cmd("silent! write")
        end)
      end,
    })
  end
  return ret:show()
end

return M
