-- Copied from: <https://github.com/neovim/neovim/issues/19624#issuecomment-1202405058>

local co = coroutine

local async_thread = {
   threads = {},
}

local function threadtostring(x)
   if jit then
      return string.format('%p', x)
   else
      return tostring(x):match('thread: (.*)')
   end
end

function async_thread.running()
   local thread = co.running()
   local id = threadtostring(thread)
   return async_thread.threads[id]
end

function async_thread.create(fn)
   local thread = co.create(fn)
   local id = threadtostring(thread)
   async_thread.threads[id] = true
   return thread
end

function async_thread.finished(x)
   if co.status(x) == 'dead' then
      local id = threadtostring(x)
      async_thread.threads[id] = nil
      return true
   end
   return false
end

--- @param async_fn function
--- @param ... any
local function execute(async_fn, ...)
   local thread = async_thread.create(async_fn)

   local function step(...)
      local ret = { co.resume(thread, ...) }
      local stat, err_or_fn, nargs = unpack(ret)

      if not stat then
         error(string.format("The coroutine failed with this message: %s\n%s",
         err_or_fn, debug.traceback(thread)))
      end

      if async_thread.finished(thread) then
         return
      end

      assert(type(err_or_fn) == "function", "The 1st parameter must be a lua function")

      local ret_fn = err_or_fn
      local args = { select(4, unpack(ret)) }
      args[nargs] = step
      ret_fn(unpack(args, 1, nargs --[[@as integer]]))
   end

   step(...)
end

local M = {}

--- @param func function
--- @param argc integer
--- @return function
M.wrap = function(func, argc)
   return function(...)
      if not async_thread.running() then
         return func(...)
      end
      return co.yield(func, argc, ...)
   end
end

--- @param func function
--- @return function
M.void = function(func)
   return function(...)
      if async_thread.running() then
         return func(...)
      end
      execute(func, ...)
   end
end

M.schedule = M.wrap(vim.schedule, 1)

return M
