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

/**
 * Partial definition of the result of networkingPrivate.getProperties()).
 * TODO(stevenjb): Replace with chrome.networkingPrivate.NetworkStateProperties
 * once that is fully speced.
 * @typedef {{
 *   ConnectionState: string,
 *   Cellular: {
 *     Family: ?string,
 *     SIMPresent: ?boolean,
 *     SIMLockStatus: { LockType: ?string },
 *     SupportNetworkScan: ?boolean
 *   },
 *   GUID: string,
 *   Name: string,
 *   Source: string,
 *   Type: string
 * }}
 * @see extensions/common/api/networking_private.idl
 */
var NetworkProperties;

cr.define('options.network', function() {
  var ArrayDataModel = cr.ui.ArrayDataModel;
  var List = cr.ui.List;
  var ListItem = cr.ui.ListItem;
  var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel;
  var Menu = cr.ui.Menu;
  var MenuItem = cr.ui.MenuItem;
  var ControlledSettingIndicator = options.ControlledSettingIndicator;

  /**
   * Network settings constants. These enums usually match their C++
   * counterparts.
   */
  function Constants() {}

  /**
   * Valid network type names.
   */
  Constants.NETWORK_TYPES = ['Ethernet', 'WiFi', 'WiMAX', 'Cellular', 'VPN'];

  /**
   * Helper function to check whether |type| is a valid network type.
   * @param {string} type A string that may contain a valid network type.
   * @return {boolean} Whether the string represents a valid network type.
   */
  function isNetworkType(type) {
    return (Constants.NETWORK_TYPES.indexOf(type) != -1);
  }

  /**
   * Order in which controls are to appear in the network list sorted by key.
   */
  Constants.NETWORK_ORDER = ['Ethernet',
                             'WiFi',
                             'WiMAX',
                             'Cellular',
                             'VPN',
                             'addConnection'];

  /**
   * ID of the menu that is currently visible.
   * @type {?string}
   * @private
   */
  var activeMenu_ = null;

  /**
   * The state of the cellular device or undefined if not available.
   * @type {string|undefined}
   * @private
   */
  var cellularDeviceState_ = undefined;

  /**
   * The active cellular network or null if none.
   * @type {?NetworkProperties}
   * @private
   */
  var cellularNetwork_ = null;

  /**
   * The active ethernet network or null if none.
   * @type {?NetworkProperties}
   * @private
   */
  var ethernetNetwork_ = null;

  /**
   * The state of the WiFi device or undefined if not available.
   * @type {string|undefined}
   * @private
   */
  var wifiDeviceState_ = undefined;

  /**
   * The state of the WiMAX device or undefined if not available.
   * @type {string|undefined}
   * @private
   */
  var wimaxDeviceState_ = undefined;

  /**
   * Indicates if mobile data roaming is enabled.
   * @type {boolean}
   * @private
   */
  var enableDataRoaming_ = false;

  /**
   * Returns the display name for 'network'.
   * @param {NetworkProperties} data The network data dictionary.
   */
  function getNetworkName(data) {
    if (data.Type == 'Ethernet')
      return loadTimeData.getString('ethernetName');
    if (data.Type == 'VPN')
      return options.VPNProviders.formatNetworkName(new cr.onc.OncData(data));
    return data.Name;
  }

  /**
   * Create an element in the network list for controlling network
   * connectivity.
   * @param {Object} data Description of the network list or command.
   * @constructor
   * @extends {cr.ui.ListItem}
   */
  function NetworkListItem(data) {
    var el = cr.doc.createElement('li');
    el.data_ = {};
    for (var key in data)
      el.data_[key] = data[key];
    NetworkListItem.decorate(el);
    return el;
  }

  /**
   * @param {string} action An action to send to coreOptionsUserMetricsAction.
   */
  function sendChromeMetricsAction(action) {
    chrome.send('coreOptionsUserMetricsAction', [action]);
  }

  /**
   * @param {string} guid The network GUID.
   */
  function showDetails(guid) {
    chrome.networkingPrivate.getManagedProperties(
      guid, DetailsInternetPage.initializeDetailsPage);
  }

  /**
   * Decorate an element as a NetworkListItem.
   * @param {!Element} el The element to decorate.
   */
  NetworkListItem.decorate = function(el) {
    el.__proto__ = NetworkListItem.prototype;
    el.decorate();
  };

  NetworkListItem.prototype = {
    __proto__: ListItem.prototype,

    /**
     * Description of the network group or control.
     * @type {Object<Object>}
     * @private
     */
    data_: null,

    /**
     * Element for the control's subtitle.
     * @type {?Element}
     * @private
     */
    subtitle_: null,

    /**
     * Div containing the list item icon.
     * @type {?Element}
     * @private
     */
    iconDiv_: null,

    /**
     * Description of the network control.
     * @type {Object}
     */
    get data() {
      return this.data_;
    },

    /**
     * Text label for the subtitle.
     * @type {string}
     */
    set subtitle(text) {
      if (text)
        this.subtitle_.textContent = text;
      this.subtitle_.hidden = !text;
    },

    /**
     * Sets the icon based on a network state object.
     * @param {!NetworkProperties} data Network state properties.
     */
    set iconData(data) {
      if (!isNetworkType(data.Type))
        return;
      var networkIcon = this.getNetworkIcon();
      networkIcon.networkState =
          /** @type {chrome.networkingPrivate.NetworkStateProperties} */ (data);
    },

    /**
     * Sets the icon based on a network type or a special type indecator, e.g.
     * 'add-connection'
     * @type {string}
     */
    set iconType(type) {
      if (isNetworkType(type)) {
        var networkIcon = this.getNetworkIcon();
        networkIcon.networkType = type;
      } else {
        // Special cases. e.g. 'add-connection'. Background images are
        // defined in browser_options.css.
        var oldIcon = /** @type {CrNetworkIconElement} */ (
            this.iconDiv_.querySelector('cr-network-icon'));
        if (oldIcon)
          this.iconDiv_.removeChild(oldIcon);
        this.iconDiv_.classList.add('network-' + type.toLowerCase());
      }
    },

    /**
     * Returns any existing network icon for the list item or creates a new one.
     * @return {!CrNetworkIconElement} The network icon for the list item.
     */
    getNetworkIcon: function() {
      var networkIcon = /** @type {CrNetworkIconElement} */ (
          this.iconDiv_.querySelector('cr-network-icon'));
      if (!networkIcon) {
        networkIcon = /** @type {!CrNetworkIconElement} */ (
            document.createElement('cr-network-icon'));
        networkIcon.isListItem = false;
        this.iconDiv_.appendChild(networkIcon);
      }
      return networkIcon;
    },

    /**
     * Set the direction of the text.
     * @param {string} direction The direction of the text, e.g. 'ltr'.
     */
    setSubtitleDirection: function(direction) {
      this.subtitle_.dir = direction;
    },

    /**
     * Indicate that the selector arrow should be shown.
     */
    showSelector: function() {
      this.subtitle_.classList.add('network-selector');
    },

    /**
     * Adds an indicator to show that the network is policy managed.
     */
    showManagedNetworkIndicator: function() {
      this.appendChild(new ManagedNetworkIndicator());
    },

    /** @override */
    decorate: function() {
      ListItem.prototype.decorate.call(this);
      this.className = 'network-group';
      this.iconDiv_ = this.ownerDocument.createElement('div');
      this.iconDiv_.className = 'network-icon';
      this.appendChild(this.iconDiv_);
      var textContent = this.ownerDocument.createElement('div');
      textContent.className = 'network-group-labels';
      this.appendChild(textContent);
      var categoryLabel = this.ownerDocument.createElement('div');
      var title;
      if (this.data_.key == 'addConnection')
        title = 'addConnectionTitle';
      else
        title = this.data_.key.toLowerCase() + 'Title';
      categoryLabel.className = 'network-title';
      categoryLabel.textContent = loadTimeData.getString(title);
      textContent.appendChild(categoryLabel);
      this.subtitle_ = this.ownerDocument.createElement('div');
      this.subtitle_.className = 'network-subtitle';
      textContent.appendChild(this.subtitle_);
    },
  };

  /**
   * Creates a control that displays a popup menu when clicked.
   * @param {Object} data  Description of the control.
   * @constructor
   * @extends {NetworkListItem}
   */
  function NetworkMenuItem(data) {
    var el = new NetworkListItem(data);
    el.__proto__ = NetworkMenuItem.prototype;
    el.decorate();
    return el;
  }

  NetworkMenuItem.prototype = {
    __proto__: NetworkListItem.prototype,

    /**
     * Popup menu element.
     * @type {?Element}
     * @private
     */
    menu_: null,

    /** @override */
    decorate: function() {
      this.subtitle = null;
      if (this.data.iconType)
        this.iconType = this.data.iconType;
      this.addEventListener('click', (function() {
        this.showMenu();
      }).bind(this));
    },

    /**
     * Retrieves the ID for the menu.
     */
    getMenuName: function() {
      return this.data_.key.toLowerCase() + '-network-menu';
    },

    /**
     * Creates a popup menu for the control.
     * @return {Element} The newly created menu.
     */
    createMenu: function() {
      if (this.data.menu) {
        var menu = this.ownerDocument.createElement('div');
        menu.id = this.getMenuName();
        menu.className = 'network-menu';
        menu.hidden = true;
        Menu.decorate(menu);
        menu.menuItemSelector = '.network-menu-item';
        for (var i = 0; i < this.data.menu.length; i++) {
          var entry = this.data.menu[i];
          createCallback_(menu, null, entry.label, entry.command);
        }
        return menu;
      }
      return null;
    },

    /**
     * Determines if a menu can be updated on the fly. Menus that cannot be
     * updated are fully regenerated using createMenu. The advantage of
     * updating a menu is that it can preserve ordering of networks avoiding
     * entries from jumping around after an update.
     * @return {boolean} Whether the menu can be updated on the fly.
     */
    canUpdateMenu: function() {
      return false;
    },

    /**
     * Removes the current menu contents, causing it to be regenerated when the
     * menu is shown the next time. If the menu is showing right now, its
     * contents are regenerated immediately and the menu remains visible.
     */
    refreshMenu: function() {
      this.menu_ = null;
      if (activeMenu_ == this.getMenuName())
        this.showMenu();
    },

    /**
     * Displays a popup menu.
     */
    showMenu: function() {
      var rebuild = false;
      // Force a rescan if opening the menu for WiFi networks to ensure the
      // list is up to date. Networks are periodically rescanned, but depending
      // on timing, there could be an excessive delay before the first rescan
      // unless forced.
      var rescan = !activeMenu_ && this.data_.key == 'WiFi';
      if (!this.menu_) {
        rebuild = true;
        var existing = $(this.getMenuName());
        if (existing) {
          if (this.canUpdateMenu() && this.updateMenu())
            return;
          closeMenu_();
        }
        this.menu_ = this.createMenu();
        this.menu_.addEventListener('mousedown', function(e) {
          // Prevent blurring of list, which would close the menu.
          e.preventDefault();
        });
        var parent = $('network-menus');
        if (existing)
          parent.replaceChild(this.menu_, existing);
        else
          parent.appendChild(this.menu_);
      }
      var top = this.offsetTop + this.clientHeight;
      var menuId = this.getMenuName();
      if (menuId != activeMenu_ || rebuild) {
        closeMenu_();
        activeMenu_ = menuId;
        this.menu_.style.setProperty('top', top + 'px');
        this.menu_.hidden = false;
      }
      if (rescan) {
        chrome.networkingPrivate.requestNetworkScan();
      }
    }
  };

  /**
   * Creates a control for selecting or configuring a network connection based
   * on the type of connection (e.g. wifi versus vpn).
   * @param {{key: string, networkList: Array<!NetworkProperties>}} data
   *     An object containing the network type (key) and an array of networks.
   * @constructor
   * @extends {NetworkMenuItem}
   */
  function NetworkSelectorItem(data) {
    var el = new NetworkMenuItem(data);
    el.__proto__ = NetworkSelectorItem.prototype;
    el.decorate();
    return el;
  }

  /**
   * Returns true if |source| is a policy managed source.
   * @param {string} source The ONC source of a network.
   * @return {boolean} Whether |source| is a managed source.
   */
  function isManaged(source) {
    return (source == 'DevicePolicy' || source == 'UserPolicy');
  }

  /**
   * Returns true if |network| is visible.
   * @param {!chrome.networkingPrivate.NetworkStateProperties} network The
   *     network state properties.
   * @return {boolean} Whether |network| is visible.
   */
  function networkIsVisible(network) {
    if (network.Type == 'WiFi')
      return !!(network.WiFi && (network.WiFi.SignalStrength > 0));
    if (network.Type == 'WiMAX')
      return !!(network.WiMAX && (network.WiMAX.SignalStrength > 0));
    // Other network types are always considered 'visible'.
    return true;
  }

  /**
   * Returns true if |cellular| is a GSM network with no sim present.
   * @param {?NetworkProperties} cellular The network state properties.
   * @return {boolean} Whether |network| is missing a SIM card.
   */
  function isCellularSimAbsent(cellular) {
    if (!cellular || !cellular.Cellular)
      return false;
    return cellular.Cellular.Family == 'GSM' && !cellular.Cellular.SIMPresent;
  }

  /**
   * Returns true if |cellular| has a locked SIM card.
   * @param {?NetworkProperties} cellular The network state properties.
   * @return {boolean} Whether |network| has a locked SIM card.
   */
  function isCellularSimLocked(cellular) {
    if (!cellular || !cellular.Cellular)
      return false;
    var simLockStatus = cellular.Cellular.SIMLockStatus;
    return !!(simLockStatus && simLockStatus.LockType);
  }

  NetworkSelectorItem.prototype = {
    __proto__: NetworkMenuItem.prototype,

    /** @override */
    decorate: function() {
      // TODO(kevers): Generalize method of setting default label.
      this.subtitle = loadTimeData.getString('OncConnectionStateNotConnected');
      var list = this.data_.networkList;
      var candidateData = null;
      for (var i = 0; i < list.length; i++) {
        var networkDetails = list[i];
        if (networkDetails.ConnectionState == 'Connecting' ||
            networkDetails.ConnectionState == 'Connected') {
          this.subtitle = getNetworkName(networkDetails);
          this.setSubtitleDirection('ltr');
          candidateData = networkDetails;
          // Only break when we see a connecting network as it is possible to
          // have a connected network and a connecting network at the same
          // time.
          if (networkDetails.ConnectionState == 'Connecting')
            break;
        }
      }
      if (candidateData)
        this.iconData = candidateData;
      else
        this.iconType = this.data.key;

      this.showSelector();

      if (candidateData && isManaged(candidateData.Source))
        this.showManagedNetworkIndicator();

      if (activeMenu_ == this.getMenuName()) {
        // Menu is already showing and needs to be updated. Explicitly calling
        // show menu will force the existing menu to be replaced.  The call
        // is deferred in order to ensure that position of this element has
        // beem properly updated.
        var self = this;
        setTimeout(function() {self.showMenu();}, 0);
      }
    },

    /**
     * Creates a menu for selecting, configuring or disconnecting from a
     * network.
     * @return {!Element} The newly created menu.
     */
    createMenu: function() {
      var menu = this.ownerDocument.createElement('div');
      menu.id = this.getMenuName();
      menu.className = 'network-menu';
      menu.hidden = true;
      Menu.decorate(menu);
      menu.menuItemSelector = '.network-menu-item';
      var addendum = [];
      if (this.data_.key == 'WiFi') {
        addendum.push({
          label: loadTimeData.getString('joinOtherNetwork'),
          command: createAddNonVPNConnectionCallback_('WiFi'),
          data: {}
        });
      } else if (this.data_.key == 'Cellular') {
        if (cellularDeviceState_ == 'Enabled' &&
            cellularNetwork_ && cellularNetwork_.Cellular &&
            cellularNetwork_.Cellular.SupportNetworkScan) {
          addendum.push({
            label: loadTimeData.getString('otherCellularNetworks'),
            command: createAddNonVPNConnectionCallback_('Cellular'),
            addClass: ['other-cellulars'],
            data: {}
          });
        }

        var label = enableDataRoaming_ ? 'disableDataRoaming' :
            'enableDataRoaming';
        var disabled = !loadTimeData.getValue('loggedInAsOwner');
        var entry = {label: loadTimeData.getString(label),
                     data: {}};
        if (disabled) {
          entry.command = null;
          entry.tooltip =
              loadTimeData.getString('dataRoamingDisableToggleTooltip');
        } else {
          var self = this;
          entry.command = function() {
            options.Preferences.setBooleanPref(
                'cros.signed.data_roaming_enabled',
                !enableDataRoaming_, true);
            // Force revalidation of the menu the next time it is displayed.
            self.menu_ = null;
          };
        }
        addendum.push(entry);
      } else if (this.data_.key == 'VPN') {
        addendum = addendum.concat(createAddVPNConnectionEntries_());
      }

      var list = this.data.rememberedNetworks;
      if (list && list.length > 0) {
        var callback = function(list) {
          $('remembered-network-list').clear();
          var dialog = options.PreferredNetworks.getInstance();
          PageManager.showPageByName('preferredNetworksPage', false);
          dialog.update(list);
          sendChromeMetricsAction('Options_NetworkShowPreferred');
        };
        addendum.push({label: loadTimeData.getString('preferredNetworks'),
                       command: callback,
                       data: list});
      }

      var networkGroup = this.ownerDocument.createElement('div');
      networkGroup.className = 'network-menu-group';
      list = this.data.networkList;
      var empty = !list || list.length == 0;
      if (list) {
        var connectedVpnGuid = '';
        for (var i = 0; i < list.length; i++) {
          var data = list[i];
          this.createNetworkOptionsCallback_(networkGroup, data);
          // For VPN only, append a 'Disconnect' item to the dropdown menu.
          if (!connectedVpnGuid && data.Type == 'VPN' &&
              (data.ConnectionState == 'Connected' ||
               data.ConnectionState == 'Connecting')) {
            connectedVpnGuid = data.GUID;
          }
        }
        if (connectedVpnGuid) {
          var disconnectCallback = function() {
            sendChromeMetricsAction('Options_NetworkDisconnectVPN');
            chrome.networkingPrivate.startDisconnect(connectedVpnGuid);
          };
          // Add separator
          addendum.push({});
          addendum.push({label: loadTimeData.getString('disconnectNetwork'),
                         command: disconnectCallback,
                         data: data});
        }
      }
      if (this.data_.key == 'WiFi' || this.data_.key == 'WiMAX' ||
          this.data_.key == 'Cellular') {
        addendum.push({});
        if (this.data_.key == 'WiFi') {
          addendum.push({
            label: loadTimeData.getString('turnOffWifi'),
            command: function() {
              sendChromeMetricsAction('Options_NetworkWifiToggle');
              chrome.networkingPrivate.disableNetworkType('WiFi');
            },
            data: {}});
        } else if (this.data_.key == 'WiMAX') {
          addendum.push({
            label: loadTimeData.getString('turnOffWimax'),
            command: function() {
              chrome.networkingPrivate.disableNetworkType('WiMAX');
            },
            data: {}});
        } else if (this.data_.key == 'Cellular') {
          addendum.push({
            label: loadTimeData.getString('turnOffCellular'),
            command: function() {
              chrome.networkingPrivate.disableNetworkType('Cellular');
            },
            data: {}});
        }
      }
      if (!empty)
        menu.appendChild(networkGroup);
      if (addendum.length > 0) {
        var separator = false;
        if (!empty) {
          menu.appendChild(MenuItem.createSeparator());
          separator = true;
        }
        for (var i = 0; i < addendum.length; i++) {
          var value = addendum[i];
          if (value.data) {
            var item = createCallback_(menu, value.data, value.label,
                                       value.command);
            if (value.tooltip)
              item.title = value.tooltip;
            if (value.addClass)
              item.classList.add(value.addClass);
            separator = false;
          } else if (!separator) {
            menu.appendChild(MenuItem.createSeparator());
            separator = true;
          }
        }
      }
      return menu;
    },

    /** @override */
    canUpdateMenu: function() {
      return this.data_.key == 'WiFi' && activeMenu_ == this.getMenuName();
    },

    /**
     * Updates an existing menu.  Updated menus preserve ordering of prior
     * entries.  During the update process, the ordering may differ from the
     * preferred ordering as determined by the network library.  If the
     * ordering becomes potentially out of sync, then the updated menu is
     * marked for disposal on close.  Reopening the menu will force a
     * regeneration, which will in turn fix the ordering. This method must only
     * be called if canUpdateMenu() returned |true|.
     * @return {boolean} True if successfully updated.
     */
    updateMenu: function() {
      var oldMenu = $(this.getMenuName());
      var group = oldMenu.getElementsByClassName('network-menu-group')[0];
      if (!group)
        return false;
      var newMenu = this.createMenu();
      var discardOnClose = false;
      var oldNetworkButtons = this.extractNetworkConnectButtons_(oldMenu);
      var newNetworkButtons = this.extractNetworkConnectButtons_(newMenu);
      for (var key in oldNetworkButtons) {
        if (newNetworkButtons[key]) {
          group.replaceChild(newNetworkButtons[key].button,
                             oldNetworkButtons[key].button);
          if (newNetworkButtons[key].index != oldNetworkButtons[key].index)
            discardOnClose = true;
          newNetworkButtons[key] = null;
        } else {
          // Leave item in list to prevent network items from jumping due to
          // deletions.
          oldNetworkButtons[key].disabled = true;
          discardOnClose = true;
        }
      }
      for (var key in newNetworkButtons) {
        var entry = newNetworkButtons[key];
        if (entry) {
          group.appendChild(entry.button);
          discardOnClose = true;
        }
      }
      oldMenu.data = {discardOnClose: discardOnClose};
      return true;
    },

    /**
     * Extracts a mapping of network names to menu element and position.
     * @param {!Element} menu The menu to process.
     * @return {Object<?{index: number, button: Element}>}
     *     Network mapping.
     * @private
     */
    extractNetworkConnectButtons_: function(menu) {
      var group = menu.getElementsByClassName('network-menu-group')[0];
      var networkButtons = {};
      if (!group)
        return networkButtons;
      var buttons = group.getElementsByClassName('network-menu-item');
      for (var i = 0; i < buttons.length; i++) {
        var label = buttons[i].data.label;
        networkButtons[label] = {index: i, button: buttons[i]};
      }
      return networkButtons;
    },

    /**
     * Adds a menu item for showing network details.
     * @param {!Element} parent The parent element.
     * @param {NetworkProperties} data Description of the network.
     * @private
     */
    createNetworkOptionsCallback_: function(parent, data) {
      var menuItem = createCallback_(parent,
                                     data,
                                     getNetworkName(data),
                                     showDetails.bind(null, data.GUID));
      if (isManaged(data.Source))
        menuItem.appendChild(new ManagedNetworkIndicator());
      if (data.ConnectionState == 'Connected' ||
          data.ConnectionState == 'Connecting') {
        var label = menuItem.getElementsByClassName(
            'network-menu-item-label')[0];
        label.classList.add('active-network');
      }
    }
  };

  /**
   * Creates a button-like control for configurating internet connectivity.
   * @param {{key: string, subtitle: string, command: Function}} data
   *     Description of the network control.
   * @constructor
   * @extends {NetworkListItem}
   */
  function NetworkButtonItem(data) {
    var el = new NetworkListItem(data);
    el.__proto__ = NetworkButtonItem.prototype;
    el.decorate();
    return el;
  }

  NetworkButtonItem.prototype = {
    __proto__: NetworkListItem.prototype,

    /** @override */
    decorate: function() {
      if (this.data.subtitle)
        this.subtitle = this.data.subtitle;
      else
       this.subtitle = null;
      if (this.data.command)
        this.addEventListener('click', this.data.command);
      if (this.data.iconData)
        this.iconData = this.data.iconData;
      else if (this.data.iconType)
        this.iconType = this.data.iconType;
      if (isManaged(this.data.Source))
        this.showManagedNetworkIndicator();
    },
  };

  /**
   * Adds a command to a menu for modifying network settings.
   * @param {!Element} menu Parent menu.
   * @param {?NetworkProperties} data Description of the network.
   * @param {!string} label Display name for the menu item.
   * @param {!Function} command Callback function.
   * @return {!Element} The created menu item.
   * @private
   */
  function createCallback_(menu, data, label, command) {
    var button = menu.ownerDocument.createElement('div');
    button.className = 'network-menu-item';

    var buttonIconDiv = menu.ownerDocument.createElement('div');
    buttonIconDiv.className = 'network-icon';
    button.appendChild(buttonIconDiv);
    if (data && isNetworkType(data.Type)) {
      var networkIcon = /** @type {!CrNetworkIconElement} */ (
          document.createElement('cr-network-icon'));
      buttonIconDiv.appendChild(networkIcon);
      networkIcon.isListItem = true;
      networkIcon.networkState =
          /** @type {chrome.networkingPrivate.NetworkStateProperties} */ (data);
    }

    var buttonLabel = menu.ownerDocument.createElement('span');
    buttonLabel.className = 'network-menu-item-label';
    buttonLabel.textContent = label;
    button.appendChild(buttonLabel);
    var callback = null;
    if (command != null) {
      if (data) {
        callback = function() {
          (/** @type {Function} */(command))(data);
          closeMenu_();
        };
      } else {
        callback = function() {
          (/** @type {Function} */(command))();
          closeMenu_();
        };
      }
    }
    if (callback != null)
      button.addEventListener('activate', callback);
    else
      buttonLabel.classList.add('network-disabled-control');

    button.data = {label: label};
    MenuItem.decorate(button);
    menu.appendChild(button);
    return button;
  }

  /**
   * A list of controls for manipulating network connectivity.
   * @constructor
   * @extends {cr.ui.List}
   */
  var NetworkList = cr.ui.define('list');

  NetworkList.prototype = {
    __proto__: List.prototype,

    /** @override */
    decorate: function() {
      List.prototype.decorate.call(this);
      this.startBatchUpdates();
      this.autoExpands = true;
      this.dataModel = new ArrayDataModel([]);
      this.selectionModel = new ListSingleSelectionModel();
      this.addEventListener('blur', this.onBlur_.bind(this));
      this.selectionModel.addEventListener('change',
                                           this.onSelectionChange_.bind(this));

      // Wi-Fi control is always visible.
      this.update({key: 'WiFi', networkList: []});

      this.updateAddConnectionMenuEntries_();

      var prefs = options.Preferences.getInstance();
      prefs.addEventListener('cros.signed.data_roaming_enabled',
          function(event) {
            enableDataRoaming_ = event.value.value;
          });
      this.endBatchUpdates();

      this.onNetworkListChanged_();  // Trigger an initial network update

      chrome.networkingPrivate.onNetworkListChanged.addListener(
          this.onNetworkListChanged_.bind(this));
      chrome.networkingPrivate.onDeviceStateListChanged.addListener(
          this.onNetworkListChanged_.bind(this));

      chrome.networkingPrivate.requestNetworkScan();

      options.VPNProviders.addObserver(this.onVPNProvidersChanged_.bind(this));
    },

    /**
     * networkingPrivate event called when the network list has changed.
     */
    onNetworkListChanged_: function() {
      var networkList = this;
      chrome.networkingPrivate.getDeviceStates(function(deviceStates) {
        var filter = { networkType: 'All' };
        chrome.networkingPrivate.getNetworks(filter, function(networkStates) {
          networkList.updateNetworkStates(deviceStates, networkStates);
        });
      });
    },

    /**
     * Called when the list of VPN providers changes. Refreshes the contents of
     * menus that list VPN providers.
     * @private
     */
    onVPNProvidersChanged_: function() {
      // Refresh the contents of the VPN menu.
      var index = this.indexOf('VPN');
      if (index != undefined)
        this.getListItemByIndex(index).refreshMenu();

      // Refresh the contents of the "add connection" menu.
      this.updateAddConnectionMenuEntries_();
      index = this.indexOf('addConnection');
      if (index != undefined)
        this.getListItemByIndex(index).refreshMenu();
    },

    /**
     * Updates the entries in the "add connection" menu, based on the VPN
     * providers currently enabled in the primary user's profile.
     * @private
     */
    updateAddConnectionMenuEntries_: function() {
      var entries = [{
        label: loadTimeData.getString('addConnectionWifi'),
        command: createAddNonVPNConnectionCallback_('WiFi')
      }];
      entries = entries.concat(createAddVPNConnectionEntries_());
      this.update({key: 'addConnection',
                   iconType: 'add-connection',
                   menu: entries
                  });
    },

    /**
     * When the list loses focus, unselect all items in the list and close the
     * active menu.
     * @private
     */
    onBlur_: function() {
      this.selectionModel.unselectAll();
      closeMenu_();
    },

    /** @override */
    handleKeyDown: function(e) {
      if (activeMenu_) {
        // keyIdentifier does not report 'Esc' correctly
        if (e.keyCode == 27 /* Esc */) {
          closeMenu_();
          return;
        }

        if ($(activeMenu_).handleKeyDown(e)) {
          e.preventDefault();
          e.stopPropagation();
        }
        return;
      }

      if (e.keyIdentifier == 'Enter' ||
          e.keyIdentifier == 'U+0020' /* Space */) {
        var selectedListItem = this.getListItemByIndex(
            this.selectionModel.selectedIndex);
        if (selectedListItem) {
          selectedListItem.click();
          return;
        }
      }

      List.prototype.handleKeyDown.call(this, e);
    },

    /**
     * Close bubble and menu when a different list item is selected.
     * @param {Event} event Event detailing the selection change.
     * @private
     */
    onSelectionChange_: function(event) {
      PageManager.hideBubble();
      // A list item may temporarily become unselected while it is constructing
      // its menu. The menu should therefore only be closed if a different item
      // is selected, not when the menu's owner item is deselected.
      if (activeMenu_) {
        for (var i = 0; i < event.changes.length; ++i) {
          if (event.changes[i].selected) {
            var item = this.dataModel.item(event.changes[i].index);
            if (!item.getMenuName || item.getMenuName() != activeMenu_) {
              closeMenu_();
              return;
            }
          }
        }
      }
    },

    /**
     * Finds the index of a network item within the data model based on
     * category.
     * @param {string} key Unique key for the item in the list.
     * @return {(number|undefined)} The index of the network item, or
     *     |undefined| if it is not found.
     */
    indexOf: function(key) {
      var size = this.dataModel.length;
      for (var i = 0; i < size; i++) {
        var entry = this.dataModel.item(i);
        if (entry.key == key)
          return i;
      }
      return undefined;
    },

    /**
     * Updates a network control.
     * @param {Object} data Description of the entry.
     */
    update: function(data) {
      this.startBatchUpdates();
      var index = this.indexOf(data.key);
      if (index == undefined) {
        // Find reference position for adding the element.  We cannot hide
        // individual list elements, thus we need to conditionally add or
        // remove elements and cannot rely on any element having a fixed index.
        for (var i = 0; i < Constants.NETWORK_ORDER.length; i++) {
          if (data.key == Constants.NETWORK_ORDER[i]) {
            data.sortIndex = i;
            break;
          }
        }
        var referenceIndex = -1;
        for (var i = 0; i < this.dataModel.length; i++) {
          var entry = this.dataModel.item(i);
          if (entry.sortIndex < data.sortIndex)
            referenceIndex = i;
          else
            break;
        }
        if (referenceIndex == -1) {
          // Prepend to the start of the list.
          this.dataModel.splice(0, 0, data);
        } else if (referenceIndex == this.dataModel.length) {
          // Append to the end of the list.
          this.dataModel.push(data);
        } else {
          // Insert after the reference element.
          this.dataModel.splice(referenceIndex + 1, 0, data);
        }
      } else {
        var entry = this.dataModel.item(index);
        data.sortIndex = entry.sortIndex;
        this.dataModel.splice(index, 1, data);
      }
      this.endBatchUpdates();
    },

    /**
     * @override
     * @param {Object} entry
     */
    createItem: function(entry) {
      if (entry.networkList)
        return new NetworkSelectorItem(
            /** @type {{key: string, networkList: Array<!NetworkProperties>}} */
            (entry));
      if (entry.command)
        return new NetworkButtonItem(
            /** @type {{key: string, subtitle: string, command: Function}} */(
                entry));
      if (entry.menu)
        return new NetworkMenuItem(entry);
      assertNotReached();
    },

    /**
     * Deletes an element from the list.
     * @param {string} key  Unique identifier for the element.
     */
    deleteItem: function(key) {
      var index = this.indexOf(key);
      if (index != undefined)
        this.dataModel.splice(index, 1);
    },

    /**
     * Updates the state of a toggle button.
     * @param {string} key Unique identifier for the element.
     * @param {boolean} active Whether the control is active.
     */
    updateToggleControl: function(key, active) {
      var index = this.indexOf(key);
      if (index != undefined) {
        var entry = this.dataModel.item(index);
        entry.iconType = active ? 'control-active' : 'control-inactive';
        this.update(entry);
      }
    },

    /**
     * Updates the state of network devices and services.
     * @param {!Array<{State: string, Type: string}>} deviceStates The result
     *     from networkingPrivate.getDeviceStates.
     * @param {!Array<!chrome.networkingPrivate.NetworkStateProperties>}
     *     networkStates The result from networkingPrivate.getNetworks.
     */
    updateNetworkStates: function(deviceStates, networkStates) {
      // Update device states.
      cellularDeviceState_ = undefined;
      wifiDeviceState_ = undefined;
      wimaxDeviceState_ = undefined;
      for (var i = 0; i < deviceStates.length; ++i) {
        var device = deviceStates[i];
        var type = device.Type;
        var state = device.State;
        if (type == 'Cellular')
          cellularDeviceState_ = cellularDeviceState_ || state;
        else if (type == 'WiFi')
          wifiDeviceState_ = wifiDeviceState_ || state;
        else if (type == 'WiMAX')
          wimaxDeviceState_ = wimaxDeviceState_ || state;
      }

      // Update active network states.
      cellularNetwork_ = null;
      ethernetNetwork_ = null;
      for (var i = 0; i < networkStates.length; i++) {
        // Note: This cast is valid since
        // networkingPrivate.NetworkStateProperties is a subset of
        // NetworkProperties and all missing properties are optional.
        var entry = /** @type {NetworkProperties} */ (networkStates[i]);
        switch (entry.Type) {
          case 'Cellular':
            cellularNetwork_ = cellularNetwork_ || entry;
            break;
          case 'Ethernet':
            // Ignore any EAP Parameters networks (which lack ConnectionState).
            if (entry.ConnectionState)
              ethernetNetwork_ = ethernetNetwork_ || entry;
            break;
        }
        if (cellularNetwork_ && ethernetNetwork_)
          break;
      }

      if (cellularNetwork_ && cellularNetwork_.GUID) {
        // Get the complete set of cellular properties which includes SIM and
        // Scan properties.
        var networkList = this;
        chrome.networkingPrivate.getProperties(
            cellularNetwork_.GUID, function(cellular) {
              cellularNetwork_ = /** @type {NetworkProperties} */ (cellular);
              networkList.updateControls(networkStates);
            });
      } else {
        this.updateControls(networkStates);
      }
    },

    /**
     * Updates network controls.
     * @param {!Array<!chrome.networkingPrivate.NetworkStateProperties>}
     *     networkStates The result from networkingPrivate.getNetworks.
     */
    updateControls: function(networkStates) {
      this.startBatchUpdates();

      // Only show Ethernet control if available.
      if (ethernetNetwork_) {
        var ethernetOptions = showDetails.bind(null, ethernetNetwork_.GUID);
        var state = ethernetNetwork_.ConnectionState;
        var subtitle;
        if (state == 'Connected')
          subtitle = loadTimeData.getString('OncConnectionStateConnected');
        else if (state == 'Connecting')
          subtitle = loadTimeData.getString('OncConnectionStateConnecting');
        else
          subtitle = loadTimeData.getString('OncConnectionStateNotConnected');
        this.update(
          { key: 'Ethernet',
            subtitle: subtitle,
            iconData: ethernetNetwork_,
            command: ethernetOptions,
            Source: ethernetNetwork_.Source }
        );
      } else {
        this.deleteItem('Ethernet');
      }

      if (wifiDeviceState_ == 'Enabled')
        loadData_('WiFi', networkStates);
      else
        addEnableNetworkButton_('WiFi');

      // Only show cellular control if available.
      if (cellularDeviceState_) {
        if (cellularDeviceState_ == 'Enabled' &&
            !isCellularSimLocked(cellularNetwork_) &&
            !isCellularSimAbsent(cellularNetwork_)) {
          loadData_('Cellular', networkStates);
        } else {
          addEnableNetworkButton_('Cellular');
        }
      } else {
        this.deleteItem('Cellular');
      }

      // Only show wimax control if available. Uses cellular icons.
      if (wimaxDeviceState_) {
        if (wimaxDeviceState_ == 'Enabled')
          loadData_('WiMAX', networkStates);
        else
          addEnableNetworkButton_('WiMAX');
      } else {
        this.deleteItem('WiMAX');
      }

      // Only show VPN control if there is at least one VPN configured.
      if (loadData_('VPN', networkStates) == 0)
        this.deleteItem('VPN');

      this.endBatchUpdates();
    }
  };

  /**
   * Replaces a network menu with a button for enabling the network type.
   * @param {string} type The type of network (WiFi, Cellular or Wimax).
   * @private
   */
  function addEnableNetworkButton_(type) {
    var subtitle = loadTimeData.getString('networkDisabled');
    var enableNetwork = function() {
      if (type == 'WiFi')
        sendChromeMetricsAction('Options_NetworkWifiToggle');
      if (type == 'Cellular') {
        if (isCellularSimLocked(cellularNetwork_)) {
          chrome.send('simOperation', ['unlock']);
          return;
        } else if (isCellularSimAbsent(cellularNetwork_)) {
          chrome.send('simOperation', ['configure']);
          return;
        }
      }
      chrome.networkingPrivate.enableNetworkType(type);
    };
    $('network-list').update({key: type,
                              subtitle: subtitle,
                              iconType: type,
                              command: enableNetwork});
  }

  /**
   * Element for indicating a policy managed network.
   * @constructor
   * @extends {options.ControlledSettingIndicator}
   */
  function ManagedNetworkIndicator() {
    var el = cr.doc.createElement('span');
    el.__proto__ = ManagedNetworkIndicator.prototype;
    el.decorate();
    return el;
  }

  ManagedNetworkIndicator.prototype = {
    __proto__: ControlledSettingIndicator.prototype,

    /** @override */
    decorate: function() {
      ControlledSettingIndicator.prototype.decorate.call(this);
      this.controlledBy = 'policy';
      var policyLabel = loadTimeData.getString('managedNetwork');
      this.setAttribute('textPolicy', policyLabel);
      this.removeAttribute('tabindex');
    },

    /** @override */
    handleEvent: function(event) {
      // Prevent focus blurring as that would close any currently open menu.
      if (event.type == 'mousedown')
        return;
      ControlledSettingIndicator.prototype.handleEvent.call(this, event);
    },

    /**
     * Handle mouse events received by the bubble, preventing focus blurring as
     * that would close any currently open menu and preventing propagation to
     * any elements located behind the bubble.
     * @param {Event} event Mouse event.
     */
    stopEvent: function(event) {
      event.preventDefault();
      event.stopPropagation();
    },

    /** @override */
    toggleBubble: function() {
      if (activeMenu_ && !$(activeMenu_).contains(this))
        closeMenu_();
      ControlledSettingIndicator.prototype.toggleBubble.call(this);
      if (this.showingBubble) {
        var bubble = PageManager.getVisibleBubble();
        bubble.addEventListener('mousedown', this.stopEvent);
        bubble.addEventListener('click', this.stopEvent);
      }
    }
  };

  /**
   * Updates the list of available networks and their status, filtered by
   * network type.
   * @param {string} type The type of network.
   * @param {Array<!chrome.networkingPrivate.NetworkStateProperties>} networks
   *     The list of network objects.
   * @return {number} The number of visible networks matching |type|.
   */
  function loadData_(type, networks) {
    var res = 0;
    var availableNetworks = [];
    var rememberedNetworks = [];
    for (var i = 0; i < networks.length; i++) {
      var network = networks[i];
      if (network.Type != type)
        continue;
      if (networkIsVisible(network)) {
        availableNetworks.push(network);
        ++res;
      }
      if ((type == 'WiFi' || type == 'VPN') && network.Source &&
          network.Source != 'None') {
        rememberedNetworks.push(network);
      }
    }
    var data = {
      key: type,
      networkList: availableNetworks,
      rememberedNetworks: rememberedNetworks
    };
    $('network-list').update(data);
    return res;
  }

  /**
   * Hides the currently visible menu.
   * @private
   */
  function closeMenu_() {
    if (activeMenu_) {
      var menu = $(activeMenu_);
      menu.hidden = true;
      if (menu.data && menu.data.discardOnClose)
        menu.parentNode.removeChild(menu);
      activeMenu_ = null;
    }
  }

  /**
   * Creates a callback function that adds a new connection of the given type.
   * This method may be used for all network types except VPN.
   * @param {string} type An ONC network type
   * @return {function()} The created callback.
   * @private
   */
  function createAddNonVPNConnectionCallback_(type) {
    return function() {
      if (type == 'WiFi')
        sendChromeMetricsAction('Options_NetworkJoinOtherWifi');
      chrome.send('addNonVPNConnection', [type]);
    };
  }

  /**
   * Creates a callback function that shows the "add network" dialog for a VPN
   * provider. If |opt_extensionID| is omitted, the dialog for the built-in
   * OpenVPN/L2TP provider is shown. Otherwise, |opt_extensionID| identifies the
   * third-party provider for which the dialog should be shown.
   * @param {string=} opt_extensionID Extension ID identifying the third-party
   *     VPN provider for which the dialog should be shown.
   * @return {function()} The created callback.
   * @private
   */
  function createVPNConnectionCallback_(opt_extensionID) {
    return function() {
      sendChromeMetricsAction(opt_extensionID ?
          'Options_NetworkAddVPNThirdParty' :
          'Options_NetworkAddVPNBuiltIn');
      chrome.send('addVPNConnection',
                  opt_extensionID ? [opt_extensionID] : undefined);
    };
  }

  /**
   * Generates an "add network" entry for each VPN provider currently enabled in
   * the primary user's profile.
   * @return {!Array<{label: string, command: function(), data: !Object}>} The
   *     list of entries.
   * @private
   */
  function createAddVPNConnectionEntries_() {
    var entries = [];
    var providers = options.VPNProviders.getProviders();
    for (var i = 0; i < providers.length; ++i) {
      entries.push({
        label: loadTimeData.getStringF('addConnectionVPNTemplate',
                                       providers[i].name),
        command: createVPNConnectionCallback_(
            providers[i].extensionID || undefined),
        data: {}
      });
    }
    return entries;
  }

  /**
   * Whether the Network list is disabled. Only used for display purpose.
   */
  cr.defineProperty(NetworkList, 'disabled', cr.PropertyKind.BOOL_ATTR);

  // Export
  return {
    NetworkList: NetworkList
  };
});
