-- Threading simulation module
-- Task.sleep()
-- @author Valansch and Grilledham
-- github: https://github.com/Refactorio/RedMew
-- ======================================================= --

local Queue = require 'utils.queue'
local PriorityQueue = require 'utils.priority_queue'
local Event = require 'utils.event'
local Token = require 'utils.token'
local Global = require 'utils.global'
local Color = require 'utils.color_presets'

local floor = math.floor
local log10 = math.log10
local Token_get = Token.get
local xpcall = xpcall
local trace = debug.traceback
local log = log
local Queue_peek = Queue.peek
local Queue_pop = Queue.pop
local Queue_push = Queue.push
local PriorityQueue_peek = PriorityQueue.peek
local PriorityQueue_pop = PriorityQueue.pop
local PriorityQueue_push = PriorityQueue.push

local Task = {}

local function comparator(a, b)
    return a.time < b.time
end

local duration_tasks = PriorityQueue.new(comparator)
local callbacks = PriorityQueue.new(comparator)
local task_queue = Queue.new()
local primitives =
{
    next_async_callback_time = -1,
    total_task_weight = 0,
    task_queue_speed = 1,
    task_per_tick = 1
}

Global.register(
    { duration_tasks = duration_tasks, callbacks = callbacks, task_queue = task_queue, primitives = primitives },
    function (tbl)
        duration_tasks = tbl.duration_tasks
        callbacks = tbl.callbacks
        task_queue = tbl.task_queue
        primitives = tbl.primitives

        PriorityQueue.load(callbacks, comparator)
        PriorityQueue.load(duration_tasks, comparator)
    end
)

local delay_print_token =
    Token.register(
        function (event)
            local text = event.text
            if not text then
                return
            end

            local color = event.color
            if not color then
                color = Color.info
            end

            game.print(text, color)
        end
    )

local function handler_error(err)
    log('\n\t' .. trace(err))
end

local function get_task_per_tick(tick)
    if tick % 300 == 0 then
        local size = primitives.total_task_weight
        local task_per_tick = floor(log10(size + 1)) * primitives.task_queue_speed
        if task_per_tick < 1 then
            task_per_tick = 1
        end

        primitives.task_per_tick = task_per_tick
        return task_per_tick
    end
    return primitives.task_per_tick
end

local function on_tick()
    local tick = game.tick

    for _ = 1, get_task_per_tick(tick) do
        local task = Queue_peek(task_queue)
        if task ~= nil then
            local func = Token_get(task.func_token)
            local success, result = xpcall(func, handler_error, task.params)
            if not success then
                if _DEBUG then
                    error(result)
                else
                    log(result)
                end
                Queue_pop(task_queue)
                primitives.total_task_weight = primitives.total_task_weight - task.weight
            elseif not result then
                Queue_pop(task_queue)
                primitives.total_task_weight = primitives.total_task_weight - task.weight
            end
        end
    end

    local callback = PriorityQueue_peek(callbacks)
    while callback ~= nil and tick >= callback.time do
        local func = Token_get(callback.func_token)
        xpcall(func, handler_error, callback.params)
        PriorityQueue_pop(callbacks)
        callback = PriorityQueue_peek(callbacks)
    end

    local duration_task = PriorityQueue_peek(duration_tasks)
    if duration_task ~= nil then
        local func = Token_get(duration_task.func_token)
        xpcall(func, handler_error, duration_task.params)
        if tick >= duration_task.duration_ticks then
            PriorityQueue_pop(duration_tasks)
        end
    end
end

--- Allows you to set a timer (in ticks) after which the tokened function will be run with params given as an argument
-- Cannot be called before init
-- @param ticks <number>
-- @param func_token <number> a token for a function store via the token system
-- @param params <any> the argument to send to the tokened function
function Task.set_timeout_in_ticks(ticks, func_token, params)
    if not game then
        error('cannot call when game is not available', 2)
    end
    local time = game.tick + ticks
    local callback = { time = time, func_token = func_token, params = params }
    PriorityQueue_push(callbacks, callback)
end

--- Creates a task that runs every tick for a specified duration
-- @param func_token <number> a token for a function stored via the token system
-- @param duration_ticks <number> how many ticks the task should run for
-- @param params <any> the argument to send to the tokened function
function Task.set_duration_task(start_tick, duration_ticks, func_token, params)
    if not game then
        error('cannot call when game is not available', 2)
    end

    local time = game.tick + start_tick
    local callback =
    {
        time = time,
        func_token = func_token,
        params = params,
        start_tick = game.tick + start_tick,
        duration_ticks = game.tick + duration_ticks + start_tick
    }

    PriorityQueue_push(duration_tasks, callback)
end

--- Allows you to set a timer (in ticks) after which the tokened function will be run with params given as an argument
-- Cannot be called before init
-- @param ticks <number>
-- @param params <any> the argument to send to the tokened function
function Task.set_timeout_in_ticks_text(ticks, params)
    if not game then
        error('cannot call when game is not available', 2)
    end
    local time = game.tick + ticks
    local callback = { time = time, func_token = delay_print_token, params = params }
    PriorityQueue_push(callbacks, callback)
end

--- Allows you to set a timer (in seconds) after which the tokened function will be run with params given as an argument
-- Cannot be called before init
-- @param sec <number>
-- @param func_token <number> a token for a function store via the token system
-- @param params <any> the argument to send to the tokened function
function Task.set_timeout(sec, func_token, params)
    if not game then
        error('cannot call when game is not available', 2)
    end
    Task.set_timeout_in_ticks(60 * sec, func_token, params)
end

--- Queueing allows you to split up heavy tasks which don't need to be completed in the same tick.
-- Queued tasks are generally run 1 per tick. If the queue backs up, more tasks will be processed per tick.
-- @param func_token <number> a token for a function stored via the token system
-- If this function returns `true` it will run again the next tick, delaying other queued tasks (see weight)
-- @param params <any> the argument to send to the tokened function
-- @param weight <number> (defaults to 1) weight is the number of ticks a task is expected to take.
-- Ex. if the task is expected to repeat multiple times (ie. the function returns true and loops several ticks)
function Task.queue_task(func_token, params, weight)
    weight = weight or 1
    primitives.total_task_weight = primitives.total_task_weight + weight
    Queue_push(task_queue, { func_token = func_token, params = params, weight = weight })
end

function Task.get_queue_speed()
    return primitives.task_queue_speed
end

function Task.set_queue_speed(value)
    value = value or 1
    if value < 0 then
        value = 0
    end

    primitives.task_queue_speed = value
end

Event.add(defines.events.on_tick, on_tick)

return Task
