-- Credit: https://gist.github.com/runiq/31aa5c4bf00f8e0843cd267880117201
--

local M = {}

---Validates args for `throttle()` and  `debounce()`.
---TODO(clason): remove shim when dropping support for Nvim 0.10
local td_validate = vim.fn.has "nvim-0.11" == 1
    and function(fn, ms)
      vim.validate("fn", fn, "function")
      vim.validate("ms", ms, function(v)
        return type(v) == "number" and v > 0
      end, "number > 0")
    end
  or function(fn, ms)
    vim.validate {
      fn = { fn, "f" },
      ms = {
        ms,
        function(v)
          return type(v) == "number" and v > 0
        end,
        "number > 0",
      },
    }
  end

--- Throttles a function on the leading edge. Automatically `schedule_wrap()`s.
---@param fn fun(...) Function to throttle
---@param ms number Timeout in ms
---@return fun(...) wrapped_fn Throttled function
---@return uv_timer_t timer Remember to call `timer.close()` at the end or you will leak memory!
function M.throttle_leading(fn, ms)
  td_validate(fn, ms)
  local timer = vim.uv.new_timer()
  local running = false

  local function wrapped_fn(...)
    if not running then
      timer:start(ms, 0, function()
        running = false
      end)
      running = true
      pcall(vim.schedule_wrap(fn), select(1, ...))
    end
  end
  return wrapped_fn, timer
end

--- Throttles a function on the trailing edge. Automatically `schedule_wrap()`s.
---@param fn fun(...) Function to throttle
---@param ms number Timeout in ms
---@param last? boolean Whether to use the arguments of the last call to `fn` within the timeframe.
--- Default: Use arguments of the first call.
---@return fun(...) wrapped_fn Throttled function
---@return uv_timer_t timer Remember to call `timer.close()` at the end or you will leak memory!
function M.throttle_trailing(fn, ms, last)
  td_validate(fn, ms)
  local timer = vim.uv.new_timer()
  local running = false

  local wrapped_fn
  if not last then
    function wrapped_fn(...)
      if not running then
        local argv = { ... }
        local argc = select("#", ...)

        timer:start(ms, 0, function()
          running = false
          pcall(vim.schedule_wrap(fn), unpack(argv, 1, argc))
        end)
        running = true
      end
    end
  else
    local argv, argc
    function wrapped_fn(...)
      argv = { ... }
      argc = select("#", ...)

      if not running then
        timer:start(ms, 0, function()
          running = false
          pcall(vim.schedule_wrap(fn), unpack(argv, 1, argc))
        end)
        running = true
      end
    end
  end
  return wrapped_fn, timer
end

--- Debounces a function on the leading edge. Automatically `schedule_wrap()`s.
---@param fn fun(...) Function to debounce
---@param ms number Timeout in ms
---@return fun(...) wrapped_fn Debounced function
---@return uv_timer_t timer Remember to call `timer.close()` at the end or you will leak memory!
function M.debounce_leading(fn, ms)
  td_validate(fn, ms)
  local timer = vim.uv.new_timer()
  local running = false

  local function wrapped_fn(...)
    timer:start(ms, 0, function()
      running = false
    end)

    if not running then
      running = true
      pcall(vim.schedule_wrap(fn), select(1, ...))
    end
  end
  return wrapped_fn, timer
end

--- Debounces a function on the trailing edge. Automatically `schedule_wrap()`s.
---@param fn fun(...) Function to debounce
---@param ms number Timeout in ms
---@param first? boolean Whether to use the arguments of the first call to `fn` within the timeframe.
--- Default: Use arguments of the last call.
---@return fun(...) wrapped_fn Debounced function
---@return uv_timer_t timer Remember to call `timer.close()` at the end or you will leak memory!
function M.debounce_trailing(fn, ms, first)
  td_validate(fn, ms)
  local timer = vim.uv.new_timer()
  local wrapped_fn

  if not first then
    function wrapped_fn(...)
      local argv = { ... }
      local argc = select("#", ...)

      timer:start(ms, 0, function()
        pcall(vim.schedule_wrap(fn), unpack(argv, 1, argc))
      end)
    end
  else
    local argv, argc
    function wrapped_fn(...)
      argv = argv or { ... }
      argc = argc or select("#", ...)

      timer:start(ms, 0, function()
        pcall(vim.schedule_wrap(fn), unpack(argv, 1, argc))
      end)
    end
  end
  return wrapped_fn, timer
end

--- Test deferment methods (`{throttle,debounce}_{leading,trailing}()`).
---@param bouncer string Bouncer function to test
---@param ms? number Timeout in ms, default 2000.
---@param firstlast? boolean Whether to use the 'other' fn call strategy.
function M.test_defer(bouncer, ms, firstlast)
  local bouncers = {
    tl = M.throttle_leading,
    tt = M.throttle_trailing,
    dl = M.debounce_leading,
    dt = M.debounce_trailing,
  }

  local timeout = ms or 2000

  local bounced = bouncers[bouncer](function(i)
    vim.cmd('echom "' .. bouncer .. ": " .. i .. '"')
  end, timeout, firstlast)

  for i, _ in ipairs { 1, 2, 3, 4, 5 } do
    bounced(i)
    vim.schedule(function()
      vim.cmd("echom " .. i)
    end)
    vim.fn.call("wait", { 1000, "v:false" })
  end
end

return M
