// Copyright (c) 2012 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
 * Module to format IQ messages so they can be displayed in the debug log.
 */

'use strict';

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

/**
 * @constructor
 */
remoting.FormatIq = function() {
  this.clientJid = '';
  this.hostJid = '';
};

/**
 * Verify that the only attributes on the given |node| are those specified
 * in the |attrs| string.
 *
 * @param {Node} node The node to verify.
 * @param {string} validAttrs Comma-separated list of valid attributes.
 *
 * @return {boolean} True if the node contains only valid attributes.
 */
remoting.FormatIq.prototype.verifyAttributes = function(node, validAttrs) {
  var attrs = ',' + validAttrs + ',';
  var len = node.attributes.length;
  for (var i = 0; i < len; i++) {
    /** @type {Node} */
    var attrNode = node.attributes[i];
    var attr = attrNode.nodeName;
    if (attrs.indexOf(',' + attr + ',') == -1) {
      return false;
    }
  }
  return true;
};

/**
 * Record the client and host JIDs so that we can check them against the
 * params in the IQ packets.
 *
 * @param {string} clientJid The client JID string.
 * @param {string} hostJid The host JID string.
 */
remoting.FormatIq.prototype.setJids = function(clientJid, hostJid) {
  this.clientJid = clientJid;
  this.hostJid = hostJid;
};

/**
 * Calculate the 'pretty' version of data from the |server| node.
 *
 * @param {Node} server Xml node with server info.
 *
 * @return {?string} Formatted server string. Null if error.
 */
remoting.FormatIq.prototype.calcServerString = function(server) {
  if (!this.verifyAttributes(server, 'host,udp,tcp,tcpssl')) {
    return null;
  }
  var host = server.getAttribute('host');
  var udp = server.getAttribute('udp');
  var tcp = server.getAttribute('tcp');
  var tcpssl = server.getAttribute('tcpssl');

  var str = "'" + host + "'";
  if (udp)
    str += ' udp:' + udp;
  if (tcp)
    str += ' tcp:' + tcp;
  if (tcpssl)
    str += ' tcpssl:' + tcpssl;

  str += '; ';
  return str;
};

/**
 * Calc the 'pretty' version of channel data.
 *
 * @param {Node} channel Xml node with channel info.
 *
 * @return {?string} Formatted channel string. Null if error.
 */
remoting.FormatIq.prototype.calcChannelString = function(channel) {
  var name = channel.nodeName;
  if (!this.verifyAttributes(channel, 'transport,version,codec')) {
    return null;
  }
  var transport = channel.getAttribute('transport');
  var version = channel.getAttribute('version');

  var str = name + ' ' + transport + ' v' + version;
  if (name == 'video') {
    str += ' codec=' + channel.getAttribute('codec');
  }
  str += '; ';
  return str;
};

/**
 * Pretty print the jingleinfo from the given Xml node.
 *
 * @param {Node} query Xml query node with jingleinfo in the child nodes.
 *
 * @return {?string} Pretty version of jingleinfo. Null if error.
 */
remoting.FormatIq.prototype.prettyJingleinfo = function(query) {
  var nodes = query.childNodes;
  var stun_servers = '';
  var result = '';
  for (var i = 0; i < nodes.length; i++) {
    /** @type {Node} */
    var node = nodes[i];
    var name = node.nodeName;
    if (name == 'stun') {
      var sserver = '';
      var stun_nodes = node.childNodes;
      for(var s = 0; s < stun_nodes.length; s++) {
        /** @type {Node} */
        var stun_node = stun_nodes[s];
        var sname = stun_node.nodeName;
        if (sname == 'server') {
          var stun_str = this.calcServerString(stun_node);
          if (!stun_str) {
            return null;
          }
          sserver += stun_str;
        }
      }
      result += '\n  stun ' + sserver;
    } else if (name == 'relay') {
      var token = '';
      var rserver = '';
      var relay_nodes = node.childNodes;
      for(var r = 0; r < relay_nodes.length; r++) {
        /** @type {Node} */
        var relay_node = relay_nodes[r];
        var rname = relay_node.nodeName;
        if (rname == 'token') {
          token = token + relay_node.textContent;
        }
        if (rname == 'server') {
          var relay_str = this.calcServerString(relay_node);
          if (!relay_str) {
            return null;
          }
          rserver += relay_str;
        }
      }
      result += '\n  relay ' + rserver + ' token: ' + token;
    } else {
      return null;
    }
  }

  return result;
};

/**
 * Pretty print the session-initiate or session-accept info from the given
 * Xml node.
 *
 * @param {Node} jingle Xml node with jingle session-initiate or session-accept
 *                      info contained in child nodes.
 *
 * @return {?string} Pretty version of jingle stanza. Null if error.
 */
remoting.FormatIq.prototype.prettySessionInitiateAccept = function(jingle) {
  if (jingle.childNodes.length != 1) {
    return null;
  }
  var content = jingle.firstChild;
  if (content.nodeName != 'content') {
    return null;
  }
  var content_children = content.childNodes;
  var result = '';
  for (var c = 0; c < content_children.length; c++) {
    /** @type {Node} */
    var content_child = content_children[c];
    var cname = content_child.nodeName;
    if (cname == 'description') {
      var channels = '';
      var resolution = '';
      var auth = '';
      var desc_children = content_child.childNodes;
      for (var d = 0; d < desc_children.length; d++) {
        /** @type {Node} */
        var desc = desc_children[d];
        var dname = desc.nodeName;
        if (dname == 'control' || dname == 'event' || dname == 'video') {
          var channel_str = this.calcChannelString(desc);
          if (!channel_str) {
            return null;
          }
          channels += channel_str;
        } else if (dname == 'initial-resolution') {
          resolution = desc.getAttribute('width') + 'x' +
              desc.getAttribute('height');
        } else if (dname == 'authentication') {
          var auth_children = desc.childNodes;
          for (var a = 0; a < auth_children.length; a++) {
            /** @type {Node} */
            var auth_info = auth_children[a];
            if (auth_info.nodeName == 'auth-token') {
              auth = auth + ' (auth-token) ' + auth_info.textContent;
            } else if (auth_info.nodeName == 'certificate') {
              auth = auth + ' (certificate) ' + auth_info.textContent;
            } else if (auth_info.nodeName == 'master-key') {
              auth = auth + ' (master-key) ' + auth_info.textContent;
            } else {
              return null;
            }
          }
        } else {
          return null;
        }
      }
      result += '\n  channels: ' + channels;
      result += '\n  auth: ' + auth;
      result += '\n  initial resolution: ' + resolution;
    } else if (cname == 'transport') {
      // The 'transport' node is currently empty.
      var transport_children = content_child.childNodes;
      if (transport_children.length != 0) {
        return null;
      }
    } else {
      return null;
    }
  }
  return result;
};

/**
 * Pretty print the session-terminate info from the given Xml node.
 *
 * @param {Node} jingle Xml node with jingle session-terminate info contained in
 *                      child nodes.
 *
 * @return {?string} Pretty version of jingle session-terminate stanza. Null if
 *                  error.
 */
remoting.FormatIq.prototype.prettySessionTerminate = function(jingle) {
  if (jingle.childNodes.length != 1) {
    return null;
  }
  var reason = jingle.firstChild;
  if (reason.nodeName != 'reason' || reason.childNodes.length != 1) {
    return null;
  }
  var info = reason.firstChild;
  if (info.nodeName == 'success' || info.nodeName == 'general-error') {
    return '\n  reason=' + info.nodeName;
  }
  return null;
};

/**
 * Pretty print the transport-info info from the given Xml node.
 *
 * @param {Node} jingle Xml node with jingle transport info contained in child
 *                      nodes.
 *
 * @return {?string} Pretty version of jingle transport-info stanza. Null if
 *                  error.
 */
remoting.FormatIq.prototype.prettyTransportInfo = function(jingle) {
  if (jingle.childNodes.length != 1) {
    return null;
  }
  var content = jingle.firstChild;
  if (content.nodeName != 'content') {
    return null;
  }
  var transport = content.firstChild;
  if (transport.nodeName != 'transport') {
    return null;
  }
  var transport_children = transport.childNodes;
  var result = '';
  for (var t = 0; t < transport_children.length; t++) {
    /** @type {Node} */
    var candidate = transport_children[t];
    if (candidate.nodeName != 'candidate') {
      return null;
    }
    if (!this.verifyAttributes(candidate, 'name,address,port,preference,' +
                               'username,protocol,generation,password,type,' +
                               'network')) {
      return null;
    }
    var name = candidate.getAttribute('name');
    var address = candidate.getAttribute('address');
    var port = candidate.getAttribute('port');
    var pref = candidate.getAttribute('preference');
    var username = candidate.getAttribute('username');
    var protocol = candidate.getAttribute('protocol');
    var generation = candidate.getAttribute('generation');
    var password = candidate.getAttribute('password');
    var type = candidate.getAttribute('type');
    var network = candidate.getAttribute('network');

    var info = name + ': ' + address + ':' + port + ' ' + protocol +
        ' name:' + username + ' pwd:' + password +
        ' pref:' + pref +
        ' ' + type;
    if (network) {
      info = info + " network:'" + network + "'";
    }
    result += '\n  ' + info;
  }
  return result;
};

/**
 * Pretty print the jingle action contained in the given Xml node.
 *
 * @param {Node} jingle Xml node with jingle action contained in child nodes.
 * @param {string} action String containing the jingle action.
 *
 * @return {?string} Pretty version of jingle action stanze. Null if error.
 */
remoting.FormatIq.prototype.prettyJingleAction = function(jingle, action) {
  if (action == 'session-initiate' || action == 'session-accept') {
    return this.prettySessionInitiateAccept(jingle);
  }
  if (action == 'session-terminate') {
    return this.prettySessionTerminate(jingle);
  }
  if (action == 'transport-info') {
    return this.prettyTransportInfo(jingle);
  }
  return null;
};

/**
 * Pretty print the jingle error information contained in the given Xml node.
 *
 * @param {Node} error Xml node containing error information in child nodes.
 *
 * @return {?string} Pretty version of error stanze. Null if error.
 */
remoting.FormatIq.prototype.prettyError = function(error) {
  if (!this.verifyAttributes(error, 'xmlns:err,code,type,err:hostname,' +
                             'err:bnsname,err:stacktrace')) {
    return null;
  }
  var code = error.getAttribute('code');
  var type = error.getAttribute('type');
  var hostname = error.getAttribute('err:hostname');
  var bnsname = error.getAttribute('err:bnsname');
  var stacktrace = error.getAttribute('err:stacktrace');

  var result = '\n  error ' + code + ' ' + type + " hostname:'" +
             hostname + "' bnsname:'" + bnsname + "'";
  var children = error.childNodes;
  for (var i = 0; i < children.length; i++) {
    /** @type {Node} */
    var child = children[i];
    result += '\n  ' + child.nodeName;
  }
  if (stacktrace) {
    var stack = stacktrace.split(' | ');
    result += '\n  stacktrace:';
    // We use 'length-1' because the stack trace ends with " | " which results
    // in an empty string at the end after the split.
    for (var s = 0; s < stack.length - 1; s++) {
      result += '\n    ' + stack[s];
    }
  }
  return result;
};

/**
 * Print out the heading line for an iq node.
 *
 * @param {string} action String describing action (send/receive).
 * @param {string} id Packet id.
 * @param {string} desc Description of iq action for this node.
 * @param {string|null} sid Session id.
 *
 * @return {string} Pretty version of stanza heading info.
 */
remoting.FormatIq.prototype.prettyIqHeading = function(action, id, desc,
                                                       sid) {
  var message = 'iq ' + action + ' id=' + id;
  if (desc) {
    message = message + ' ' + desc;
  }
  if (sid) {
    message = message + ' sid=' + sid;
  }
  return message;
};

/**
 * Print out an iq 'result'-type node.
 *
 * @param {string} action String describing action (send/receive).
 * @param {NodeList} iq_list Node list containing the 'result' xml.
 *
 * @return {?string} Pretty version of Iq result stanza. Null if error.
 */
remoting.FormatIq.prototype.prettyIqResult = function(action, iq_list) {
  /** @type {Node} */
  var iq = iq_list[0];
  var id = iq.getAttribute('id');
  var iq_children = iq.childNodes;

  if (iq_children.length == 0) {
    return this.prettyIqHeading(action, id, 'result (empty)', null);
  } else if (iq_children.length == 1) {
    /** @type {Node} */
    var child = iq_children[0];
    if (child.nodeName == 'query') {
      if (!this.verifyAttributes(child, 'xmlns')) {
        return null;
      }
      var xmlns = child.getAttribute('xmlns');
      if (xmlns == 'google:jingleinfo') {
        var result = this.prettyIqHeading(action, id, 'result ' + xmlns, null);
        result += this.prettyJingleinfo(child);
        return result;
      }
      return '';
    } else if (child.nodeName == 'rem:log-result') {
      if (!this.verifyAttributes(child, 'xmlns:rem')) {
        return null;
      }
      return this.prettyIqHeading(action, id, 'result (log-result)', null);
    }
  }
  return null;
};

/**
 * Print out an Iq 'get'-type node.
 *
 * @param {string} action String describing action (send/receive).
 * @param {NodeList} iq_list Node containing the 'get' xml.
 *
 * @return {?string} Pretty version of Iq get stanza. Null if error.
 */
remoting.FormatIq.prototype.prettyIqGet = function(action, iq_list) {
  /** @type {Node} */
  var iq = iq_list[0];
  var id = iq.getAttribute('id');
  var iq_children = iq.childNodes;

  if (iq_children.length != 1) {
    return null;
  }

  /** @type {Node} */
  var query = iq_children[0];
  if (query.nodeName != 'query') {
    return null;
  }
  if (!this.verifyAttributes(query, 'xmlns')) {
    return null;
  }
  var xmlns = query.getAttribute('xmlns');
  return this.prettyIqHeading(action, id, 'get ' + xmlns, null);
};

/**
 * Print out an iq 'set'-type node.
 *
 * @param {string} action String describing action (send/receive).
 * @param {NodeList} iq_list Node containing the 'set' xml.
 *
 * @return {?string} Pretty version of Iq set stanza. Null if error.
 */
remoting.FormatIq.prototype.prettyIqSet = function(action, iq_list) {
  /** @type {Node} */
  var iq = iq_list[0];
  var id = iq.getAttribute('id');
  var iq_children = iq.childNodes;

  var children = iq_children.length;
  if (children == 1) {
    /** @type {Node} */
    var child = iq_children[0];
    if (child.nodeName == 'gr:log') {
      var grlog = child;
      if (!this.verifyAttributes(grlog, 'xmlns:gr')) {
        return null;
      }

      if (grlog.childNodes.length != 1) {
        return null;
      }
      var grentry = grlog.firstChild;
      if (grentry.nodeName != 'gr:entry') {
        return null;
      }
      if (!this.verifyAttributes(grentry, 'role,event-name,session-state,' +
                                 'os-name,cpu,browser-version,' +
                                 'webapp-version')) {
        return null;
      }
      var role = grentry.getAttribute('role');
      var event_name = grentry.getAttribute('event-name');
      var session_state = grentry.getAttribute('session-state');
      var os_name = grentry.getAttribute('os-name');
      var cpu = grentry.getAttribute('cpu');
      var browser_version = grentry.getAttribute('browser-version');
      var webapp_version = grentry.getAttribute('webapp-version');

      var result = this.prettyIqHeading(action, id, role + ' ' + event_name +
                                        ' ' + session_state, null);
      result += '\n  ' + os_name + ' ' + cpu + " browser:" + browser_version +
                     " webapp:" + webapp_version;
      return result;
    }
    if (child.nodeName == 'jingle') {
      var jingle = child;
      if (!this.verifyAttributes(jingle, 'xmlns,action,sid,initiator')) {
        return null;
      }

      var jingle_action = jingle.getAttribute('action');
      var sid = jingle.getAttribute('sid');

      var result = this.prettyIqHeading(action, id, 'set ' + jingle_action,
                                        sid);
      var action_str = this.prettyJingleAction(jingle, jingle_action);
      if (!action_str) {
        return null;
      }
      return result + action_str;
    }
  }
  return null;
};

/**
 * Print out an iq 'error'-type node.
 *
 * @param {string} action String describing action (send/receive).
 * @param {NodeList} iq_list Node containing the 'error' xml.
 *
 * @return {?string} Pretty version of iq error stanza. Null if error parsing
 *                  this stanza.
 */
remoting.FormatIq.prototype.prettyIqError = function(action, iq_list) {
  /** @type {Node} */
  var iq = iq_list[0];
  var id = iq.getAttribute('id');
  var iq_children = iq.childNodes;

  var children = iq_children.length;
  if (children != 2) {
    return null;
  }

  /** @type {Node} */
  var jingle = iq_children[0];
  if (jingle.nodeName != 'jingle') {
    return null;
  }
  if (!this.verifyAttributes(jingle, 'xmlns,action,sid,initiator')) {
    return null;
  }
  var jingle_action = jingle.getAttribute('action');
  var sid = jingle.getAttribute('sid');
  var result = this.prettyIqHeading(action, id, 'error from ' + jingle_action,
                                    sid);
  var action_str = this.prettyJingleAction(jingle, jingle_action);
  if (!action_str) {
    return null;
  }
  result += action_str;

  /** @type {Node} */
  var error = iq_children[1];
  if (error.nodeName != 'cli:error') {
    return null;
  }

  var error_str = this.prettyError(error);
  if (!error_str) {
    return null;
  }
  result += error_str;
  return result;
};

/**
 * Try to log a pretty-print the given IQ stanza (XML).
 * Return true if the stanza was successfully printed.
 *
 * @param {boolean} send True if we're sending this stanza; false for receiving.
 * @param {string} message The XML stanza to add to the log.
 *
 * @return {?string} Pretty version of the Iq stanza. Null if error.
 */
remoting.FormatIq.prototype.prettyIq = function(send, message) {
  var parser = new DOMParser();
  var xml = parser.parseFromString(message, 'text/xml');

  var iq_list = xml.getElementsByTagName('iq');

  if (iq_list && iq_list.length > 0) {
    /** @type {Node} */
    var iq = iq_list[0];
    if (!this.verifyAttributes(iq, 'xmlns,xmlns:cli,id,to,from,type'))
      return null;

    // Verify that the to/from fields match the expected sender/receiver.
    var to = iq.getAttribute('to');
    var from = iq.getAttribute('from');
    var action = '';
    var bot = remoting.settings.DIRECTORY_BOT_JID;
    if (send) {
      if (to && to != this.hostJid && to != bot) {
        console.warn('FormatIq: bad to: ' + to);
        return null;
      }
      if (from && from != this.clientJid) {
        console.warn('FormatIq: bad from: ' + from);
        return null;
      }

      action = "send";
      if (to == bot) {
        action = action + " (to bot)";
      }
    } else {
      if (to && to != this.clientJid) {
        console.warn('FormatIq: bad to: ' + to);
        return null;
      }
      if (from && from != this.hostJid && from != bot) {
        console.warn('FormatIq: bad from: ' + from);
        return null;
      }

      action = "receive";
      if (from == bot) {
        action = action + " (from bot)";
      }
    }

    var type = iq.getAttribute('type');
    if (type == 'result') {
      return this.prettyIqResult(action, iq_list);
    } else if (type == 'get') {
      return this.prettyIqGet(action, iq_list);
    } else if (type == 'set') {
      return this.prettyIqSet(action, iq_list);
    } else  if (type == 'error') {
      return this.prettyIqError(action, iq_list);
    }
  }

  return null;
};

/**
 * Return a pretty-formatted string for the IQ stanza being sent.
 * If the stanza cannot be made pretty, then a string with a raw dump of the
 * stanza will be returned.
 *
 * @param {string} message The XML stanza to make pretty.
 *
 * @return {string} Pretty version of XML stanza being sent. A raw dump of the
 *                  stanza is returned if there was a parsing error.
 */
remoting.FormatIq.prototype.prettifySendIq = function(message) {
  var result = this.prettyIq(true, message);
  if (!result) {
    // Fall back to showing the raw stanza.
    return 'Sending Iq: ' + message;
  }
  return result;
};

/**
 * Return a pretty-formatted string for the IQ stanza that was received.
 * If the stanza cannot be made pretty, then a string with a raw dump of the
 * stanza will be returned.
 *
 * @param {string} message The XML stanza to make pretty.
 *
 * @return {string} Pretty version of XML stanza that was received. A raw dump
 *                  of the stanza is returned if there was a parsing error.
 */
remoting.FormatIq.prototype.prettifyReceiveIq = function(message) {
  var result = this.prettyIq(false, message);
  if (!result) {
    // Fall back to showing the raw stanza.
    return 'Receiving Iq: ' + message;
  }
  return result;
};

/** @type {remoting.FormatIq} */
remoting.formatIq = null;