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

const Animation = imports.ui.animation;
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
const MessageTray = imports.ui.messageTray;
const ModalDialog = imports.ui.modalDialog;
const ModemManager = imports.misc.modemManager;
const Rfkill = imports.ui.status.rfkill;
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'];

const PortalHelperResult = {
    CANCELLED: 0,
    COMPLETED: 1,
    RECHECK: 2
};

const PortalHelperIface = '<node> \
<interface name="org.gnome.Shell.PortalHelper"> \
<method name="Authenticate"> \
    <arg type="o" direction="in" name="connection" /> \
    <arg type="s" direction="in" name="url" /> \
    <arg type="u" direction="in" name="timestamp" /> \
</method> \
<method name="Close"> \
    <arg type="o" direction="in" name="connection" /> \
</method> \
<method name="Refresh"> \
    <arg type="o" direction="in" name="connection" /> \
</method> \
<signal name="Done"> \
    <arg type="o" name="connection" /> \
    <arg type="u" name="result" /> \
</signal> \
</interface> \
</node>';
const PortalHelperProxy = Gio.DBusProxy.makeProxyWrapper(PortalHelperIface);

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;
}

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;
}

function ensureActiveConnectionProps(active, settings) {
    if (!active._connection) {
        active._connection = settings.get_connection_by_path(active.connection);

        // This list is guaranteed to have only one device in it.
        let device = active.get_devices()[0]._delegate;
        active._primaryDevice = device;
    }
}

function createSettingsAction(label, device) {
    let item = new PopupMenu.PopupMenuItem(label);

    item.connect('activate', function() {
        Util.spawnApp(['gnome-control-center', 'network', 'show-device',
                       device.get_path()]);
    });

    return item;
}

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

    _init: function(section, connection) {
        this._section = section;
        this._connection = connection;
        this._activeConnection = null;
        this._activeConnectionChangedId = 0;

        this._buildUI();
        this._sync();
    },

    _buildUI: function() {
        this.labelItem = new PopupMenu.PopupMenuItem('');
        this.labelItem.connect('activate', Lang.bind(this, this._toggle));

        this.radioItem = new PopupMenu.PopupMenuItem(this._connection.get_id(), false);
        this.radioItem.connect('activate', Lang.bind(this, this._activate));
    },

    destroy: function() {
        this.labelItem.destroy();
        this.radioItem.destroy();
    },

    updateForConnection: function(connection) {
        // connection should always be the same object
        // (and object path) as this._connection, but
        // this can be false if NetworkManager was restarted
        // and picked up connections in a different order
        // Just to be safe, we set it here again

        this._connection = connection;
        this.radioItem.label.text = connection.get_id();
        this._sync();
        this.emit('name-changed');
    },

    getName: function() {
        return this._connection.get_id();
    },

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

        return this._activeConnection.state <= NetworkManager.ActiveConnectionState.ACTIVATED;
    },

    _sync: function() {
        let isActive = this.isActive();
        this.labelItem.label.text = isActive ? _("Turn Off") : this._section.getConnectLabel();
        this.radioItem.setOrnament(isActive ? PopupMenu.Ornament.DOT : PopupMenu.Ornament.NONE);
        this.emit('icon-changed');
    },

    _toggle: function() {
        if (this._activeConnection == null)
            this._section.activateConnection(this._connection);
        else
            this._section.deactivateConnection(this._activeConnection);

        this._sync();
    },

    _activate: function() {
        if (this._activeConnection == null)
            this._section.activateConnection(this._connection);

        this._sync();
    },

    _connectionStateChanged: function(ac, newstate, 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('notify::state',
                                                                             Lang.bind(this, this._connectionStateChanged));

        this._sync();
    },
});
Signals.addSignalMethods(NMConnectionItem.prototype);

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

    _init: function(client) {
        this._client = client;

        this._connectionItems = new Map();
        this._connections = [];

        this._labelSection = new PopupMenu.PopupMenuSection();
        this._radioSection = new PopupMenu.PopupMenuSection();

        this.item = new PopupMenu.PopupSubMenuMenuItem('', true);
        this.item.menu.addMenuItem(this._labelSection);
        this.item.menu.addMenuItem(this._radioSection);

        this._notifyConnectivityId = this._client.connect('notify::connectivity', Lang.bind(this, this._iconChanged));
    },

    destroy: function() {
        if (this._notifyConnectivityId != 0) {
            this._client.disconnect(this._notifyConnectivityId);
            this._notifyConnectivityId = 0;
        }

        this.item.destroy();
    },

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

    _sync: function() {
        let nItems = this._connectionItems.size;

        this._radioSection.actor.visible = (nItems > 1);
        this._labelSection.actor.visible = (nItems == 1);

        this.item.status.text = this._getStatus();
        this.item.icon.icon_name = this._getMenuIcon();

        // desc can be undefined at cold-plug, before we called
        // NMGtk.disambiguate_device_names() at least once
        let desc = this._getDescription();
        if (desc)
            this.item.label.text = desc;
        else
            this.item.label.text = '';
    },

    _getMenuIcon: function() {
        return this.getIndicatorIcon();
    },

    getConnectLabel: function() {
        return _("Connect");
    },

    _connectionValid: function(connection) {
        return true;
    },

    _connectionSortFunction: function(one, two) {
        return GLib.utf8_collate(one.get_id(), two.get_id());
    },

    _makeConnectionItem: function(connection) {
        return new NMConnectionItem(this, connection);
    },

    checkConnection: function(connection) {
        if (!this._connectionValid(connection))
            return;

        // This function is called everytime connection is added or updated
        // In the usual case, we already added this connection and UUID
        // didn't change. So we need to check if we already have an item,
        // and update it for properties in the connection that changed
        // (the only one we care about is the name)
        // But it's also possible we didn't know about this connection
        // (eg, during coldplug, or because it was updated and suddenly
        // it's valid for this device), in which case we add a new item

        let item = this._connectionItems.get(connection.get_uuid());
        if (item)
            item.updateForConnection(connection);
        else
            this._addConnection(connection);
    },

    _addConnection: function(connection) {
        let item = this._makeConnectionItem(connection);
        if (!item)
            return;

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

        let pos = Util.insertSorted(this._connections, connection, Lang.bind(this, this._connectionSortFunction));
        this._labelSection.addMenuItem(item.labelItem, pos);
        this._radioSection.addMenuItem(item.radioItem, pos);
        this._connectionItems.set(connection.get_uuid(), item);
        this._sync();
    },

    removeConnection: function(connection) {
        let uuid = connection.get_uuid();
        let item = this._connectionItems.get(uuid);
        if (item == undefined)
            return;

        item.destroy();
        this._connectionItems.delete(uuid);

        let pos = this._connections.indexOf(connection);
        this._connections.splice(pos, 1);

        this._sync();
    },
});
Signals.addSignalMethods(NMConnectionSection.prototype);

const NMConnectionDevice = new Lang.Class({
    Name: 'NMConnectionDevice',
    Extends: NMConnectionSection,
    Abstract: true,

    _init: function(client, device, settings) {
        this.parent(client);
        this._device = device;
        this._settings = settings;

        this._autoConnectItem = this.item.menu.addAction(_("Connect"), Lang.bind(this, this._autoConnect));
        this._deactivateItem = this._radioSection.addAction(_("Turn Off"), Lang.bind(this, this.deactivateConnection));

        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));
    },

    _canReachInternet: function() {
        if (this._client.primary_connection != this._device.active_connection)
            return true;

        return this._client.connectivity == NetworkManager.ConnectivityState.FULL;
    },

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

    destroy: function() {
        if (this._stateChangedId) {
            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._activeConnectionChangedId = 0;
        }

        this.parent();
    },

    _activeConnectionChanged: function() {
        if (this._activeConnection) {
            let item = this._connectionItems.get(this._activeConnection._connection.get_uuid());
            item.setActiveConnection(null);
        }

        this._activeConnection = this._device.active_connection;

        if (this._activeConnection) {
            ensureActiveConnectionProps(this._activeConnection, this._settings);
            let item = this._connectionItems.get(this._activeConnection._connection.get_uuid());
            item.setActiveConnection(this._activeConnection);
        }
    },

    _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._sync();
    },

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

    activateConnection: function(connection) {
        this._client.activate_connection(connection, this._device, null, null);
    },

    deactivateConnection: function(activeConnection) {
        this._device.disconnect(null);
    },

    setDeviceDescription: function(desc) {
        this._description = desc;
        this._sync();
    },

    _getDescription: function() {
        return this._description;
    },

    _sync: function() {
        let nItems = this._connectionItems.size;
        this._autoConnectItem.actor.visible = (nItems == 0);
        this._deactivateItem.actor.visible = this._device.state > NetworkManager.DeviceState.DISCONNECTED;
        this.parent();
    },

    _getStatus: function() {
        if (!this._device)
            return '';

        switch(this._device.state) {
        case NetworkManager.DeviceState.DISCONNECTED:
            return _("Off");
        case NetworkManager.DeviceState.ACTIVATED:
            return _("Connected");
        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) {
                /* 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';
        }
    },
});

const NMDeviceWired = new Lang.Class({
    Name: 'NMDeviceWired',
    Extends: NMConnectionDevice,
    category: NMConnectionCategory.WIRED,

    _init: function(client, device, settings) {
        this.parent(client, device, settings);

        this.item.menu.addMenuItem(createSettingsAction(_("Wired Settings"), device));
    },

    _hasCarrier: function() {
        if (this._device instanceof NMClient.DeviceEthernet)
            return this._device.carrier;
        else
            return true;
    },

    _sync: function() {
        this.item.actor.visible = this._hasCarrier();
        this.parent();
    },

    getIndicatorIcon: function() {
        if (this._device.active_connection) {
            let state = this._device.active_connection.state;

            if (state == NetworkManager.ActiveConnectionState.ACTIVATING) {
                return 'network-wired-acquiring-symbolic';
            } else if (state == NetworkManager.ActiveConnectionState.ACTIVATED) {
                if (this._canReachInternet())
                    return 'network-wired-symbolic';
                else
                    return 'network-wired-no-route-symbolic';
            } else {
                return 'network-wired-disconnected-symbolic';
            }
        } else
            return 'network-wired-disconnected-symbolic';
    }
});

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

    _init: function(client, device, settings) {
        this.parent(client, device, settings);

        this.item.menu.addMenuItem(createSettingsAction(_("Mobile Broadband Settings"), device));

        this._mobileDevice = null;

        let capabilities = device.current_capabilities;
        if (device.udi.indexOf('/org/freedesktop/ModemManager1/Modem') == 0)
            this._mobileDevice = new ModemManager.BroadbandModem(device.udi, capabilities);
        else if (capabilities & NetworkManager.DeviceModemCapabilities.GSM_UMTS)
            this._mobileDevice = new ModemManager.ModemGsm(device.udi);
        else if (capabilities & NetworkManager.DeviceModemCapabilities.CDMA_EVDO)
            this._mobileDevice = new ModemManager.ModemCdma(device.udi);
        else if (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, this._sync));
            this._signalQualityId = this._mobileDevice.connect('notify::signal-quality', Lang.bind(this, function() {
                this._iconChanged();
            }));
        }
    },

    _autoConnect: function() {
        Util.spawn(['gnome-control-center', 'network',
                    'connect-3g', this._device.get_path()]);
    },

    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();
    },

    _getStatus: function() {
        if (!this._client.wwan_hardware_enabled)
            return _("Hardware Disabled");
        else if (!this._client.wwan_enabled)
            /* Translators: this is for a network device that cannot be activated
               because it's disabled by rfkill (airplane mode) */
            return _("Disabled");
        else if (this._device.state == NetworkManager.DeviceState.ACTIVATED &&
                 this._mobileDevice && this._mobileDevice.operator_name)
            return this._mobileDevice.operator_name;
        else
            return this.parent();
    },

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

            return this._getSignalIcon();
        } else {
            return 'network-cellular-signal-none-symbolic';
        }
    },

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

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

    _init: function(client, device, settings) {
        this.parent(client, device, settings);

        this.item.menu.addMenuItem(createSettingsAction(_("Mobile Broadband Settings"), device));
    },

    _getDescription: function() {
        return this._device.name;
    },

    getConnectLabel: function() {
        return _("Use as Internet connection");
    },

    getIndicatorIcon: function() {
        if (this._device.active_connection) {
            let state = this._device.active_connection.state;
            if (state == NetworkManager.ActiveConnectionState.ACTIVATING)
                return 'network-cellular-acquiring-symbolic';
            else if (state == NetworkManager.ActiveConnectionState.ACTIVATED)
                return 'network-cellular-connected-symbolic';
            else
                return 'network-cellular-signal-none-symbolic';
        } else {
            return 'network-cellular-signal-none-symbolic';
        }
    }
});

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

    _init: function(network) {
        this._network = network;
        this._ap = network.accessPoints[0];

        this.actor = new St.BoxLayout({ style_class: 'nm-dialog-item',
                                        can_focus: true,
                                        reactive: true });
        this.actor.connect('key-focus-in', Lang.bind(this, function() {
            this.emit('selected');
        }));
        let action = new Clutter.ClickAction();
        action.connect('clicked', Lang.bind(this, function() {
            this.actor.grab_key_focus();
        }));
        this.actor.add_action(action);

        let title = ssidToLabel(this._ap.get_ssid());
        this._label = new St.Label({ text: title });

        this.actor.label_actor = this._label;
        this.actor.add(this._label, { x_align: St.Align.START });

        this._selectedIcon = new St.Icon({ style_class: 'nm-dialog-icon',
                                           icon_name: 'object-select-symbolic' });
        this.actor.add(this._selectedIcon);

        this._icons = new St.BoxLayout({ style_class: 'nm-dialog-icons' });
        this.actor.add(this._icons, { expand: true, x_fill: false, x_align: St.Align.END });

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

        this._signalIcon = new St.Icon({ style_class: 'nm-dialog-icon' });
        this._icons.add_actor(this._signalIcon);

        this._sync();
    },

    _sync: function() {
        this._signalIcon.icon_name = this._getSignalIcon();
    },

    updateBestAP: function(ap) {
        this._ap = ap;
        this._sync();
    },

    setActive: function(isActive) {
        this._selectedIcon.opacity = isActive ? 255 : 0;
    },

    _getSignalIcon: function() {
        if (this._ap.mode == NM80211Mode.ADHOC)
            return 'network-workgroup-symbolic';
        else
            return 'network-wireless-signal-' + signalToIcon(this._ap.strength) + '-symbolic';
    }
});
Signals.addSignalMethods(NMWirelessDialogItem.prototype);

const NMWirelessDialog = new Lang.Class({
    Name: 'NMWirelessDialog',
    Extends: ModalDialog.ModalDialog,

    _init: function(client, device, settings) {
        this.parent({ styleClass: 'nm-dialog' });

        this._client = client;
        this._device = device;

        this._wirelessEnabledChangedId = this._client.connect('notify::wireless-enabled',
                                                              Lang.bind(this, this._syncView));

        this._rfkill = Rfkill.getRfkillManager();
        this._airplaneModeChangedId = this._rfkill.connect('airplane-mode-changed',
                                                           Lang.bind(this, this._syncView));

        this._networks = [];
        this._buildLayout();

        let connections = settings.list_connections();
        this._connections = connections.filter(Lang.bind(this, function(connection) {
            return device.connection_valid(connection);
        }));

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

        // accessPointAdded will also create dialog items
        let accessPoints = device.get_access_points() || [ ];
        accessPoints.forEach(Lang.bind(this, function(ap) {
            this._accessPointAdded(this._device, ap);
        }));

        this._selectedNetwork = null;
        this._activeApChanged();
        this._updateSensitivity();
        this._syncView();

        if (accessPoints.length == 0) {
            /* If there are no visible access points, request a scan */
            this._device.request_scan_simple(null);
        }
    },

    destroy: function() {
        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;
        }
        if (this._activeApChangedId) {
            GObject.Object.prototype.disconnect.call(this._device, this._activeApChangedId);
            this._activeApChangedId = 0;
        }
        if (this._wirelessEnabledChangedId) {
            this._client.disconnect(this._wirelessEnabledChangedId);
            this._wirelessEnabledChangedId = 0;
        }
        if (this._airplaneModeChangedId) {
            this._rfkill.disconnect(this._airplaneModeChangedId);
            this._airplaneModeChangedId = 0;
        }

        this.parent();
    },

    _activeApChanged: function() {
        if (this._activeNetwork)
            this._activeNetwork.item.setActive(false);

        this._activeNetwork = null;
        if (this._device.active_access_point) {
            let idx = this._findNetwork(this._device.active_access_point);
            if (idx >= 0)
                this._activeNetwork = this._networks[idx];
        }

        if (this._activeNetwork)
            this._activeNetwork.item.setActive(true);
        this._updateSensitivity();
    },

    _updateSensitivity: function() {
        let connectSensitive = this._client.wireless_enabled && this._selectedNetwork && (this._selectedNetwork != this._activeNetwork);
        this._connectButton.reactive = connectSensitive;
        this._connectButton.can_focus = connectSensitive;
    },

    _syncView: function() {
        if (this._rfkill.airplaneMode) {
            this._airplaneBox.show();

            this._airplaneIcon.icon_name = 'airplane-mode-symbolic';
            this._airplaneHeadline.text = _("Airplane Mode is On");
            this._airplaneText.text = _("Wi-Fi is disabled when airplane mode is on.");
            this._airplaneButton.label = _("Turn Off Airplane Mode");

            this._airplaneButton.visible = !this._rfkill.hwAirplaneMode;
            this._airplaneInactive.visible = this._rfkill.hwAirplaneMode;
            this._noNetworksBox.hide();
        } else if (!this._client.wireless_enabled) {
            this._airplaneBox.show();

            this._airplaneIcon.icon_name = 'dialog-information-symbolic';
            this._airplaneHeadline.text = _("Wi-Fi is Off");
            this._airplaneText.text = _("Wi-Fi needs to be turned on in order to connect to a network.");
            this._airplaneButton.label = _("Turn On Wi-Fi");

            this._airplaneButton.show();
            this._airplaneInactive.hide();
            this._noNetworksBox.hide();
        } else {
            this._airplaneBox.hide();

            this._noNetworksBox.visible = (this._networks.length == 0);
        }

        if (this._noNetworksBox.visible)
            this._noNetworksSpinner.play();
        else
            this._noNetworksSpinner.stop();
    },

    _buildLayout: function() {
        let headline = new St.BoxLayout({ style_class: 'nm-dialog-header-hbox' });

        let icon = new St.Icon({ style_class: 'nm-dialog-header-icon',
                                 icon_name: 'network-wireless-signal-excellent-symbolic' });

        let titleBox = new St.BoxLayout({ vertical: true });
        let title = new St.Label({ style_class: 'nm-dialog-header',
                                   text: _("Wi-Fi Networks") });
        let subtitle = new St.Label({ style_class: 'nm-dialog-subheader',
                                      text: _("Select a network") });
        titleBox.add(title);
        titleBox.add(subtitle);

        headline.add(icon);
        headline.add(titleBox);

        this.contentLayout.style_class = 'nm-dialog-content';
        this.contentLayout.add(headline);

        this._stack = new St.Widget({ layout_manager: new Clutter.BinLayout() });

        this._itemBox = new St.BoxLayout({ vertical: true });
        this._scrollView = new St.ScrollView({ style_class: 'nm-dialog-scroll-view' });
        this._scrollView.set_x_expand(true);
        this._scrollView.set_y_expand(true);
        this._scrollView.set_policy(Gtk.PolicyType.NEVER,
                                    Gtk.PolicyType.AUTOMATIC);
        this._scrollView.add_actor(this._itemBox);
        this._stack.add_child(this._scrollView);

        this._noNetworksBox = new St.BoxLayout({ vertical: true,
                                                 style_class: 'no-networks-box',
                                                 x_align: Clutter.ActorAlign.CENTER,
                                                 y_align: Clutter.ActorAlign.CENTER });

        this._noNetworksSpinner = new Animation.AnimatedIcon(global.datadir + '/theme/process-working.svg', 24, 24);
        this._noNetworksBox.add_actor(this._noNetworksSpinner.actor);
        this._noNetworksBox.add_actor(new St.Label({ style_class: 'no-networks-label',
                                                     text: _("No Networks") }));
        this._stack.add_child(this._noNetworksBox);

        this._airplaneBox = new St.BoxLayout({ vertical: true,
                                               style_class: 'nm-dialog-airplane-box',
                                               x_align: Clutter.ActorAlign.CENTER,
                                               y_align: Clutter.ActorAlign.CENTER });
        this._airplaneIcon = new St.Icon({ icon_size: 48 });
        this._airplaneHeadline = new St.Label({ style_class: 'nm-dialog-airplane-headline' });
        this._airplaneText = new St.Label({ style_class: 'nm-dialog-airplane-text' });

        let airplaneSubStack = new St.Widget({ layout_manager: new Clutter.BinLayout });
        this._airplaneButton = new St.Button({ style_class: 'modal-dialog-button' });
        this._airplaneButton.connect('clicked', Lang.bind(this, function() {
            if (this._rfkill.airplaneMode)
                this._rfkill.airplaneMode = false;
            else
                this._client.wireless_enabled = true;
        }));
        airplaneSubStack.add_actor(this._airplaneButton);
        this._airplaneInactive = new St.Label({ style_class: 'nm-dialog-airplane-text',
                                                text: _("Use hardware switch to turn off") });
        airplaneSubStack.add_actor(this._airplaneInactive);

        this._airplaneBox.add(this._airplaneIcon, { x_align: St.Align.MIDDLE });
        this._airplaneBox.add(this._airplaneHeadline, { x_align: St.Align.MIDDLE });
        this._airplaneBox.add(this._airplaneText, { x_align: St.Align.MIDDLE });
        this._airplaneBox.add(airplaneSubStack, { x_align: St.Align.MIDDLE });
        this._stack.add_child(this._airplaneBox);

        this.contentLayout.add(this._stack, { expand: true });

        this._disconnectButton = this.addButton({ action: Lang.bind(this, this.close),
                                                  label: _("Cancel"),
                                                  key: Clutter.Escape });
        this._connectButton = this.addButton({ action: Lang.bind(this, this._connect),
                                               label: _("Connect"),
                                               key: Clutter.Return },
                                             { expand: true,
                                               x_fill: false,
                                               x_align: St.Align.END });
    },

    _connect: function() {
        let network = this._selectedNetwork;
        if (network.connections.length > 0) {
            let connection = network.connections[0];
            this._client.activate_connection(connection, this._device, null, null);
        } else {
            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)
            }
        }

        this.close();
    },

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

    _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;
    },

    _checkConnections: function(network, accessPoint) {
        this._connections.forEach(function(connection) {
            if (accessPoint.connection_valid(connection) &&
                network.connections.indexOf(connection) == -1) {
                network.connections.push(connection);
            }
        });
    },

    _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;

        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;
            });
            network.item.updateBestAP(network.accessPoints[0]);
            this._checkConnections(network, accessPoint);

            this._resortItems();
        } else {
            network = { ssid: accessPoint.get_ssid(),
                        mode: accessPoint.mode,
                        security: this._getApSecurityType(accessPoint),
                        connections: [ ],
                        item: null,
                        accessPoints: [ accessPoint ]
                      };
            network.ssidText = ssidToLabel(network.ssid);
            this._checkConnections(network, accessPoint);

            let newPos = Util.insertSorted(this._networks, network, this._networkSortFunction);
            this._createNetworkItem(network);
            this._itemBox.insert_child_at_index(network.item.actor, newPos);
        }

        this._syncView();
    },

    _accessPointRemoved: function(device, accessPoint) {
        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) {
            network.item.actor.destroy();
            this._networks.splice(res.network, 1);
        } else {
            network.item.updateBestAP(network.accessPoints[0]);
            this._resortItems();
        }

        this._syncView();
    },

    _resortItems: function() {
        let adjustment = this._scrollView.vscroll.adjustment;
        let scrollValue = adjustment.value;

        this._itemBox.remove_all_children();
        this._networks.forEach(Lang.bind(this, function(network) {
            this._itemBox.add_child(network.item.actor);
        }));

        adjustment.value = scrollValue;
    },

    _selectNetwork: function(network) {
        if (this._selectedNetwork)
            this._selectedNetwork.item.actor.remove_style_pseudo_class('selected');

        this._selectedNetwork = network;
        this._updateSensitivity();

        if (this._selectedNetwork)
            this._selectedNetwork.item.actor.add_style_pseudo_class('selected');
    },

    _createNetworkItem: function(network) {
        network.item = new NMWirelessDialogItem(network);
        network.item.setActive(network == this._selectedNetwork);
        network.item.connect('selected', Lang.bind(this, function() {
            Util.ensureActorVisibleInScrollView(this._scrollView, network.item.actor);
            this._selectNetwork(network);
        }));
    },
});

const NMDeviceWireless = new Lang.Class({
    Name: 'NMDeviceWireless',
    category: NMConnectionCategory.WIRELESS,

    _init: function(client, device, settings) {
        this._client = client;
        this._device = device;
        this._settings = settings;

        this._description = '';

        this.item = new PopupMenu.PopupSubMenuMenuItem('', true);
        this.item.menu.addAction(_("Select Network"), Lang.bind(this, this._showDialog));

        this._toggleItem = new PopupMenu.PopupMenuItem('');
        this._toggleItem.connect('activate', Lang.bind(this, this._toggleWifi));
        this.item.menu.addMenuItem(this._toggleItem);

        this.item.menu.addMenuItem(createSettingsAction(_("Wi-Fi Settings"), device));

        this._wirelessEnabledChangedId = this._client.connect('notify::wireless-enabled', Lang.bind(this, this._sync));
        this._wirelessHwEnabledChangedId = this._client.connect('notify::wireless-hardware-enabled', Lang.bind(this, this._sync));
        this._activeApChangedId = this._device.connect('notify::active-access-point', Lang.bind(this, this._activeApChanged));
        this._stateChangedId = this._device.connect('state-changed', Lang.bind(this, this._deviceStateChanged));
        this._notifyConnectivityId = this._client.connect('notify::connectivity', Lang.bind(this, this._iconChanged));

        this._sync();
    },

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

    destroy: function() {
        if (this._activeApChangedId) {
            GObject.Object.prototype.disconnect.call(this._device, this._activeApChangedId);
            this._activeApChangedId = 0;
        }
        if (this._stateChangedId) {
            GObject.Object.prototype.disconnect.call(this._device, this._stateChangedId);
            this._stateChangedId = 0;
        }
        if (this._strengthChangedId > 0) {
            this._activeAccessPoint.disconnect(this._strengthChangedId);
            this._strengthChangedId = 0;
        }
        if (this._wirelessEnabledChangedId) {
            this._client.disconnect(this._wirelessEnabledChangedId);
            this._wirelessEnabledChangedId = 0;
        }
        if (this._wirelessHwEnabledChangedId) {
            this._client.disconnect(this._wirelessHwEnabledChangedId);
            this._wirelessHwEnabledChangedId = 0;
        }
        if (this._dialog) {
            this._dialog.destroy();
            this._dialog = null;
        }
        if (this._notifyConnectivityId) {
            this._client.disconnect(this._notifyConnectivityId);
            this._notifyConnectivityId = 0;
        }

        this.item.destroy();
    },

    _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._sync();
    },

    _toggleWifi: function() {
        this._client.wireless_enabled = !this._client.wireless_enabled;
    },

    _showDialog: function() {
        this._dialog = new NMWirelessDialog(this._client, this._device, this._settings);
        this._dialog.connect('closed', Lang.bind(this, this._dialogClosed));
        this._dialog.open();
    },

    _dialogClosed: function() {
        this._dialog.destroy();
        this._dialog = null;
    },

    _strengthChanged: function() {
        this._iconChanged();
    },

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

        this._activeAccessPoint = this._device.active_access_point;

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

        this._sync();
    },

    _sync: function() {
        this._toggleItem.label.text = this._client.wireless_enabled ? _("Turn Off") : _("Turn On");
        this._toggleItem.actor.visible = this._client.wireless_hardware_enabled;

        this.item.status.text = this._getStatus();
        this.item.icon.icon_name = this._getMenuIcon();
        this.item.label.text = this._description;
    },

    setDeviceDescription: function(desc) {
        this._description = desc;
        this._sync();
    },

    _getStatus: function() {
        let ap = this._device.active_access_point;

        if (this._isHotSpotMaster())
            return _("Hotspot Active");
        else if (this._device.state >= NetworkManager.DeviceState.PREPARE &&
                 this._device.state < NetworkManager.DeviceState.ACTIVATED)
            return _("Connecting");
        else if (ap)
            return ssidToLabel(ap.get_ssid());
        else if (!this._client.wireless_hardware_enabled)
            return _("Hardware Disabled");
        else if (!this._client.wireless_enabled)
            return _("Off");
        else if (this._device.state == NetworkManager.DeviceState.DISCONNECTED)
            return _("Not Connected");
        else
            return '';
    },

    _getMenuIcon: function() {
        if (this._device.active_connection)
            return this.getIndicatorIcon();
        else
            return 'network-wireless-signal-none-symbolic';
    },

    _canReachInternet: function() {
        if (this._client.primary_connection != this._device.active_connection)
            return true;

        return this._client.connectivity == NetworkManager.ConnectivityState.FULL;
    },

    _isHotSpotMaster: function() {
        if (!this._device.active_connection)
            return false;

        let connection = this._settings.get_connection_by_path(this._device.active_connection.connection);
        if (!connection)
            return false;

        let ip4config = connection.get_setting_ip4_config();
        if (!ip4config)
            return false;

        return ip4config.get_method() == NetworkManager.SETTING_IP4_CONFIG_METHOD_SHARED;
    },

    getIndicatorIcon: function() {
        if (this._device.state < NetworkManager.DeviceState.PREPARE)
            return 'network-wireless-disconnected-symbolic';
        if (this._device.state < NetworkManager.DeviceState.ACTIVATED)
            return 'network-wireless-acquiring-symbolic';

        if (this._isHotSpotMaster())
            return 'network-wireless-hotspot-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?');

            if (this._canReachInternet())
                return 'network-wireless-connected-symbolic';
            else
                return 'network-wireless-no-route-symbolic';
        }

        if (this._canReachInternet())
            return 'network-wireless-signal-' + signalToIcon(ap.strength) + '-symbolic';
        else
            return 'network-wireless-no-route-symbolic';
    },
});
Signals.addSignalMethods(NMDeviceWireless.prototype);

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

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

        return this._activeConnection.vpn_state != NetworkManager.VPNConnectionState.DISCONNECTED;
    },

    _buildUI: function() {
        this.labelItem = new PopupMenu.PopupMenuItem('');
        this.labelItem.connect('activate', Lang.bind(this, this._toggle));

        this.radioItem = new PopupMenu.PopupSwitchMenuItem(this._connection.get_id(), false);
        this.radioItem.connect('toggled', Lang.bind(this, this._toggle));
    },

    _sync: function() {
        let isActive = this.isActive();
        this.labelItem.label.text = isActive ? _("Turn Off") : this._section.getConnectLabel();
        this.radioItem.setToggleState(isActive);
        this.radioItem.setStatus(this._getStatus());
        this.emit('icon-changed');
    },

    _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';
        }
    },

    _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.emit('icon-changed');
        this.parent();
    },

    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.vpn_state < NetworkManager.VPNConnectionState.ACTIVATED)
                return 'network-vpn-acquiring-symbolic';
            else
                return 'network-vpn-symbolic';
        } else {
            return '';
        }
    },
});

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

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

        this._vpnSettings = new PopupMenu.PopupMenuItem('');
        this.item.menu.addMenuItem(this._vpnSettings);
        this._vpnSettings.connect('activate', Lang.bind(this, this._onSettingsActivate));

        this._sync();
    },

    _sync: function() {
        let nItems = this._connectionItems.size;
        this.item.actor.visible = (nItems > 0);

        if (nItems > 1)
            this._vpnSettings.label.text = _("Network Settings");
        else
            this._vpnSettings.label.text = _("VPN Settings");

        this.parent();
    },

    _onSettingsActivate: function() {
        let nItems = this._connectionItems.size;
        if (nItems > 1) {
            let appSys = Shell.AppSystem.get_default();
            let app = appSys.lookup_app('gnome-network-panel.desktop');
            app.launch(0, -1);
        } else {
            let connection = this._connections[0];
            Util.spawnApp(['gnome-control-center', 'network', 'show-device',
                           connection.get_path()]);
        }
    },

    _getDescription: function() {
        return _("VPN");
    },

    _getStatus: function() {
        let values = this._connectionItems.values();
        for (let item of values) {
            if (item.isActive())
                return item.getName();
        }

        return _("Off");
    },

    _getMenuIcon: function() {
        return this.getIndicatorIcon() || 'network-vpn-symbolic';
    },

    activateConnection: function(connection) {
        this._client.activate_connection(connection, null, null, null);
    },

    deactivateConnection: function(activeConnection) {
        this._client.deactivate_connection(activeConnection);
    },

    setActiveConnections: function(vpnConnections) {
        let connections = this._connectionItems.values();
        for (let item of connections) {
            item.setActiveConnection(null);
        }
        vpnConnections.forEach(Lang.bind(this, function(a) {
            let item = this._connectionItems.get(a._connection.get_uuid());
            item.setActiveConnection(a);
        }));
    },

    _makeConnectionItem: function(connection) {
        return new NMVPNConnectionItem(this, connection);
    },

    getIndicatorIcon: function() {
        let items = this._connectionItems.values();
        for (let item of items) {
            let icon = item.getIndicatorIcon();
            if (icon)
                return icon;
        }
        return '';
    },
});
Signals.addSignalMethods(NMVPNSection.prototype);

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

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

        this._primaryIndicator = this._addIndicator();
        this._vpnIndicator = this._addIndicator();

        // 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;
        // TODO: WiMax support

        // Connection types
        this._ctypes = { };
        this._ctypes[NetworkManager.SETTING_WIRED_SETTING_NAME] = NMConnectionCategory.WIRED;
        this._ctypes[NetworkManager.SETTING_WIRELESS_SETTING_NAME] = NMConnectionCategory.WIRELESS;
        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_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();
    },

    _createDeviceCategory: function() {
        let category = {
            section: new PopupMenu.PopupMenuSection(),
            devices: [ ],
        };
        this.menu.addMenuItem(category.section);
        return category;
    },

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

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

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

        this._notification = null;

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

        this._devices.wired = this._createDeviceCategory();
        this._devices.wireless = this._createDeviceCategory();
        this._devices.wwan = this._createDeviceCategory();

        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.item);

        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::primary-connection', Lang.bind(this, this._syncMainConnection));
        this._client.connect('notify::activating-connection', Lang.bind(this, this._syncMainConnection));
        this._client.connect('notify::active-connections', Lang.bind(this, this._syncVPNConnections));
        this._client.connect('notify::connectivity', Lang.bind(this, this._syncConnectivity));
        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));

        Main.sessionMode.connect('updated', Lang.bind(this, this._sessionUpdated));
        this._sessionUpdated();
    },

    _sessionUpdated: function() {
        let sensitive = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
        this.menu.setSensitive(sensitive);
    },

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

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

    _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();
    },

    _notify: function(iconName, title, text, urgency) {
        if (this._notification)
            this._notification.destroy();

        this._ensureSource();

        let gicon = new Gio.ThemedIcon({ name: iconName });
        this._notification = new MessageTray.Notification(this._source, title, text, { gicon: gicon });
        this._notification.setUrgency(urgency);
        this._notification.setTransient(true);
        this._notification.connect('destroy', function() {
            this._notification = null;
        });
        this._source.notify(this._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._notify('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];
            let description = names[i];
            if (device._delegate)
                device._delegate.setDeviceDescription(description);
        }
    },

    _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._settings);
            device._delegate = wrapper;
            this._addDeviceWrapper(wrapper);

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

            if (wrapper instanceof NMConnectionSection) {
                this._connections.forEach(function(connection) {
                    wrapper.checkConnection(connection);
                });
            }
        }
    },

    _addDeviceWrapper: function(wrapper) {
        wrapper._activationFailedId = wrapper.connect('activation-failed',
                                                      Lang.bind(this, this._onActivationFailed));

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

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

    _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.destroy();

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

    _getMainConnection: function() {
        let connection;

        connection = this._client.get_primary_connection();
        if (connection) {
            ensureActiveConnectionProps(connection, this._settings);
            return connection;
        }

        connection = this._client.get_activating_connection();
        if (connection) {
            ensureActiveConnectionProps(connection, this._settings);
            return connection;
        }

        return null;
    },

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

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

        this._mainConnection = this._getMainConnection();

        if (this._mainConnection) {
            if (this._mainConnection._primaryDevice)
                this._mainConnectionIconChangedId = this._mainConnection._primaryDevice.connect('icon-changed', Lang.bind(this, this._updateIcon));
            this._mainConnectionStateChangedId = this._mainConnection.connect('notify::state', Lang.bind(this, this._mainConnectionStateChanged));
            this._mainConnectionStateChanged();
        }

        this._updateIcon();
        this._syncConnectivity();
    },

    _syncVPNConnections: function() {
        let activeConnections = this._client.get_active_connections() || [];
        let vpnConnections = activeConnections.filter(function(a) {
            return (a instanceof NMClient.VPNConnection);
        });
        vpnConnections.forEach(Lang.bind(this, function(a) {
            ensureActiveConnectionProps(a, this._settings);
        }));
        this._vpnSection.setActiveConnections(vpnConnections);

        this._updateIcon();
    },

    _mainConnectionStateChanged: function() {
        if (this._mainConnection.state == NetworkManager.ActiveConnectionState.ACTIVATED && this._notification)
            this._notification.destroy();
    },

    _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);
    },

    _connectionRemoved: function(connection) {
        let pos = this._connections.indexOf(connection);
        if (pos != -1)
            this._connections.splice(pos, 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++) {
                if (devices[i] instanceof NMConnectionSection)
                    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;

        let section = connection._section;

        if (section == NMConnectionCategory.INVALID)
            return;

        if (section == NMConnectionCategory.VPN) {
            this._vpnSection.checkConnection(connection);
        } else {
            let devices = this._devices[section].devices;
            devices.forEach(function(wrapper) {
                if (wrapper instanceof NMConnectionSection)
                    wrapper.checkConnection(connection);
            });
        }
    },

    _syncNMState: function() {
        this.indicators.visible = this._client.manager_running;
        this.menu.actor.visible = this._client.networking_enabled;

        this._syncConnectivity();
    },

    _flushConnectivityQueue: function() {
        if (this._portalHelperProxy) {
            for (let item of this._connectivityQueue)
                this._portalHelperProxy.CloseRemote(item);
        }

        this._connectivityQueue = [];
    },

    _closeConnectivityCheck: function(path) {
        let index = this._connectivityQueue.indexOf(path);

        if (index >= 0) {
            if (this._portalHelperProxy)
                this._portalHelperProxy.CloseRemote(path);

            this._connectivityQueue.splice(index, 1);
        }
    },

    _portalHelperDone: function(proxy, emitter, parameters) {
        let [path, result] = parameters;

        if (result == PortalHelperResult.CANCELLED) {
            // Keep the connection in the queue, so the user is not
            // spammed with more logins until we next flush the queue,
            // which will happen once he chooses a better connection
            // or we get to full connectivity through other means
        } else if (result == PortalHelperResult.COMPLETED) {
            this._closeConnectivityCheck(path);
            return;
        } else if (result == PortalHelperResult.RECHECK) {
            this._client.check_connectivity_async(null, Lang.bind(this, function(client, result) {
                try {
                    let state = client.check_connectivity_finish(result);
                    if (state >= NetworkManager.ConnectivityState.FULL)
                        this._closeConnectivityCheck(path);
                } catch(e) { }
            }));
        } else {
            log('Invalid result from portal helper: ' + result);
        }
    },

    _syncConnectivity: function() {
        if (this._mainConnection == null ||
            this._mainConnection.state != NetworkManager.ActiveConnectionState.ACTIVATED) {
            this._flushConnectivityQueue();
            return;
        }

        let isPortal = this._client.connectivity == NetworkManager.ConnectivityState.PORTAL;
        // For testing, allow interpreting any value != FULL as PORTAL, because
        // LIMITED (no upstream route after the default gateway) is easy to obtain
        // with a tethered phone
        // NONE is also possible, with a connection configured to force no default route
        // (but in general we should only prompt a portal if we know there is a portal)
        if (GLib.getenv('GNOME_SHELL_CONNECTIVITY_TEST') != null)
            isPortal = isPortal || this._client.connectivity < NetworkManager.ConnectivityState.FULL;
        if (!isPortal)
            return;

        let path = this._mainConnection.get_path();
        for (let item of this._connectivityQueue) {
            if (item == path)
                return;
        }

        let timestamp = global.get_current_time();
        if (this._portalHelperProxy) {
            this._portalHelperProxy.AuthenticateRemote(path, '', timestamp);
        } else {
            new PortalHelperProxy(Gio.DBus.session, 'org.gnome.Shell.PortalHelper',
                                  '/org/gnome/Shell/PortalHelper', Lang.bind(this, function (proxy, error) {
                                      if (error) {
                                          log('Error launching the portal helper: ' + error);
                                          return;
                                      }

                                      this._portalHelperProxy = proxy;
                                      proxy.connectSignal('Done', Lang.bind(this, this._portalHelperDone));

                                      proxy.AuthenticateRemote(path, '', timestamp);
                                  }));
        }

        this._connectivityQueue.push(path);
    },

    _updateIcon: function() {
        if (!this._client.networking_enabled || !this._mainConnection) {
            this._primaryIndicator.visible = false;
        } else {
            let dev = this._mainConnection._primaryDevice;
            this._primaryIndicator.visible = (dev != null);
            if (dev)
                this._primaryIndicator.icon_name = dev.getIndicatorIcon();
        }

        this._vpnIndicator.icon_name = this._vpnSection.getIndicatorIcon();
        this._vpnIndicator.visible = (this._vpnIndicator.icon_name != '');
    }
});
