// Copyright 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.

/**
 * @fileoverview
 * Class to communicate with the It2me Host component via Native Messaging.
 */

'use strict';

/** @suppress {duplicate} */
var remoting = remoting || {};

/**
 * @constructor
 */
remoting.It2MeHostFacade = function() {
  /**
   * @type {number}
   * @private
   */
  this.nextId_ = 0;

  /**
   * @type {?chrome.runtime.Port}
   * @private
   */
  this.port_ = null;

  /**
   * @type {string}
   * @private
   */
  this.accessCode_ = '';

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

  /**
   * @type {string}
   * @private
   */
  this.clientId_ = '';

  /**
   * @type {boolean}
   * @private
   */
  this.initialized_ = false;

  /**
   * @type {?function():void}
 * @private
   */
  this.onInitialized_ = function() {};

  /**
   * Called if Native Messaging host has failed to start.
   * @private
   * */
  this.onInitializationFailed_ = function() {};

  /**
   * Called if the It2Me Native Messaging host sends a malformed message:
   * missing required attributes, attributes with incorrect types, etc.
   * @param {remoting.Error} error
   * @private
   */
  this.onError_ = function(error) {};

  /**
   * @type {?function(remoting.HostSession.State):void}
   * @private
   */
  this.onStateChanged_ = function() {};

  /**
   * @type {?function(boolean):void}
   * @private
   */
  this.onNatPolicyChanged_ = function() {};
};

/**
 * Sets up connection to the Native Messaging host process and exchanges
 * 'hello' messages. If Native Messaging is not supported or if the it2me
 * native messaging host is not installed, onInitializationFailed is invoked.
 * Otherwise, onInitialized is invoked.
 *
 * @param {function():void} onInitialized Called after successful
 *     initialization.
 * @param {function():void} onInitializationFailed Called if cannot connect to
 *     the native messaging host.
 * @return {void}
 */
remoting.It2MeHostFacade.prototype.initialize =
    function(onInitialized, onInitializationFailed) {
  this.onInitialized_ = onInitialized;
  this.onInitializationFailed_ = onInitializationFailed;

  try {
    this.port_ = chrome.runtime.connectNative(
        'com.google.chrome.remote_assistance');
    this.port_.onMessage.addListener(this.onIncomingMessage_.bind(this));
    this.port_.onDisconnect.addListener(this.onHostDisconnect_.bind(this));
    this.port_.postMessage({type: 'hello'});
  } catch (err) {
    console.log('Native Messaging initialization failed: ',
                /** @type {*} */ (err));
    onInitializationFailed();
    return;
  }
};

/**
 * @param {string} email The user's email address.
 * @param {string} authServiceWithToken Concatenation of the auth service
 *     (e.g. oauth2) and the access token.
 * @param {function(remoting.HostSession.State):void} onStateChanged Callback to
 *     invoke when the host state changes.
 * @param {function(boolean):void} onNatPolicyChanged Callback to invoke when
 *     the nat traversal policy changes.
 * @param {function(string):void} logDebugInfo Callback allowing the plugin
 *     to log messages to the debug log.
 * @param {string} xmppServerAddress XMPP server host name (or IP address) and
 *     port.
 * @param {boolean} xmppServerUseTls Whether to use TLS on connections to the
 *     XMPP server
 * @param {string} directoryBotJid XMPP JID for the remoting directory server
 *     bot.
 * @param {function(remoting.Error):void} onError Callback to invoke in case of
 *     an error.
 * @return {void}
 */
remoting.It2MeHostFacade.prototype.connect =
    function(email, authServiceWithToken, onStateChanged, onNatPolicyChanged,
             logDebugInfo, xmppServerAddress, xmppServerUseTls, directoryBotJid,
             onError) {
  if (!this.port_) {
    console.error(
        'remoting.It2MeHostFacade.connect() without initialization.');
    onError(remoting.Error.UNEXPECTED);
    return;
  }

  this.onStateChanged_ = onStateChanged;
  this.onNatPolicyChanged_ = onNatPolicyChanged;
  this.onError_ = onError;
  this.port_.postMessage({
    type: 'connect',
    userName: email,
    authServiceWithToken: authServiceWithToken,
    xmppServerAddress: xmppServerAddress,
    xmppServerUseTls: xmppServerUseTls,
    directoryBotJid: directoryBotJid
  });
};

/**
 * Unhooks the |onStateChanged|, |onError|, |onNatPolicyChanged| and
 * |onInitalized| callbacks.  This is called when the client shuts down so that
 * the callbacks will not be invoked on a disposed client.
 *
 * @return {void}
 */
remoting.It2MeHostFacade.prototype.unhookCallbacks = function() {
  this.onStateChanged_ = null;
  this.onNatPolicyChanged_ = null;
  this.onError_ = null;
  this.onInitialized_ = null;
};

/**
 * @return {void}
 */
remoting.It2MeHostFacade.prototype.disconnect = function() {
  if (this.port_)
    this.port_.postMessage({type: 'disconnect'});
};

/**
 * @return {boolean}
 */
remoting.It2MeHostFacade.prototype.initialized = function() {
  return this.initialized_;
};

/**
 * @return {string}
 */
remoting.It2MeHostFacade.prototype.getAccessCode = function() {
  return this.accessCode_;
};

/**
 * @return {number}
 */
remoting.It2MeHostFacade.prototype.getAccessCodeLifetime = function() {
  return this.accessCodeLifetime_;
};

/**
 * @return {string}
 */
remoting.It2MeHostFacade.prototype.getClient = function() {
  return this.clientId_;
};

/**
 * Handler for incoming messages.
 *
 * @param {Object} message The received message.
 * @return {void}
 * @private
 */
remoting.It2MeHostFacade.prototype.onIncomingMessage_ =
    function(message) {
  var type = getStringAttr(message, 'type');

  switch (type) {
    case 'helloResponse':
      var version = getStringAttr(message, 'version');
      console.log('Host version: ', version);
      this.initialized_ = true;
      // A "hello" request is sent immediately after the native messaging host
      // is started. We can proceed to the next task once we receive the
      // "helloReponse".
      if (this.onInitialized_) {
        this.onInitialized_();
      }
      break;

    case 'connectResponse':
      console.log('connectResponse received');
      // Response to the "connect" request. No action is needed until we
      // receive the corresponding "hostStateChanged" message.
      break;

    case 'disconnectResponse':
      console.log('disconnectResponse received');
      // Response to the "disconnect" request. No action is needed until we
      // receive the corresponding "hostStateChanged" message.
      break;

    case 'hostStateChanged':
      var stateString = getStringAttr(message, 'state');
      console.log('hostStateChanged received: ', stateString);
      var state = remoting.HostSession.State.fromString(stateString);

      switch (state) {
        case remoting.HostSession.State.RECEIVED_ACCESS_CODE:
          var accessCode = getStringAttr(message, 'accessCode');
          var accessCodeLifetime = getNumberAttr(message, 'accessCodeLifetime');
          this.onReceivedAccessCode_(accessCode, accessCodeLifetime);
          break;

        case remoting.HostSession.State.CONNECTED:
          var client = getStringAttr(message, 'client');
          this.onConnected_(client);
          break;
      }
      if (this.onStateChanged_) {
        this.onStateChanged_(state);
      }
      break;

    case 'natPolicyChanged':
      if (this.onNatPolicyChanged_) {
        var natTraversalEnabled =
            getBooleanAttr(message, 'natTraversalEnabled');
        this.onNatPolicyChanged_(natTraversalEnabled);
      }
      break;

    case 'error':
      console.error(getStringAttr(message, 'description'));
      if (this.onError_) {
        this.onError_(remoting.Error.UNEXPECTED);
      }
      break;

    default:
      throw 'Unexpected native message: ' + message;
  }
};

/**
 * @param {string} accessCode
 * @param {number} accessCodeLifetime
 * @return {void}
 * @private
 */
remoting.It2MeHostFacade.prototype.onReceivedAccessCode_ =
    function(accessCode, accessCodeLifetime) {
  this.accessCode_ = accessCode;
  this.accessCodeLifetime_ = accessCodeLifetime;
};

/**
 * @param {string} clientId
 * @return {void}
 * @private
 */
remoting.It2MeHostFacade.prototype.onConnected_ = function(clientId) {
  this.clientId_ = clientId;
};

/**
 * @return {void}
 * @private
 */
remoting.It2MeHostFacade.prototype.onHostDisconnect_ = function() {
  if (!this.initialized_) {
    // If the host is disconnected before it is initialized, it probably means
    // the host is not propertly installed (or not installed at all).
    // E.g., if the host manifest is not present we get "Specified native
    // messaging host not found" error. If the host manifest is present but
    // the host binary cannot be found we get the "Native host has exited"
    // error.
    console.log('Native Messaging initialization failed: ' +
                chrome.runtime.lastError.message);
    this.onInitializationFailed_();
  } else {
    console.error('Native Messaging port disconnected.');
    this.port_ = null;
    this.onError_(remoting.Error.UNEXPECTED);
  }
}