text: /**
   *
   * wechaty: Wechat for Bot. and for human who talk to bot/robot
   *
   * Class PuppetWeb Events
   *
   * use to control wechat in web browser.
   *
   * Licenst: ISC
   * https://github.com/zixia/wechaty
   *
   */
  
  /**************************************
   *
   * Class PuppetWeb
   *
   ***************************************/
  const util  = require('util')
  const fs    = require('fs')
  const co    = require('co')
  
  const log = require('../npmlog-env')
  const Contact = require('../contact')
  const Message = require('../message')
  const MediaMessage = require('../message-media')
  
  const PuppetWebEvent = {
    onBrowserDead
  
    , onServerLogin
    , onServerLogout
  
    , onServerConnection
    , onServerDisconnect
  
    , onServerDing
    , onServerScan
    , onServerUnload
    , onServerLog
  
    , onServerMessage
  }
  
  function onBrowserDead(e) {
    log.verbose('PuppetWebEvent', 'onBrowserDead(%s)', e)
    // because this function is async, so maybe entry more than one times.
    // guard by variable: isBrowserBirthing to prevent the 2nd time entrance.
    if (this.isBrowserBirthing) {
      if (this.isBrowserBirthing === true) {
        log.verbose('PuppetWebEvent', 'onBrowserDead() Im busy, dont call me again before I return. this time will return and do nothing')
        return
      } else {
        log.warn('PuppetWebEvent', 'onBrowserDead() Im FAKE busy? isBrowserBirthing is not boolean true!')
      }
    }
  
    this.scan = null
    
    return co.call(this, function* () {
      log.verbose('PuppetWebEvent', 'onBrowserDead() co() set isBrowserBirthing true')
      this.isBrowserBirthing = true
  
      const TIMEOUT = 180000 // 180s / 3m
      // this.watchDog(`onBrowserDead() set a timeout of ${Math.floor(TIMEOUT / 1000)} seconds to prevent unknown state change`, {timeout: TIMEOUT})
      this.emit('watchdog', {
        data: `onBrowserDead() set a timeout of ${Math.floor(TIMEOUT / 1000)} seconds to prevent unknown state change`
        , timeout: TIMEOUT
      })
  
      log.verbose('PuppetWebEvent', 'onBrowserDead(%s)', e.message || e)
      if (!this.browser || !this.bridge) {
        log.error('PuppetWebEvent', 'onBrowserDead() browser or bridge not found. do nothing')
        throw new Error('no browser or no bridge')
      }
  
      log.verbose('PuppetWebEvent', 'onBrowserDead() try to reborn browser')
  
      yield this.browser.quit()
      .catch(e => { // fail safe
        log.warn('PuppetWebEvent', 'browser.quit() exception: %s', e.message)
      })
      log.verbose('PuppetWebEvent', 'old browser quited')
  
      this.browser = yield this.initBrowser()
      log.verbose('PuppetWebEvent', 'new browser inited')
  
      // this.bridge = yield this.bridge.init()
      this.bridge = yield this.initBridge()
      log.verbose('PuppetWebEvent', 'bridge re-inited')
  
      const dong = yield this.ding()
      if (/dong/i.test(dong)) {
        log.verbose('PuppetWebEvent', 'ding() works well after reset')
      } else {
        log.warn('PuppetWebEvent', 'ding() get error return after reset: ' + dong)
      }
    })
    .catch(e => { // Exception
      log.error('PuppetWebEvent', 'onBrowserDead() exception: %s', e.message)
  
      log.warn('PuppetWebEvent', 'onBrowserDead() try to re-init PuppetWeb itself')
      return this.quit().then(_ => this.init())
    })
    .then(() => { // Finally
      log.verbose('PuppetWebEvent', 'onBrowserDead() new browser borned')
      this.isBrowserBirthing = false
    })
  }
  
  function onServerDing(data) {
    log.silly('PuppetWebEvent', 'onServerDing(%s)', data)
    // this.watchDog(data)
    this.emit('watchdog', { data })
  }
  
  function onServerScan(data) {
    log.verbose('PuppetWebEvent', 'onServerScan(%d)', data && data.code)
  
    this.scan = data // ScanInfo
    
    /**
     * When wx.qq.com push a new QRCode to Scan, there will be cookie updates(?)
     */
    this.browser.saveSession()
        .catch(() => {/* fail safe */})
  
    if (this.userId) {
      log.verbose('PuppetWebEvent', 'onServerScan() there has userId when got a scan event. emit logout and set userId to null')
      this.emit('logout', this.user || this.userId)
      this.userId = this.user = null
    }
  
    // feed watchDog a `scan` type of food
    // this.watchDog(data, {type: 'scan'})
    this.emit('watchdog', { data, type: 'SCAN' })
  
    this.emit('scan', data)
  }
  
  function onServerConnection(data) {
    log.verbose('PuppetWebEvent', 'onServerConnection: %s', typeof data)
  }
  
  function onServerDisconnect(data) {
    log.verbose('PuppetWebEvent', 'onServerDisconnect: %s', data)
  
    if (this.userId) {
      log.verbose('PuppetWebEvent', 'onServerDisconnect() there has userId set. emit a logout event and set userId to null')
      this.emit('logout', this.user || this.userId ) //'onServerDisconnect(' + data + ')')
      this.userId = null
      this.user = null
    }
  
    /**
     * conditions:
     * 1. browser crash(i.e.: be killed)
     * 2. quiting
     */
    if (!this.browser) {                // no browser, quiting?
      log.verbose('PuppetWebEvent', 'onServerDisconnect() no browser. maybe Im quiting, do nothing')
      return
    } else if (this.browser.dead()) {   // browser is dead
      log.verbose('PuppetWebEvent', 'onServerDisconnect() found dead browser. wait it to restore')
      return
    } else if (!this.bridge) {          // no bridge, quiting???
      log.verbose('PuppetWebEvent', 'onServerDisconnect() no bridge. maybe Im quiting, do nothing')
      return
    }
  
    this.browser.readyLive()
    .then(r => {  // browser is alive, and we have a bridge to it
      log.verbose('PuppetWebEvent', 'onServerDisconnect() re-initing bridge')
      // must use setTimeout to wait a while.
      // because the browser has just refreshed, need some time to re-init to ready.
      // if the browser is not ready, bridge init will fail,
      // caused browser dead and have to be restarted. 2016/6/12
      setTimeout(_ => {
        if (!this.bridge) {
          return // XXX: sometimes this.bridge gone in this timeout. why? what's happend between the last if(!this.bridge) check and the timeout call?
        }
        this.bridge.init()
        .then(r  => log.verbose('PuppetWebEvent', 'onServerDisconnect() bridge re-inited: %s', r))
        .catch(e => log.error('PuppetWebEvent', 'onServerDisconnect() exception: [%s]', e))
      }, 1000) // 1 second instead of 10 seconds? try. (should be enough to wait)
      return
    })
    .catch(e => { // browser is in indeed dead, or almost dead. readyLive() will auto recover itself.
      log.verbose('PuppetWebEvent', 'onServerDisconnect() browser dead, waiting it recover itself: %s', e.message)
      return
    })
  }
  
  /**
   * `unload` event is sent from js@browser to webserver via socketio
   * after received `unload`, we should fix bridge by re-inject the Wechaty js code into browser.
   * possible conditions:
   * 1. browser refresh
   * 2. browser navigated to a new url
   * 3. browser quit(crash?)
   * 4. ...
   */
  function onServerUnload(data) {
    log.warn('PuppetWebEvent', 'onServerUnload(%s)', typeof data)
    // onServerLogout.call(this, data) // XXX: should emit event[logout] from browser
  
    if (!this.browser) {
      log.warn('PuppetWebEvent', 'onServerUnload() found browser gone, should be quiting now')
      return
    } else if (!this.bridge) {
      log.warn('PuppetWebEvent', 'onServerUnload() found bridge gone, should be quiting now')
      return
    } else if (this.browser.dead()) {
      log.error('PuppetWebEvent', 'onServerUnload() found browser dead. wait it to restore itself')
      return
    }
    // re-init bridge after 1 second XXX: better method to confirm unload/reload finished?
    return setTimeout(() => {
      this.bridge.init()
      .then(r  => log.verbose('PuppetWebEvent', 'onServerUnload() bridge.init() done: %s', r))
      .catch(e => log.error('PuppetWebEvent', 'onServerUnload() bridge.init() exceptoin: %s', e.message))
    }, 1000)
  }
  
  function onServerLog(data) {
    log.verbose('PuppetWebEvent', 'onServerLog(%s)', data)
  }
  
  function onServerLogin(data, attempt = 0) {
    log.verbose('PuppetWebEvent', 'onServerLogin(%s, %d)', data, attempt)
  
    this.scan = null
    
    if (this.userId) {
      log.verbose('PuppetWebEvent', 'onServerLogin() be called but with userId set?')
    }
  
    co.call(this, function* () {
      // co.call to make `this` context work inside generator.
      // See also: https://github.com/tj/co/issues/274
  
      /**
       * save login user id to this.userId
       */
      this.userId = yield this.bridge.getUserName()
  
      if (!this.userId) {
        log.verbose('PuppetWebEvent', 'onServerLogin: browser not full loaded(%d), retry later', attempt)
        setTimeout(onServerLogin.bind(this, data, ++attempt), 500)
        return
      }
  
      log.verbose('PuppetWebEvent', 'bridge.getUserName: %s', this.userId)
      this.user = yield Contact.load(this.userId).ready()
      log.verbose('PuppetWebEvent', `onServerLogin() user ${this.user.name()} logined`)
      this.emit('login', this.user)
  
      yield this.browser.saveSession()
                .catch(e => { // fail safe
                  log.warn('PuppetWebEvent', 'onServerLogin() browser.saveSession exception: %s', e.message)
                })
    }).catch(e => {
      log.error('PuppetWebEvent', 'onServerLogin() exception: %s', e.message)
    })
  }
  
  function onServerLogout(data) {
    if (this.user) {
      this.emit('logout', this.user)
    } else if (this.userId) {
      this.emit('logout', this.userId)
    } else { log.verbose('PuppetWebEvent', 'onServerLogout() without this.user or userId initialized') }
  
    this.userId = null
    this.user = null
  
    // this.browser.cleanSession()
    // .catch(e => { /* fail safe */
    //   log.verbose('PuppetWebEvent', 'onServerLogout() browser.cleanSession() exception: %s', e.message)
    // })
  }
  
  function onServerMessage(data) {
    let m
    // log.warn('PuppetWebEvent', 'MsgType: %s', data.MsgType)
    switch (data.MsgType) {
      case Message.Type.IMAGE:
        // log.verbose('PuppetWebEvent', 'onServerMessage() IMAGE message')
        m = new MediaMessage(data)
        break
  
      case 'TEXT':
      default:
        m = new Message(data)
        break
    }
  
    // To Be Deleted: set self...
    if (this.userId) {
      m.set('self', this.userId)
    } else {
      log.warn('PuppetWebEvent', 'onServerMessage() without this.userId')
    }
  
    m.ready() // TODO: EventEmitter2 for video/audio/app/sys....
    .then(() => this.emit('message', m))
    .catch(e => {
      log.error('PuppetWebEvent', 'onServerMessage() message ready exception: %s', e)
      /**
       * FIXME: add retry here...
       * setTimeout(onServerMessage.bind(this, data, ++attempt), 1000)
       */
    })
  }
  
  module.exports = PuppetWebEvent
  