// Copyright (c) 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

/**
 * @fileoverview This is the audio client content script injected into eligible
 *  Google.com and New tab pages for interaction between the Webpage and the
 *  Hotword extension.
 */



(function() {
  /**
   * @constructor
   */
  var AudioClient = function() {
    /** @private {Element} */
    this.speechOverlay_ = null;

    /** @private {number} */
    this.checkSpeechUiRetries_ = 0;

    /**
     * Port used to communicate with the audio manager.
     * @private {?Port}
     */
    this.port_ = null;

    /**
     * Keeps track of the effects of different commands. Used to verify that
     * proper UIs are shown to the user.
     * @private {Object.<AudioClient.CommandToPage, Object>}
     */
    this.uiStatus_ = null;

    /**
     * Bound function used to handle commands sent from the page to this script.
     * @private {Function}
     */
    this.handleCommandFromPageFunc_ = null;
  };


  /**
   * Messages sent to the page to control the voice search UI.
   * @enum {string}
   */
  AudioClient.CommandToPage = {
    HOTWORD_VOICE_TRIGGER: 'vt',
    HOTWORD_STARTED: 'hs',
    HOTWORD_ENDED: 'hd',
    HOTWORD_TIMEOUT: 'ht',
    HOTWORD_ERROR: 'he'
  };


  /**
   * Messages received from the page used to indicate voice search state.
   * @enum {string}
   */
  AudioClient.CommandFromPage = {
    SPEECH_START: 'ss',
    SPEECH_END: 'se',
    SPEECH_RESET: 'sr',
    SHOWING_HOTWORD_START: 'shs',
    SHOWING_ERROR_MESSAGE: 'sem',
    SHOWING_TIMEOUT_MESSAGE: 'stm',
    CLICKED_RESUME: 'hcc',
    CLICKED_RESTART: 'hcr',
    CLICKED_DEBUG: 'hcd'
  };


  /**
   * Errors that are sent to the hotword extension.
   * @enum {string}
   */
  AudioClient.Error = {
    NO_SPEECH_UI: 'ac1',
    NO_HOTWORD_STARTED_UI: 'ac2',
    NO_HOTWORD_TIMEOUT_UI: 'ac3',
    NO_HOTWORD_ERROR_UI: 'ac4'
  };


  /**
   * @const {string}
   * @private
   */
  AudioClient.HOTWORD_EXTENSION_ID_ = 'bepbmhgboaologfdajaanbcjmnhjmhfn';


  /**
   * Number of times to retry checking a transient error.
   * @const {number}
   * @private
   */
  AudioClient.MAX_RETRIES = 3;


  /**
   * Delay to wait in milliseconds before rechecking for any transient errors.
   * @const {number}
   * @private
   */
  AudioClient.RETRY_TIME_MS_ = 2000;


  /**
   * DOM ID for the speech UI overlay.
   * @const {string}
   * @private
   */
  AudioClient.SPEECH_UI_OVERLAY_ID_ = 'spch';


  /**
   * @const {string}
   * @private
   */
  AudioClient.HELP_CENTER_URL_ =
      'https://support.google.com/chrome/?p=ui_hotword_search';


  /**
   * @const {string}
   * @private
   */
  AudioClient.CLIENT_PORT_NAME_ = 'chwcpn';

  /**
   * Existence of the Audio Client.
   * @const {string}
   * @private
   */
  AudioClient.EXISTS_ = 'chwace';


  /**
   * Checks for the presence of speech overlay UI DOM elements.
   * @private
   */
  AudioClient.prototype.checkSpeechOverlayUi_ = function() {
    if (!this.speechOverlay_) {
      window.setTimeout(this.delayedCheckSpeechOverlayUi_.bind(this),
                        AudioClient.RETRY_TIME_MS_);
    } else {
      this.checkSpeechUiRetries_ = 0;
    }
  };


  /**
   * Function called to check for the speech UI overlay after some time has
   * passed since an initial check. Will either retry triggering the speech
   * or sends an error message depending on the number of retries.
   * @private
   */
  AudioClient.prototype.delayedCheckSpeechOverlayUi_ = function() {
    this.speechOverlay_ = document.getElementById(
        AudioClient.SPEECH_UI_OVERLAY_ID_);
    if (!this.speechOverlay_) {
      if (this.checkSpeechUiRetries_++ < AudioClient.MAX_RETRIES) {
        this.sendCommandToPage_(AudioClient.CommandToPage.VOICE_TRIGGER);
        this.checkSpeechOverlayUi_();
      } else {
        this.sendCommandToExtension_(AudioClient.Error.NO_SPEECH_UI);
      }
    } else {
      this.checkSpeechUiRetries_ = 0;
    }
  };


  /**
   * Checks that the triggered UI is actually displayed.
   * @param {AudioClient.CommandToPage} command Command that was send.
   * @private
   */
  AudioClient.prototype.checkUi_ = function(command) {
    this.uiStatus_[command].timeoutId =
        window.setTimeout(this.failedCheckUi_.bind(this, command),
                          AudioClient.RETRY_TIME_MS_);
  };


  /**
   * Function called when the UI verification is not called in time. Will either
   * retry the command or sends an error message, depending on the number of
   * retries for the command.
   * @param {AudioClient.CommandToPage} command Command that was sent.
   * @private
   */
  AudioClient.prototype.failedCheckUi_ = function(command) {
    if (this.uiStatus_[command].tries++ < AudioClient.MAX_RETRIES) {
      this.sendCommandToPage_(command);
      this.checkUi_(command);
    } else {
      this.sendCommandToExtension_(this.uiStatus_[command].error);
    }
  };


  /**
   * Confirm that an UI element has been shown.
   * @param {AudioClient.CommandToPage} command UI to confirm.
   * @private
   */
  AudioClient.prototype.verifyUi_ = function(command) {
    if (this.uiStatus_[command].timeoutId) {
      window.clearTimeout(this.uiStatus_[command].timeoutId);
      this.uiStatus_[command].timeoutId = null;
      this.uiStatus_[command].tries = 0;
    }
  };


  /**
   * Sends a command to the audio manager.
   * @param {string} commandStr command to send to plugin.
   * @private
   */
  AudioClient.prototype.sendCommandToExtension_ = function(commandStr) {
    if (this.port_)
      this.port_.postMessage({'cmd': commandStr});
  };


  /**
   * Handles a message from the audio manager.
   * @param {{cmd: string}} commandObj Command from the audio manager.
   * @private
   */
  AudioClient.prototype.handleCommandFromExtension_ = function(commandObj) {
    var command = commandObj['cmd'];
    if (command) {
      switch (command) {
        case AudioClient.CommandToPage.HOTWORD_VOICE_TRIGGER:
          this.sendCommandToPage_(command);
          this.checkSpeechOverlayUi_();
          break;
        case AudioClient.CommandToPage.HOTWORD_STARTED:
          this.sendCommandToPage_(command);
          this.checkUi_(command);
          break;
        case AudioClient.CommandToPage.HOTWORD_ENDED:
          this.sendCommandToPage_(command);
          break;
        case AudioClient.CommandToPage.HOTWORD_TIMEOUT:
          this.sendCommandToPage_(command);
          this.checkUi_(command);
          break;
        case AudioClient.CommandToPage.HOTWORD_ERROR:
          this.sendCommandToPage_(command);
          this.checkUi_(command);
          break;
      }
    }
  };


  /**
   * @param {AudioClient.CommandToPage} commandStr Command to send.
   * @private
   */
  AudioClient.prototype.sendCommandToPage_ = function(commandStr) {
    window.postMessage({'type': commandStr}, '*');
  };


  /**
   * Handles a message from the html window.
   * @param {!MessageEvent} messageEvent Message event from the window.
   * @private
   */
  AudioClient.prototype.handleCommandFromPage_ = function(messageEvent) {
    if (messageEvent.source == window && messageEvent.data.type) {
      var command = messageEvent.data.type;
      switch (command) {
        case AudioClient.CommandFromPage.SPEECH_START:
          this.speechActive_ = true;
          this.sendCommandToExtension_(command);
          break;
        case AudioClient.CommandFromPage.SPEECH_END:
          this.speechActive_ = false;
          this.sendCommandToExtension_(command);
          break;
        case AudioClient.CommandFromPage.SPEECH_RESET:
          this.speechActive_ = false;
          this.sendCommandToExtension_(command);
          break;
        case 'SPEECH_RESET':  // Legacy, for embedded NTP.
          this.speechActive_ = false;
          this.sendCommandToExtension_(AudioClient.CommandFromPage.SPEECH_END);
          break;
        case AudioClient.CommandFromPage.CLICKED_RESUME:
          this.sendCommandToExtension_(command);
          break;
        case AudioClient.CommandFromPage.CLICKED_RESTART:
          this.sendCommandToExtension_(command);
          break;
        case AudioClient.CommandFromPage.CLICKED_DEBUG:
          window.open(AudioClient.HELP_CENTER_URL_, '_blank');
          break;
        case AudioClient.CommandFromPage.SHOWING_HOTWORD_START:
          this.verifyUi_(AudioClient.CommandToPage.HOTWORD_STARTED);
          break;
        case AudioClient.CommandFromPage.SHOWING_ERROR_MESSAGE:
          this.verifyUi_(AudioClient.CommandToPage.HOTWORD_ERROR);
          break;
        case AudioClient.CommandFromPage.SHOWING_TIMEOUT_MESSAGE:
          this.verifyUi_(AudioClient.CommandToPage.HOTWORD_TIMEOUT);
          break;
      }
    }
  };


  /**
   * Initialize the content script.
   */
  AudioClient.prototype.initialize = function() {
    if (AudioClient.EXISTS_ in window)
      return;
    window[AudioClient.EXISTS_] = true;

    // UI verification object.
    this.uiStatus_ = {};
    this.uiStatus_[AudioClient.CommandToPage.HOTWORD_STARTED] = {
      timeoutId: null,
      tries: 0,
      error: AudioClient.Error.NO_HOTWORD_STARTED_UI
    };
    this.uiStatus_[AudioClient.CommandToPage.HOTWORD_TIMEOUT] = {
      timeoutId: null,
      tries: 0,
      error: AudioClient.Error.NO_HOTWORD_TIMEOUT_UI
    };
    this.uiStatus_[AudioClient.CommandToPage.HOTWORD_ERROR] = {
      timeoutId: null,
      tries: 0,
      error: AudioClient.Error.NO_HOTWORD_ERROR_UI
    };

    this.handleCommandFromPageFunc_ = this.handleCommandFromPage_.bind(this);
    window.addEventListener('message', this.handleCommandFromPageFunc_, false);
    this.initPort_();
  };


  /**
   * Initialize the communications port with the audio manager. This
   * function will be also be called again if the audio-manager
   * disconnects for some reason (such as the extension
   * background.html page being reloaded).
   * @private
   */
  AudioClient.prototype.initPort_ = function() {
    this.port_ = chrome.runtime.connect(
        AudioClient.HOTWORD_EXTENSION_ID_,
        {'name': AudioClient.CLIENT_PORT_NAME_});
    // Note that this listen may have to be destroyed manually if AudioClient
    // is ever destroyed on this tab.
    this.port_.onDisconnect.addListener(
        (function(e) {
          if (this.handleCommandFromPageFunc_) {
            window.removeEventListener(
                'message', this.handleCommandFromPageFunc_, false);
          }
          delete window[AudioClient.EXISTS_];
        }).bind(this));

    // See note above.
    this.port_.onMessage.addListener(
        this.handleCommandFromExtension_.bind(this));

    if (this.speechActive_)
      this.sendCommandToExtension_(AudioClient.CommandFromPage.SPEECH_START);
  };


  // Initializes as soon as the code is ready, do not wait for the page.
  new AudioClient().initialize();
})();
