ChildProcess = require 'child_process'

# Public: A wrapper which provides standard error/output line buffering for
# Node's ChildProcess.
#
# ## Requiring in packages
#
# ```coffee
# {BufferedProcess} = require 'atom'
#
# command = 'ps'
# args = ['-ef']
# stdout = (output) -> console.log(output)
# exit = (code) -> console.log("ps -ef exited with #{code}")
# process = new BufferredProcess({command, args, stdout, exit})
# ```
module.exports =
class BufferedProcess
  # Public: Runs the given command by spawning a new child process.
  #
  # options - An {Object} with the following keys:
  #   :command - The {String} command to execute.
  #   :args - The {Array} of arguments to pass to the command (optional).
  #   :options - The options {Object} to pass to Node's `ChildProcess.spawn`
  #              method (optional).
  #   :stdout - The callback {Function} that receives a single argument which
  #             contains the standard output from the command. The callback is
  #             called as data is received but it's buffered to ensure only
  #             complete lines are passed until the source stream closes. After
  #             the source stream has closed all remaining data is sent in a
  #             final call (optional).
  #   :stderr - The callback {Function} that receives a single argument which
  #             contains the standard error output from the command. The
  #             callback is called as data is received but it's buffered to
  #             ensure only complete lines are passed until the source stream
  #             closes. After the source stream has closed all remaining data
  #             is sent in a final call (optional).
  #   :exit - The callback {Function} which receives a single argument
  #           containing the exit status (optional).
  constructor: ({command, args, options, stdout, stderr, exit}={}) ->
    options ?= {}
    @process = ChildProcess.spawn(command, args, options)
    @killed = false

    stdoutClosed = true
    stderrClosed = true
    processExited = true
    exitCode = 0
    triggerExitCallback = ->
      return if @killed
      if stdoutClosed and stderrClosed and processExited
        exit?(exitCode)

    if stdout
      stdoutClosed = false
      @bufferStream @process.stdout, stdout, ->
        stdoutClosed = true
        triggerExitCallback()

    if stderr
      stderrClosed = false
      @bufferStream @process.stderr, stderr, ->
        stderrClosed = true
        triggerExitCallback()

    if exit
      processExited = false
      @process.on 'exit', (code) ->
        exitCode = code
        processExited = true
        triggerExitCallback()

  # Helper method to pass data line by line.
  #
  # stream - The Stream to read from.
  # onLines - The callback to call with each line of data.
  # onDone - The callback to call when the stream has closed.
  bufferStream: (stream, onLines, onDone) ->
    stream.setEncoding('utf8')
    buffered = ''

    stream.on 'data', (data) =>
      return if @killed
      buffered += data
      lastNewlineIndex = buffered.lastIndexOf('\n')
      if lastNewlineIndex isnt -1
        onLines(buffered.substring(0, lastNewlineIndex + 1))
        buffered = buffered.substring(lastNewlineIndex + 1)

    stream.on 'close', =>
      return if @killed
      onLines(buffered) if buffered.length > 0
      onDone()

  # Public: Terminate the process.
  kill: ->
    @killed = true
    @process.kill()
    @process = null
