text: const ffi = require('ffi-napi')
      , ref = require('ref-napi')
      , path = require('path')
      , EventEmitter = require('events')
      , debug = require('debug')('libbwt')
  
  const LIB_PATH = process.env.BWT_LIB || path.join(__dirname, 'libbwt')
  
  // Low-level private API
  
  const OK = 0
  
  const shutdownPtr = ref.refType('void')
  
  const libbwt = ffi.Library(LIB_PATH, {
    bwt_start: [ 'int', [ 'string', 'pointer', 'pointer' ] ]
  , bwt_shutdown: [ 'int', [ shutdownPtr ] ]
  })
  
  function bwt_start(options, init_cb, notify_cb, done) {
    const opt_json = JSON.stringify(options)
        , init_cb_ffi = ffi.Callback('void', [ shutdownPtr ], init_cb)
        , notify_cb_ffi = ffi.Callback('void', [ 'string', 'float', 'uint32', 'string' ], notify_cb)
  
    libbwt.bwt_start.async(opt_json, init_cb_ffi, notify_cb_ffi, function(err, code) {
      debug('stopped with', { err, code })
      if (err) return done(err)
      if (code != OK) return done(new Error(`bwt failed with code ${code}`))
      done(null)
    })
  }
  
  function bwt_shutdown(shutdown_ptr) {
    const code = libbwt.bwt_shutdown(shutdown_ptr)
    shutdown_ptr.deref()
    if (code != OK) throw new Error(`bwt shutdown failed with code ${code}`)
  }
  
  // High-level public API
  
  class BwtDaemon extends EventEmitter {
    constructor(options) {
      super()
  
      if (options.sync_progress) this.on('progress:sync', take_prop(options, 'sync_progress'))
      if (options.scan_progress) this.on('progress:scan', take_prop(options, 'scan_progress'))
  
      this.options = normalize_options(options)
  
      this.ready = new Promise((resolve, reject) => {
        this.on('ready', _ => resolve(this))
        this.on('error', reject)
        this.on('exit', err => {
          if (err) return this.emit('error', err)
          // this `reject` will be ignored if the daemon already started up successfully
          // and the promise was resolved, which can happen following a shutdown() call.
          // imagine this is wrapped in an `if (!promise.resolved)`.
          reject(new Error('daemon stopped while starting up'))
        })
      })
    }
  
    start() {
      if (this._started) throw new Error('daemon already started')
      this._started = true
  
      debug('starting with %O', { ...this.options, bitcoind_auth: '**SCRUBBED**' });
      bwt_start(this.options, this._init.bind(this), this._notify.bind(this)
              , err => this.emit('exit', err /*null for successful exit*/))
      return this.ready
    }
  
    shutdown() {
      debug('shutdown', this._shutdown_ptr != null)
      // we cannot shut down yet, mark for later termination
      if (!this._shutdown_ptr) return this._terminate = true
  
      bwt_shutdown(take_prop(this, '_shutdown_ptr'))
    }
  
    _init(shutdown_ptr) {
      this._shutdown_ptr = shutdown_ptr
      if (take_prop(this, '_terminate')) this.shutdown()
    }
  
    _notify(msg_type, progress_n, detail_n, detail_s) {
      debug('notify %s %s %s', msg_type, progress_n, detail_n, detail_s)
      switch (msg_type) {
        case 'error':
          this.emit('error', detail_s)
          break
        case 'progress:sync':
          const tip_time = new Date(detail_n*1000)
          this.emit('progress:sync', progress_n, tip_time)
          break
        case 'progress:scan':
          this.emit('progress:scan', progress_n, detail_n /*ETA in seconds*/)
          break
        case 'access_token':
          this.access_token = detail_s
          break
        case 'ready:http':
          this.http_addr = detail_s
          this.http_url = `http://${this.http_addr}/`
          break
        case 'ready:electrum':
          this.electrum_addr = detail_s
          break
        case 'ready':
          this.emit('ready')
          break
        default: debug('unknown msg', msg_type)
      }
    }
  }
  
  // optional 'new'
  exports.BwtDaemon = module.exports = function(opt) { return new BwtDaemon(opt) }
  exports._BwtDaemon = BwtDaemon
  exports.start = opt => new BwtDaemon(opt).start()
  
  // Utility
  
  function normalize_options(options) {
    if (options.rescan_since) {
      options.rescan_since = parse_timestamp(options.rescan_since)
    }
    if (take_prop(options, 'electrum') && !options.electrum_addr) {
      options.electrum_addr = '127.0.0.1:0'
    }
    if (take_prop(options, 'http') && !options.http_addr) {
      options.http_addr = '127.0.0.1:0'
    }
  
    if (!options.electrum_addr && !options.http_addr) {
      throw new Error('None of the bwt services are enabled')
    }
  
    // Delete nully options so that they get their default value
    Object.entries(options)
      .filter(([ _, val ]) => val == null)
      .forEach(([ key, _ ]) => delete options[key])
  
    return options
  }
  
  function take_prop(obj, key) {
    const val = obj[key]
    delete obj[key]
    return val
  }
  
  function parse_timestamp(ts) {
    // Pass 'now' as is
    if (ts == 'now') return ts
    // Date objects
    if (ts.getTime) return ts.getTime()/1000|0
    // Unix timestamp
    if (!isNaN(ts)) return +ts
    // Date string (e.g. YYYY-MM-DD)
    const dt = new Date(ts)
    if (!isNaN(dt.getTime())) return dt.getTime()/1000|0
  
    throw new Error(`Invalid rescan since value: ${ts}`)
  }
  