--[[--
output,           -- collected data from stdout if process exited successfully
errmsg =          -- error message if execution failed or if process didn't exit successfully
execute.command{
  command               = { filename, arg1, arg2, ... },  -- command and arguments
  stdin_data            = stdin_data,                     -- optional data to be sent to process via stdin
  stdout_result_handler = stdout_result_handler,          -- callback receiving: stdout data, success boolean, optional error message
  stderr_line_handler   = stderr_line_handler,            -- callback for processing stderr line-wise
  exit_handler          = exit_handler,                   -- callback when process exited
  signal_handler        = signal_handler,                 -- callback when process terminated due to signal
  timeout_handler       = timeout_handler,                -- callback when process gets killed due to timeout
  abort_handler         = abort_handler,                  -- callback when process gets killed due to request by poll function
  abortable             = abortable,                      -- set to true if process shall be terminated if poll function requests termination
  poll                  = poll,                           -- alternative poll command with moonbridge_io.poll(...) semantics
  db                    = db,                             -- database handle for waiting for notifies
  db_notify_handler     = db_notify_handler               -- handler for database notifications which may return true to kill process
}

--]]--

function execute.command(args)

  local moonbridge_io = require("moonbridge_io")
  local poll = args.poll or moonbridge_io.poll

  local stdout_chunks, stderr_chunks = {}, {}

  local function return_error(errmsg)
    if args.stdout_result_handler then
      args.stdout_result_handler(table.concat(stdout_chunks), false, errmsg)
    end
    return nil, errmsg
  end

  if args.abortable then
    local pollready, pollmsg, pollterm = poll(nil, nil, 0, true)
    if pollterm then
      if args.abort_handler then args.abort_handler(pollmsg) end
      return_error(pollmsg)
    end
  end

  local start = moonbridge_io.timeref()
  local process, errmsg = moonbridge_io.exec(table.unpack(args.command))
  if not process then return nil, errmsg end

  local read_fds = {
    [process] = true,
    [process.stdout] = true,
    [process.stderr] = true
  }
  local write_fds = {
    [process.stdin] = true
  }
  if args.db then
    read_fds[args.db.fd] = true
  end

  local function write(...)
    if write_fds[process.stdin] then
      local buffered = process.stdin:flush_nb(...)
      if not buffered or buffered == 0 then
        process.stdin:close()
        write_fds[process.stdin] = nil
      end
    end
  end
  write(args.stdin_data or "")

  local status

  while
    not status or
    read_fds[process.stdout] or read_fds[process.stderr] or
    write_fds[process.stdin]
  do
    local timeout = args.timeout and args.timeout-moonbridge_io.timeref(start)
    local pollready, pollmsg, pollterm =
      poll(read_fds, write_fds, timeout, args.abortable)
    if not pollready then
      if not status then
        process:kill():wait()
      end
      if pollterm then
        if args.abort_handler then args.abort_handler(pollmsg) end
      else
        if args.timeout_handler then args.timeout_handler() end
      end
      return return_error(pollmsg)
    end
    if not status then
      status = process:wait_nb()
      if status then
        read_fds[process] = nil
      end
    end
    if args.db then
      local channel, payload, pid = db:wait(0)
      if channel then
        if args.db_notify_handler(channel, payload, pid) then
          process:kill():wait()
          return return_error("Database event received")
        end
      end
    end
    if read_fds[process.stdout] then
      local chunk, status = process.stdout:read_nb()
      if not chunk or status == "eof" then
        process.stdout:close()
        read_fds[process.stdout] = nil
      end
      if chunk and chunk ~= "" then
        stdout_chunks[#stdout_chunks+1] = chunk
      end
    end
    if read_fds[process.stderr] then
      local chunk, status = process.stderr:read_nb()
      if not chunk or status == "eof" then
        process.stderr:close()
        read_fds[process.stderr] = nil
      end
      if chunk and args.stderr_line_handler then
        while true do
          local chunk1, chunk2 = string.match(chunk, "(.-)\n(.*)")
          if not chunk1 then break end
          stderr_chunks[#stderr_chunks+1] = chunk1
          args.stderr_line_handler(table.concat(stderr_chunks))
          stderr_chunks = {}
          chunk = chunk2
        end
        if chunk ~= "" then
          stderr_chunks[#stderr_chunks+1] = chunk
        end
        if status == "eof" then
          local line = table.concat(stderr_chunks)
          if #line > 0 then args.stderr_line_handler(line) end
        end
      end
    end
    write()
  end

  if status < 0 then
    if args.signal_handler then
      args.signal_handler(-status)
    end
    return return_error("Command terminated by signal " .. -status)
  elseif status > 0 then
    if args.exit_handler then
      args.exit_handler(status)
    end
    return return_error("Command returned exit code " .. status)
  elseif args.stdout_result_handler then
    args.stdout_result_handler(table.concat(stdout_chunks), true)
    return true
  else
    return table.concat(stdout_chunks)
  end

end
