/** @define {boolean} Use proxy or non-proxy data fetch. */
var DEF_PROXY_FETCH = true;

/** @define {boolean} Turn on test mode to expose internal functions.
 *    The test file is located in:
 *    //google3/gadgets/modules/tabnews/tabnews_test.js
 */
var TESTING = false;

/**
 * Wrap everything into a function to avoid cloberring global space (the gadget
 * is rendered inline in iGoogle), and for passing in module id and bidi info.
 * @param {number} moduleId The __MODULE_ID__ variable passed in from gadget.
 * @param {string} bidi_end_edge The __BIDI_END_EDGE__ variable.
 */
function _tabnews_init(moduleId, bidi_end_edge) {

// Get userprefs for this gadget.
var prefs = new _IG_Prefs(moduleId);

/**
 * Number of articles to show for a News section.
 * @type {number}
 */
var itemCounts = prefs.getInt('items');


var onloadTriggered = false;
var delayedImages = [];

/**
 * Callback function to show the delayed images.
 */
function triggerOnloadHandler() {
  onloadTriggered = true;
  for (var i = 0; i < delayedImages.length; i++) {
    var e = _gel('newsimg_' + i);
    if (e != null) {
      e.src = delayedImages[i];
    }
  }
  delayedImages = [];
}

/**
 * Returns the html for the news image thumbnail.
 * We delay loading of the image thumbnail until postload since the images are
 * less important than other UI elements.
 * @param {string} tbUrl The url of the image thumbnail.
 * @param {number} tbWidth The width of the image thumbnail.
 * @param {number} tbHeight The height of the image thumbnail.
 * @return {string} The html of the news image thumbnail, including the scaled-
 *   down width and height specification and an unique id to allow defer load.
 */
function renderImgTag(tbUrl, tbWidth, tbHeight) {
  // Reduce thumbnail size to be 60x45 or less.
  var MAXIMUM_THUMBNAIL_WIDTH = 60;
  var MAXIMUM_THUMBNAIL_HEIGHT = 45;
  if (tbWidth > MAXIMUM_THUMBNAIL_WIDTH) {
    tbHeight *= MAXIMUM_THUMBNAIL_WIDTH / tbWidth;
    tbWidth = MAXIMUM_THUMBNAIL_WIDTH;
  }
  if (tbHeight > MAXIMUM_THUMBNAIL_HEIGHT) {
    tbWidth *= MAXIMUM_THUMBNAIL_HEIGHT / tbHeight;
    tbHeight = MAXIMUM_THUMBNAIL_HEIGHT;
  }
  var html = ['<img class=newsimg src="'];
  if (!onloadTriggered) {
    delayedImages.push(tbUrl);
    // Set src to be empty and add unique id so we can update the src later.
    var EMPTY_IMAGE_SRC = 'http://www.google.com/c.gif';
    html.push(EMPTY_IMAGE_SRC, '" id="newsimg_',
              delayedImages.length - 1);
  } else {
    html.push(tbUrl);
  }
  html.push('" width=', Math.round(tbWidth),
            ' height=', Math.round(tbHeight), '>');
  return html.join('');
}

// Make all images delay to show up.
_IG_RegisterOnloadHandler(triggerOnloadHandler);

/**
 * Object that holds all relevant information about a News edition,
 * such as the sections available in the News edition, the language direction,
 * and News feed url to get News articles from News FE.
 * @param {string} section_data Serialized news section data in this format:
 *     Top Stories|h,U.S.|n,World|w,Entertainment|e,Sci/Tech|t,Business|b,...
 * @param {string} title_url Url of News site, used to construct feed url.
 * @param {string} ned News edition.
 * @param {string} direction Language direction of this News edition, can either
 *     be 'rtl', 'ltr' or '' (which defaults to ltr).
 * @constructor
 */
function News(section_data, title_url, ned, direction) {
  this.sections_ = [];
  this.sectionName_ = {};
  var sections = section_data.split(',');
  for (var i = 0; i < sections.length; ++i) {
    var s = sections[i].split('|');
    this.sections_[i] = s[1];
    this.sectionName_[s[1]] = s[0];
  }

  this.feed_ = 'http://ajax.googleapis.com/ajax/services/search/news?' +
               'v=1.0&hide=related&key=internal-ig-tabnews&ned=' + ned;
  this.direction_ = direction == 'rtl' ? 'rtl' : 'ltr';

  this.tabs_ = this.getTabs_();
}

/**
 * Get the feed url for the specified topic for this news edition.
 * @param {string} topic News topic key (h, po, w, ...) or query.
 * @return {string} news Feed url.
 */
News.prototype.getUrl = function(topic) {
  var feed = this.feed_;

  if (isQuery(topic)) {
    feed += '&ie=UTF-8&oe=UTF-8&q=' + _esc(isQuery(topic));
  } else {
    feed += '&topic=' + _esc(topic);
  }

  return feed;
}

/**
 * Get the ordered list of tabs to show.
 * By default we show the first five News sections.
 * The user can customize this in the "+" tab by checking/unchecking sections
 * and/or enter custom queries.
 * @return {Array.<string>} Array of tabs (topic or query).
 */
News.prototype.getTabs_ = function() {
  var tabs = [];
  var userTabs = prefs.getString('tabs');
  var queryList = prefs.getArray('queryList');
  if (userTabs) {
    userTabs = userTabs.split(',');
    // Put selected sections into object map for quick lookup.
    var selectedSections = {};
    for (var i = 0; i < userTabs.length; ++i) {
      if (!isQuery(userTabs[i])) {
        selectedSections[userTabs[i]] = true;
      }
    }
    // Add only sections in tabs that are available in the current News
    // edition.
    for (var i = 0; i < this.sections_.length; ++i) {
      var section = this.sections_[i];
      if (selectedSections[section]) {
        tabs.push(section);
      }
    }
  } else {
    if (!queryList.length) {
      // Show the first five sections by default.
      for (var i = 0; i < 5 && i < this.sections_.length; ++i) {
        tabs.push(this.sections_[i]);
      }
    }
  }

  // Add custom queries at end of list.
  for (var i = 0; i < queryList.length; ++i) {
    var q = asQuery(queryList[i]);
    tabs.push(q);
  }

  // Add query from onebox query if exist.
  var onebox = prefs.getString('onebox');
  if (onebox) {
    // Check for duplicate.
    var duplicate = -1;
    for (var i = 0; i < queryList.length; ++i) {
      if (queryList[i] == onebox) {
        duplicate = i;
        break;
      }
    }
    if (duplicate >= 0) {
      // Clear onebox paramater and jump to tab.
      prefs.set('onebox', '',
                'selectedTab', tabs.length - queryList.length + duplicate);
    } else {
      // Add the new query.
      queryList.push(onebox);
      prefs.set('onebox', '', 'selectedTab', tabs.length);
      prefs.setArray('queryList', queryList);
      tabs.push(asQuery(onebox));
    }
  }
  return tabs;
}

// Cache News feed for 4 mins
var cache = {};
var CACHE_SECONDS = 4 * 60;
var CACHE_MILLISECONDS = CACHE_SECONDS * 1000;

// Previously selected tab index. The '+' tab is not considered a real tab.
var prevTabIndex = 0;

var QUERY_PREFIX = 'query=';
var QUERY_PREFIX_LENGTH = 6;

// Get language/country specific News sections, feed url, and
// language direction.
var newsData = new News(prefs.getMsg('sections'),
                        prefs.getMsg('title_url'),
                        prefs.getMsg('ned'),
                        prefs.getMsg('direction'));

/**
 * Create a specially formatted string to denote a query.
 * @param {string} q The query to format.
 * @return {string} Formatted query string.
 */
function asQuery(q) {
  return QUERY_PREFIX + q;
}

/**
 * Check if the passed in string is a query string created by asQuery,
 * and if so returns the original query.
 * @param {string} s The string to check.
 * @return {string|boolean} The original query, or false if not a query.
 */
function isQuery(s) {
  if (s.indexOf(QUERY_PREFIX) == 0) {
    return s.slice(QUERY_PREFIX_LENGTH);
  }
  return false;
}

/**
 * Triggered on tab switch.  Do nothing if cache is fresh,
 * and try load the relevant feed if cache is stale.
 * @param {string} tabId The tab id in tabs to display news in.
 * @param {number} index The index of the tab.
 * @param {string} topic The topic id or query to fetch to show.
 */
function showNews(tabId, index, topic) {
  // Reset the edit form in case we are previously in '+' tab.
  // Save the index for switching back when user click 'Cancel' in config tab.
  prevTabIndex = index;
  // Check the last time the tab is updated.
  var cache_time = cache[tabId];
  var now = new Date().getTime();
  if (cache_time && (now - cache_time < CACHE_MILLISECONDS)) {
    // When the content is still fresh, we will not load news but only adjust
    // iframe's height.
    _IG_AdjustIFrameHeight();
    return;
  }

  var rssUrl = newsData.getUrl(topic);

  showMsg(tabId, prefs.getMsg('loading'));
  loadNews(tabId, topic, rssUrl);
}

/**
 * Fetch news feed into the "tabId" Tab with specified topic.
 * @param {string} tabId The DOM id of the tab to render in.
 * @param {string} topic News topic key (h, po, w, ...) or query.
 * @param {string} rssUrl news Feed url.
 */
function loadNews(tabId, topic, rssUrl) {
  if (itemCounts > 4) {
    rssUrl += '&rsz=large';
  }
  function callback(content, opt_eval) {
    var items = [];
    try {
      if (opt_eval) content = window.eval('(' + content + ')');
      items = content.responseData.results;
    } catch (e) {}
    if (!items.length) {
      showMsg(tabId, content ?
          prefs.getMsg('no_news') : prefs.getMsg('error'));
      return;
    }
    // Preload data is used only in proxied fetch.
    prefs.set('last_url', DEF_PROXY_FETCH ? rssUrl : '');
    cache[tabId] = new Date().getTime();
    var moreLink = content.responseData.cursor.moreResultsUrl;
    renderNews(tabId, items, moreLink, isQuery(topic));
    _IG_AdjustIFrameHeight();
  }
  if (DEF_PROXY_FETCH) {
    _IG_FetchContent(rssUrl,
                     _IG_Callback(callback, true),
                     { refreshInterval: CACHE_SECONDS });
  } else {
    var callback_name = '_tabnews_' + tabId;
    window[callback_name] = callback;
    _IG_LoadScriptXDomain(rssUrl + '&callback=' + _esc(callback_name));
  }
}

// Approximate char width, based on code used in AdWords (awCreateAdUtil).
function charWidth(ch) {
  if (ch <= '\u04f9' ||
      ch == '\u05be' ||
      (ch >= '\u05d0' && ch <= '\u05ea') ||
      ch == '\u05F3' ||
      ch == '\u05f4' ||
      (ch >= '\u0e00' && ch <= '\u0e7f') ||
      (ch >= '\u1e00' && ch <= '\u20af') ||
      (ch >= '\u2100' && ch <= '\u213a') ||
      (ch >= '\uff61' && ch <= '\uffdc')) {
    return 1;
  }
  return 2;
}

var reFindAllAmpEntity = /&amp;/g;
var reFindAllAmpersand = /&/g;

/**
 * Trim a message and add "..." at end if it is too long.
 * We consider double-width characters and treat &amp; as a single character.
 * @param {string} msg Message to be trimmed.
 * @param {number} maxLength Maximum length of string.
 * @return {string} Trimmed message.
 */
function trimMessage(msg, maxLength) {
  msg = msg.replace(reFindAllAmpEntity, '&');
  for (var i = 0, count = 0; i < msg.length; ++i) {
    var ch = msg.charAt(i);
    count += charWidth(ch);
    if (count >= maxLength &&
        // don't trim if fit exactly
        !(count == maxLength && i == msg.length - 1)) {
      msg = msg.slice(0, i) + '...';
      break;
    }
  }
  return msg.replace(reFindAllAmpersand, '&amp;');
}

/**
 * Estimate the length of a message.
 * We consider double-width characters and treat &amp; as a single character.
 * @param {string} msg Message.
 * @return {number} Estimated message length.
 */
function messageWidth(msg) {
  msg = msg.replace(reFindAllAmpEntity, '&');
  var count = 0;
  for (var i = 0; i < msg.length; ++i) {
    var ch = msg.charAt(i);
    count += charWidth(ch);
  }
  return count;
}

/**
 * Shows a message centered in a tab.
 * @param {string} tabId The DOM id for a tab.
 * @param {string} msg The message to show.
 */
function showMsg(tabId, msg) {
  _gel(tabId).innerHTML =
      '<div class="newsmsg">' + msg + '</div>';
}

/**
 * Render news articles contained in feed in a tab.
 * @param {string} tabId The DOM id of the tab to render in.
 * @param {Object} items The NodeList object containing news article data.
 * @param {string} moreLink The link to show more News articles.
 * @param {boolean|string} query Evaluates to boolean true if the news articles
 *     are from a search query, and false otherwise.
 */
function renderNews(tabId, items, moreLink, query) {
  // Start building HTML string that will be displayed in gadget.
  var html = ['<div class="newsmod dir_' + newsData.direction_ +
              '"><table border=0 cellpadding=0 cellspacing=0 ' +
              'style="font-size:' +
              parseInt(prefs.getString('font_size'), 10) +
              'pt">'];
  var itemsShown = 0;
  for (var i = 0; i < items.length; ++i) {
    var r = items[i];
    var item = '<tr><td class="img" align="' + bidi_end_edge + '">';
    // Remove all formatting tags and remove unneeded stuff before <a href=".
    if (!r.image) {
      // If the cluster doesn't have a image, we will not render any image.
      item += '&nbsp;';
    } else {
      if (prefs.getBool('show_image')) {
        var img = r.image;
        item += '<div><a href="' + img.originalContextUrl + '" target=_blank>' +
                renderImgTag(img.tbUrl, img.tbWidth, img.tbHeight) +
                '<div class="imgsrc" title="' + img.publisher + '">' +
                trimMessage(img.publisher, 10) + '</div></a></div>';
      }
    }
    item += '</td><td class=txt>';
    // Don't wrap source if it's very short.
    var source = r.publisher;
    if (messageWidth(source) < 20) source = '<nobr>' + source + '</nobr>';
    item += '<h3><a href="' + r.unescapedUrl + '" target=_blank>' +
            r.titleNoFormatting + '</a>' +
            ' - <font color=#666666>' + source + '</font></h3>' +
            '<div class="snippet" id="' + tabId + '_s' + i +
            '">' + r.content + '</div>' +
            '<div class="related" id="' + tabId + '_r' + i +
            '">... ';
    // Add related news if present.
    if (r.clusterUrl) {
      item += '<a class=fl href="' + r.clusterUrl + '">' +
          prefs.getMsg('related_articles') + '</a>';
    }
    html.push(item + '</div></td></tr>');
    if (++itemsShown >= itemCounts) {
      break;
    }
  }
  if (moreLink) {
    // Add a more link at end.
    html.push('</table><div class=more><a href="' + _hesc(moreLink) + '">' +
              prefs.getMsg('more') + '</a></div></div>');
  }

  // Make all links open new window.
  html = html.join('').replace(/<a /g, '<a target=_blank ');
  _gel(tabId).innerHTML = html;
}


/**
 * The news edition information used in the custom edit tab.
 * @type {News}
 */
var customEditNewsData;

/**
 * The user query list in the news custom edit tab.
 * @type {Array}
 */
var customizedQueryList = [];

/**
 * Shows available News sections in our custom edit tab.
 * @param {News} opt_newsData The news edition object to use, defaulting to
 *     newsData if not specified.
 */
function updateCustomEditEdition(opt_newsData) {
  customEditNewsData = opt_newsData ? opt_newsData : newsData;
  // Get which tabs are being shown to check the checkboxes next to the
  // sections.
  var showing = {};
  for (var i = 0; i < customEditNewsData.tabs_.length; ++i) {
    showing[customEditNewsData.tabs_[i]] = true;
  }

  var html = [];
  for (var i = 0; i < customEditNewsData.sections_.length; ++i) {
    html.push(
        '<div><input id="news' + moduleId + 'topic' + i + '" type="checkbox"' +
        (showing[customEditNewsData.sections_[i]] ? ' checked' : '') + ' /> ' +
        '<span dir=' + customEditNewsData.direction_ + '>' +
        customEditNewsData.sectionName_[customEditNewsData.sections_[i]] +
        '</span></div>');
  }
  _gel('customeditedition').innerHTML = html.join('');
  _IG_AdjustIFrameHeight();
}

/**
 * Show the UI of config tab.
 * @param {string} tabId The DOM id of the config tab to render in.
 * @param {News} opt_newsData The news edition object to use, defaulting to
 *     newsData if not specified.
 */
function updateCustomEdit(tabId, opt_newsData) {
  var html = [];

  // Generate the html for edition selection.
  var editionListStr = prefs.getMsg('edition_list');
  var editionList = editionListStr.split(',');
  html.push('<table width=100% class=editpanel>');
  html.push('<tr><td align=right width=35%>' +
            prefs.getMsg('select_edition') + '</td><td width=65% nowrap>');
  html.push('<select id="prefs_ned" size="1">');
  html.push('<option value="">' + prefs.getMsg('default_edition') +
            '</option>');
  for (var i = 0; i < editionList.length; i++) {
    var editions = editionList[i].split('|');
    html.push('<option value="' + editions[0] + '">' + editions[1] +
              '</option>');
  }

  // Generates html for selection of default number of items.
  var itemNumberList = [3, 4, 5, 6, 7, 8];
  var defaultNumberofItem = prefs.getInt('items');
  html.push('<tr><td align=right width=35%>' +
            prefs.getMsg('items_to_display') + '</td><td width=65% nowrap>');
  html.push('<select id="prefs_items" size="1">');
  for (var i = 0; i < itemNumberList.length; i++) {
    html.push('<option value="', itemNumberList[i] , '">',
              itemNumberList[i], '</option>');
  }
  html.push('</select></td></tr>');

  // Generates html for checkbox of showing image.
  html.push('<tr><td align=right width=35%>' +
            prefs.getMsg('show_image') + '</td><td width=65% nowrap>');
  html.push('<input name="prefs_show_image" id="prefs_show_image" ',
            'type="checkbox" checked="">');
  html.push('</td></tr>');

  // Generates html for selection of font size.
  html.push('<tr><td align=right width=35%>' +
            prefs.getMsg('font_size') + '</td><td width=65% nowrap>');
  html.push('<select id="prefs_font_size" size="1">');
  var fontSizeList = [11, 12, 13, 14, 15];
  for (var i = 0; i < fontSizeList.length; i++) {
    html.push('<option value="', fontSizeList[i], 'pt">',
              fontSizeList[i], 'pt</option>');
  }
  html.push('</select></td></tr>');

  // Generates html of customized query list.
  html.push('<tr><td align=right valign=top width=35%>' +
            prefs.getMsg('custom_section') + '</td><td width=65% nowrap>');
  html.push('<input type="text" id="prefs_query_list"/>&nbsp;&nbsp;',
            '<input type="button" value="' + prefs.getMsg('add') +
            '" id="btn_add_query"/>',
            '<div id="query_list_panel"></div>');
  html.push('</td></tr>');

  // Generates html of selected tabs panel.
  html.push('<tr valign=top><td align=right width=35%>',
            prefs.getMsg('select_tabs'), '</td><td width=65% nowrap>');
  html.push('<div id="customeditedition">customedition</div>');
  html.push('</td></tr>');
  html.push('<tr><td colspan=2 align=right><input type="button" ',
            'value="' + prefs.getMsg('save') + '" id="btn_save"/>&nbsp;',
            '<input type="button" value="' + prefs.getMsg('cancel') +
            '" id="btn_cancel"/>',
            '</td></tr></table>');

  _gel(tabId).innerHTML = html.join('');

  updateCustomEditEdition(opt_newsData);
  customizedQueryList = prefs.getArray('queryList');
  updateCustomizedQueryList();
  _gel('prefs_ned').onchange = updateEdition;
  _gel('btn_add_query').onclick = addQueryIntoCustomizedQueryList;
  _gel('btn_save').onclick = saveConfig;
  _gel('btn_cancel').onclick = cancelConfig;
  _gel('prefs_ned').value = prefs.getString('ned');
  // News api changes maximum number of news items into 8 from 9.
  // To make pref conversion successfully, we leave this hack code here.
  _gel('prefs_items').value = itemCounts > 8 ? 8 : itemCounts;
  _gel('prefs_show_image').checked = prefs.getBool('show_image') ? 'true' : '';
  _gel('prefs_font_size').value =  prefs.getString('font_size');
  _IG_AdjustIFrameHeight();
}

/**
 * Add query into the query list in config tab.
 */
function addQueryIntoCustomizedQueryList() {
  var query = _trim(_gel('prefs_query_list').value);
  if (query) {
    customizedQueryList.push(query);
    updateCustomizedQueryList();
    _IG_AdjustIFrameHeight();
  }
}

/**
 * Delete specified query from the query list in config tab.
 * @param {number} index The id of added query
 */
function _deleteQueryFromCustomizedQueryList(index) {
  customizedQueryList.splice(index, 1);
  updateCustomizedQueryList();
  _IG_AdjustIFrameHeight();
}

/**
 * Generate the customzied query list html of UI.
 */
function updateCustomizedQueryList() {
  var html = '';
  var parent = _gel('query_list_panel');
  parent.innerHTML = '';
  for (var i = 0; i < customizedQueryList.length; i++) {
     var node = document.createElement("a");
     node.className = 'delbox';
     node.tag = i;
     node.onclick = function() { return _deleteQueryFromCustomizedQueryList(this.tag);};
     node.href = '#';
     parent.appendChild(node);
     node = document.createTextNode(customizedQueryList[i]);
     parent.appendChild(node);
     node = document.createElement("div");
     node.className = 'c';
     parent.appendChild(node);
  }
}

/**
 * The callback function to cancel the "+" tab.
 */
function cancelConfig() {
  tabs.setSelectedTab(prevTabIndex);
}

/**
 * Called when user switches to '+' tab, or clicked on edit settings.
 * @param {string} tabId The DOM id for the '+' tab.
 */
function configNews(tabId) {
  updateCustomEdit(tabId);
}

/**
 * @type _IG_Tabs
 */
var tabs;

/**
 * DOM object to hold a label for checking/unchecking News sections.
 * @type Element
 */
var customLabel;

/**
 * The syndication/container indicator, normally 'ig' for iGoogle, and can be
 * 'open' (syndicated), 'gd' (Desktop), 'navclient' (Toolbar) etc.
 * @type string
 */
var SYND = _args()['synd'];

/**
 * Determine whether we click the "Save" button in the "+" tab.
 */
var isConfigSaved = false;

/**
 * Main initialization routine. Creates the tabs object, adds a tab for each
 * News section, adds a '+' tab at the end, and set up event handlers.
 */
function initTabs() {
  if (isConfigSaved) {
    // If we saved the config, we will not create a new tab but delete all
    // existed tabs.
    isConfigSaved = false;
  } else {
    tabs = new _IG_Tabs(moduleId);
  }
  for (var i = 0; i < newsData.tabs_.length; ++i) {
    var topic = newsData.tabs_[i];
    var title = isQuery(topic) ||
                newsData.sectionName_[topic];
    var tabLinkId = 'm_' + moduleId + '_tablink_' + i;
    tabs.addTab('<span id=' + tabLinkId + ' dir=' + newsData.direction_ + '>' +
                 _hesc(trimMessage(title, 20)) + '</span>', {
                 callback: _IG_Callback(showNews, i, topic),
                 tooltip: title });
  }
  // Edit settings don't work in syndication mode because we
  // can't save userprefs
  if (SYND && SYND != 'ig') return;

  // Append custom edits (choosing news sections) to existing edit settings form
  // and set up event handlers.
  var tabId = tabs.addTab('+', {
      callback: configNews,
      tooltip: prefs.getMsg('config_tab_tooltip') });
  // Make the edit tab smaller.
  tabs.getTabs()[tabs.getTabs().length - 1].getNameContainer().style.width = '30px';
  _IG_AdjustIFrameHeight();
}

/**
 * Manually parse messagebundle for the fetched news edition and perform
 * initialization.
 * @param {Object} xmlDoc XML content containing News edition information.
 */
function fetchedEdition(xmlDoc) {
  if (!xmlDoc || !xmlDoc.firstChild) {
    // Show error if initial load failed;
    // fail silently if triggered by onchange event during edit settings.
    if (!tabs) {
      _getGadgetContainer(moduleId).innerHTML = prefs.getMsg('error');
    }
    return;
  }
  // Save messages in messagebundle into a dictionary for easy use.
  var data = {};
  var msgNodes = xmlDoc.getElementsByTagName('msg');
  for (var i = 0; i < msgNodes.length; ++i) {
    data[msgNodes[i].getAttribute('name')] = msgNodes[i].firstChild.nodeValue;
  }
  // Create a temp News edition object to show its News sections if in
  // edit settings mode.
  var tempNews = new News(
      data['sections'],
      data['title_url'],
      data['ned'],
      data['direction']);
  if (!isConfigSaved && tabs) {
    // We are in plus tab, so just update the news sections.
    updateCustomEditEdition(tempNews);
    return;
  }
  newsData = tempNews;
  // Change the title of gadget.
  _IG_SetTitle(data['title']);
  initTabs();
}

/**
 * Fetch custom news edition by loading in the relevant messagebundle manually.
 * @param {string} ned News edition to load.
 */
function fetchEdition(ned) {
  // This is to fix the China News spec xml, because we don't have ALL_cnn.xml.
  if (ned == 'ccn') {
    ned = 'cn';
  }
  // We have different naming convention than News, ned (News naming) does not
  // have ALL_ prefix like we do.
  if (!/_/.test(ned)) {
    ned = 'ALL_' + ned;
  }
  var url = 'http://www.google.com/ig/modules/tabnews/tabnews_content/' +
      _esc(ned) + '.xml';
  _IG_FetchXmlContent(url, fetchedEdition);
}

/**
 * Updates the news sections according to the changed News edition.
 * @this {Element} The select element which fires the onchange event
 */
function updateEdition() {
  var ned = this.options[this.selectedIndex].value;
  if (!ned) {
    // Use default news edition.
    updateCustomEditEdition();
  } else {
    fetchEdition(ned);
  }
}

/**
 * Save all configs into prefs, deleted all existed tabs, and add new tabs if
 * the user specified.
 */
function saveConfig() {
  isConfigSaved = true;
  // Get News sections that are checked by user.
  var checked = {};
  for (var i = 0; i < customEditNewsData.sections_.length; ++i) {
    var topic = customEditNewsData.sections_[i];
    var checkbox = _gel('news' + moduleId + 'topic' + i);
    if (checkbox.checked) {
      checked[topic] = true;
    }
  }
  // Copy over tabs except the News sections that are unchecked.
  var userTabs = [];
  var lastSection = -1;
  for (var i = 0; i < customEditNewsData.tabs_.length; ++i) {
    var topic = customEditNewsData.tabs_[i];
    if (checked[topic]) {
      userTabs.push(topic);
      checked[topic] = false;
      lastSection = i;
    }
  }
  // Add newly checked News section after the last News section
  for (var i = 0; i < customEditNewsData.sections_.length; ++i) {
    var topic = customEditNewsData.sections_[i];
    if (checked[topic]) {
      userTabs.splice(++lastSection, 0, topic);
    }
  }
  var userNed = _gel('prefs_ned').value;
  var userTabsStr = userTabs.join();
  prefs.set('tabs', userTabsStr,
            'selectedTab', 0,
            'ned', _gel('prefs_ned').value,
            'items', _gel('prefs_items').value,
            'font_size', _gel('prefs_font_size').value,
            'show_image', _gel('prefs_show_image').checked ? true : false);
  prefs.setArray('queryList', customizedQueryList);
  prevTabIndex = 0;
  itemCounts = parseInt(_gel('prefs_items').value, 10);

  if (tabs.getTabs().length) {
    for (var i = tabs.getTabs().length - 1; i >= 0; --i) {
      tabs.removeTab(0);
    }
  }
  if (!userNed) {
    userNed = prefs.getMsg('ned');
  }
  fetchEdition(userNed);
}

// Initialization. Fetch a different News edition data if user wants
// a different news edition than the default, or initialize immediately.
var userNed = prefs.getString('ned');
var currNed = prefs.getMsg('ned');
// We are only allowed to show CN news on CN domain
var isCN = /\.cn$/.test(location.host);
if (isCN || !userNed || userNed == currNed) {
  initTabs();
} else {
  fetchEdition(userNed);
}

// Expose internal functions and variables for unit testing.
// See details in:
//     //google3/gadgets/modules/tabnews/tabnews_test.js
if (TESTING) {
  this.setOnloadTriggered = function(value) { onloadTriggered = value; };
  this.getDelayedImages = function() { return delayedImages; };
  this.setDelayedImages = function(value) { delayedImages = value; };
  this.renderImgTag = renderImgTag;
  this.isQuery = isQuery;
}

} // _tabnews_init
