text: 'use strict'
  
  const Jobs = require('qjobs')
  
  const log = require('./logger').create('launcher')
  
  const baseDecorator = require('./launchers/base').decoratorFactory
  const captureTimeoutDecorator = require('./launchers/capture_timeout').decoratorFactory
  const retryDecorator = require('./launchers/retry').decoratorFactory
  const processDecorator = require('./launchers/process').decoratorFactory
  
  // TODO(vojta): remove once nobody uses it
  const baseBrowserDecoratorFactory = function (
    baseLauncherDecorator,
    captureTimeoutLauncherDecorator,
    retryLauncherDecorator,
    processLauncherDecorator,
    processKillTimeout
  ) {
    return function (launcher) {
      baseLauncherDecorator(launcher)
      captureTimeoutLauncherDecorator(launcher)
      retryLauncherDecorator(launcher)
      processLauncherDecorator(launcher, processKillTimeout)
    }
  }
  
  class Launcher {
    constructor (server, emitter, injector) {
      this._server = server
      this._emitter = emitter
      this._injector = injector
      this._browsers = []
      this._lastStartTime = null
  
      // Attach list of dependency injection parameters to methods.
      this.launch.$inject = [
        'config.browsers',
        'config.concurrency'
      ]
  
      this.launchSingle.$inject = [
        'config.protocol',
        'config.hostname',
        'config.port',
        'config.urlRoot',
        'config.upstreamProxy',
        'config.processKillTimeout'
      ]
  
      this._emitter.on('exit', (callback) => this.killAll(callback))
    }
  
    getBrowserById (id) {
      return this._browsers.find((browser) => browser.id === id)
    }
  
    launchSingle (protocol, hostname, port, urlRoot, upstreamProxy, processKillTimeout) {
      if (upstreamProxy) {
        protocol = upstreamProxy.protocol
        hostname = upstreamProxy.hostname
        port = upstreamProxy.port
        urlRoot = upstreamProxy.path + urlRoot.slice(1)
      }
  
      return (name) => {
        let browser
        const locals = {
          id: ['value', Launcher.generateId()],
          name: ['value', name],
          processKillTimeout: ['value', processKillTimeout],
          baseLauncherDecorator: ['factory', baseDecorator],
          captureTimeoutLauncherDecorator: ['factory', captureTimeoutDecorator],
          retryLauncherDecorator: ['factory', retryDecorator],
          processLauncherDecorator: ['factory', processDecorator],
          baseBrowserDecorator: ['factory', baseBrowserDecoratorFactory]
        }
  
        // TODO(vojta): determine script from name
        if (name.includes('/')) {
          name = 'Script'
        }
  
        try {
          browser = this._injector.createChild([locals], ['launcher:' + name]).get('launcher:' + name)
        } catch (e) {
          if (e.message.includes(`No provider for "launcher:${name}"`)) {
            log.error(`Cannot load browser "${name}": it is not registered! Perhaps you are missing some plugin?`)
          } else {
            log.error(`Cannot load browser "${name}"!\n  ` + e.stack)
          }
  
          this._emitter.emit('load_error', 'launcher', name)
          return
        }
  
        this.jobs.add((args, done) => {
          log.info(`Starting browser ${browser.displayName || browser.name}`)
  
          browser.on('browser_process_failure', () => done(browser.error))
  
          browser.on('done', () => {
            if (!browser.error && browser.state !== browser.STATE_RESTARTING) {
              done(null, browser)
            }
          })
  
          browser.start(`${protocol}//${hostname}:${port}${urlRoot}`)
        }, [])
  
        this.jobs.run()
        this._browsers.push(browser)
      }
    }
  
    launch (names, concurrency) {
      log.info(`Launching browsers ${names.join(', ')} with concurrency ${concurrency === Infinity ? 'unlimited' : concurrency}`)
      this.jobs = new Jobs({ maxConcurrency: concurrency })
  
      this._lastStartTime = Date.now()
  
      if (this._server.loadErrors.length) {
        this.jobs.add((args, done) => done(), [])
      } else {
        names.forEach((name) => this._injector.invoke(this.launchSingle, this)(name))
      }
  
      this.jobs.on('end', (err) => {
        log.debug('Finished all browsers')
  
        if (err) {
          log.error(err)
        }
      })
  
      this.jobs.run()
  
      return this._browsers
    }
  
    kill (id, callback) {
      callback = callback || function () {}
      const browser = this.getBrowserById(id)
  
      if (browser) {
        browser.forceKill().then(callback)
        return true
      }
      process.nextTick(callback)
      return false
    }
  
    restart (id) {
      const browser = this.getBrowserById(id)
      if (browser) {
        browser.restart()
        return true
      }
      return false
    }
  
    killAll (callback) {
      callback = callback || function () {}
      log.debug('Disconnecting all browsers')
  
      if (!this._browsers.length) {
        return process.nextTick(callback)
      }
  
      Promise.all(
        this._browsers
          .map((browser) => browser.forceKill())
      ).then(callback)
    }
  
    areAllCaptured () {
      return this._browsers.every((browser) => browser.isCaptured())
    }
  
    markCaptured (id) {
      const browser = this.getBrowserById(id)
      if (browser) {
        browser.markCaptured()
        log.debug(`${browser.name} (id ${browser.id}) captured in ${(Date.now() - this._lastStartTime) / 1000} secs`)
      }
    }
  
    static generateId () {
      return Math.floor(Math.random() * 100000000).toString()
    }
  }
  
  Launcher.factory = function (server, emitter, injector) {
    return new Launcher(server, emitter, injector)
  }
  
  Launcher.factory.$inject = ['server', 'emitter', 'injector']
  
  exports.Launcher = Launcher
  