text: var arrayRemove = require('unordered-array-remove')
  var debug = require('debug')('webtorrent:peer')
  var Wire = require('bittorrent-protocol')
  
  var WebConn = require('./webconn')
  
  var CONNECT_TIMEOUT_TCP = 5000
  var CONNECT_TIMEOUT_WEBRTC = 25000
  var HANDSHAKE_TIMEOUT = 25000
  
  /**
   * WebRTC peer connections start out connected, because WebRTC peers require an
   * "introduction" (i.e. WebRTC signaling), and there's no equivalent to an IP address
   * that lets you refer to a WebRTC endpoint.
   */
  exports.createWebRTCPeer = function (conn, swarm) {
    var peer = new Peer(conn.id, 'webrtc')
    peer.conn = conn
    peer.swarm = swarm
  
    if (peer.conn.connected) {
      peer.onConnect()
    } else {
      peer.conn.once('connect', function () { peer.onConnect() })
      peer.conn.once('error', function (err) { peer.destroy(err) })
      peer.startConnectTimeout()
    }
  
    return peer
  }
  
  /**
   * Incoming TCP peers start out connected, because the remote peer connected to the
   * listening port of the TCP server. Until the remote peer sends a handshake, we don't
   * know what swarm the connection is intended for.
   */
  exports.createTCPIncomingPeer = function (conn) {
    var addr = conn.remoteAddress + ':' + conn.remotePort
    var peer = new Peer(addr, 'tcpIncoming')
    peer.conn = conn
    peer.addr = addr
  
    peer.onConnect()
  
    return peer
  }
  
  /**
   * Outgoing TCP peers start out with just an IP address. At some point (when there is an
   * available connection), the client can attempt to connect to the address.
   */
  exports.createTCPOutgoingPeer = function (addr, swarm) {
    var peer = new Peer(addr, 'tcpOutgoing')
    peer.addr = addr
    peer.swarm = swarm
  
    return peer
  }
  
  /**
   * Peer that represents a Web Seed (BEP17 / BEP19).
   */
  exports.createWebSeedPeer = function (url, swarm) {
    var peer = new Peer(url, 'webSeed')
    peer.swarm = swarm
    peer.conn = new WebConn(url, swarm)
  
    peer.onConnect()
  
    return peer
  }
  
  /**
   * Peer. Represents a peer in the torrent swarm.
   *
   * @param {string} id "ip:port" string, peer id (for WebRTC peers), or url (for Web Seeds)
   * @param {string} type the type of the peer
   */
  function Peer (id, type) {
    var self = this
    self.id = id
    self.type = type
  
    debug('new Peer %s', id)
  
    self.addr = null
    self.conn = null
    self.swarm = null
    self.wire = null
  
    self.connected = false
    self.destroyed = false
    self.timeout = null // handshake timeout
    self.retries = 0 // outgoing TCP connection retry count
  
    self.sentHandshake = false
  }
  
  /**
   * Called once the peer is connected (i.e. fired 'connect' event)
   * @param {Socket} conn
   */
  Peer.prototype.onConnect = function () {
    var self = this
    if (self.destroyed) return
    self.connected = true
  
    debug('Peer %s connected', self.id)
  
    clearTimeout(self.connectTimeout)
  
    var conn = self.conn
    conn.once('end', function () {
      self.destroy()
    })
    conn.once('close', function () {
      self.destroy()
    })
    conn.once('finish', function () {
      self.destroy()
    })
    conn.once('error', function (err) {
      self.destroy(err)
    })
  
    var wire = self.wire = new Wire()
    wire.type = self.type
    wire.once('end', function () {
      self.destroy()
    })
    wire.once('close', function () {
      self.destroy()
    })
    wire.once('finish', function () {
      self.destroy()
    })
    wire.once('error', function (err) {
      self.destroy(err)
    })
  
    wire.once('handshake', function (infoHash, peerId) {
      self.onHandshake(infoHash, peerId)
    })
    self.startHandshakeTimeout()
  
    conn.pipe(wire).pipe(conn)
    if (self.swarm && !self.sentHandshake) self.handshake()
  }
  
  /**
   * Called when handshake is received from remote peer.
   * @param {string} infoHash
   * @param {string} peerId
   */
  Peer.prototype.onHandshake = function (infoHash, peerId) {
    var self = this
    if (!self.swarm) return // `self.swarm` not set yet, so do nothing
    if (self.destroyed) return
  
    if (self.swarm.destroyed) {
      return self.destroy(new Error('swarm already destroyed'))
    }
    if (infoHash !== self.swarm.infoHash) {
      return self.destroy(new Error('unexpected handshake info hash for this swarm'))
    }
    if (peerId === self.swarm.peerId) {
      return self.destroy(new Error('refusing to connect to ourselves'))
    }
  
    debug('Peer %s got handshake %s', self.id, infoHash)
  
    clearTimeout(self.handshakeTimeout)
  
    self.retries = 0
  
    var addr = self.addr
    if (!addr && self.conn.remoteAddress && self.conn.remotePort) {
      addr = self.conn.remoteAddress + ':' + self.conn.remotePort
    }
    self.swarm._onWire(self.wire, addr)
  
    // swarm could be destroyed in user's 'wire' event handler
    if (!self.swarm || self.swarm.destroyed) return
  
    if (!self.sentHandshake) self.handshake()
  }
  
  Peer.prototype.handshake = function () {
    var self = this
    var opts = {
      dht: self.swarm.private ? false : !!self.swarm.client.dht
    }
    self.wire.handshake(self.swarm.infoHash, self.swarm.client.peerId, opts)
    self.sentHandshake = true
  }
  
  Peer.prototype.startConnectTimeout = function () {
    var self = this
    clearTimeout(self.connectTimeout)
    self.connectTimeout = setTimeout(function () {
      self.destroy(new Error('connect timeout'))
    }, self.type === 'webrtc' ? CONNECT_TIMEOUT_WEBRTC : CONNECT_TIMEOUT_TCP)
    if (self.connectTimeout.unref) self.connectTimeout.unref()
  }
  
  Peer.prototype.startHandshakeTimeout = function () {
    var self = this
    clearTimeout(self.handshakeTimeout)
    self.handshakeTimeout = setTimeout(function () {
      self.destroy(new Error('handshake timeout'))
    }, HANDSHAKE_TIMEOUT)
    if (self.handshakeTimeout.unref) self.handshakeTimeout.unref()
  }
  
  Peer.prototype.destroy = function (err) {
    var self = this
    if (self.destroyed) return
    self.destroyed = true
    self.connected = false
  
    debug('destroy %s (error: %s)', self.id, err && (err.message || err))
  
    clearTimeout(self.connectTimeout)
    clearTimeout(self.handshakeTimeout)
  
    var swarm = self.swarm
    var conn = self.conn
    var wire = self.wire
  
    self.swarm = null
    self.conn = null
    self.wire = null
  
    if (swarm && wire) {
      arrayRemove(swarm.wires, swarm.wires.indexOf(wire))
    }
    if (conn) {
      conn.on('error', noop)
      conn.destroy()
    }
    if (wire) wire.destroy()
    if (swarm) swarm.removePeer(self.id)
  }
  
  function noop () {}
  