---------------------------------------------------------------------------
--- Class to execute code at specific intervals.
--
-- @usage
--    -- Create a widget and update its content using the output of a shell
--    -- command every 10 seconds:
--    local mybatterybar = wibox.widget {
--        {
--            min_value    = 0,
--            max_value    = 100,
--            value        = 0,
--            paddings     = 1,
--            border_width = 1,
--            forced_width = 50,
--            border_color = "#0000ff",
--            id           = "mypb",
--            widget       = wibox.widget.progressbar,
--        },
--        {
--            id           = "mytb",
--            text         = "100%",
--            widget       = wibox.widget.textbox,
--        },
--        layout      = wibox.layout.stack,
--        set_battery = function(self, val)
--            self.mytb.text  = tonumber(val).."%"
--            self.mypb.value = tonumber(val)
--        end,
--    }
--
--    gears.timer {
--        timeout   = 10,
--        call_now  = true,
--        autostart = true,
--        callback  = function()
--            -- You should read it from `/sys/class/power_supply/` (on Linux)
--            -- instead of spawning a shell. This is only an example.
--            awful.spawn.easy_async(
--                {"sh", "-c", "acpi | sed -n 's/^.*, \([0-9]*\)%/\1/p'"},
--                function(out)
--                    mybatterybar.battery = out
--                end
--            )
--        end
--    }
--
-- @author Uli Schlachter
-- @copyright 2014 Uli Schlachter
-- @coreclassmod gears.timer
---------------------------------------------------------------------------

local capi = { awesome = awesome }
local ipairs = ipairs
local pairs = pairs
local setmetatable = setmetatable
local table = table
local tonumber = tonumber
local traceback = debug.traceback
local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
local glib = require("lgi").GLib
local object = require("gears.object")
local protected_call = require("gears.protected_call")
local gdebug = require("gears.debug")
local gmath = require("gears.math")

--- Timer objects. This type of object is useful when triggering events repeatedly.
--
-- The timer will emit the "timeout" signal every N seconds, N being the timeout
-- value. Note that a started timer will not be garbage collected. Call `:stop`
-- before dropping the last reference to prevent leaking the object.
--
-- @tfield number timeout Interval in seconds to emit the timeout signal.
--   Can be any value, including floating point ones (e.g. `1.5` seconds).
-- @tfield boolean started Read-only boolean field indicating if the timer has been
--   started.
-- @table timer

--- Emitted when the timer is started.
-- @signal start

--- Emitted when the timer is stopped.
-- @signal stop

--- Emitted when `timeout` seconds have elapsed.
--
-- This will be emitted repeatedly, unless `single_shot` has been set to `true`
-- for the timer.
-- @signal timeout

local timer = { mt = {} }

--- Start the timer.
-- @method start
-- @noreturn
-- @emits start
function timer:start()
    if self.data.source_id ~= nil then
        gdebug.print_error(traceback("timer already started"))
        return
    end
    local timeout_ms = gmath.round(self.data.timeout * 1000)
    self.data.source_id = glib.timeout_add(glib.PRIORITY_DEFAULT, timeout_ms, function()
        protected_call(self.emit_signal, self, "timeout")
        return glib.SOURCE_CONTINUE
    end)
    self:emit_signal("start")
end

--- Stop the timer.
--
-- Does nothing if the timer isn't running.
--
-- @method stop
-- @noreturn
-- @emits stop
function timer:stop()
    if self.data.source_id == nil then
        return
    end
    glib.source_remove(self.data.source_id)
    self.data.source_id = nil
    self:emit_signal("stop")
end

--- Restart the timer.
-- This is equivalent to stopping the timer if it is running and then starting
-- it.
-- @method again
-- @noreturn
-- @emits start
-- @emits stop
function timer:again()
    if self.data.source_id ~= nil then
        self:stop()
    end
    self:start()
end

--- The timer is started.
--
-- For this to be `true` by default, pass `autostart` to the constructor.
--
-- @property started
-- @tparam[opt=false] boolean started
-- @see start
-- @see stop

--- The timer timeout value in seconds.
--
-- @property timeout
-- @tparam[opt=0] number timeout
-- @propertyunit second
-- @negativeallowed false
-- @propemits true false

local timer_instance_mt = {
    __index = function(self, property)
        if property == "timeout" then
            return self.data.timeout
        elseif property == "started" then
            return self.data.source_id ~= nil
        end

        return timer[property]
    end,

    __newindex = function(self, property, value)
        if property == "timeout" then
            self.data.timeout = tonumber(value)
            self:emit_signal("property::timeout", value)
        end
    end
}

--- Create a new timer object.
--
-- `call_now` only takes effect when a `callback` is provided. `single_shot`,
-- on the other hand, also stops signals connected to the `timeout` signal.
--
-- Specifying a function `func` as `args.callback` is equivalent to calling
-- `:connect_signal(func)` on the timer object.
--
-- @tparam table args Arguments.
-- @tparam number args.timeout Timeout in seconds (e.g. `1.5`).
-- @tparam[opt=false] boolean args.autostart Immediately start the timer countdown.
-- @tparam[opt=false] boolean args.call_now Immediately call the callback function.
-- @tparam[opt] function args.callback Callback function to connect to the
--  "timeout" signal.
-- @tparam[opt=false] boolean args.single_shot Run only once then stop.
-- @treturn timer
-- @constructorfct gears.timer
function timer.new(args)
    args = args or {}
    local ret = object()

    ret.data = { timeout = 0 } --TODO v5 rename to ._private
    setmetatable(ret, timer_instance_mt)

    for k, v in pairs(args) do
        ret[k] = v
    end

    if args.autostart then
        ret:start()
    end

    if args.callback then
        ret:connect_signal("timeout", args.callback)
    end
    if args.call_now then
        ret:emit_signal("timeout")
    end
    if args.single_shot then
        ret:connect_signal("timeout", function() ret:stop() end)
    end

    return ret
end

--- Create a simple timer for calling the `callback` function continuously.
--
-- This is a small wrapper around `gears.timer`, that creates a timer based on
-- `callback`.
-- The timer will run continuously and call `callback` every `timeout` seconds.
-- It is stopped when `callback` returns `false`, when `callback` throws an
-- error or when the `:stop()` method is called on the return value.
--
-- @tparam number timeout Timeout in seconds (e.g. 1.5).
-- @tparam function callback Function to run.
-- @treturn timer The new timer object.
-- @staticfct gears.timer.start_new
-- @see gears.timer.weak_start_new
function timer.start_new(timeout, callback)
    local t = timer.new({ timeout = timeout })
    t:connect_signal("timeout", function()
        local cont = protected_call(callback)
        if not cont then
            t:stop()
        end
    end)
    t:start()
    return t
end

--- Create a simple timer for calling the `callback` function continuously.
--
-- This function is almost identical to `gears.timer.start_new`. The only
-- difference is that this does not prevent the callback function from being
-- garbage collected.
-- In addition to the conditions in `gears.timer.start_new`,
-- this timer will also stop if `callback` was garbage collected since the
-- previous run.
--
-- @tparam number timeout Timeout in seconds (e.g. 1.5).
-- @tparam function callback Function to start.
-- @treturn timer The new timer object.
-- @staticfct gears.timer.weak_start_new
-- @see gears.timer.start_new
function timer.weak_start_new(timeout, callback)
    local indirection = setmetatable({}, { __mode = "v" })
    indirection.callback = callback
    return timer.start_new(timeout, function()
        local cb = indirection.callback
        if cb then
            return cb()
        end
    end)
end

local delayed_calls = {}

--- Run all pending delayed calls now. This function should best not be used at
-- all, because it means that less batching happens and the delayed calls run
-- prematurely.
-- @staticfct gears.timer.run_delayed_calls_now
-- @noreturn
function timer.run_delayed_calls_now()
    for _, callback in ipairs(delayed_calls) do
        protected_call(unpack(callback))
    end
    delayed_calls = {}
end

--- Call the given function at the end of the current GLib event loop iteration.
-- @tparam function callback The function that should be called
-- @param ... Arguments to the callback function
-- @noreturn
-- @staticfct gears.timer.delayed_call
function timer.delayed_call(callback, ...)
    assert(type(callback) == "function", "callback must be a function, got: " .. type(callback))
    table.insert(delayed_calls, { callback, ... })
end

capi.awesome.connect_signal("refresh", timer.run_delayed_calls_now)

function timer.mt.__call(_, ...)
    return timer.new(...)
end

--
--- Disconnect from a signal.
-- @tparam string name The name of the signal.
-- @tparam function func The callback that should be disconnected.
-- @method disconnect_signal
-- @treturn boolean `true` when the function was disconnected or `false` if it
--  wasn't found.
-- @baseclass gears.object

--- Emit a signal.
--
-- @tparam string name The name of the signal.
-- @param ... Extra arguments for the callback functions. Each connected
--   function receives the object as first argument and then any extra
--   arguments that are given to emit_signal().
-- @method emit_signal
-- @noreturn
-- @baseclass gears.object

--- Connect to a signal.
-- @tparam string name The name of the signal.
-- @tparam function func The callback to call when the signal is emitted.
-- @method connect_signal
-- @noreturn
-- @baseclass gears.object

--- Connect to a signal weakly.
--
-- This allows the callback function to be garbage collected and
-- automatically disconnects the signal when that happens.
--
-- **Warning:**
-- Only use this function if you really, really, really know what you
-- are doing.
-- @tparam string name The name of the signal.
-- @tparam function func The callback to call when the signal is emitted.
-- @method weak_connect_signal
-- @noreturn
-- @baseclass gears.object

return setmetatable(timer, timer.mt)

-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
