# ws_adapter.js

    'use strict'

    const Promise = require('bluebird')
    const _pick = require('lodash/pick')
    const _isEqual = require('lodash/isEqual')
    const _isObject = require('lodash/isObject')
    const _isString = require('lodash/isString')
    const _isEmpty = require('lodash/isEmpty')
    const { EventEmitter } = require('events')
    const debug = require('debug')('bfx:hf:ext-plugin:bitfinex:ao-adpater')
    const ManagedOB = require('bfx-api-node-plugin-managed-ob')
    const ManagedCandles = require('bfx-api-node-plugin-managed-candles')
    const Watchdog = require('bfx-api-node-plugin-wd')
    const { Order } = require('bfx-api-node-models')
    const {
      subscribe, unsubscribe, findChannelId, Manager, cancelOrder, submitOrder,
      send
    } = require('bfx-api-node-core')

    const HB_INTERVAL_MS = 2500

    module.exports = class AOAdapter extends EventEmitter {
      static getTimeFrames () {
        return {
          '1 Minute': '1m',
          '5 Minutes': '5m',
          '15 Minutes': '15m',
          '30 Minutes': '30m',
          '1 Hour': '1h',
          '3 Hours': '3h',
          '6 Hours': '6h',
          '12 Hours': '12h',
          '1 Day': '1D',
          '7 Days': '7D',
          '14 Days': '14D',
          '1 Month': '1M'
        }
      }

      constructor ({
        wsURL, restURL, apiKey, apiSecret, agent, dms, withHeartbeat, affiliateCode
      }) {
        super()

        this.pendingOrderSubmitCancelTimeouts = []
        this.affiliateCode = affiliateCode
        this.hbEnabled = withHeartbeat
        this.hbInterval = null
        this.m = new Manager({
          plugins: [ManagedOB(), ManagedCandles(), Watchdog({
            packetWDDelay: 30 * 1000
          })],

          transform: true,
          dms,
          apiSecret,
          apiKey,
          agent,
          wsURL,
          restURL
        })

        this.m.on('ws2:error', this.propagateEvent.bind(this, 'meta:error'))
        this.m.on('ws2:ticker', this.propagateDataEvent.bind(this, 'ticker'))
        this.m.on('ws2:trades', this.propagateDataEvent.bind(this, 'trades'))
        this.m.on('ws2:candles', this.propagateDataEvent.bind(this, 'candles'))
        this.m.on('ws2:book', this.propagateDataEvent.bind(this, 'book'))
        this.m.on('ws2:managed:book', this.propagateDataEvent.bind(this, 'managed:book'))
        this.m.on('ws2:managed:candles', this.propagateDataEvent.bind(this, 'managed:candles'))
        this.m.on('ws2:notification', this.propagateDataEvent.bind(this, 'notification'))

        this.m.on('socket:updated', this.onSocketUpdate.bind(this))
        this.m.on('ws2:event:info-server-restart', this.onServerRestart.bind(this))
        this.m.on('ws2:reopen', this.onWSReopen.bind(this))

        this.m.on('ws2:open', this.propagateEvent.bind(this, 'open'))
        this.m.on('ws2:event:auth:success', this.propagateEvent.bind(this, 'auth:success'))
        this.m.on('ws2:event:auth:error', this.propagateEvent.bind(this, 'auth:error'))
        this.m.on('ws2:auth:n', this.propagateEvent.bind(this, 'auth:n'))
        this.m.on('ws2:auth:os', this.propagateEvent.bind(this, 'order:snapshot'))
        this.m.on('ws2:auth:on', this.propagateEvent.bind(this, 'order:new'))
        this.m.on('ws2:auth:ou', this.propagateEvent.bind(this, 'order:update'))
        this.m.on('ws2:auth:oc', this.propagateEvent.bind(this, 'order:close'))
        this.m.on('ws2:data:trades', this.propagateEvent.bind(this, 'trades'))
        this.m.on('ws2:data:book', this.propagateEvent.bind(this, 'book'))
      }

      updateAuthArgs (args = {}) {
        this.m.updateAuthArgs(args)
      }

      reconnect () {
        if (this.hbInterval !== null) {
          clearInterval(this.hbInterval)
          this.hbInterval = null
        }

        this.m.reconnectAllSockets()

        if (this.hbEnabled) {
          this.hbInterval = setInterval(this.sendHB.bind(this), HB_INTERVAL_MS)
        }
      }

      connect () {
        this.m.openWS()

        if (this.hbEnabled) {
          this.hbInterval = setInterval(this.sendHB.bind(this), HB_INTERVAL_MS)
        }
      }

      /**
        * @return {Promise} p
        */
      disconnect () {
        if (this.hbInterval !== null) {
          clearInterval(this.hbInterval)
          this.hbInterval = null
        }

        // Clean up pending actions
        this.pendingOrderSubmitCancelTimeouts.forEach(timeoutObject => {
          if (timeoutObject.t !== null) {
            clearTimeout(timeoutObject.t)
            timeoutObject.t = null
          }
        })

        this.pendingOrderSubmitCancelTimeouts = []

        return this.m.closeAllSockets()
      }

      sendHB () {
        this.m.withAuthSocket((ws) => {
          send(ws, [0, 'n', null, {
            mid: Date.now(),
            type: 'ucm-hb',
            info: {}
          }])
        })
      }

      getConnection () {
        const id = this.m.sampleWSI()
        const c = this.m.getWSByIndex(id)

        return { id, c }
      }

      propagateEvent (name, ...args) {
        this.emit(name, ...args)
      }

      propagateDataEvent (name, data, meta = {}) {
        this.emit(`data:${name}`, data, meta)
      }

      onSocketUpdate (i, state) {
        this.emit('meta:connection:update', i, state)
      }

      onServerRestart () {
        // Otherwise the DMS flag closes all orders, and the packets are received
        // after a server restart, before the connection drops. We cannot
        // differentiate between manual user cancellations and those packets.
        this._ignoreOrderEvents = true
      }

      onWSReopen () {
        this._ignoreOrderEvents = false // see onServerRestart
        this.emit('meta:reload')
      }

      subscribe (connection, channel, filter) {
        subscribe(connection.c, channel, filter)
      }

      unsubscribe (connection, channel, filter) {
        const cid = findChannelId(connection.c, (data) => {
          if (data.channel !== channel) {
            return false
          }

          const fv = _pick(data, Object.keys(filter))
          return _isEqual(filter, fv)
        })

        if (!cid) {
          return debug('error unsubscribing: unknown channel %s', channel)
        }

        unsubscribe(connection.c, cid)
      }

      orderEventsValid () {
        return !this._ignoreOrderEvents
      }

      async submitOrderWithDelay (connection, delay, order) {
        const { c } = connection

        if (_isString(this.affiliateCode) && !_isEmpty(this.affiliateCode)) {
          if (order instanceof Order) {
            order.affiliateCode = this.affiliateCode
          } else if (_isObject(order)) {
            if (!order.meta) {
              order.meta = {}
            }

            order.meta.aff_code = this.affiliateCode // eslint-disable-line
          }
        }

        return new Promise((resolve, reject) => {
          const t = setTimeout(() => {
            timeoutObject.t = null

            submitOrder(c, order)
              .then(resolve)
              .catch(reject)
          }, delay)

          const timeoutObject = { t }
          this.pendingOrderSubmitCancelTimeouts.push(timeoutObject)
        })
      }

      async cancelOrderWithDelay (connection, delay, order) {
        const { c } = connection

        return new Promise((resolve, reject) => {
          const t = setTimeout(() => {
            timeoutObject.t = null

            cancelOrder(c, order)
              .then(resolve)
              .catch(reject)
          }, delay)

          const timeoutObject = { t }
          this.pendingOrderSubmitCancelTimeouts.push(timeoutObject)
        })
      }

      sendWithAnyConnection (packet) {
        this.m.withAuthSocket((ws) => {
          send(ws, packet)
        })
      }

      notify (ws, level, message) {
        send(ws, [0, 'n', null, {
          type: 'ucm-notify-ui',
          info: {
            level,
            message
          }
        }])
      }
    }