/**
 * @module run-tasks-in-parallel
 * @author Toru Nagashima
 * @copyright 2015 Toru Nagashima. All rights reserved.
 * See LICENSE file in root directory for full license.
 */
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const MemoryStream = require('memorystream')
const NpmRunAllError = require('./npm-run-all-error')
const runTask = require('./run-task')

// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------

/**
 * Remove the given value from the array.
 * @template T
 * @param {T[]} array - The array to remove.
 * @param {T} x - The item to be removed.
 * @returns {void}
 */
function remove (array, x) {
  const index = array.indexOf(x)
  if (index !== -1) {
    array.splice(index, 1)
  }
}

const signals = {
  SIGABRT: 6,
  SIGALRM: 14,
  SIGBUS: 10,
  SIGCHLD: 20,
  SIGCONT: 19,
  SIGFPE: 8,
  SIGHUP: 1,
  SIGILL: 4,
  SIGINT: 2,
  SIGKILL: 9,
  SIGPIPE: 13,
  SIGQUIT: 3,
  SIGSEGV: 11,
  SIGSTOP: 17,
  SIGTERM: 15,
  SIGTRAP: 5,
  SIGTSTP: 18,
  SIGTTIN: 21,
  SIGTTOU: 22,
  SIGUSR1: 30,
  SIGUSR2: 31
}

/**
 * Converts a signal name to a number.
 * @param {string} signal - the signal name to convert into a number
 * @returns {number} - the return code for the signal
 */
function convert (signal) {
  return signals[signal] || 0
}

// ------------------------------------------------------------------------------
// Public Interface
// ------------------------------------------------------------------------------

/**
 * Run npm-scripts of given names in parallel.
 *
 * If a npm-script exited with a non-zero code, this aborts other all npm-scripts.
 *
 * @param {string} tasks - A list of npm-script name to run in parallel.
 * @param {object} options - An option object.
 * @returns {Promise} A promise object which becomes fullfilled when all npm-scripts are completed.
 * @private
 */
module.exports = function runTasks (tasks, options) {
  return new Promise((resolve, reject) => {
    if (tasks.length === 0) {
      resolve([])
      return
    }

    const results = tasks.map(task => ({ name: task, code: undefined }))
    const queue = tasks.map((task, index) => ({ name: task, index }))
    const promises = []
    let error = null
    let aborted = false

    /**
         * Done.
         * @returns {void}
         */
    function done () {
      if (error == null) {
        resolve(results)
      } else {
        reject(error)
      }
    }

    /**
         * Aborts all tasks.
         * @returns {void}
         */
    function abort () {
      if (aborted) {
        return
      }
      aborted = true

      if (promises.length === 0) {
        done()
      } else {
        for (const p of promises) {
          p.abort()
        }
        Promise.all(promises).then(done, reject)
      }
    }

    /**
         * Runs a next task.
         * @returns {void}
         */
    function next () {
      if (aborted) {
        return
      }
      if (queue.length === 0) {
        if (promises.length === 0) {
          done()
        }
        return
      }

      const originalOutputStream = options.stdout
      const optionsClone = Object.assign({}, options)
      const writer = new MemoryStream(null, {
        readable: false
      })

      if (options.aggregateOutput) {
        optionsClone.stdout = writer
      }

      const task = queue.shift()
      const promise = runTask(task.name, optionsClone)

      promises.push(promise)
      promise.then(
        (result) => {
          remove(promises, promise)
          if (aborted) {
            return
          }

          if (options.aggregateOutput) {
            originalOutputStream.write(writer.toString())
          }

          // Check if the task failed as a result of a signal, and
          // amend the exit code as a result.
          if (result.code === null && result.signal !== null) {
            // An exit caused by a signal must return a status code
            // of 128 plus the value of the signal code.
            // Ref: https://nodejs.org/api/process.html#process_exit_codes
            result.code = 128 + convert(result.signal)
          }

          // Save the result.
          results[task.index].code = result.code

          // Aborts all tasks if it's an error.
          if (result.code) {
            error = new NpmRunAllError(result, results)
            if (!options.continueOnError) {
              abort()
              return
            }
          }

          // Aborts all tasks if options.race is true.
          if (options.race && !result.code) {
            abort()
            return
          }

          // Call the next task.
          next()
        },
        (thisError) => {
          remove(promises, promise)
          if (!options.continueOnError || options.race) {
            error = thisError
            abort()
            return
          }
          next()
        }
      )
    }

    const max = options.maxParallel
    const end = (typeof max === 'number' && max > 0)
      ? Math.min(tasks.length, max)
      : tasks.length
    for (let i = 0; i < end; ++i) {
      next()
    }
  })
}
