text: var logger = require('logger')('turen')
  
  var _ = require('@yoda/util')._
  var wifi = require('@yoda/wifi')
  var bluetooth = require('@yoda/bluetooth')
  
  var VT_WORDS_ADD_WORD_CHANNEL = 'rokid.turen.addVtWord'
  var VT_WORDS_DEL_WORD_CHANNEL = 'rokid.turen.removeVtWord'
  
  module.exports = Turen
  function Turen (runtime) {
    this.runtime = runtime
  
    /**
     * indicates microphone muted or not.
     */
    this.muted = false
  
    /** if device is awaken */
    this.awaken = false
    /**
     * asr parsing state, possible values:
     * - pending
     * - fake
     * - end
     */
    this.asrState = 'end'
    /**
     * Turen picking up state.
     */
    this.pickingUp = false
    /**
     * if next nlp shall be discarded.
     */
    this.pickingUpDiscardNext = false
  
    /**
     * handle of timer to determines if current 'voice coming' session is alone,
     * no upcoming asr pending/end is sent in company with it.
     */
    this.solitaryVoiceComingTimeout = process.env.YODA_SOLITARY_VOICE_COMING_TIMEOUT || 9000
    this.solitaryVoiceComingTimer = null
    /**
     * handle of timer to determines if current awaken session is no voice input available so far,
     * no upcoming asr pending would be sent any way.
     */
    this.noVoiceInputTimeout = process.env.YODA_NO_VOICE_INPUT_TIMEOUT || 6000
    this.noVoiceInputTimer = null
  }
  
  Turen.prototype.init = function init () {
    if (this.bluetoothPlayer) {
      this.destruct()
    }
    this.bluetoothPlayer = bluetooth.getPlayer()
  }
  
  Turen.prototype.destruct = function destruct () {
    if (this.bluetoothPlayer == null) {
      return
    }
    this.bluetoothPlayer._flora.destruct()
  }
  
  /**
   * handles event received from turenproc
   * @param {string} name -
   * @param {object} data -
   * @private
   */
  Turen.prototype.handleEvent = function (name, data) {
    if (this.muted) {
      logger.error('Mic muted, unexpected event from Turen:', name)
      return
    }
    var handler = null
    switch (name) {
      case 'voice coming':
        handler = this.handleVoiceComing
        break
      case 'voice local awake':
        handler = this.handleVoiceLocalAwake
        break
      case 'asr accept':
        handler = this.handleAsrProgress.bind(this, 'accept')
        break
      case 'asr pending':
        handler = this.handleAsrProgress.bind(this, 'pending')
        break
      case 'asr extra':
        handler = this.handleAsrProgress.bind(this, 'extra')
        break
      case 'asr end':
        handler = this.handleAsrEnd
        break
      case 'asr fake':
        handler = this.handleAsrFake
        break
      case 'asr reject':
        handler = this.handleAsrReject
        break
      case 'start voice':
        handler = this.handleStartVoice
        break
      case 'end voice':
        handler = this.handleEndVoice
        break
      case 'nlp':
        handler = this.handleNlpResult
        break
      case 'malicious nlp':
        handler = this.handleMaliciousNlpResult
        break
      case 'speech error':
        handler = this.handleSpeechError
        break
    }
    if (typeof handler !== 'function') {
      logger.info(`skip turen event "${name}" for no handler existing`)
      return
    }
    logger.debug(`handling turen event "${name}"`)
    return handler.call(this, data)
  }
  
  /**
   * Set device awaken state and appearance.
   */
  Turen.prototype.setAwaken = function setAwaken () {
    var promises = []
    if (this.awaken) {
      logger.warn('already awaken')
    }
    this.awaken = true
  
    var currAppId = this.runtime.life.getCurrentAppId()
    logger.info('awaking, current app', currAppId)
  
    /**
     * pause lifetime to prevent incoming app preemption;
     * doesn't care when pauseLifetime ends.
     */
    this.runtime.life.pauseLifetime()
  
    /**
     * no need to determine if tts is previously been paused.
     */
    return Promise.all(promises)
  }
  
  /**
   * Set device end of awaken and remove awaken effects.
   *
   * @private
   * @param {object} [options] -
   * @param {boolean} [options.recover] - if recover previous paused app
   */
  Turen.prototype.resetAwaken = function resetAwaken (options) {
    var recover = _.get(options, 'recover', true)
  
    if (!this.awaken) {
      logger.warn('runtime was not awaken, skipping reset awaken')
      return Promise.resolve()
    }
    this.awaken = false
    logger.info('reset awaken, recovering?', recover)
    clearTimeout(this.solitaryVoiceComingTimer)
    clearTimeout(this.noVoiceInputTimer)
  
    var promises = [
      this.runtime.light.stop('@yoda', 'system://awake.js'),
      this.runtime.life.resumeLifetime({ recover: recover })
    ]
  
    if (!recover) {
      // do not stop previously paused tts. let the app handle it theirself
      return Promise.all(promises)
    }
  
    return Promise.all(promises.concat(this.recoverPausedOnAwaken()))
  }
  
  /**
   * Recovers paused tts/media on awaken.
   * @private
   */
  Turen.prototype.recoverPausedOnAwaken = function recoverPausedOnAwaken () {
    var currentAppId = this.runtime.life.getCurrentAppId()
  
    logger.info('unmute possibly paused bluetooth player')
    this.bluetoothPlayer && this.bluetoothPlayer.resume()
  
    logger.info('trying to resume previously awaken paused tts/media', currentAppId)
    return Promise.all([
      this.runtime.ttsMethod('resetAwaken', [ currentAppId ]),
      this.runtime.multimediaMethod('resetAwaken', [ currentAppId ])
    ])
  }
  
  /**
   * Clears memoized paused tts/media on awaken.
   * @private
   */
  Turen.prototype.resetPausedOnAwaken = function resetPausedOnAwaken () {
    logger.info('trying to reset previously awaken paused tts/media')
    return Promise.all([
      this.runtime.ttsMethod('resetAwaken', [ '' ]),
      this.runtime.multimediaMethod('resetAwaken', [ '' ])
    ])
  }
  
  /**
   * Handle the "voice coming" event.
   * @private
   */
  Turen.prototype.handleVoiceComing = function handleVoiceComing (data) {
    if (!this.runtime.custodian.isPrepared()) {
      logger.warn('Network not connected, preparing to announce unavailability.')
      this.pickup(false)
  
      var currentAppId = this.runtime.life.getCurrentAppId()
      if (this.runtime.custodian.isConfiguringNetwork()) {
        /**
         * Configuring network, delegates event to network app.
         */
        logger.info('configuring network, renewing timer.')
        return this.runtime.openUrl('yoda-skill://network/renew')
      }
  
      if (wifi.getNumOfHistory() === 0) {
        if (currentAppId) {
          /**
           * although there is no WiFi history, yet some app is running out there,
           * continuing currently app.
           */
          logger.info('no WiFi history exists, continuing currently running app.')
          return this.runtime.light.ttsSound('@yoda', 'system://guide_config_network.ogg')
            .then(() =>
            /** awaken is not set for no network available, recover media directly */
              this.recoverPausedOnAwaken()
            )
        }
        /**
         * No WiFi connection history found, introduce device setup procedure.
         */
        logger.info('no WiFi history exists, announcing guide to network configuration.')
        return this.runtime.light.ttsSound('@yoda', 'system://guide_config_network.ogg')
          .then(() =>
            /** awaken is not set for no network available, recover media directly */
            this.recoverPausedOnAwaken()
          )
      }
  
      /**
       * if runtime is logging in or network is unavailable,
       * and there is WiFi history existing,
       * announce WiFi is connecting.
       */
      logger.info('announcing network connecting on voice coming.')
      wifi.enableScanPassively()
      return this.runtime.light.ttsSound('@yoda', 'system://wifi_is_connecting.ogg')
        .then(() =>
          /** awaken is not set for no network available, recover media directly */
          this.recoverPausedOnAwaken()
        )
    }
  
    var future = this.setAwaken()
    clearTimeout(this.solitaryVoiceComingTimer)
    this.solitaryVoiceComingTimer = setTimeout(() => {
      logger.warn('detected a solitary voice coming, resetting awaken')
      this.pickup(false)
  
      if (this.awaken) {
        return this.announceNetworkLag()
      }
    }, this.solitaryVoiceComingTimeout)
  
    if (this.runtime.forceUpdateAvailable) {
      future.then(
        () => this.runtime.startForceUpdate(),
        err => {
          logger.error('unexpected error on set awaken', err.stack)
          return this.runtime.startForceUpdate()
        }
      )
    }
  
    /**
     * reset picking up discarding state to enable next nlp process
     */
    this.pickingUpDiscardNext = false
  
    return future
  }
  
  /**
   * Handle the "voice local awake" event.
   * @private
   */
  Turen.prototype.handleVoiceLocalAwake = function handleVoiceLocalAwake (data) {
    /**
     * Nothing to do in local_awake event.
     */
  }
  
  /**
   * Handle the "asr accept"/"asr pending" event.
   * @private
   */
  Turen.prototype.handleAsrProgress = function handleAsrProgress (state) {
    this.asrState = state
    clearTimeout(this.solitaryVoiceComingTimer)
  
    clearTimeout(this.noVoiceInputTimer)
    this.noVoiceInputTimer = setTimeout(() => {
      logger.warn('no more voice input detected, closing pickup')
      this.pickup(false)
    }, this.noVoiceInputTimeout)
  }
  
  /**
   * Handle the "asr end" event.
   * @private
   */
  Turen.prototype.handleAsrEnd = function handleAsrEnd () {
    this.asrState = 'end'
    clearTimeout(this.noVoiceInputTimer)
  
    var promises = [
      this.resetAwaken({
        recover: /** no recovery shall be made on nlp coming */ false
      })
    ]
  
    if (this.pickingUpDiscardNext) {
      /**
       * current session of picking up has been manually discarded,
       * no loading state shall be presented.
       */
      return Promise.all(promises)
    }
    return Promise.all(promises.concat(this.runtime.light.play('@yoda', 'system://loading.js')))
  }
  
  /**
   * Handle the "asr reject" event.
   */
  Turen.prototype.handleAsrReject = function handleAsrReject () {
    this.asrState = 'reject'
    this.resetAwaken()
  }
  
  /**
   * Handle the "asr fake" event.
   * @private
   */
  Turen.prototype.handleAsrFake = function handleAsrFake () {
    this.asrState = 'fake'
    clearTimeout(this.noVoiceInputTimer)
  
    return this.resetAwaken()
  }
  
  /**
   * Handle the "start voice" event.
   * @private
   */
  Turen.prototype.handleStartVoice = function handleStartVoice () {
    this.pickingUp = true
  }
  
  /**
   * Handle the "end voice" event.
   * @private
   */
  Turen.prototype.handleEndVoice = function handleEndVoice () {
    this.pickingUp = false
    logger.info('on end of voice, asr:', this.asrState)
  }
  
  /**
   * Handle the "nlp" event.
   * @private
   */
  Turen.prototype.handleNlpResult = function handleNlpResult (data) {
    if (this.pickingUpDiscardNext) {
      /**
       * current session of picking up has been manually discarded.
       */
      this.pickingUpDiscardNext = false
      logger.warn(`discarding nlp for pick up discarded, ASR(${_.get(data, 'nlp.asr')}).`)
      return
    }
  
    var future
    if (this.awaken) {
      future = this.resetAwaken({
        recover: /** no recovery shall be made on nlp coming */ false
      })
    } else {
      future = Promise.resolve()
    }
    return future.then(() => this.runtime.onVoiceCommand(data.asr, data.nlp, data.action))
      .then(success => {
        this.runtime.light.stop('@yoda', 'system://loading.js')
        if (success) {
          /**
           * Reset previously paused media to prevent un-intended recovering
           */
          return this.resetPausedOnAwaken()
        }
        /**
         * try to recover paused tts/media on awaken in case of
         * failed to handle incoming nlp request.
         */
        this.recoverPausedOnAwaken()
      }, err => {
        this.runtime.light.stop('@yoda', 'system://loading.js')
        logger.error('Unexpected error on open handling nlp', err.stack)
      })
  }
  
  /**
   * Handle the "nlp" event, which are emitted on incoming unexpected malicious nlp.
   */
  Turen.prototype.handleMaliciousNlpResult = function handleMaliciousNlpResult () {
    if (this.awaken) {
      this.pickup(false)
      this.resetAwaken({ recover: false })
    }
    if (!this.runtime.custodian.isPrepared()) {
      logger.warn('Network not connected, recovering players.')
      return this.recoverPausedOnAwaken()
    }
  
    /**
     * Reset previously paused media to prevent un-intended recovering
     */
    this.resetPausedOnAwaken()
    return this.runtime.openUrl('yoda-skill://rokid-exception/malicious-nlp')
      .then(
        () => this.runtime.light.stop('@yoda', 'system://loading.js'),
        err => {
          this.runtime.light.stop('@yoda', 'system://loading.js')
          logger.error('Unexpected error on open handling malicious nlp', err.stack)
        })
  }
  
  /**
   * Handle 'speech error' events, which are emitted on unexpected speech faults.
   */
  Turen.prototype.handleSpeechError = function handleSpeechError (errCode) {
    if (this.awaken) {
      this.pickup(false)
      this.resetAwaken({ recover: false })
    }
    if (!this.runtime.custodian.isPrepared()) {
      logger.warn('Network not connected or not logged in, recovering players.')
      return this.recoverPausedOnAwaken()
    }
  
    if (errCode >= 100) {
      /** network error */
      return this.announceNetworkLag()
    }
  
    /**
     * FIXME: Raison d'etre
     * cut app like alarm/timer shall be deactivated on awaken.
     * Currently @yoda/system handles speech error in a such quick way, yet for
     * some reason apps like cloud-app-client could not determines tts/media
     * status in such a short time(events have to be transferred through
     * 2/3 ipc).
     * Thus just closing cut app here works as expected, and shall be fixed
     * with a invocation queue in translator-ipc.
     */
    this.runtime.life.deactivateCutApp()
      .then(() => {
        this.recoverPausedOnAwaken()
        return this.runtime.light.stop('@yoda', 'system://loading.js')
      }, err => {
        this.runtime.light.stop('@yoda', 'system://loading.js')
        logger.error('Unexpected error on deactivating cut app', err.stack)
      })
  }
  
  /**
   * Set whether or not turenproc is picked up.
   * @param {boolean} isPickup
   */
  Turen.prototype.pickup = function pickup (isPickup) {
    /**
     * if set not to picking up, discard next coming nlp,
     * otherwise reset picking up discarding state to enable next nlp process,
     */
    this.pickingUpDiscardNext = !isPickup
    this.runtime.flora.post('rokid.turen.pickup', [ isPickup ? 1 : 0 ])
  
    if (!isPickup) {
      clearTimeout(this.solitaryVoiceComingTimer)
      clearTimeout(this.noVoiceInputTimer)
    }
  }
  
  /**
   * Set whether or not turenproc is muted. By default toggles mute.
   * @param {boolean} [mute]
   */
  Turen.prototype.toggleMute = function toggleMute (mute) {
    if (mute == null) {
      mute = !this.muted
    }
    this.muted = mute
    /** if mute is true, set rokid.turen.mute to 1 to disable turen */
    this.runtime.flora.post('rokid.turen.mute', [ mute ? 1 : 0 ])
  
    if (this.asrState === 'pending' && mute) {
      this.resetAwaken()
    }
  
    return this.muted
  }
  
  /**
   * Add an activation word.
   * @param {string} activationTxt
   * @param {string} activationPy
   */
  Turen.prototype.addVtWord = function addVtWord (activationWord, activationPy) {
    this.runtime.flora.post(VT_WORDS_ADD_WORD_CHANNEL, [
      activationWord,
      activationPy,
      1
    ])
  }
  
  /**
   * Delete an activation word
   * @param {string} activationTxt
   */
  Turen.prototype.deleteVtWord = function deleteVtWord (activationWord) {
    this.runtime.flora.post(VT_WORDS_DEL_WORD_CHANNEL, [ activationWord ])
  }
  
  /**
   * Announce possible network lag. Reset awaken/recover paused media on end of announcements.
   */
  Turen.prototype.announceNetworkLag = function announceNetworkLag () {
    if (this.awaken) {
      this.resetAwaken({ recover: false })
    }
    return this.runtime.light.lightMethod('networkLagSound', [ '/opt/media/network_lag_common.ogg' ])
      .then(
        () => {
          /** stop network lag light effects */
          this.runtime.light.lightMethod('stopNetworkLagSound', [])
          if (this.awaken) {
            return
          }
          return this.recoverPausedOnAwaken()
        },
        err => {
          logger.error('Unexpected error on playing network lag sound', err.stack)
          /** stop network lag light effects */
          this.runtime.light.lightMethod('stopNetworkLagSound', [])
          if (this.awaken) {
            return
          }
          return this.recoverPausedOnAwaken()
        }
      )
  }
  