/**
 * Waits for the first successful promise.
 * If no promise succeeds, returns the last failure.
 */
export function anyPromise(promises) {
  return new Promise((resolve, reject) => {
    let failed = 0
    for (const promise of promises) {
      promise.then(resolve, error => {
        if (++failed >= promises.length) reject(error)
      })
    }
  })
}

/**
 * If the promise doesn't resolve in the given time,
 * reject it with the provided error, or a generic error if none is provided.
 */
export function timeout(
  promise,
  ms,
  error = new Error(`Timeout of ${ms}ms exceeded`)
) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => reject(error), ms)
    promise.then(
      ok => {
        clearTimeout(timer)
        resolve(ok)
      },
      error => {
        clearTimeout(timer)
        reject(error)
      }
    )
  })
}

/**
 * Waits for a collection of promises.
 * Returns all the promises that manage to resolve within the timeout.
 * If no promises mange to resolve within the timeout,
 * returns the first promise that resolves.
 * If all promises reject, rejects an array of errors.
 */
export function fuzzyTimeout(
  promises,
  timeoutMs
) {
  return new Promise((resolve, reject) => {
    let done = false
    const results = []
    const errors = []

    // Set up the timer:
    let timer = setTimeout(() => {
      timer = undefined
      if (results.length > 0) {
        done = true
        resolve({ results, errors })
      }
    }, timeoutMs)

    function checkEnd() {
      const allDone = results.length + errors.length === promises.length
      if (allDone && timer != null) {
        clearTimeout(timer)
      }
      if (allDone || timer == null) {
        done = true
        if (results.length > 0) resolve({ results, errors })
        else reject(errors)
      }
    }
    checkEnd() // Handle empty lists

    // Attach to the promises:
    for (const promise of promises) {
      promise.then(
        result => {
          if (done) return
          results.push(result)
          checkEnd()
        },
        failure => {
          if (done) return
          errors.push(failure)
          checkEnd()
        }
      )
    }
  })
}
