_ = require 'underscore'
{fork} = require 'child_process'
{Emitter} = require 'emissary'
Grim = require 'grim'

# Extended: Run a node script in a separate process.
#
# Used by the fuzzy-finder and [find in project](https://github.com/atom/atom/blob/master/src/scan-handler.coffee).
#
# For a real-world example, see the [scan-handler](https://github.com/atom/atom/blob/master/src/scan-handler.coffee)
# and the [instantiation of the task](https://github.com/atom/atom/blob/4a20f13162f65afc816b512ad7201e528c3443d7/src/project.coffee#L245).
#
# ## Examples
#
# In your package code:
#
# ```coffee
# {Task} = require 'atom'
#
# task = Task.once '/path/to/task-file.coffee', parameter1, parameter2, ->
#   console.log 'task has finished'
#
# task.on 'some-event-from-the-task', (data) =>
#   console.log data.someString # prints 'yep this is it'
# ```
#
# In `'/path/to/task-file.coffee'`:
#
# ```coffee
# module.exports = (parameter1, parameter2) ->
#   # Indicates that this task will be async.
#   # Call the `callback` to finish the task
#   callback = @async()
#
#   emit('some-event-from-the-task', {someString: 'yep this is it'})
#
#   callback()
# ```
module.exports =
class Task
  Emitter.includeInto(this)

  # Public: A helper method to easily launch and run a task once.
  #
  # * `taskPath` The {String} path to the CoffeeScript/JavaScript file which
  #   exports a single {Function} to execute.
  # * `args` The arguments to pass to the exported function.
  #
  # Returns the created {Task}.
  @once: (taskPath, args...) ->
    task = new Task(taskPath)
    task.once 'task:completed', -> task.terminate()
    task.start(args...)
    task

  # Called upon task completion.
  #
  # It receives the same arguments that were passed to the task.
  #
  # If subclassed, this is intended to be overridden. However if {::start}
  # receives a completion callback, this is overridden.
  callback: null

  # Public: Creates a task. You should probably use {.once}
  #
  # * `taskPath` The {String} path to the CoffeeScript/JavaScript file that
  #   exports a single {Function} to execute.
  constructor: (taskPath) ->
    coffeeCacheRequire = "require('#{require.resolve('./coffee-cache')}').register();"
    coffeeScriptRequire = "require('#{require.resolve('coffee-script')}').register();"
    taskBootstrapRequire = "require('#{require.resolve('./task-bootstrap')}');"
    bootstrap = """
      #{coffeeScriptRequire}
      #{coffeeCacheRequire}
      #{taskBootstrapRequire}
    """
    bootstrap = bootstrap.replace(/\\/g, "\\\\")

    taskPath = require.resolve(taskPath)
    taskPath = taskPath.replace(/\\/g, "\\\\")

    env = _.extend({}, process.env, {taskPath, userAgent: process.env.userAgent})
    @childProcess = fork '--eval', [bootstrap], {env, silent: true}

    @on "task:log", -> console.log(arguments...)
    @on "task:warn", -> console.warn(arguments...)
    @on "task:error", -> console.error(arguments...)
    @on "task:deprecations", (deprecations) ->
      Grim.addSerializedDeprecation(deprecation) for deprecation in deprecations
      return
    @on "task:completed", (args...) => @callback?(args...)

    @handleEvents()

  # Routes messages from the child to the appropriate event.
  handleEvents: ->
    @childProcess.removeAllListeners()
    @childProcess.on 'message', ({event, args}) =>
      @emit(event, args...) if @childProcess?
    # Catch the errors that happened before task-bootstrap.
    @childProcess.stdout.on 'data', (data) ->
      console.log data.toString()
    @childProcess.stderr.on 'data', (data) ->
      console.error data.toString()

  # Public: Starts the task.
  #
  # Throws an error if this task has already been terminated or if sending a
  # message to the child process fails.
  #
  # * `args` The arguments to pass to the function exported by this task's script.
  # * `callback` (optional) A {Function} to call when the task completes.
  start: (args..., callback) ->
    throw new Error('Cannot start terminated process') unless @childProcess?

    @handleEvents()
    if _.isFunction(callback)
      @callback = callback
    else
      args.push(callback)
    @send({event: 'start', args})
    undefined

  # Public: Send message to the task.
  #
  # Throws an error if this task has already been terminated or if sending a
  # message to the child process fails.
  #
  # * `message` The message to send to the task.
  send: (message) ->
    if @childProcess?
      @childProcess.send(message)
    else
      throw new Error('Cannot send message to terminated process')
    undefined

  # Public: Describe the function of the task. Each task should override this
  # to explain its individual function
  description: ->
    ''

  # Public: Call a function when an event is emitted by the child process
  #
  # * `eventName` The {String} name of the event to handle.
  # * `callback` The {Function} to call when the event is emitted.
  #
  # Returns a {Disposable} that can be used to stop listening for the event.
  on: (eventName, callback) -> Emitter::on.call(this, eventName, callback)

  # Public: Forcefully stop the running task.
  #
  # No more events are emitted once this method is called.
  terminate: ->
    return unless @childProcess?

    @childProcess.removeAllListeners()
    @childProcess.kill()
    @childProcess = null

    undefined
