---@class snacks.dim
---@overload fun(opts: snacks.dim.Config)
local M = setmetatable({}, {
  __call = function(M, ...)
    return M.enable(...)
  end,
})

M.meta = {
  desc = "Focus on the active scope by dimming the rest",
}

---@class snacks.dim.Config
local defaults = {
  ---@type snacks.scope.Config
  scope = {
    min_size = 5,
    max_size = 20,
    siblings = true,
  },
  -- animate scopes. Enabled by default for Neovim >= 0.10
  -- Works on older versions but has to trigger redraws during animation.
  ---@type snacks.animate.Config|{enabled?: boolean}
  animate = {
    enabled = vim.fn.has("nvim-0.10") == 1,
    easing = "outQuad",
    duration = {
      step = 20, -- ms per step
      total = 300, -- maximum duration
    },
  },
  -- what buffers to dim
  filter = function(buf)
    return vim.g.snacks_dim ~= false and vim.b[buf].snacks_dim ~= false and vim.bo[buf].buftype == ""
  end,
}

M.enabled = false
local ns = vim.api.nvim_create_namespace("snacks_dim")
local scopes ---@type snacks.scope.Listener?
local scopes_anim = {} ---@type table<number, {from:number, to:number, buf:number}>

Snacks.util.set_hl({
  [""] = "DiagnosticUnnecessary",
}, { prefix = "SnacksDim", default = true })

--- Called during every redraw cycle, so it should be fast.
--- Everything that can be cached should be cached.
---@param win number
---@param buf number
---@param top number -- 1-indexed
---@param bottom number -- 1-indexed
---@private
function M.on_win(win, buf, top, bottom)
  local scope = scopes and scopes:get(win)
  if not scope then
    return
  end
  local function add(l)
    vim.api.nvim_buf_set_extmark(buf, ns, l - 1, 0, {
      end_row = l,
      end_col = 0,
      hl_group = "SnacksDim",
      ephemeral = true,
    })
  end
  local animating = Snacks.animate.enabled({ buf = buf, name = "dim" })
  local from = animating and scopes_anim[win] and scopes_anim[win].from or scope.from
  local to = animating and scopes_anim[win] and scopes_anim[win].to or scope.to
  for l = top, math.min(from - 1, bottom) do
    add(l)
  end
  for l = math.max(to + 1, top), bottom do
    add(l)
  end
end

---@param opts? snacks.dim.Config
function M.enable(opts)
  if M.enabled then
    return
  end
  opts = Snacks.config.get("dim", defaults, opts)

  M.enabled = true

  vim.g.snacks_animate_dim = opts.animate.enabled

  -- setup decoration provider
  vim.api.nvim_set_decoration_provider(ns, {
    on_win = function(_, win, buf, top, bottom)
      if M.enabled and opts.filter(buf) then
        M.on_win(win, buf, top + 1, bottom + 1)
      end
    end,
  })

  scopes = scopes
    or Snacks.scope.attach(function(win, buf, scope)
      if not Snacks.animate.enabled({ buf = buf, name = "dim" }) then
        Snacks.util.redraw(win)
      else
        if not (scopes_anim[win] and scopes_anim[win].buf == buf) then
          local info = vim.fn.getwininfo(win)[1]
          scopes_anim[win] = {
            from = info.topline,
            to = info.botline,
            buf = buf,
          }
        end

        Snacks.animate(scopes_anim[win].from, scope.from, function(v)
          if not scopes_anim[win] or not vim.api.nvim_win_is_valid(win) then
            return
          end
          scopes_anim[win].from = v
          Snacks.util.redraw(win)
        end, vim.tbl_extend("keep", { int = true, id = "snacks_dim_from_" .. win, buf = buf }, opts.animate))

        Snacks.animate(scopes_anim[win].to, scope.to, function(v)
          if not scopes_anim[win] or not vim.api.nvim_win_is_valid(win) then
            return
          end
          scopes_anim[win].to = v
          Snacks.util.redraw(win)
        end, vim.tbl_extend("keep", { int = true, id = "snacks_dim_to_" .. win, buf = buf }, opts.animate))
      end
    end, opts.scope)
  if not scopes.enabled then
    scopes:enable()
  end
end

-- Disable dimming
function M.disable()
  if not M.enabled then
    return
  end
  M.enabled = false
  if scopes and scopes.enabled then
    scopes:disable()
  end
  scopes_anim = {}
  vim.cmd([[redraw!]])
end

return M
