---------------------------------------------------------------------------
--- Spawning of programs.
--
-- This module provides methods to start programs and supports startup
-- notifications, which allows for callbacks and applying properties to the
-- program after it has been launched.  This requires currently that the
-- applicaton supports them.
--
-- @author Julien Danjou <julien@danjou.info>
-- @author Emmanuel Lepage Vallee <elv1313@gmail.com>
-- @copyright 2008 Julien Danjou
-- @copyright 2014 Emmanuel Lepage Vallee
-- @release @AWESOME_VERSION@
-- @module awful.spawn
---------------------------------------------------------------------------

local capi =
{
    awesome = awesome,
    mouse = mouse,
    client = client,
}
local lgi = require("lgi")
local Gio = lgi.Gio
local GLib = lgi.GLib
local util   = require("awful.util")
local protected_call = require("gears.protected_call")

local spawn = {}


local end_of_file
do
    -- API changes, bug fixes and lots of fun. Figure out how a EOF is signalled.
    local input
    if not pcall(function()
        -- No idea when this API changed, but some versions expect a string,
        -- others a table with some special(?) entries
        input = Gio.DataInputStream.new(Gio.MemoryInputStream.new_from_data(""))
    end) then
        input = Gio.DataInputStream.new(Gio.MemoryInputStream.new_from_data({}))
    end
    local line, length = input:read_line()
    if not line then
        -- Fixed in 2016: NULL on the C side is transformed to nil in Lua
        end_of_file = function(arg)
            return not arg
        end
    elseif tostring(line) == "" and #line ~= length then
        -- "Historic" behaviour for end-of-file:
        -- - NULL is turned into an empty string
        -- - The length variable is not initialized
        -- It's highly unlikely that the uninitialized variable has value zero.
        -- Use this hack to detect EOF.
        end_of_file = function(arg1, arg2)
            return #arg1 ~= arg2
        end
    else
        assert(tostring(line) == "", "Cannot determine how to detect EOF")
        -- The above uninitialized variable was fixed and thus length is
        -- always 0 when line is NULL in C. We cannot tell apart an empty line and
        -- EOF in this case.
        require("gears.debug").print_warning("Cannot reliably detect EOF on an "
                .. "GIOInputStream with this LGI version")
        end_of_file = function(arg)
            return tostring(arg) == ""
        end
    end
end

spawn.snid_buffer = {}

function spawn.on_snid_callback(c)
    local props = spawn.snid_buffer[c.startup_id]
    if props then
        c:emit_signal("spawn::completed_with_payload", props)
        spawn.snid_buffer[c.startup_id] = nil
    end
end

function spawn.on_snid_cancel(id)
    if spawn.snid_buffer[id] then
        spawn.snid_buffer[id] = nil
    end
end

--- Spawn a program, and optionally apply properties and/or run a callback.
--
-- Applying properties or running a callback requires the program/client to
-- support startup notifications.
--
-- See `awful.rules.execute` for more details about the format of `sn_rules`.
--
-- @tparam string|table cmd The command.
-- @tparam[opt=true] table|boolean sn_rules A table of properties to be applied
--   after startup; `false` to disable startup notifications.
-- @tparam[opt] function callback A callback function to be run after startup.
-- @treturn[1] integer The forked PID.
-- @treturn[1] ?string The startup notification ID, if `sn` is not false, or
--   a `callback` is provided.
-- @treturn[2] string Error message.
function spawn.spawn(cmd, sn_rules, callback)
    if cmd and cmd ~= "" then
        local enable_sn = (sn_rules ~= false or callback)
        enable_sn = not not enable_sn -- Force into a boolean.
        if not sn_rules and callback then
            sn_rules = {callback=callback}
        elseif callback then
            sn_rules.callback = callback
        end
        local pid, snid = capi.awesome.spawn(cmd, enable_sn)
        -- The snid will be nil in case of failure
        if snid and type(sn_rules) == "table" then
            spawn.snid_buffer[snid] = sn_rules
        end
        return pid, snid
    end
    -- For consistency
    return "Error: No command to execute"
end

--- Spawn a program using the shell.
-- This calls `cmd` with `$SHELL -c` (via `awful.util.shell`).
-- @tparam string cmd The command.
function spawn.with_shell(cmd)
    if cmd and cmd ~= "" then
        cmd = { util.shell, "-c", cmd }
        return capi.awesome.spawn(cmd, false)
    end
end

--- Spawn a program and asynchronously capture its output line by line.
-- @tparam string|table cmd The command.
-- @tab callbacks Table containing callbacks that should be invoked on
--   various conditions.
-- @tparam[opt] function callbacks.stdout Function that is called with each
--   line of output on stdout, e.g. `stdout(line)`.
-- @tparam[opt] function callbacks.stderr Function that is called with each
--   line of output on stderr, e.g. `stderr(line)`.
-- @tparam[opt] function callbacks.output_done Function to call when no more
--   output is produced.
-- @tparam[opt] function callbacks.exit Function to call when the spawned
--   process exits. This function gets the exit reason and code as its
--   arguments.
--   The reason can be "exit" or "signal".
--   For "exit", the second argument is the exit code.
--   For "signal", the second argument is the signal causing process
--   termination.
-- @treturn[1] Integer the PID of the forked process.
-- @treturn[2] string Error message.
function spawn.with_line_callback(cmd, callbacks)
    local stdout_callback, stderr_callback, done_callback, exit_callback =
        callbacks.stdout, callbacks.stderr, callbacks.output_done, callbacks.exit
    local have_stdout, have_stderr = stdout_callback ~= nil, stderr_callback ~= nil
    local pid, _, stdin, stdout, stderr = capi.awesome.spawn(cmd,
            false, false, have_stdout, have_stderr, exit_callback)
    if type(pid) == "string" then
        -- Error
        return pid
    end

    local done_before = false
    local function step_done()
        if have_stdout and have_stderr and not done_before then
            done_before = true
            return
        end
        if done_callback then
            done_callback()
        end
    end
    if have_stdout then
        spawn.read_lines(Gio.UnixInputStream.new(stdout, true),
                stdout_callback, step_done, true)
    end
    if have_stderr then
        spawn.read_lines(Gio.UnixInputStream.new(stderr, true),
                stderr_callback, step_done, true)
    end
    assert(stdin == nil)
    return pid
end

--- Asynchronously spawn a program and capture its output.
-- (wraps `spawn.with_line_callback`).
-- @tparam string|table cmd The command.
-- @tab callback Function with the following arguments
-- @tparam string callback.stdout Output on stdout.
-- @tparam string callback.stderr Output on stderr.
-- @tparam string callback.exitreason Exit Reason.
-- The reason can be "exit" or "signal".
-- @tparam integer callback.exitcode Exit code.
-- For "exit" reason it's the exit code.
-- For "signal" reason — the signal causing process termination.
-- @treturn[1] Integer the PID of the forked process.
-- @treturn[2] string Error message.
-- @see spawn.with_line_callback
function spawn.easy_async(cmd, callback)
    local stdout = ''
    local stderr = ''
    local exitcode, exitreason
    local function parse_stdout(str)
        stdout = stdout .. str .. "\n"
    end
    local function parse_stderr(str)
        stderr = stderr .. str .. "\n"
    end
    local function done_callback()
        return callback(stdout, stderr, exitreason, exitcode)
    end
    local exit_callback_fired = false
    local output_done_callback_fired = false
    local function exit_callback(reason, code)
        exitcode = code
        exitreason = reason
        exit_callback_fired = true
        if output_done_callback_fired then
            return done_callback()
        end
    end
    local function output_done_callback()
        output_done_callback_fired = true
        if exit_callback_fired then
            return done_callback()
        end
    end
    return spawn.with_line_callback(
        cmd, {
        stdout=parse_stdout,
        stderr=parse_stderr,
        exit=exit_callback,
        output_done=output_done_callback
    })
end

--- Read lines from a Gio input stream
-- @tparam Gio.InputStream input_stream The input stream to read from.
-- @tparam function line_callback Function that is called with each line
--   read, e.g. `line_callback(line_from_stream)`.
-- @tparam[opt] function done_callback Function that is called when the
--   operation finishes (e.g. due to end of file).
-- @tparam[opt=false] boolean close Should the stream be closed after end-of-file?
function spawn.read_lines(input_stream, line_callback, done_callback, close)
    local stream = Gio.DataInputStream.new(input_stream)
    local function done()
        if close then
            stream:close()
        end
        if done_callback then
            protected_call(done_callback)
        end
    end
    local start_read, finish_read
    start_read = function()
        stream:read_line_async(GLib.PRIORITY_DEFAULT, nil, finish_read)
    end
    finish_read = function(obj, res)
        local line, length = obj:read_line_finish(res)
        if type(length) ~= "number" then
            -- Error
            print("Error in awful.spawn.read_lines:", tostring(length))
            done()
        elseif end_of_file(line, length) then
            -- End of file
            done()
        else
            -- Read a line
            -- This needs tostring() for older lgi versions which returned
            -- "GLib.Bytes" instead of Lua strings (I guess)
            protected_call(line_callback, tostring(line))

            -- Read the next line
            start_read()
        end
    end
    start_read()
end

--- Read a program output and return its output as a string.
-- @tparam string cmd The command to run.
-- @treturn string A string with the program output, or the error if one
--   occured.
function spawn.pread(cmd)
    if cmd and cmd ~= "" then
        local f, err = io.popen(cmd, 'r')
        if f then
            local s = f:read("*all")
            f:close()
            return s
        else
            return err
        end
    end
end

capi.awesome.connect_signal("spawn::canceled" , spawn.on_snid_cancel   )
capi.awesome.connect_signal("spawn::timeout"  , spawn.on_snid_cancel   )
capi.client.connect_signal ("manage"          , spawn.on_snid_callback )

capi.client.add_signal    ("spawn::completed_with_payload"            )

return setmetatable(spawn, { __call = function(_, ...) return spawn.spawn(...) end })
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80