text: module.exports = Client
  
  var debug = require('debug')('bittorrent-tracker')
  var EventEmitter = require('events').EventEmitter
  var extend = require('xtend')
  var inherits = require('inherits')
  var once = require('once')
  var parallel = require('run-parallel')
  var Peer = require('simple-peer')
  var uniq = require('uniq')
  var url = require('url')
  
  var common = require('./lib/common')
  var HTTPTracker = require('./lib/client/http-tracker') // empty object in browser
  var UDPTracker = require('./lib/client/udp-tracker') // empty object in browser
  var WebSocketTracker = require('./lib/client/websocket-tracker')
  
  inherits(Client, EventEmitter)
  
  /**
   * BitTorrent tracker client.
   *
   * Find torrent peers, to help a torrent client participate in a torrent swarm.
   *
   * @param {Object} opts                          options object
   * @param {string|Buffer} opts.infoHash          torrent info hash
   * @param {string|Buffer} opts.peerId            peer id
   * @param {string|Array.<string>} opts.announce  announce
   * @param {number} opts.port                     torrent client listening port
   * @param {function} opts.getAnnounceOpts        callback to provide data to tracker
   * @param {number} opts.rtcConfig                RTCPeerConnection configuration object
   * @param {number} opts.wrtc                     custom webrtc impl (useful in node.js)
   */
  function Client (opts) {
    var self = this
    if (!(self instanceof Client)) return new Client(opts)
    EventEmitter.call(self)
    if (!opts) opts = {}
  
    if (!opts.peerId) throw new Error('Option `peerId` is required')
    if (!opts.infoHash) throw new Error('Option `infoHash` is required')
    if (!opts.announce) throw new Error('Option `announce` is required')
    if (!process.browser && !opts.port) throw new Error('Option `port` is required')
  
    // required
    self.peerId = typeof opts.peerId === 'string'
      ? opts.peerId
      : opts.peerId.toString('hex')
    self._peerIdBuffer = new Buffer(self.peerId, 'hex')
    self._peerIdBinary = self._peerIdBuffer.toString('binary')
  
    self.infoHash = typeof opts.infoHash === 'string'
      ? opts.infoHash
      : opts.infoHash.toString('hex')
    self._infoHashBuffer = new Buffer(self.infoHash, 'hex')
    self._infoHashBinary = self._infoHashBuffer.toString('binary')
  
    self._port = opts.port
  
    self.destroyed = false
  
    self._rtcConfig = opts.rtcConfig
    self._wrtc = opts.wrtc
    self._getAnnounceOpts = opts.getAnnounceOpts
  
    debug('new client %s', self.infoHash)
  
    var webrtcSupport = self._wrtc !== false && (!!self._wrtc || Peer.WEBRTC_SUPPORT)
  
    var announce = (typeof opts.announce === 'string')
      ? [ opts.announce ]
      : opts.announce == null
        ? []
        : opts.announce
  
    announce = announce.map(function (announceUrl) {
      announceUrl = announceUrl.toString()
      if (announceUrl[announceUrl.length - 1] === '/') {
        // remove trailing slash from trackers to catch duplicates
        announceUrl = announceUrl.substring(0, announceUrl.length - 1)
      }
      return announceUrl
    })
  
    announce = uniq(announce)
  
    self._trackers = announce
      .map(function (announceUrl) {
        var protocol = url.parse(announceUrl).protocol
        if ((protocol === 'http:' || protocol === 'https:') &&
            typeof HTTPTracker === 'function') {
          return new HTTPTracker(self, announceUrl)
        } else if (protocol === 'udp:' && typeof UDPTracker === 'function') {
          return new UDPTracker(self, announceUrl)
        } else if ((protocol === 'ws:' || protocol === 'wss:') && webrtcSupport) {
          // Skip ws:// trackers on https:// sites because they throw SecurityError
          if (protocol === 'ws:' && typeof window !== 'undefined' &&
              window.location.protocol === 'https:') {
            nextTickWarn(new Error('Unsupported tracker protocol: ' + announceUrl))
            return null
          }
          return new WebSocketTracker(self, announceUrl)
        } else {
          nextTickWarn(new Error('Unsupported tracker protocol: ' + announceUrl))
          return null
        }
      })
      .filter(Boolean)
  
    function nextTickWarn (err) {
      process.nextTick(function () {
        self.emit('warning', err)
      })
    }
  }
  
  /**
   * Simple convenience function to scrape a tracker for an info hash without needing to
   * create a Client, pass it a parsed torrent, etc. Support scraping a tracker for multiple
   * torrents at the same time.
   * @params {Object} opts
   * @param  {string|Array.<string>} opts.infoHash
   * @param  {string} opts.announce
   * @param  {function} cb
   */
  Client.scrape = function (opts, cb) {
    cb = once(cb)
  
    if (!opts.infoHash) throw new Error('Option `infoHash` is required')
    if (!opts.announce) throw new Error('Option `announce` is required')
  
    var clientOpts = extend(opts, {
      infoHash: Array.isArray(opts.infoHash) ? opts.infoHash[0] : opts.infoHash,
      peerId: new Buffer('01234567890123456789'), // dummy value
      port: 6881 // dummy value
    })
  
    var client = new Client(clientOpts)
    client.once('error', cb)
  
    var len = Array.isArray(opts.infoHash) ? opts.infoHash.length : 1
    var results = {}
    client.on('scrape', function (data) {
      len -= 1
      results[data.infoHash] = data
      if (len === 0) {
        client.destroy()
        var keys = Object.keys(results)
        if (keys.length === 1) {
          cb(null, results[keys[0]])
        } else {
          cb(null, results)
        }
      }
    })
  
    opts.infoHash = Array.isArray(opts.infoHash)
      ? opts.infoHash.map(function (infoHash) { return new Buffer(infoHash, 'hex') })
      : new Buffer(opts.infoHash, 'hex')
    client.scrape({ infoHash: opts.infoHash })
    return client
  }
  
  /**
   * Send a `start` announce to the trackers.
   * @param {Object} opts
   * @param {number=} opts.uploaded
   * @param {number=} opts.downloaded
   * @param {number=} opts.left (if not set, calculated automatically)
   */
  Client.prototype.start = function (opts) {
    var self = this
    debug('send `start`')
    opts = self._defaultAnnounceOpts(opts)
    opts.event = 'started'
    self._announce(opts)
  
    // start announcing on intervals
    self._trackers.forEach(function (tracker) {
      tracker.setInterval()
    })
  }
  
  /**
   * Send a `stop` announce to the trackers.
   * @param {Object} opts
   * @param {number=} opts.uploaded
   * @param {number=} opts.downloaded
   * @param {number=} opts.numwant
   * @param {number=} opts.left (if not set, calculated automatically)
   */
  Client.prototype.stop = function (opts) {
    var self = this
    debug('send `stop`')
    opts = self._defaultAnnounceOpts(opts)
    opts.event = 'stopped'
    self._announce(opts)
  }
  
  /**
   * Send a `complete` announce to the trackers.
   * @param {Object} opts
   * @param {number=} opts.uploaded
   * @param {number=} opts.downloaded
   * @param {number=} opts.numwant
   * @param {number=} opts.left (if not set, calculated automatically)
   */
  Client.prototype.complete = function (opts) {
    var self = this
    debug('send `complete`')
    if (!opts) opts = {}
    opts = self._defaultAnnounceOpts(opts)
    opts.event = 'completed'
    self._announce(opts)
  }
  
  /**
   * Send a `update` announce to the trackers.
   * @param {Object} opts
   * @param {number=} opts.uploaded
   * @param {number=} opts.downloaded
   * @param {number=} opts.numwant
   * @param {number=} opts.left (if not set, calculated automatically)
   */
  Client.prototype.update = function (opts) {
    var self = this
    debug('send `update`')
    opts = self._defaultAnnounceOpts(opts)
    if (opts.event) delete opts.event
    self._announce(opts)
  }
  
  Client.prototype._announce = function (opts) {
    var self = this
    self._trackers.forEach(function (tracker) {
      // tracker should not modify `opts` object, it's passed to all trackers
      tracker.announce(opts)
    })
  }
  
  /**
   * Send a scrape request to the trackers.
   * @param {Object} opts
   */
  Client.prototype.scrape = function (opts) {
    var self = this
    debug('send `scrape`')
    if (!opts) opts = {}
    self._trackers.forEach(function (tracker) {
      // tracker should not modify `opts` object, it's passed to all trackers
      tracker.scrape(opts)
    })
  }
  
  Client.prototype.setInterval = function (intervalMs) {
    var self = this
    debug('setInterval %d', intervalMs)
    self._trackers.forEach(function (tracker) {
      tracker.setInterval(intervalMs)
    })
  }
  
  Client.prototype.destroy = function (cb) {
    var self = this
    if (self.destroyed) return
    self.destroyed = true
    debug('destroy')
  
    var tasks = self._trackers.map(function (tracker) {
      return function (cb) {
        tracker.destroy(cb)
      }
    })
  
    parallel(tasks, cb)
  
    self._trackers = []
    self._getAnnounceOpts = null
  }
  
  Client.prototype._defaultAnnounceOpts = function (opts) {
    var self = this
    if (!opts) opts = {}
  
    if (opts.numwant == null) opts.numwant = common.DEFAULT_ANNOUNCE_PEERS
  
    if (opts.uploaded == null) opts.uploaded = 0
    if (opts.downloaded == null) opts.downloaded = 0
  
    if (self._getAnnounceOpts) opts = extend(opts, self._getAnnounceOpts())
    return opts
  }
  