// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gio = imports.gi.Gio;
const Lang = imports.lang;
const NetworkManager = imports.gi.NetworkManager;
const NMClient = imports.gi.NMClient;
const NMGtk = imports.gi.NMGtk;
const Signals = imports.signals;
const St = imports.gi.St;

const Hash = imports.misc.hash;
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
const MessageTray = imports.ui.messageTray;
const NotificationDaemon = imports.ui.notificationDaemon;
const ModemManager = imports.misc.modemManager;
const Util = imports.misc.util;

const NMConnectionCategory = {
    INVALID: 'invalid',
    WIRED: 'wired',
    WIRELESS: 'wireless',
    WWAN: 'wwan',
    VPN: 'vpn'
};

const NMAccessPointSecurity = {
    NONE: 1,
    WEP: 2,
    WPA_PSK: 3,
    WPA2_PSK: 4,
    WPA_ENT: 5,
    WPA2_ENT: 6
};

// small optimization, to avoid using [] all the time
const NM80211Mode = NetworkManager['80211Mode'];
const NM80211ApFlags = NetworkManager['80211ApFlags'];
const NM80211ApSecurityFlags = NetworkManager['80211ApSecurityFlags'];

// number of wireless networks that should be visible
// (the remaining are placed into More…)
const NUM_VISIBLE_NETWORKS = 5;

function ssidCompare(one, two) {
    if (!one || !two)
        return false;
    if (one.length != two.length)
        return false;
    for (let i = 0; i < one.length; i++) {
        if (one[i] != two[i])
            return false;
    }
    return true;
}

// shared between NMNetworkMenuItem and NMDeviceWWAN
function signalToIcon(value) {
    if (value > 80)
        return 'excellent';
    if (value > 55)
        return 'good';
    if (value > 30)
        return 'ok';
    if (value > 5)
        return 'weak';
    return 'none';
}

function ssidToLabel(ssid) {
    let label = NetworkManager.utils_ssid_to_utf8(ssid);
    if (!label)
        label = _("<unknown>");
    return label;
}

const NMNetworkMenuItem = new Lang.Class({
    Name: 'NMNetworkMenuItem',
    Extends: PopupMenu.PopupBaseMenuItem,

    _init: function(bestAP) {
        this.parent();

        this.bestAP = bestAP;

        let ssid = this.bestAP.get_ssid();
        let title = ssidToLabel(ssid);

        this._label = new St.Label({ text: title });
        this.actor.label_actor = this._label;
        this.addActor(this._label);
        this._icons = new St.BoxLayout({ style_class: 'nm-menu-item-icons' });
        this.addActor(this._icons, { align: St.Align.END });

        this._signalIcon = new St.Icon({ icon_name: this._getIcon(),
                                         style_class: 'popup-menu-icon' });
        this._icons.add_actor(this._signalIcon);

        this._secureIcon = new St.Icon({ style_class: 'popup-menu-icon' });
        if (this.bestAP._secType != NMAccessPointSecurity.NONE)
            this._secureIcon.icon_name = 'network-wireless-encrypted-symbolic';
        this._icons.add_actor(this._secureIcon);
    },

    updateBestAP: function(ap) {
        this.bestAP = ap;
        this._signalIcon.icon_name = this._getIcon();
    },

    _getIcon: function() {
        if (this.bestAP.mode == NM80211Mode.ADHOC)
            return 'network-workgroup-symbolic';
        else
            return 'network-wireless-signal-' + signalToIcon(this.bestAP.strength) + '-symbolic';
    }
});

const NMWirelessSectionTitleMenuItem = new Lang.Class({
    Name: 'NMWirelessSectionTitleMenuItem',
    Extends: PopupMenu.PopupSwitchMenuItem,

    _init: function(client) {
        this.parent(_("Wi-Fi"), false, { style_class: 'popup-subtitle-menu-item' });

        this._client = client;
        this._client.connect('notify::wireless-enabled', Lang.bind(this, this._propertyChanged));
        this._client.connect('notify::wireless-hardware-enabled', Lang.bind(this, this._propertyChanged));

        this._propertyChanged();
    },

    updateForDevice: function(device) {
        // we show the switch
        // - if there not just one device
        // - if the switch is off (but it can be turned on)
        // - if the device is activated or disconnected
        if (!this._hardwareEnabled) {
            this.setStatus(_("hardware disabled"));
        } else if (device && this._softwareEnabled) {
            let text = device.getStatusLabel();
            this.setStatus(text);
        } else
            this.setStatus(null);
    },

    activate: function(event) {
        this.parent(event);

        this._client.wireless_set_enabled(this._switch.state);
    },

    _propertyChanged: function() {
        this._softwareEnabled = this._client.wireless_enabled;
        this._hardwareEnabled = this._client.wireless_hardware_enabled;

        let enabled = this._softwareEnabled && this._hardwareEnabled;
        this.setToggleState(enabled);
        if (!this._hardwareEnabled)
            /* Translators: this indicates that wireless or wwan is disabled by hardware killswitch */
            this.setStatus(_("disabled"));

        this.emit('enabled-changed', enabled);
    }
});

const NMDevice = new Lang.Class({
    Name: 'NMDevice',
    Abstract: true,

    _init: function(client, device, connections) {
        this._client = client;
        this._setDevice(device);

        this._connections = [];
        connections.forEach(Lang.bind(this, this.checkConnection));

        this._activeConnection = null;
        this._activeConnectionItem = null;
        this._overflowItem = null;

        this.statusItem = new PopupMenu.PopupSwitchMenuItem('', this.connected, { style_class: 'popup-subtitle-menu-item' });
        this._statusChanged = this.statusItem.connect('toggled', Lang.bind(this, function(item, state) {
            let ok;
            if (state)
                ok = this.activate();
            else
                ok = this.deactivate();

            if (!ok)
                item.setToggleState(!state);
        }));

        this._updateStatusItem();
        this.section = new PopupMenu.PopupMenuSection();

        this._deferredWorkId = Main.initializeDeferredWork(this.section.actor, Lang.bind(this, this._createSection));
    },

    checkConnection: function(connection) {
        let pos = this._findConnection(connection.get_uuid());
        let exists = pos != -1;
        let valid = this.connectionValid(connection);
        let similar = false;
        if (exists) {
            let existing = this._connections[pos];

            // Check if connection changed name or id
            similar = existing.name == connection.get_id() &&
                existing.timestamp == connection._timestamp;
        }

        if (exists && valid && similar) {
            // Nothing to do
            return;
        }

        if (exists)
            this.removeConnection(connection);
        if (valid)
            this.addConnection(connection);
    },

    addConnection: function(connection) {
        // record the connection
        let obj = {
            connection: connection,
            name: connection.get_id(),
            uuid: connection.get_uuid(),
            timestamp: connection._timestamp,
            item: null,
        };
        Util.insertSorted(this._connections, obj, this._connectionSortFunction);

        this._queueCreateSection();
    },

    removeConnection: function(connection) {
        let pos = this._findConnection(connection.get_uuid());
        if (pos == -1) {
            // this connection was never added, nothing to do here
            return;
        }

        let obj = this._connections[pos];
        if (obj.item)
            obj.item.destroy();
        this._connections.splice(pos, 1);

        if (this._connections.length <= 1) {
            // We need to show the automatic connection again
            // (or in the case of NMDeviceWired, we want to hide
            // the only explicit connection)
            this._queueCreateSection();
        }
    },

    _findConnection: function(uuid) {
        for (let i = 0; i < this._connections.length; i++) {
            let obj = this._connections[i];
            if (obj.uuid == uuid)
                return i;
        }
        return -1;
    },

    _connectionSortFunction: function(one, two) {
        if (one.timestamp == two.timestamp)
            return GLib.utf8_collate(one.name, two.name);

        return two.timestamp - one.timestamp;
    },

    destroy: function() {
        this._setDevice(null);

        if (this._deferredWorkId) {
            // Just clear out, the actual removal is handled when the
            // actor is destroyed
            this._deferredWorkId = 0;
        }

        this._clearSection();
        if (this.statusItem)
            this.statusItem.destroy();
        this.section.destroy();
    },

    _setDevice: function(device) {
        if (device) {
            this._device = device;
            this._device._delegate = this;
            this._stateChangedId = this._device.connect('state-changed', Lang.bind(this, this._deviceStateChanged));
            this._activeConnectionChangedId = this._device.connect('notify::active-connection', Lang.bind(this, this._activeConnectionChanged));
        } else if (this._device) {
            this._device._delegate = null;

            if (this._stateChangedId) {
                // Need to go through GObject.Object.prototype because
                // nm_device_disconnect conflicts with g_signal_disconnect
                GObject.Object.prototype.disconnect.call(this._device, this._stateChangedId);
                this._stateChangedId = 0;
            }
            if (this._activeConnectionChangedId) {
                GObject.Object.prototype.disconnect.call(this._device, this._activeConnectionChangedId);
                this._stateChangedId = 0;
            }
            if (this._firmwareChangedId) {
                GObject.Object.prototype.disconnect.call(this._device, this._firmwareChangedId);
                this._firmwareChangedId = 0;
            }

            this._device = null;
        }
    },

    deactivate: function() {
        this._device.disconnect(null);
        return true;
    },

    activate: function() {
        if (this._activeConnection)
            // nothing to do
            return true;

        // If there is only one connection available, use that
        // Otherwise, if no connection is currently configured,
        // try automatic configuration (or summon the config dialog)
        if (this._connections.length == 1) {
            this._client.activate_connection(this._connections[0].connection, this._device || null, null, null);
            return true;
        } else if (this._connections.length == 0) {
            return this._activateAutomaticConnection();
        }

        return false;
    },

    _activateAutomaticConnection: function() {
        let connection = new NetworkManager.Connection();
        this._client.add_and_activate_connection(connection, this._device, null, null);
        return true;
    },

    get connected() {
        return this._device && this._device.state == NetworkManager.DeviceState.ACTIVATED;
    },

    _activeConnectionChanged: function() {
        let activeConnection = this._device.active_connection;

        if (activeConnection == this._activeConnection)
            // nothing to do
            return;

        // remove any UI
        if (this._activeConnectionItem) {
            this._activeConnectionItem.destroy();
            this._activeConnectionItem = null;
        }

        this._activeConnection = activeConnection;

        this._queueCreateSection();
    },

    connectionValid: function(connection) {
        return this._device.connection_valid(connection);
    },

    setEnabled: function(enabled) {
        // do nothing by default, we want to keep the conneciton list visible
        // in the majority of cases (wired, wwan)
    },

    _firmwareChanged: function() {
        if (this._firmwareChangedId) {
            GObject.Object.prototype.disconnect.call(this._device, this._firmwareChangedId);
            this._firmwareChangedId = 0;
        }
        this._substateChanged();
    },

    getStatusLabel: function() {
        if (!this._device)
            return null;

        switch(this._device.state) {
        case NetworkManager.DeviceState.DISCONNECTED:
        case NetworkManager.DeviceState.ACTIVATED:
            return null;
        case NetworkManager.DeviceState.UNMANAGED:
            /* Translators: this is for network devices that are physically present but are not
               under NetworkManager's control (and thus cannot be used in the menu) */
            return _("unmanaged");
        case NetworkManager.DeviceState.DEACTIVATING:
            return _("disconnecting...");
        case NetworkManager.DeviceState.PREPARE:
        case NetworkManager.DeviceState.CONFIG:
        case NetworkManager.DeviceState.IP_CONFIG:
        case NetworkManager.DeviceState.IP_CHECK:
        case NetworkManager.DeviceState.SECONDARIES:
            return _("connecting...");
        case NetworkManager.DeviceState.NEED_AUTH:
            /* Translators: this is for network connections that require some kind of key or password */
            return _("authentication required");
        case NetworkManager.DeviceState.UNAVAILABLE:
            // This state is actually a compound of various states (generically unavailable,
            // firmware missing), that are exposed by different properties (whose state may
            // or may not updated when we receive state-changed).
            if (this._device.firmware_missing) {
                if (!this._firmwareChangedId)
                    this._firmwareChangedId = this._device.connect('notify::firmware-missing', Lang.bind(this, this._firmwareChanged));

                /* Translators: this is for devices that require some kind of firmware or kernel
                   module, which is missing */
                return _("firmware missing");
            }
            /* Translators: this is for a network device that cannot be activated (for example it
               is disabled by rfkill, or it has no coverage */
            return _("unavailable");
        case NetworkManager.DeviceState.FAILED:
            return _("connection failed");
        default:
            log('Device state invalid, is %d'.format(this._device.state));
            return 'invalid';
        }
    },

    syncDescription: function() {
        if (this._device && this._device._description)
            this.statusItem.label.text = this._device._description;
    },

    _queueCreateSection: function() {
        if (this._deferredWorkId) {
            this._clearSection();
            Main.queueDeferredWork(this._deferredWorkId);
        }
    },

    _clearSection: function() {
        // Clear everything
        this.section.removeAll();
        this._activeConnectionItem = null;
        this._overflowItem = null;
        for (let i = 0; i < this._connections.length; i++) {
            this._connections[i].item = null;
        }
    },

    _shouldShowConnectionList: function() {
        return (this._device.state >= NetworkManager.DeviceState.DISCONNECTED);
    },

    _createSection: function() {
        if (!this._shouldShowConnectionList())
            return;

        if (this._activeConnection) {
            this._createActiveConnectionItem();
            this.section.addMenuItem(this._activeConnectionItem);
        }
        if (this._connections.length > 0) {
            let activeOffset = this._activeConnectionItem ? 1 : 0;

            for(let j = 0; j < this._connections.length; ++j) {
                let obj = this._connections[j];
                if (this._activeConnection &&
                    obj.connection == this._activeConnection._connection)
                    continue;
                obj.item = this._createConnectionItem(obj);

                if (j + activeOffset >= NUM_VISIBLE_NETWORKS) {
                    if (!this._overflowItem) {
                        this._overflowItem = new PopupMenu.PopupSubMenuMenuItem(_("More…"));
                        this.section.addMenuItem(this._overflowItem);
                    }
                    this._overflowItem.menu.addMenuItem(obj.item);
                } else
                    this.section.addMenuItem(obj.item);
            }
        }
    },

    _createConnectionItem: function(obj) {
        let connection = obj.connection;
        let item = new PopupMenu.PopupMenuItem(obj.name);

        item.connect('activate', Lang.bind(this, function() {
            this._client.activate_connection(connection, this._device, null, null);
        }));
        return item;
    },

    _createActiveConnectionItem: function() {
        let title;
        let active = this._activeConnection._connection;
        if (active) {
            title = active.get_id();
        } else {
            /* TRANSLATORS: this is the indication that a connection for another logged in user is active,
               and we cannot access its settings (including the name) */
            title = _("Connected (private)");
        }
        this._activeConnectionItem = new PopupMenu.PopupMenuItem(title, { reactive: false });
        this._activeConnectionItem.setOrnament(PopupMenu.Ornament.DOT);
    },

    _deviceStateChanged: function(device, newstate, oldstate, reason) {
        if (newstate == oldstate) {
            log('device emitted state-changed without actually changing state');
            return;
        }

        /* Emit a notification if activation fails, but don't do it
           if the reason is no secrets, as that indicates the user
           cancelled the agent dialog */
        if (newstate == NetworkManager.DeviceState.FAILED &&
            reason != NetworkManager.DeviceStateReason.NO_SECRETS) {
            this.emit('activation-failed', reason);
        }

        this._updateStatusItem();

        this._queueCreateSection();
        this.emit('state-changed');
    },

    _updateStatusItem: function() {
        this.statusItem.setStatus(this.getStatusLabel());
        this.statusItem.setToggleState(this.connected);
    },

    _substateChanged: function() {
        this.statusItem.setStatus(this.getStatusLabel());

        this.emit('state-changed');
    }
});
Signals.addSignalMethods(NMDevice.prototype);

const NMDeviceSimple = new Lang.Class({
    Name: 'NMDeviceSimple',
    Extends: NMDevice,

    _init: function(client, device, connections) {
        this.category = NMConnectionCategory.WIRED;

        this.parent(client, device, connections);
    },

    _createSection: function() {
        this.parent();

        // if we have only one connection (normal or automatic)
        // we hide the connection list, and use the switch to control
        // the device
        // we can do it here because addConnection and removeConnection
        // both call _createSection at some point
        this.section.actor.visible = this._connections.length > 1;
    }
});

const NMDeviceWired = new Lang.Class({
    Name: 'NMDeviceWired',
    Extends: NMDeviceSimple,

    _init: function(client, device, connections) {
        device._description = _("Wired");
        this.category = NMConnectionCategory.WIRED;

        this.parent(client, device, connections);
    },

    getIndicatorIcon: function() {
        if (this._device.active_connection.state == NetworkManager.ActiveConnectionState.ACTIVATING)
            return 'network-wired-acquiring-symbolic';
        else
            return 'network-wired-symbolic';
    },
});

const NMDeviceModem = new Lang.Class({
    Name: 'NMDeviceModem',
    Extends: NMDevice,
    category: NMConnectionCategory.WWAN,

    _init: function(client, device, connections) {
        device._description = _("Mobile broadband");
        this._enabled = true;
        this.mobileDevice = null;

        this._capabilities = device.current_capabilities;
        // Support new ModemManager1 devices
        if (device.udi.indexOf('/org/freedesktop/ModemManager1/Modem') == 0) {
            this.mobileDevice = new ModemManager.BroadbandModem(device.udi, device.current_capabilities);
        } else if (this._capabilities & NetworkManager.DeviceModemCapabilities.GSM_UMTS) {
            this.mobileDevice = new ModemManager.ModemGsm(device.udi);
        } else if (this._capabilities & NetworkManager.DeviceModemCapabilities.CDMA_EVDO) {
            this.mobileDevice = new ModemManager.ModemCdma(device.udi);
        } else if (this._capabilities & NetworkManager.DeviceModemCapabilities.LTE) {
            this.mobileDevice = new ModemManager.ModemGsm(device.udi);
        }

        if (this.mobileDevice) {
            this._operatorNameId = this.mobileDevice.connect('notify::operator-name', Lang.bind(this, function() {
                if (this._operatorItem) {
                    let name = this.mobileDevice.operator_name;
                    if (name) {
                        this._operatorItem.label.text = name;
                        this._operatorItem.actor.show();
                    } else
                        this._operatorItem.actor.hide();
                }
            }));
            this._signalQualityId = this.mobileDevice.connect('notify::signal-quality', Lang.bind(this, function() {
                if (this._operatorItem) {
                    this._operatorItem.setIcon(this._getSignalIcon());
                }
                this.emit('icon-changed');
            }));
        }

        this.parent(client, device, connections);
    },

    setEnabled: function(enabled) {
        this._enabled = enabled;
        if (this.category == NMConnectionCategory.WWAN) {
            if (enabled) {
                // prevent "network unavailable" statuses
                this.statusItem.setStatus(null);
            } else
                this.statusItem.setStatus(this.getStatusLabel());
        }

        this.parent(enabled);
    },

    get connected() {
        return this._enabled && this._device.state == NetworkManager.DeviceState.ACTIVATED;
    },

    destroy: function() {
        if (this._operatorNameId) {
            this.mobileDevice.disconnect(this._operatorNameId);
            this._operatorNameId = 0;
        }
        if (this._signalQualityId) {
            this.mobileDevice.disconnect(this._signalQualityId);
            this._signalQualityId = 0;
        }

        this.parent();
    },

    _getSignalIcon: function() {
        return 'network-cellular-signal-' + signalToIcon(this.mobileDevice.signal_quality) + '-symbolic';
    },

    _createSection: function() {
        if (!this._shouldShowConnectionList())
            return;

        if (this.mobileDevice) {
            // If operator_name is null, just pass the empty string, as the item is hidden anyway
            this._operatorItem = new PopupMenu.PopupImageMenuItem(this.mobileDevice.operator_name || '',
                                                                  this._getSignalIcon(),
                                                                  { reactive: false });
            if (!this.mobileDevice.operator_name)
                this._operatorItem.actor.hide();
            this.section.addMenuItem(this._operatorItem);
        }

        this.parent();
    },

    _clearSection: function() {
        this._operatorItem = null;

        this.parent();
    },

    _activateAutomaticConnection: function() {
        // Mobile wizard is too complex for the shell UI and
        // is handled by the network panel
        Util.spawn(['gnome-control-center', 'network',
                    'connect-3g', this._device.get_path()]);
        return true;
    },

    getIndicatorIcon: function() {
        if (this._device.active_connection.state == NetworkManager.ActiveConnectionState.ACTIVATING)
            return 'network-cellular-acquiring-symbolic';

        if (!this.mobileDevice) {
            // this can happen for bluetooth in PAN mode
            return 'network-cellular-connected-symbolic';
        }

        return this._getSignalIcon();
    }
});

const NMDeviceBluetooth = new Lang.Class({
    Name: 'NMDeviceBluetooth',
    Extends: NMDevice,

    _init: function(client, device, connections) {
        device._description = _("Bluetooth");

        this.category = NMConnectionCategory.WWAN;

        this.parent(client, device, connections);
    },

    _activateAutomaticConnection: function() {
        // FIXME: DUN devices are configured like modems, so
        // We need to spawn the mobile wizard
        // but the network panel doesn't support bluetooth at the moment
        // so we just create an empty connection and hope
        // that this phone supports PAN

        return this.parent();
    },

    getIndicatorIcon: function() {
        if (this._device.active_connection.state == NetworkManager.ActiveConnectionState.ACTIVATING)
            return 'network-wired-acquiring-symbolic';
        else
            return 'network-wired-symbolic';
    },
});

const NMDeviceWireless = new Lang.Class({
    Name: 'NMDeviceWireless',
    Extends: NMDevice,

    _init: function(client, device, connections) {
        this.category = NMConnectionCategory.WIRELESS;

        this._overflowItem = null;
        this._networks = [ ];

        this.parent(client, device, connections);

        let accessPoints = device.get_access_points() || [ ];
        accessPoints.forEach(Lang.bind(this, function(ap) {
            this._accessPointAdded(this._device, ap);
        }));

        this._activeApChanged();
        this._networks.sort(this._networkSortFunction);

        this._apChangedId = device.connect('notify::active-access-point', Lang.bind(this, this._activeApChanged));
        this._apAddedId = device.connect('access-point-added', Lang.bind(this, this._accessPointAdded));
        this._apRemovedId = device.connect('access-point-removed', Lang.bind(this, this._accessPointRemoved));
    },

    destroy: function() {
        if (this._apChangedId) {
            // see above for this HACK
            GObject.Object.prototype.disconnect.call(this._device, this._apChangedId);
            this._apChangedId = 0;
        }

        if (this._apAddedId) {
            GObject.Object.prototype.disconnect.call(this._device, this._apAddedId);
            this._apAddedId = 0;
        }

        if (this._apRemovedId) {
            GObject.Object.prototype.disconnect.call(this._device, this._apRemovedId);
            this._apRemovedId = 0;
        }

        this.parent();
    },

    setEnabled: function(enabled) {
        this.statusItem.actor.visible = enabled;
        this.section.actor.visible = enabled;
    },

    activate: function() {
        if (this._activeConnection)
            // nothing to do
            return true;

        // All possible policy we can have here is just broken
        // NM autoconnects when wifi devices are enabled, and if it
        // didn't, there is a good reason
        // User, pick a connection from the list, thank you
        return false;
    },

    _notifySsidCb: function(accessPoint) {
        if (accessPoint.get_ssid() != null) {
            accessPoint.disconnect(accessPoint._notifySsidId);
            accessPoint._notifySsidId = 0;
            this._accessPointAdded(this._device, accessPoint);
        }
    },

    _activeApChanged: function() {
        this._activeNetwork = null;

        let activeAp = this._device.active_access_point;

        if (activeAp) {
            let res = this._findExistingNetwork(activeAp);

            if (res != null)
                this._activeNetwork = this._networks[res.network];
        }

        // we don't refresh the view here, _activeConnectionChanged will
    },

    _getApSecurityType: function(accessPoint) {
        if (accessPoint._secType)
            return accessPoint._secType;

        let flags = accessPoint.flags;
        let wpa_flags = accessPoint.wpa_flags;
        let rsn_flags = accessPoint.rsn_flags;
        let type;
        if (rsn_flags != NM80211ApSecurityFlags.NONE) {
            /* RSN check first so that WPA+WPA2 APs are treated as RSN/WPA2 */
            if (rsn_flags & NM80211ApSecurityFlags.KEY_MGMT_802_1X)
	        type = NMAccessPointSecurity.WPA2_ENT;
	    else if (rsn_flags & NM80211ApSecurityFlags.KEY_MGMT_PSK)
	        type = NMAccessPointSecurity.WPA2_PSK;
        } else if (wpa_flags != NM80211ApSecurityFlags.NONE) {
            if (wpa_flags & NM80211ApSecurityFlags.KEY_MGMT_802_1X)
                type = NMAccessPointSecurity.WPA_ENT;
            else if (wpa_flags & NM80211ApSecurityFlags.KEY_MGMT_PSK)
	        type = NMAccessPointSecurity.WPA_PSK;
        } else {
            if (flags & NM80211ApFlags.PRIVACY)
                type = NMAccessPointSecurity.WEP;
            else
                type = NMAccessPointSecurity.NONE;
        }

        // cache the found value to avoid checking flags all the time
        accessPoint._secType = type;
        return type;
    },

    _networkSortFunction: function(one, two) {
        let oneHasConnection = one.connections.length != 0;
        let twoHasConnection = two.connections.length != 0;

        // place known connections first
        // (-1 = good order, 1 = wrong order)
        if (oneHasConnection && !twoHasConnection)
            return -1;
        else if (!oneHasConnection && twoHasConnection)
            return 1;

        let oneStrength = one.accessPoints[0].strength;
        let twoStrength = two.accessPoints[0].strength;

        // place stronger connections first
        if (oneStrength != twoStrength)
            return oneStrength < twoStrength ? 1 : -1;

        let oneHasSecurity = one.security != NMAccessPointSecurity.NONE;
        let twoHasSecurity = two.security != NMAccessPointSecurity.NONE;

        // place secure connections first
        // (we treat WEP/WPA/WPA2 the same as there is no way to
        // take them apart from the UI)
        if (oneHasSecurity && !twoHasSecurity)
            return -1;
        else if (!oneHasSecurity && twoHasSecurity)
            return 1;

        // sort alphabetically
        return GLib.utf8_collate(one.ssidText, two.ssidText);
    },

    _networkCompare: function(network, accessPoint) {
        if (!ssidCompare(network.ssid, accessPoint.get_ssid()))
            return false;
        if (network.mode != accessPoint.mode)
            return false;
        if (network.security != this._getApSecurityType(accessPoint))
            return false;

        return true;
    },

    _findExistingNetwork: function(accessPoint) {
        for (let i = 0; i < this._networks.length; i++) {
            let network = this._networks[i];
            for (let j = 0; j < network.accessPoints.length; j++) {
                if (network.accessPoints[j] == accessPoint)
                    return { network: i, ap: j };
            }
        }

        return null;
    },

    _findNetwork: function(accessPoint) {
        if (accessPoint.get_ssid() == null)
            return -1;

        for (let i = 0; i < this._networks.length; i++) {
            if (this._networkCompare(this._networks[i], accessPoint))
                return i;
        }
        return -1;
    },

    _onApStrengthChanged: function(ap) {
        let res = this._findExistingNetwork(ap);
        if (res == null) {
            // Uhm... stale signal?
            return;
        }

        let network = this._networks[res.network];
        network.accessPoints.splice(res.ap, 1);
        Util.insertSorted(network.accessPoints, ap, function(one, two) {
            return two.strength - one.strength;
        });

        this._networks.splice(res.network, 1);
        let newPos = Util.insertSorted(this._networks, network, Lang.bind(this, this._networkSortFunction));

        if (newPos != res.network)
            this._queueCreateSection();
    },

    _accessPointAdded: function(device, accessPoint) {
        if (accessPoint.get_ssid() == null) {
            // This access point is not visible yet
            // Wait for it to get a ssid
            accessPoint._notifySsidId = accessPoint.connect('notify::ssid', Lang.bind(this, this._notifySsidCb));
            return;
        }

        let pos = this._findNetwork(accessPoint);
        let network;
        let needsupdate = false;

        if (pos != -1) {
            network = this._networks[pos];
            if (network.accessPoints.indexOf(accessPoint) != -1) {
                log('Access point was already seen, not adding again');
                return;
            }

            Util.insertSorted(network.accessPoints, accessPoint, function(one, two) {
                return two.strength - one.strength;
            });
            if (network.item)
                network.item.updateBestAP(network.accessPoints[0]);
        } else {
            network = { ssid: accessPoint.get_ssid(),
                        mode: accessPoint.mode,
                        security: this._getApSecurityType(accessPoint),
                        connections: [ ],
                        item: null,
                        accessPoints: [ accessPoint ]
                      };
            network.ssidText = ssidToLabel(network.ssid);
        }
        accessPoint._updateId = accessPoint.connect('notify::strength', Lang.bind(this, this._onApStrengthChanged));

        // check if this enables new connections for this group
        for (let i = 0; i < this._connections.length; i++) {
            let connection = this._connections[i].connection;
            if (accessPoint.connection_valid(connection) &&
                network.connections.indexOf(connection) == -1) {
                network.connections.push(connection);
            }
        }

        if (pos != -1)
            this._networks.splice(pos, 1);
        let newPos = Util.insertSorted(this._networks, network, this._networkSortFunction);

        // Queue an update of the UI if we changed the order
        if (newPos != pos)
            this._queueCreateSection();
    },

    _accessPointRemoved: function(device, accessPoint) {
        if (accessPoint._updateId) {
            accessPoint.disconnect(accessPoint._updateId);
            accessPoint._updateId = 0;
        }

        let res = this._findExistingNetwork(accessPoint);

        if (res == null) {
            log('Removing an access point that was never added');
            return;
        }

        let network = this._networks[res.network];
        network.accessPoints.splice(res.ap, 1);

        if (network.accessPoints.length == 0) {
            if (this._activeNetwork == network)
                this._activeNetwork = null;

            if (network.item)
                network.item.destroy();

            if (this._overflowItem) {
                if (!network.isMore) {
                    // we removed an item in the main menu, and we have a more submenu
                    // we need to extract the first item in more and move it to the submenu

                    let item = this._overflowItem.menu.firstMenuItem;
                    if (item && item._network) {
                        item.destroy();
                        // clear the cycle, and allow the construction of the new item
                        item._network.item = null;

                        this._createNetworkItem(item._network, NUM_VISIBLE_NETWORKS-1);
                    } else {
                        log('The more... menu was existing and empty! This should not happen');
                    }
                }

                // This can happen if the removed connection is from the overflow
                // menu, or if we just moved the last connection out from the menu
                if (this._overflowItem.menu.numMenuItems == 0) {
                    this._overflowItem.destroy();
                    this._overflowItem = null;
                }
            }

            this._networks.splice(res.network, 1);
        } else {
            let okPrev = true, okNext = true;

            if (res.network > 0)
                okPrev = this._networkSortFunction(this._networks[res.network - 1], network) >= 0;
            if (res.network < this._networks.length-1)
                okNext = this._networkSortFunction(this._networks[res.network + 1], network) <= 0;

            if (!okPrev || !okNext)
                this._queueCreateSection();
            else if (network.item)
                network.item.updateBestAP(network.accessPoints[0]);
        }
    },

    _clearSection: function() {
        this.parent();

        for (let i = 0; i < this._networks.length; i++)
            this._networks[i].item = null;
        this._overflowItem = null;
    },

    removeConnection: function(connection) {
        let pos = this._findConnection(connection.get_uuid());
        if (pos == -1) {
            // removing connection that was never added
            return;
        }

        let obj = this._connections[pos];
        this._connections.splice(pos, 1);

        let forceupdate = false;
        for (let i = 0; i < this._networks.length; i++) {
            let network = this._networks[i];
            let connections = network.connections;
            for (let k = 0; k < connections.length; k++) {
                if (connections[k].get_uuid() == connection.get_uuid()) {
                    // remove the connection from the access point group
                    connections.splice(k, 1);
                    forceupdate = forceupdate || connections.length == 0;

                    if (forceupdate)
                        break;

                    if (network.item) {
                        network.item.destroy();
                        network.item = null;
                    }
                    break;
                }
            }
        }

        if (forceupdate) {
            this._networks.sort(this._networkSortFunction);
            this._queueCreateSection();
        }
    },

    addConnection: function(connection) {
        // record the connection
        let obj = {
            connection: connection,
            name: connection.get_id(),
            uuid: connection.get_uuid(),
        };
        this._connections.push(obj);

        // find an appropriate access point
        let forceupdate = false;
        for (let i = 0; i < this._networks.length; i++) {
            let network = this._networks[i];

            // Check if connection is valid for any of these access points
            for (let k = 0; k < network.accessPoints.length; k++) {
                let ap = network.accessPoints[k];
                if (ap.connection_valid(connection)) {
                    network.connections.push(connection);
                    // this potentially changes the sorting order
                    forceupdate = true;
                    break;
                }
            }
        }

        if (forceupdate) {
            this._networks.sort(this._networkSortFunction);
            this._queueCreateSection();
        }
    },

    _createActiveConnectionItem: function() {
        let title;
        if (this._activeConnection && this._activeConnection._connection)
            title = this._activeConnection._connection.get_id();
        else
            title = _("Connected (private)");

        this._activeConnectionItem = new NMNetworkMenuItem(this._device.active_access_point);
        this._activeConnectionItem.setSensitive(false);
        this._activeConnectionItem.setOrnament(PopupMenu.Ornament.DOT);
    },

    _createNetworkItem: function(network, position) {
        if(!network.accessPoints || network.accessPoints.length == 0) {
            // this should not happen, but I have no idea why it happens
            return;
        }

        network.item = new NMNetworkMenuItem(network.accessPoints[0]);
        if(network.connections.length > 0) {
            let connection = network.connections[0];
            network.item._connection = connection;
            network.item.connect('activate', Lang.bind(this, function() {
                let accessPoints = network.accessPoints;
                for (let i = 0; i < accessPoints.length; i++) {
                    if (accessPoints[i].connection_valid(connection)) {
                        this._client.activate_connection(connection, this._device, accessPoints[i].dbus_path, null);
                        break;
                    }
                }
            }));
        } else {
            network.item.connect('activate', Lang.bind(this, function() {
                let accessPoints = network.accessPoints;
                if (   (accessPoints[0]._secType == NMAccessPointSecurity.WPA2_ENT)
                    || (accessPoints[0]._secType == NMAccessPointSecurity.WPA_ENT)) {
                    // 802.1x-enabled APs require further configuration, so they're
                    // handled in gnome-control-center
                    Util.spawn(['gnome-control-center', 'network', 'connect-8021x-wifi',
                                this._device.get_path(), accessPoints[0].dbus_path]);
                } else {
                    let connection = new NetworkManager.Connection();
                    this._client.add_and_activate_connection(connection, this._device, accessPoints[0].dbus_path, null)
                }
            }));
        }
        network.item._network = network;

        if (position < NUM_VISIBLE_NETWORKS) {
            network.isMore = false;
            this.section.addMenuItem(network.item, position);
        } else {
            if (!this._overflowItem) {
                this._overflowItem = new PopupMenu.PopupSubMenuMenuItem(_("More…"));
                this.section.addMenuItem(this._overflowItem);
            }
            this._overflowItem.menu.addMenuItem(network.item, position - NUM_VISIBLE_NETWORKS);
            network.isMore = true;
        }
    },

    _createSection: function() {
        if (!this._shouldShowConnectionList())
            return;

        if (this._activeNetwork) {
            this._createActiveConnectionItem();
            this.section.addMenuItem(this._activeConnectionItem);
        }

        let activeOffset = this._activeConnectionItem ? 1 : 0;

        for(let j = 0; j < this._networks.length; j++) {
            let network = this._networks[j];
            if (network == this._activeNetwork) {
                activeOffset--;
                continue;
            }

            this._createNetworkItem(network, j + activeOffset);
        }
    },

    _updateAccessPoint: function() {
        let ap = this._device.active_access_point;
        if (this._activeAccessPoint == ap)
            return;

        if (this._activeAccessPoint) {
            this._activeAccessPoint.disconnect(this._strengthChangedId);
            this._strengthChangedId = 0;
        }

        this._activeAccessPoint = ap;

        if (this._activeAccessPoint) {
            this._strengthChangedId = this._activeAccessPoint.connect('notify::strength',
                                                                      Lang.bind(this, this._strengthChanged));
        }

        this._syncStatusLabel();
    },

    _strengthChanged: function() {
        this.emit('icon-changed');
    },

    getIndicatorIcon: function() {
        if (this._device.active_connection.state == NetworkManager.ActiveConnectionState.ACTIVATING)
            return 'network-wireless-acquiring-symbolic';

        let ap = this._device.active_access_point;
        if (!ap) {
            if (this._device.mode != NM80211Mode.ADHOC)
                log('An active wireless connection, in infrastructure mode, involves no access point?');

            return 'network-wireless-connected-symbolic';
        }

        return 'network-wireless-signal-' + signalToIcon(ap.strength) + '-symbolic';
    },
});

const NMVPNConnectionItem = new Lang.Class({
    Name: 'NMVPNConnectionItem',

    _init: function(client, connection) {
        this._client = client;
        this.connection = connection;
        this._activeConnection = null;
        this._activeConnectionChangedId = 0;

        this.menuItem = new PopupMenu.PopupSwitchMenuItem(connection.get_id(), false,
                                                          { style_class: 'popup-subtitle-menu-item' });
        this.menuItem.connect('toggled', Lang.bind(this, this._toggle));

        this._sync();
    },

    destroy: function() {
        this.menuItem.destroy();
    },

    isActive: function() {
        if (this._activeConnection == null)
            return false;

        return this._activeConnection.vpn_state == NetworkManager.VPNConnectionState.ACTIVATED;
    },

    _getStatus: function() {
        if (this._activeConnection == null)
            return null;

        switch(this._activeConnection.vpn_state) {
        case NetworkManager.VPNConnectionState.DISCONNECTED:
        case NetworkManager.VPNConnectionState.ACTIVATED:
            return null;
        case NetworkManager.VPNConnectionState.PREPARE:
        case NetworkManager.VPNConnectionState.CONNECT:
        case NetworkManager.VPNConnectionState.IP_CONFIG_GET:
            return _("connecting...");
        case NetworkManager.VPNConnectionState.NEED_AUTH:
            /* Translators: this is for network connections that require some kind of key or password */
            return _("authentication required");
        case NetworkManager.VPNConnectionState.FAILED:
            return _("connection failed");
        default:
            return 'invalid';
        }
    },

    _toggle: function() {
        if (this._activeConnection == null)
            this._client.activate_connection(this.connection, null, null, null);
        else
            this._client.deactivate_connection(this._activeConnection);

        this._sync();
    },

    _sync: function() {
        this.menuItem.setToggleState(this.isActive());
        this.menuItem.setStatus(this._getStatus());
        this.emit('icon-changed');
    },

    _connectionStateChanged: function(ac, newstate, reason) {
        if (newstate == NetworkManager.VPNConnectionState.FAILED &&
            reason != NetworkManager.VPNConnectionStateReason.NO_SECRETS) {
            // FIXME: if we ever want to show something based on reason,
            // we need to convert from NetworkManager.VPNConnectionStateReason
            // to NetworkManager.DeviceStateReason
            this.emit('activation-failed', reason);
        }

        this._sync();
    },

    setActiveConnection: function(activeConnection) {
        if (this._activeConnectionChangedId > 0) {
            this._activeConnection.disconnect(this._activeConnectionChangedId);
            this._activeConnectionChangedId = 0;
        }

        this._activeConnection = activeConnection;

        if (this._activeConnection)
            this._activeConnectionChangedId = this._activeConnection.connect('vpn-state-changed',
                                                                             Lang.bind(this, this._connectionStateChanged));

        this._sync();
    },

    getIndicatorIcon: function() {
        if (this._activeConnection) {
            if (this._activeConnection.state == NetworkManager.ActiveConnectionState.ACTIVATING)
                return 'network-vpn-acquiring-symbolic';
            else
                return 'network-vpn-symbolic';
        } else {
            return '';
        }
    },
});
Signals.addSignalMethods(NMVPNConnectionItem.prototype);

const NMVPNSection = new Lang.Class({
    Name: 'NMVPNSection',
    category: NMConnectionCategory.VPN,

    _init: function(client) {
        this._client = client;
        this._connectionItems = new Hash.Map();

        this.section = new PopupMenu.PopupMenuSection();
    },

    checkConnection: function(connection) {
        if (this._connectionItems.has(connection.get_uuid()))
            return;

        let item = new NMVPNConnectionItem(this._client, connection);

        item.connect('icon-changed', Lang.bind(this, function() {
            this.emit('icon-changed');
        }));
        item.connect('activation-failed', Lang.bind(this, function(item, reason) {
            this.emit('activation-failed', reason);
        }));

        this.section.addMenuItem(item.menuItem);
        this._connectionItems.set(connection.get_uuid(), item);
    },

    removeConnection: function(connection) {
        this._connectionItems.get(connection.get_uuid()).destroy();
        this._connectionItems.delete(connection.get_uuid());
    },

    addActiveConnection: function(activeConnection) {
        let item = this._connectionItems.get(activeConnection._connection.get_uuid());
        item.setActiveConnection(activeConnection);
    },

    removeActiveConnection: function(activeConnection) {
        let item = this._connectionItems.get(activeConnection._connection.get_uuid());
        item.setActiveConnection(null);
    },

    getIndicatorIcon: function() {
        let items = this._connectionItems.values();
        for (let i = 0; i < items.length; i++) {
            let item = items[i];
            let icon = item.getIndicatorIcon();
            if (icon)
                return icon;
        }
        return '';
    },
});
Signals.addSignalMethods(NMVPNSection.prototype);

const NMApplet = new Lang.Class({
    Name: 'NMApplet',
    Extends: PanelMenu.SystemStatusButton,

    _init: function() {
        this.parent('network-offline-symbolic', _('Network'));

        this._vpnIcon = this.addIcon(null);
        this._vpnIcon.hide();

        // Device types
        this._dtypes = { };
        this._dtypes[NetworkManager.DeviceType.ETHERNET] = NMDeviceWired;
        this._dtypes[NetworkManager.DeviceType.WIFI] = NMDeviceWireless;
        this._dtypes[NetworkManager.DeviceType.MODEM] = NMDeviceModem;
        this._dtypes[NetworkManager.DeviceType.BT] = NMDeviceBluetooth;
        this._dtypes[NetworkManager.DeviceType.INFINIBAND] = NMDeviceSimple;
        // TODO: WiMax support

        // Connection types
        this._ctypes = { };
        this._ctypes[NetworkManager.SETTING_WIRELESS_SETTING_NAME] = NMConnectionCategory.WIRELESS;
        this._ctypes[NetworkManager.SETTING_WIRED_SETTING_NAME] = NMConnectionCategory.WIRED;
        this._ctypes[NetworkManager.SETTING_PPPOE_SETTING_NAME] = NMConnectionCategory.WIRED;
        this._ctypes[NetworkManager.SETTING_PPP_SETTING_NAME] = NMConnectionCategory.WIRED;
        this._ctypes[NetworkManager.SETTING_BLUETOOTH_SETTING_NAME] = NMConnectionCategory.WWAN;
        this._ctypes[NetworkManager.SETTING_CDMA_SETTING_NAME] = NMConnectionCategory.WWAN;
        this._ctypes[NetworkManager.SETTING_GSM_SETTING_NAME] = NMConnectionCategory.WWAN;
        this._ctypes[NetworkManager.SETTING_INFINIBAND_SETTING_NAME] = NMConnectionCategory.WIRED;
        this._ctypes[NetworkManager.SETTING_VPN_SETTING_NAME] = NMConnectionCategory.VPN;

        NMClient.Client.new_async(null, Lang.bind(this, this._clientGot));
        NMClient.RemoteSettings.new_async(null, null, Lang.bind(this, this._remoteSettingsGot));
    },

    _clientGot: function(obj, result) {
        this._client = NMClient.Client.new_finish(result);

        this._tryLateInit();
    },

    _remoteSettingsGot: function(obj, result) {
        this._settings = NMClient.RemoteSettings.new_finish(result);

        this._tryLateInit();
    },

    _tryLateInit: function() {
        if (!this._client || !this._settings)
            return;

        this._statusSection = new PopupMenu.PopupMenuSection();
        this._statusItem = new PopupMenu.PopupMenuItem('', { reactive: false });
        this._statusSection.addMenuItem(this._statusItem);
        this._statusSection.addAction(_("Enable networking"), Lang.bind(this, function() {
            this._client.networking_enabled = true;
        }));
        this._statusSection.actor.hide();
        this.menu.addMenuItem(this._statusSection);
        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

        this._activeConnections = [ ];
        this._connections = [ ];

        this._mainConnection = null;
        this._mainConnectionIconChangedId = 0;

        this._nmDevices = [];
        this._devices = { };

        this._devices.wired = {
            section: new PopupMenu.PopupMenuSection(),
            devices: [ ],
        };

        this._devices.wired.section.actor.hide();
        this.menu.addMenuItem(this._devices.wired.section);
        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

        this._devices.wireless = {
            section: new PopupMenu.PopupMenuSection(),
            devices: [ ],
            item: this._makeWirelessToggle()
        };
        this._devices.wireless.section.addMenuItem(this._devices.wireless.item);
        this._devices.wireless.section.actor.hide();
        this.menu.addMenuItem(this._devices.wireless.section);
        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

        this._devices.wwan = {
            section: new PopupMenu.PopupMenuSection(),
            devices: [ ],
        };
        this._devices.wwan.section.actor.hide();
        this.menu.addMenuItem(this._devices.wwan.section);
        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

        this._vpnSection = new NMVPNSection(this._client);
        this._vpnSection.connect('activation-failed', Lang.bind(this, this._onActivationFailed));
        this._vpnSection.connect('icon-changed', Lang.bind(this, this._updateIcon));
        this.menu.addMenuItem(this._vpnSection.section);

        this._readConnections();
        this._readDevices();
        this._syncNMState();

        this._client.connect('notify::manager-running', Lang.bind(this, this._syncNMState));
        this._client.connect('notify::networking-enabled', Lang.bind(this, this._syncNMState));
        this._client.connect('notify::state', Lang.bind(this, this._syncNMState));
        this._client.connect('notify::active-connections', Lang.bind(this, this._syncActiveConnections));
        this._client.connect('device-added', Lang.bind(this, this._deviceAdded));
        this._client.connect('device-removed', Lang.bind(this, this._deviceRemoved));
        this._settings.connect('new-connection', Lang.bind(this, this._newConnection));
    },

    _ensureSource: function() {
        if (!this._source) {
            this._source = new MessageTray.Source(_("Network Manager"),
                                                  'network-transmit-receive');
            this._source.policy = new NotificationDaemon.NotificationApplicationPolicy('gnome-network-panel');

            this._source.connect('destroy', Lang.bind(this, function() {
                this._source = null;
            }));
            Main.messageTray.add(this._source);
        }
    },

    _makeWirelessToggle: function() {
        let item = new NMWirelessSectionTitleMenuItem(this._client);
        item.connect('enabled-changed', Lang.bind(this, function(item, enabled) {
            let devices = this._devices.wireless.devices;
            devices.forEach(function(dev) {
                dev.setEnabled(enabled);
            });
            this._syncSectionTitle('wireless');
        }));
        return item;
    },

    _syncSectionTitle: function(category) {
        let devices = this._devices[category].devices;
        let item = this._devices[category].item;
        let section = this._devices[category].section;

        if (devices.length == 0)
            section.actor.hide();
        else {
            section.actor.show();

            // Sync the relation between the section title
            // item (the one with the airplane mode switch)
            // and the individual device switches
            if (item) {
                if (devices.length == 1) {
                    let dev = devices[0];
                    dev.statusItem.actor.hide();
                    item.updateForDevice(dev);
                } else {
                    devices.forEach(function(dev) {
                        dev.statusItem.actor.show();
                    });

                    // remove status text from the section title item
                    item.updateForDevice(null);
                }
            }
        }
    },

    _readDevices: function() {
        let devices = this._client.get_devices() || [ ];
        for (let i = 0; i < devices.length; ++i) {
            this._deviceAdded(this._client, devices[i], true);
        }
        this._syncDeviceNames();
    },

    _notifyForDevice: function(device, iconName, title, text, urgency) {
        if (device._notification)
            device._notification.destroy();

        /* must call after destroying previous notification,
           or this._source will be cleared */
        this._ensureSource();

        let gicon = new Gio.ThemedIcon({ name: iconName });
        device._notification = new MessageTray.Notification(this._source, title, text,
                                                            { gicon: gicon });
        device._notification.setUrgency(urgency);
        device._notification.setTransient(true);
        device._notification.connect('destroy', function() {
            device._notification = null;
        });
        this._source.notify(device._notification);
    },

    _onActivationFailed: function(device, reason) {
        // XXX: nm-applet has no special text depending on reason
        // but I'm not sure of this generic message
        this._notifyForDevice(device, 'network-error-symbolic',
                              _("Connection failed"),
                              _("Activation of network connection failed"),
                              MessageTray.Urgency.HIGH);
    },

    _syncDeviceNames: function() {
        let names = NMGtk.utils_disambiguate_device_names(this._nmDevices);
        for (let i = 0; i < this._nmDevices.length; i++) {
            let device = this._nmDevices[i];
            device._description = names[i];
            if (device._delegate)
                device._delegate.syncDescription();
        }
    },

    _deviceAdded: function(client, device, skipSyncDeviceNames) {
        if (device._delegate) {
            // already seen, not adding again
            return;
        }

        let wrapperClass = this._dtypes[device.get_device_type()];
        if (wrapperClass) {
            let wrapper = new wrapperClass(this._client, device, this._connections);
            this._addDeviceWrapper(wrapper);

            this._nmDevices.push(device);
            if (!skipSyncDeviceNames)
                this._syncDeviceNames();
        }
    },

    _addDeviceWrapper: function(wrapper) {
        wrapper._activationFailedId = wrapper.connect('activation-failed',
                                                      Lang.bind(this, this._onActivationFailed));
        wrapper._deviceStateChangedId = wrapper.connect('state-changed', Lang.bind(this, function(dev) {
            this._syncSectionTitle(dev.category);
        }));

        let section = this._devices[wrapper.category].section;
        section.addMenuItem(wrapper.statusItem);
        section.addMenuItem(wrapper.section);

        let devices = this._devices[wrapper.category].devices;
        devices.push(wrapper);

        this._syncSectionTitle(wrapper.category);
    },

    _deviceRemoved: function(client, device) {
        let pos = this._nmDevices.indexOf(device);
        if (pos != -1) {
            this._nmDevices.splice(pos, 1);
            this._syncDeviceNames();
        }

        let wrapper = device._delegate;
        if (!wrapper) {
            log('Removing a network device that was not added');
            return;
        }

        this._removeDeviceWrapper(wrapper);
    },

    _removeDeviceWrapper: function(wrapper) {
        wrapper.disconnect(wrapper._activationFailedId);
        wrapper.disconnect(wrapper._deviceStateChangedId);
        wrapper.destroy();

        let devices = this._devices[wrapper.category].devices;
        let pos = devices.indexOf(wrapper);
        devices.splice(pos, 1);

        this._syncSectionTitle(wrapper.category)
    },

    _getSupportedActiveConnections: function() {
        let activeConnections = this._client.get_active_connections() || [ ];
        let supportedConnections = [];

        for (let i = 0; i < activeConnections.length; i++) {
            let devices = activeConnections[i].get_devices();
            if (!devices || !devices[0])
                continue;
            // Ignore connections via unrecognized device types
            if (!this._dtypes[devices[0].device_type])
                continue;

            // Ignore slave connections
            let connectionPath = activeConnections[i].connection;
            let connection = this._settings.get_connection_by_path(connectionPath);

            // connection might be null, if libnm-glib fails to create
            // the object due to version incompatibility, or if the
            // connection is not visible to the current user
            if (connection && this._ignoreConnection(connection))
                continue;

            supportedConnections.push(activeConnections[i]);
        }
        return supportedConnections;
    },

    _syncActiveConnections: function() {
        let closedConnections = [ ];
        let newActiveConnections = this._getSupportedActiveConnections();
        for (let i = 0; i < this._activeConnections.length; i++) {
            let a = this._activeConnections[i];
            if (newActiveConnections.indexOf(a) == -1) // connection is removed
                closedConnections.push(a);
        }

        for (let i = 0; i < closedConnections.length; i++) {
            let a = closedConnections[i];
            if (a._type == NetworkManager.SETTING_VPN_SETTING_NAME)
                this._vpnSection.removeActiveConnection(a);
            if (a._inited) {
                a.disconnect(a._notifyStateId);
                a.disconnect(a._notifyDefaultId);
                a.disconnect(a._notifyDefault6Id);
                a._inited = false;
            }
        }

        if (this._mainConnectionIconChangedId > 0) {
            this._mainConnection._primaryDevice.disconnect(this._mainConnectionIconChangedId);
            this._mainConnectionIconChangedId = 0;
        }

        this._activeConnections = newActiveConnections;
        this._mainConnection = null;

        let activating = null;
        let default_ip4 = null;
        let default_ip6 = null;
        let active_any = null;
        for (let i = 0; i < this._activeConnections.length; i++) {
            let a = this._activeConnections[i];

            if (!a._inited) {
                a._notifyDefaultId = a.connect('notify::default', Lang.bind(this, this._syncActiveConnections));
                a._notifyDefault6Id = a.connect('notify::default6', Lang.bind(this, this._syncActiveConnections));
                a._notifyStateId = a.connect('notify::state', Lang.bind(this, this._notifyActivated));

                a._inited = true;
            }

            if (!a._connection) {
                a._connection = this._settings.get_connection_by_path(a.connection);

                if (a._connection) {
                    a._type = a._connection._type;
                    a._section = this._ctypes[a._type];
                } else {
                    a._connection = null;
                    a._type = null;
                    a._section = null;
                    log('Cannot find connection for active (or connection cannot be read)');
                }
            }

            if (a['default'])
                default_ip4 = a;
            if (a.default6)
                default_ip6 = a;

            if (a.state == NetworkManager.ActiveConnectionState.ACTIVATING)
                activating = a;
            else if (a.state == NetworkManager.ActiveConnectionState.ACTIVATED)
                active_any = a;

            if (!a._primaryDevice) {
                if (a._type != NetworkManager.SETTING_VPN_SETTING_NAME) {
                    // This list is guaranteed to have one device in it.
                    a._primaryDevice = a.get_devices()[0]._delegate;
                } else {
                    a._primaryDevice = this._vpnSection;
                    this._vpnSection.addActiveConnection(a);
                }

                if (a.state == NetworkManager.ActiveConnectionState.ACTIVATED
                    && a._primaryDevice && a._primaryDevice._notification) {
                    a._primaryDevice._notification.destroy();
                    a._primaryDevice._notification = null;
                }
            }
        }

        this._mainConnection = activating || default_ip4 || default_ip6 || active_any || null;

        if (this._mainConnection) {
            let dev = this._mainConnection._primaryDevice;
            this._mainConnectionIconChangedId = dev.connect('icon-changed', Lang.bind(this, this._updateIcon));
            this._updateIcon();
        }
    },

    _notifyActivated: function(activeConnection) {
        if (activeConnection.state == NetworkManager.ActiveConnectionState.ACTIVATED
            && activeConnection._primaryDevice && activeConnection._primaryDevice._notification) {
            activeConnection._primaryDevice._notification.destroy();
            activeConnection._primaryDevice._notification = null;
        }

        this._syncActiveConnections();
    },

    _ignoreConnection: function(connection) {
        let setting = connection.get_setting_connection();
        if (!setting)
            return true;

        // Ignore slave connections
        if (setting.get_master())
            return true;

        return false;
    },

    _addConnection: function(connection) {
        if (this._ignoreConnection(connection))
            return;
        if (connection._updatedId) {
            // connection was already seen
            return;
        }

        connection._removedId = connection.connect('removed', Lang.bind(this, this._connectionRemoved));
        connection._updatedId = connection.connect('updated', Lang.bind(this, this._updateConnection));

        this._updateConnection(connection);
        this._connections.push(connection);
    },

    _readConnections: function() {
        let connections = this._settings.list_connections();
        connections.forEach(Lang.bind(this, this._addConnection));
    },

    _newConnection: function(settings, connection) {
        this._addConnection(connection);
        this._syncActiveConnections();
    },

    _connectionRemoved: function(connection) {
        let pos = this._connections.indexOf(connection);
        if (pos != -1)
            this._connections.splice(connection, 1);

        let section = connection._section;

        if (section == NMConnectionCategory.INVALID)
            return;

        if (section == NMConnectionCategory.VPN) {
            this._vpnSection.removeConnection(connection);
        } else {
            let devices = this._devices[section].devices;
            for (let i = 0; i < devices.length; i++)
                devices[i].removeConnection(connection);
        }

        connection.disconnect(connection._removedId);
        connection.disconnect(connection._updatedId);
        connection._removedId = connection._updatedId = 0;
    },

    _updateConnection: function(connection) {
        let connectionSettings = connection.get_setting_by_name(NetworkManager.SETTING_CONNECTION_SETTING_NAME);
        connection._type = connectionSettings.type;
        connection._section = this._ctypes[connection._type] || NMConnectionCategory.INVALID;
        connection._timestamp = connectionSettings.timestamp;

        let section = connection._section;

        if (section == NMConnectionCategory.INVALID)
            return;

        if (section == NMConnectionCategory.VPN) {
            this._vpnSection.checkConnection(connection);
        } else {
            let devices = this._devices[section].devices;
            for (let i = 0; i < devices.length; i++) {
                devices[i].checkConnection(connection);
            }
        }
    },

    _hideDevices: function() {
        this._devicesHidden = true;

        for (let category in this._devices)
            this._devices[category].section.actor.hide();
    },

    _showNormal: function() {
        if (!this._devicesHidden) // nothing to do
            return;
        this._devicesHidden = false;

        this._statusSection.actor.hide();

        this._syncSectionTitle(NMConnectionCategory.WIRED);
        this._syncSectionTitle(NMConnectionCategory.WIRELESS);
        this._syncSectionTitle(NMConnectionCategory.WWAN);
    },

    _syncNMState: function() {
        this.mainIcon.visible = this._client.manager_running;
        this.actor.visible = this.mainIcon.visible;

        if (!this._client.networking_enabled) {
            this.setIcon('network-offline-symbolic');
            this._hideDevices();
            this._statusItem.label.text = _("Networking is disabled");
            this._statusSection.actor.show();
            return;
        }

        this._showNormal();
        this._syncActiveConnections();
    },

    _updateIcon: function() {
        let hasApIcon = false;
        let hasMobileIcon = false;

        if (!this._mainConnection) {
            this.setIcon('network-offline-symbolic');
        } else {
            let dev = this._mainConnection._primaryDevice;
            if (!dev) {
                log('Active connection with no primary device?');
                return;
            }
            this.setIcon(dev.getIndicatorIcon());
        }

        this._vpnIcon.icon_name = this._vpnSection.getIndicatorIcon();
    }
});