local coUtils = {}
local create = coroutine.create
local running = coroutine.running
local safeResume = function(co, ...)
  local flag, msg = coroutine.resume(co, ...)
  if not flag then
    error(debug.traceback(co, msg))
  end
end
local yield = coroutine.yield
local unpack = table.unpack
local timerMap = {}
setmetatable(timerMap, {__mode = "kv"})
local cbMap = {}
setmetatable(cbMap, {__mode = "kv"})

function coUtils.start(f, ...)
  local co = create(f)
  if running() == nil then
    safeResume(co, ...)
  else
    local args = {
      ...
    }
    local timer
    local action = function()
      timerMap[co] = nil
      timer:destroy()
      safeResume(co, unpack(args))
    end
    timer = Timer.new(action, 0, 1)
    timerMap[co] = timer
    timer:start()
  end
  return co
end

function coUtils.wait(t, unscaled)
  local co = running()
  if co == nil then
    error("cannot call in main thread!")
  end
  if t <= 0 then
    return -t
  end
  local timer
  local action = function()
    timerMap[co] = nil
    local consume = timer.consume
    timer:destroy()
    safeResume(co, -consume)
  end
  timer = Timer.new(action, t, 1, not unscaled)
  timerMap[co] = timer
  timer:start()
  return yield()
end

function coUtils.step(n)
  local co = running()
  if co == nil then
    error("cannot call in main thread!")
  end
  n = n or 1
  local timer
  local targetFrame = C_Time.frameCount + n
  local action = function()
    if C_Time.frameCount < targetFrame then
      return
    end
    timerMap[co] = nil
    timer:destroy()
    safeResume(co)
  end
  timer = Timer.new(action, 0, n + 1)
  timerMap[co] = timer
  timer:start()
  return yield()
end

function coUtils.callback(...)
end

local replaceCallback = function(params, callback)
  for k, v in pairs(params) do
    if v == coUtils.callback then
      params[k] = callback
      break
    end
  end
  return params
end

function coUtils.call(f, ...)
  local co = running()
  if co == nil then
    error("cannot call in main thread!")
  end
  local rets
  local callback = function(...)
    if cbMap[co] then
      cbMap[co] = nil
      safeResume(co, ...)
    else
      rets = {
        ...
      }
    end
  end
  local params = replaceCallback({
    ...
  }, callback)
  f(unpack(params))
  if not rets then
    cbMap[co] = callback
    return yield()
  else
    return unpack(rets)
  end
end

function coUtils.waitUntil(cond, timeout, unscaled)
  local co = running()
  if co == nil then
    error("cannot call in main thread!")
  end
  local timer
  local checkTime = 0
  local endTime = 0
  if timeout and 0 < timeout then
    if unscaled then
      checkTime = 2
      endTime = C_Time.unscaledTime + timeout
    else
      checkTime = 1
      endTime = C_Time.time + timeout
    end
  end
  local action = function()
    local res = cond()
    if not res then
      if checkTime == 0 then
        return
      elseif checkTime == 1 then
        if C_Time.time < endTime then
          return
        end
      elseif checkTime == 2 and C_Time.unscaledTime < endTime then
        return
      end
    end
    timerMap[co] = nil
    timer:destroy()
    safeResume(co, res)
  end
  timer = Timer.new(action, 0, -1)
  timerMap[co] = timer
  timer:start()
  return yield()
end

function coUtils.stop(co)
  local timer = timerMap[co]
  if timer ~= nil then
    timerMap[co] = nil
    timer:destroy()
    return
  end
  local cb = cbMap[co]
  if cb ~= nil then
    cbMap[co] = nil
    return
  end
end

return coUtils
