local util = require "rx.util"
local Subscription = require "rx.subscription"

--- @class CooperativeScheduler
-- @description Manages Observables using coroutines and a virtual clock that must be updated
-- manually.
local CooperativeScheduler = {}
CooperativeScheduler.__index = CooperativeScheduler
CooperativeScheduler.__tostring = util.constant("CooperativeScheduler")

--- Creates a new CooperativeScheduler.
-- @arg {number=0} currentTime - A time to start the scheduler at.
-- @returns {CooperativeScheduler}
function CooperativeScheduler.create(currentTime)
    local self = {
        tasks = {},
        currentTime = currentTime or 0,
    }

    return setmetatable(self, CooperativeScheduler)
end

--- Schedules a function to be run after an optional delay.  Returns a subscription that will stop
-- the action from running.
-- @arg {function} action - The function to execute. Will be converted into a coroutine. The
--                          coroutine may yield execution back to the scheduler with an optional
--                          number, which will put it to sleep for a time period.
-- @arg {number=0} delay - Delay execution of the action by a virtual time period.
-- @returns {Subscription}
function CooperativeScheduler:schedule(action, delay)
    local task = {
        thread = coroutine.create(action),
        due = self.currentTime + (delay or 0),
    }

    table.insert(self.tasks, task)

    return Subscription.create(function()
        return self:unschedule(task)
    end)
end

function CooperativeScheduler:unschedule(task)
    for i = 1, #self.tasks do
        if self.tasks[i] == task then
            table.remove(self.tasks, i)
        end
    end
end

--- Triggers an update of the CooperativeScheduler. The clock will be advanced and the scheduler
-- will run any coroutines that are due to be run.
-- @arg {number=0} delta - An amount of time to advance the clock by. It is common to pass in the
--                         time in seconds or milliseconds elapsed since this function was last
--                         called.
function CooperativeScheduler:update(delta)
    self.currentTime = self.currentTime + (delta or 0)

    local i = 1
    while i <= #self.tasks do
        local task = self.tasks[i]

        if self.currentTime >= task.due then
            local success, delay = coroutine.resume(task.thread)

            if coroutine.status(task.thread) == "dead" then
                table.remove(self.tasks, i)
            else
                task.due = math.max(task.due + (delay or 0), self.currentTime)
                i = i + 1
            end

            if not success then
                error(delay)
            end
        else
            i = i + 1
        end
    end
end

--- Returns whether or not the CooperativeScheduler's queue is empty.
function CooperativeScheduler:isEmpty()
    return not next(self.tasks)
end

return CooperativeScheduler
