text: /*
  __/\\\________/\\\____/\\\\\\\\\______/\\\\\\\\\\\\\_________/\\\\\\\\\_
  __\/\\\_______\/\\\__/\\\///////\\\___\/\\\/////////\\\____/\\\////////__
   __\//\\\______/\\\__\/\\\_____\/\\\___\/\\\_______\/\\\__/\\\/___________
    ___\//\\\____/\\\___\/\\\\\\\\\\\/____\/\\\\\\\\\\\\\/__/\\\_____________
     ____\//\\\__/\\\____\/\\\//////\\\____\/\\\/////////___\/\\\_____________
      _____\//\\\/\\\_____\/\\\____\//\\\___\/\\\____________\//\\\____________
       ______\//\\\\\______\/\\\_____\//\\\__\/\\\_____________\///\\\__________
        _______\//\\\_______\/\\\______\//\\\_\/\\\_______________\////\\\\\\\\\_
         ________\///________\///________\///__\///___________________\/////////__
  
  
  Non-intrusively binds code and provides access in form of asynchronous remote
  procedure calls (RPC).
  Author: Dr. Burkhard C. Heisen (https://github.com/bheisen/vrpc)
  
  
  Licensed under the MIT License <http://opensource.org/licenses/MIT>.
  Copyright (c) 2018 - 2020 Dr. Burkhard C. Heisen <burkhard.heisen@xsmail.com>.
  
  Permission is hereby  granted, free of charge, to any  person obtaining a copy
  of this software and associated  documentation files (the "Software"), to deal
  in the Software  without restriction, including without  limitation the rights
  to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
  copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
  furnished to do so, subject to the following conditions:
  
  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
  
  THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
  IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
  FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
  AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
  LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
  SOFTWARE.
  */
  
  const os = require('os')
  const crypto = require('crypto')
  const mqtt = require('mqtt')
  const EventEmitter = require('events')
  
  /**
   * Allows to work with code that is made available through one or more agents.
   *
   * This class provides the following events:
   * - agentInfo
   * - classInfo
   *
   * reflecting the content of the corresponding agentInfo and classInfo
   * messages.
   *
   */
  class VrpcRemote extends EventEmitter {
    /**
     * Upon construction, a connection is tried to be established asynchronously.
     *
     * NOTE: Each instance creates its own physical connection to the broker.
     *
     * @param {string} token Access token as generated by: https://app.vrpc.io
     * @param {string} username MQTT username (if no token is used)
     * @param {string} password MQTT password (if no token is provided)
     * @param {string} domain Sets default domain.
     * @param {string} agent Sets default agent.
     * @param {string} broker Broker url in form: <scheme>://<host>:<port>
     * @param {number} timeout Maximum time in ms to wait for a RPC answer.
     */
    constructor ({
      token,
      username,
      password,
      agent = '*',
      domain = '*',
      broker = 'mqtts://vrpc.io:8883',
      timeout = 6 * 1000,
      log = 'console'
    } = {}) {
      super()
      this._token = token
      this._username = username
      this._password = password
      this._agent = agent
      this._domain = domain
      this._broker = broker
      this._timeout = timeout
      this._instance = crypto.randomBytes(2).toString('hex')
      this._mqttClientId = this._createClientId(this._instance)
      this._vrpcClientId = `${domain}/${os.hostname()}/${this._instance}`
      this._domains = {}
      this._eventEmitter = new EventEmitter()
      this._invokeId = 0
      this._client = null
      this._cachedSubscriptions = new Map()
      if (log === 'console') {
        this._log = console
        this._log.debug = () => {}
      } else {
        this._log = log
      }
    }
  
    async connect () {
      if (this._client && this._client.connected) return
      let username = this._username
      let password = this._password
      if (this._token) {
        username = '__token__'
        password = this._token
      }
      const options = {
        username,
        password,
        clean: true,
        keepalive: 30,
        clientId: this._mqttClientId,
        rejectUnauthorized: false,
        will: {
          topic: `${this._vrpcClientId}/__clientInfo__`,
          payload: JSON.stringify({ status: 'offline' })
        }
      }
      this._client = mqtt.connect(this._broker, options)
  
      this._client.on('error', (err) => {
        this.emit('error', err)
      })
  
      this._client.stream.on('error', (err) => {
        this.emit('error', err)
      })
  
      this.on('error', (err) => {
        this._log.debug(`Encountered MQTT error: ${err.message}`)
      })
  
      this._client.on('connect', () => {
        // This will give us an overview of all remotely available classes
        const domain = this._domain === '*' ? '+' : this._domain
        const agent = this._agent === '*' ? '+' : this._agent
        // Agent info
        this._mqttSubscribe(`${domain}/${agent}/__agentInfo__`)
        // Class info
        this._mqttSubscribe(`${domain}/${agent}/+/__classInfo__`)
        // RPC responses
        this._mqttSubscribe(this._vrpcClientId)
        this.emit('connect')
      })
  
      this._client.on('message', (topic, message) => {
        if (message.length === 0) return
        const tokens = topic.split('/')
        const [domain, agent, klass, instance] = tokens
        // AgentInfo message
        if (klass === '__agentInfo__') {
          const { status, hostname } = JSON.parse(message.toString())
          this._createIfNotExist(domain, agent)
          this._domains[domain].agents[agent].status = status
          this._domains[domain].agents[agent].hostname = hostname
          this.emit('agent', { domain, agent, status, hostname })
        // ClassInfo message
        } else if (instance === '__classInfo__') {
          // Json properties: { className, instances, memberFunctions, staticFunctions }
          const json = JSON.parse(message.toString())
          this._createIfNotExist(domain, agent)
          const oldClassInfo = this._domains[domain].agents[agent].classes[klass]
          const newInstances = json.instances || []
          const oldInstances = oldClassInfo ? oldClassInfo.instances : []
          const removed = oldInstances.filter(x => !newInstances.includes(x))
          const added = newInstances.filter(x => !oldInstances.includes(x))
          this._domains[domain].agents[agent].classes[klass] = json
          const {
            className,
            instances,
            memberFunctions,
            staticFunctions,
            meta
          } = json
          if (removed.length !== 0) this.emit('instanceGone', removed, { domain, agent, className })
          if (added.length !== 0) this.emit('instanceNew', added, { domain, agent, className })
          this.emit(
            'class',
            {
              domain,
              agent,
              className,
              instances,
              memberFunctions,
              staticFunctions,
              meta
            }
          )
        // RPC message
        } else {
          const { id, data } = JSON.parse(message.toString())
          this._eventEmitter.emit(id, data)
        }
      })
      await new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
          reject(new Error(`Connection trial timed out (> ${this._timeout} ms)`))
        }, this._timeout)
        this.once('connect', () => {
          clearTimeout(timer)
          resolve()
        })
      })
    }
  
    async connected () {
      this._log.warn('connected(): This API usage will be deprecated, use "connect()" instead')
      return this.connect()
    }
  
    /**
     * Creates a new remote instance.
     *
     * @param {string} className Name of the class which should be instantiated.
     * @param {string} instance Optional. Name of the created instance.
     * @param {Array} args Array of constructor arguments (positional)
     * @param {string} agent Agent name. If not provided class default is used.
     * @param {string} domain Domain name. If not provided class default is used.
     * @return {Promise} Resolves to an object reflecting a proxy to the original
     * one handled by the agent.
     */
    async create ({
      className,
      instance,
      args = [],
      agent = this._agent,
      domain = this._domain
    } = {}) {
      if (agent === '*') throw new Error('Agent must be specified')
      if (domain === '*') throw new Error('Domain must be specified')
      const data = instance ? { _1: instance } : {}
      const offset = instance ? 2 : 1
      args.forEach((value, index) => {
        data[`_${index + offset}`] = value
      })
      const json = {
        context: className,
        method: instance ? '__createNamed__' : '__create__',
        id: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
        sender: `${domain}/${os.hostname()}/${this._instance}`,
        data
      }
      return this._getProxy(domain, agent, className, json)
    }
  
    /**
     * Get a remotely existing instance.
     *
     * Either provide a string only, then VRPC tries to find the instance using
     * client information, or additionally provide an object with explicit meta
     * data.
     *
     * @param {string} instance The instance to be retrieved
     * @param {object} options Explicitly define domain, agent and class
     * @param {string} options.className Name of the instance's class
     * @param {string} options.agent Agent name. If not provided class default is used.
     * @param {string} options.domain Domain name. If not provided class default is used.
     * @param {bool} options.noWait If true immediately fail if instance could not be found in local cache.
     * @return Proxy object reflecting the remotely existing instance.
     */
    async getInstance (instance, options) {
      if (typeof instance === 'string') {
        const {
          domain,
          agent,
          className,
          instance: instanceString
        } = await this._getInstanceData(instance, options)
        return this._createProxy(domain, agent, className, instanceString)
      } else { // deprecate this
        this._log.warn('This API usage will be deprecated, use "getInstance(instance, options)" instead')
        console.trace()
        const fakedOptions = { ...instance }
        const fakedInstance = fakedOptions.instance
        delete fakedOptions.instance
        const {
          domain,
          agent,
          className,
          instance: instanceString
        } = await this._getInstanceData(fakedInstance, fakedOptions)
        return this._createProxy(domain, agent, className, instanceString)
      }
    }
  
    /**
     * Delete a remotely existing instance.
     *
     * Either provide a string only, then VRPC tries to find the instance using
     * client information, or provide an object with explicit meta data.
     *
     * @param {string} instance The instance to be deleted
     * @param {object} options Explicitly define domain, agent and class
     * @param {string} options.className Name of the instance's class
     * @param {string} options.agent Agent name. If not provided class default is used.
     * @param {string} options.domain Domain name. If not provided class default is used.
     * @return true if successful, false otherwise
     */
    async delete (instance, options) {
      let instanceData = { domain: this._domain, agent: this._agent }
      if (typeof instance === 'string') {
        instanceData = await this._getInstanceData(instance)
        if (options) instanceData = { ...options, ...instanceData }
      } else { // deprecate this
        this._log.warn('This API usage will be deprecated, use "delete(instance, options)" instead')
        instanceData = { ...instanceData, ...instance }
      }
      const { domain, agent, className, instance: instanceString } = instanceData
      const json = {
        context: className,
        method: '__delete__',
        id: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
        sender: `${domain}/${os.hostname()}/${this._instance}`,
        data: { _1: instanceString }
      }
      const topic = `${domain}/${agent}/${className}/__static__/__delete__`
      this._mqttPublish(topic, JSON.stringify(json))
      return this._handleAgentAnswer(json)
    }
  
    /**
     * Calls a static function on a remote class
     *
     * @param {string} className Name of the static function's class
     * @param {string} functionName Name of the static function to be called
     * @param {Array} args Positional arguments of the static function call
     * @param {string} agent Agent name. If not provided class default is used.
     * @param {string} domain Domain name. If not provided class default is used.
     * @return Return value of the remotely called function
     */
    async callStatic ({
      className,
      functionName,
      args = [],
      agent = this._agent,
      domain = this._domain
    } = {}) {
      if (domain === '*') throw new Error('You must specify a domain')
      const json = {
        context: className,
        method: functionName,
        id: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
        sender: this._vrpcClientId,
        data: this._packData(className, functionName, ...args)
      }
      const topic = `${domain}/${agent}/${className}/__static__/${functionName}`
      await this._waitUntilClassIsOnline(domain, agent, className)
      this._mqttPublish(topic, JSON.stringify(json))
      return this._handleAgentAnswer(json)
    }
  
    /**
     * Retrieves all domains, agents, instances, classes, member and static
     * functions potentially available for remote control.
     *
     * @return {Object} Object with structure:
     * <domain>.agents.<agent>.classes.<className>.instances: []
     * <domain>.agents.<agent>.classes.<className>.memberFunctions: []
     * <domain>.agents.<agent>.classes.<className>.staticFunctions: []
     * <domain>.agents.<agent>.status: 'offline'|'online'
     * <domain>.agents.<agent>.hostname: <hostname>
     */
    getAvailabilities () {
      return this._domains
    }
  
    /**
     * Retrieves all domains on which agents can be remote controlled.
     *
     * @return {Array} Array of domain names.
     */
    getAvailableDomains () {
      return Object.keys(this._domains)
    }
  
    /**
     * Retrieves all available agents on specific domain.
     *
     * @param {string} domain Domain name. If not provided class default is used.
     * @return {Array} Array of agent names.
     */
    getAvailableAgents (domain = this._domain) {
      if (domain === '*') throw new Error('Domain must be specified')
      return this._domains[domain]
        ? Object.keys(this._domains[domain].agents)
        : []
    }
  
    /**
     * Retrieves all available classes on specific agent and domain.
     *
     * @param {string} agent Agent name. If not provided class default is used.
     * @param {string} domain Domain name. If not provided class default is used.
     * @return {Array} Array of class names.
     */
    getAvailableClasses (agent = this._agent, domain = this._domain) {
      if (agent === '*') throw new Error('Agent must be specified')
      if (domain === '*') throw new Error('Domain must be specified')
      return this._domains[domain]
        ? this._domains[domain].agents[agent]
          ? Object.keys(this._domains[domain].agents[agent].classes)
          : []
        : []
    }
  
    /**
     * Retrieves all (named) instances on specific class, agent and domain.
     *
     * @param {string} className Class name.
     * @param {string} agent Agent name. If not provided class default is used.
     * @param {string} domain Domain name. If not provided class default is used.
     * @return {Array} Array of instance names.
     */
    getAvailableInstances (className, agent = this._agent, domain = this._domain) {
      if (agent === '*') throw new Error('Agent must be specified')
      if (domain === '*') throw new Error('Domain must be specified')
      return this._domains[domain]
        ? this._domains[domain].agents[agent]
          ? this._domains[domain].agents[agent].classes[className]
            ? this._domains[domain].agents[agent].classes[className].instances
            : []
          : []
        : []
    }
  
    /**
     * Retrieves all member functions of specific class, agent and domain.
     *
     * @param {string} className Class name.
     * @param {string} agent Agent name. If not provided class default is used.
     * @param {string} domain Domain name. If not provided class default is used.
     * @return {Array} Array of member function names.
     */
    getAvailableMemberFunctions (className, agent = this._agent, domain = this._domain) {
      if (agent === '*') throw new Error('Agent must be specified')
      if (domain === '*') throw new Error('Domain must be specified')
      return this._domains[domain]
        ? this._domains[domain].agents[agent]
          ? this._domains[domain].agents[agent].classes[className]
            ? this._domains[domain].agents[agent].classes[className].memberFunctions.map(name => this._stripSignature(name))
            : []
          : []
        : []
    }
  
    /**
     * Retrieves all static functions of specific class, agent and domain.
     *
     * @param {string} className Class name.
     * @param {string} agent Agent name. If not provided class default is used.
     * @param {string} domain Domain name. If not provided class default is used.
     * @return {Array} Array of static function names.
     */
    getAvailableStaticFunctions (className, agent = this._agent, domain = this._domain) {
      if (agent === '*') throw new Error('Agent must be specified')
      if (domain === '*') throw new Error('Domain must be specified')
      return this._domains[domain]
        ? this._domains[domain].agents[agent]
          ? this._domains[domain].agents[agent].classes[className]
            ? this._domains[domain].agents[agent].classes[className].staticFunctions.map(name => this._stripSignature(name))
            : []
          : []
        : []
    }
  
    /**
     * Reconnects to the broker by using a different token
     * @param {string} token Access token as generated by: https://app.vrpc.io
     * @param {string} agent Agent name. If not provided class default is used.
     * @param {string} domain Domain name. If not provided class default is used.
     * @return {PRomise} Resolve once re-connected.
     */
    async reconnectWithToken (
      token,
      { agent = this._agent, domain = this._domain } = {}
    ) {
      this._token = token
      this._agent = agent
      this._domain = domain
      this._client.end(() => this._init())
      return new Promise(resolve => {
        this._client.once('connect', resolve)
      })
    }
  
    /**
     * Ends the connection to the broker.
     *
     * @return {Promise} Resolves when ended.
     */
    async end () {
      this._mqttPublish(
        `${this._vrpcClientId}/__clientInfo__`,
        JSON.stringify({ status: 'offline' })
      )
      return new Promise(resolve => this._client.end(false, {}, resolve))
    }
  
    _createClientId (instance) {
      const clientInfo = os.arch() + JSON.stringify(os.cpus()) + os.homedir() +
      os.hostname() + JSON.stringify(os.networkInterfaces()) + os.platform() +
      os.release() + os.totalmem() + os.type()
      // console.log('ClientInfo:', clientInfo)
      const md5 = crypto.createHash('md5').update(clientInfo).digest('hex').substr(0, 13)
      return `vrpcp${instance}X${md5}` // 5 + 4 + 1 + 13 = 23 (max clientId)
    }
  
    _mqttPublish (topic, message, options) {
      this._client.publish(topic, message, { qos: 0, ...options }, (err) => {
        if (err) {
          this._log.warn(`Could not publish MQTT message because: ${err.message}`)
        }
      })
    }
  
    _mqttSubscribe (topic, options) {
      this._client.subscribe(topic, { qos: 0, ...options }, (err, granted) => {
        if (err) {
          this._log.warn(`Could not subscribe to topic: ${topic} because: ${err.message}`)
        } else {
          if (granted.length === 0) {
            this._log.warn(`No permission for subscribing to topic: ${topic}`)
          }
        }
      })
    }
  
    _mqttUnsubscribe (topic, options) {
      this._client.unsubscribe(topic, options, (err) => {
        if (err) {
          this._log.warn(`Could not unsubscribe from topic: ${topic} because: ${err.message}`)
        }
      })
    }
  
    _createIfNotExist (domain, agent) {
      if (!this._domains[domain]) {
        this._domains[domain] = { agents: {} }
      }
      if (!this._domains[domain].agents[agent]) {
        this._domains[domain].agents[agent] = { classes: {} }
      }
    }
  
    async _getProxy (domain, agent, className, json) {
      const { method } = json
      const topic = `${domain}/${agent}/${className}/__static__/${method}`
      await this._waitUntilClassIsOnline(domain, agent, className)
      this._mqttPublish(topic, JSON.stringify(json))
      return new Promise((resolve, reject) => {
        const msg = `Proxy creation timed out (> ${this._timeout} ms)`
        const timer = setTimeout(
          () => {
            this._eventEmitter.removeAllListeners(json.id)
            reject(new Error(msg))
          },
          this._timeout
        )
        this._eventEmitter.once(json.id, data => {
          clearTimeout(timer)
          if (data.e) {
            reject(new Error(data.e))
          } else {
            const proxy = this._createProxy(domain, agent, className, data.r)
            resolve(proxy)
          }
        })
      })
    }
  
    async _waitUntilClassIsOnline (domain, agent, className) {
      if (this._hasClassOnline(domain, agent, className)) return
      return new Promise((resolve, reject) => {
        let timer = null
        const checkClass = () => {
          if (this._hasClassOnline(domain, agent, className)) {
            if (timer) clearTimeout(timer)
            this.removeListener('class', checkClass)
            resolve()
          }
        }
        timer = setTimeout(() => {
          this.removeListener('class', checkClass)
          reject(new Error(`Proxy creation timed out (> ${this._timeout} ms)`))
        }, this._timeout)
        this.on('class', checkClass)
      })
    }
  
    _hasClassOnline (domain, agent, className) {
      return (this._domains[domain] &&
        this._domains[domain].agents[agent] &&
        this._domains[domain].agents[agent].status === 'online' &&
        this._domains[domain].agents[agent].classes[className]
      )
    }
  
    _createProxy (domain, agent, className, instance) {
      const targetTopic = `${domain}/${agent}/${className}/${instance}`
      const proxyId = crypto.randomBytes(2).toString('hex')
      const proxy = {
        _targetId: instance,
        _proxyId: proxyId
      }
      let functions = this._domains[domain].agents[agent].classes[className].memberFunctions
      // Strip off argument signature
      functions = functions.map(name => {
        const pos = name.indexOf('-')
        if (pos > 0) return name.substring(0, pos)
        return name
      })
      // Remove overloads
      const uniqueFuncs = new Set(functions)
      // Build proxy
      uniqueFuncs.forEach(name => {
        proxy[name] = async (...args) => {
          try {
            const json = {
              context: instance,
              method: name,
              id: `${this._instance}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`,
              sender: this._vrpcClientId,
              data: this._packData(proxyId, name, ...args)
            }
            // Skipping remote call -> handled locally
            if (json.data === null) return
            this._mqttPublish(`${targetTopic}/${name}`, JSON.stringify(json))
            return this._handleAgentAnswer(json)
          } catch (err) {
            throw new Error(`Could not remotely call "${name}" because: ${err.message}`)
          }
        }
      })
      return proxy
    }
  
    async _handleAgentAnswer ({ id, context, method }) {
      return new Promise((resolve, reject) => {
        const msg = `Function call "${context}::${method}()" timed out (> ${this._timeout} ms)`
        const timer = setTimeout(
          () => {
            this._eventEmitter.removeAllListeners(id)
            reject(new Error(msg))
          },
          this._timeout
        )
        this._eventEmitter.once(id, data => {
          clearTimeout(timer)
          if (data.e) {
            reject(new Error(data.e))
          } else {
            const ret = data.r
            // Handle functions returning a promise
            if (typeof ret === 'string' && ret.substr(0, 5) === '__p__') {
              this._eventEmitter.once(ret, promiseData => {
                if (promiseData.e) reject(new Error(promiseData.e))
                else resolve(promiseData.r)
              })
            } else {
              resolve(ret)
            }
          }
        })
      })
    }
  
    async _waitForInstance (instance, options = {}) {
      return new Promise((resolve, reject) => {
        const handler = (timer) => (instances, { domain, agent, className }) => {
          if (instances.includes(instance)) {
            if (options.domain && domain !== options.domain) return
            if (options.agent && agent !== options.agent) return
            if (options.className && className !== options.className) return
            clearTimeout(timer)
            this.removeListener('instanceNew', handler)
            resolve({ domain, agent, className, instance })
          }
        }
        const timer = setTimeout(
          () => {
            this.removeListener('instanceNew', handler(timer))
            const msg = `Could not find instance: ${instance} (> ${this._timeout} ms)`
            reject(new Error(msg))
          },
          this._timeout
        )
        this.on('instanceNew', handler(timer))
      })
    }
  
    _packData (proxyId, functionName, ...args) {
      const data = {}
      let isHandledLocally = false
      args.forEach((value, index) => {
        // Check whether provided argument is a function
        if (this._isFunction(value)) {
          // Check special case of an event emitter registration
          // We test three conditions:
          // 1) functionName must be "on"
          // 2) callback is second argument
          // 3) first argument was string
          if (functionName === 'on' &&
            index === 1 &&
            typeof args[0] === 'string'
          ) {
            const id = this._addEventSubscription(proxyId, args[0], value)
            if (!id) isHandledLocally = true
            data[`_${index + 1}`] = id
          } else if (
            (functionName === 'off' || functionName === 'removeListener') &&
            index === 1 &&
            typeof args[0] === 'string'
          ) {
            const id = this._removeEventSubscription(proxyId, args[0], value)
            if (!id) isHandledLocally = true
            data[`_${index + 1}`] = id
          // Regular function callback
          } else {
            const id = `__f__${proxyId}-${functionName}-${index}-${this._invokeId++ % Number.MAX_SAFE_INTEGER}`
            data[`_${index + 1}`] = id
            this._eventEmitter.once(id, data => {
              const args = Object.keys(data).sort()
                .filter(x => x[0] === '_')
                .map(x => data[x])
              value.apply(null, args) // This is the actual function call
            })
          }
        } else if (this._isEmitter(value)) {
          const { emitter, event } = value
          const id = `__f__${proxyId}-${functionName}-${index}-${event}`
          data[`_${index + 1}`] = id
          this._eventEmitter.on(id, data => {
            const args = Object.keys(data).sort()
              .filter(x => x[0] === '_')
              .map(x => data[x])
            emitter.emit(event, ...args)
          })
        } else {
          data[`_${index + 1}`] = value
        }
      })
      if (isHandledLocally) return null
      return data
    }
  
    _addEventSubscription (proxyId, event, callback) {
      const id = `__f__${proxyId}-${event}`
      if (this._cachedSubscriptions.has(id)) {
        this._cachedSubscriptions.get(id).on(id, callback)
        return
      }
      const emitter = new EventEmitter()
      emitter.on(id, callback)
      this._cachedSubscriptions.set(id, emitter)
      this._eventEmitter.on(id, (data) => {
        const args = Object.keys(data).sort()
          .filter(x => x[0] === '_')
          .map(x => data[x])
        emitter.emit(id, ...args)
      })
      return id
    }
  
    _removeEventSubscription (proxyId, event, callback) {
      const id = `__f__${proxyId}-${event}`
      if (this._cachedSubscriptions.has(id)) {
        const emitter = this._cachedSubscriptions.get(id)
        emitter.removeListener(id, callback)
        if (emitter.listenerCount(id) === 0) {
          this._cachedSubscriptions.delete(id)
          this._eventEmitter.removeAllListeners(id)
          return id
        }
      }
    }
  
    _stripSignature (method) {
      const pos = method.indexOf('-')
      if (pos > 0) return method.substring(0, pos)
      return method
    }
  
    _isFunction (variable) {
      const getType = {}
      const type = getType.toString.call(variable)
      return variable &&
        (type === '[object Function]' || type === '[object AsyncFunction]')
    }
  
    _isEmitter (variable) {
      return (
        typeof variable === 'object' &&
        variable.hasOwnProperty('emitter') &&
        variable.hasOwnProperty('event') &&
        typeof variable.emitter === 'object' &&
        typeof variable.emitter.emit === 'function'
      )
    }
  
    async _getInstanceData (instance, options = {}) {
      const instanceData = this._getInstanceFromCache(instance, options)
      if (!instanceData) {
        if (options.noWait) throw new Error(`Could not find instance: ${instance}`)
        return this._waitForInstance(instance, options)
      }
      return instanceData
    }
  
    _getInstanceFromCache (instance, options = {}) {
      // loop domains
      for (const domain in this._domains) {
        if (options.domain && domain !== options.domain) continue
        const { agents } = this._domains[domain]
        if (!agents) break
        for (const agent in agents) {
          if (options.agent && agent !== options.agent) continue
          const { classes, status } = agents[agent]
          if (!classes || status === 'offline') break
          for (const className in classes) {
            if (options.className && className !== options.className) continue
            const { instances } = classes[className]
            if (!instances) break
            if (instances.includes(instance)) {
              return { domain, agent, className, instance }
            }
          }
        }
      }
    }
  }
  
  module.exports = VrpcRemote
  