const Applet = imports.ui.applet;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const GObject = imports.gi.GObject;
const Lang = imports.lang;
const NM = imports.gi.NM;
const Signals = imports.signals;
const St = imports.gi.St;
const Mainloop = imports.mainloop;
const Main = imports.ui.main;
const PopupMenu = imports.ui.popupMenu;
const MessageTray = imports.ui.messageTray;
const ModemManager = imports.misc.modemManager;
const Util = imports.misc.util;
const Settings = imports.ui.settings;

const DEFAULT_PERIODIC_UPDATE_FREQUENCY_SECONDS = 10;
const FAST_PERIODIC_UPDATE_FREQUENCY_SECONDS = 2;

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

const NMAccessPointSecurity = {
    UNKNOWN: 0,
    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 = NM['80211Mode'];
const NM80211ApFlags = NM['80211ApFlags'];
const NM80211ApSecurityFlags = NM['80211ApSecurityFlags'];

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

var NMIface = '\
<node> \
  <interface name="org.freedesktop.NetworkManager"> \
    <method name="CheckConnectivity"> \
      <arg type="u" name="connectivity" direction="out"/> \
    </method> \
  </interface> \
</node>';

var NMProxy = Gio.DBusProxy.makeProxyWrapper(NMIface);
function NMDBus(initCallback, cancellable) {
    return new NMProxy(Gio.DBus.system,
                       'org.freedesktop.NetworkManager',
                       '/org/freedesktop/NetworkManager',
                       initCallback, cancellable);
}

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

// shared between NMNetworkMenuItem and NMDeviceWireless
function sortAccessPoints(accessPoints) {
    return accessPoints.sort(function (one, two) {
        return two.strength - one.strength;
    });
}

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

function NMNetworkMenuItem() {
    this._init.apply(this, arguments);
}

function default_to_private_connections(client) {
    let perms = client.get_permission_result (NM.ClientPermission.SETTINGS_MODIFY_SYSTEM);
    return (perms != NM.ClientPermissionResult.YES);
}

NMNetworkMenuItem.prototype = {
    __proto__: PopupMenu.PopupBaseMenuItem.prototype,

    _init: function(accessPoints, title, params) {
        PopupMenu.PopupBaseMenuItem.prototype._init.call(this, params);

        accessPoints = sortAccessPoints(accessPoints);
        this.bestAP = accessPoints[0];

        if (!title) {
            let ssid = this.bestAP.get_ssid();
            title = ssidToLabel(ssid);
        }

        this._label = new St.Label({ text: title });
        this.addActor(this._label);
        let strStrengh = String(this.bestAP.strength);
        strStrengh = strStrengh + '%';
        this._labelStrength = new St.Label({ text: strStrengh });
        this.addActor(this._labelStrength, { align: St.Align.END });
        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._accessPoints = [ ];
        for (let i = 0; i < accessPoints.length; i++) {
            let ap = accessPoints[i];
            // need a wrapper object here, because the access points can be shared
            // between many NMNetworkMenuItems
            let apObj = {
                ap: ap,
                updateId: ap.connect('notify::strength', Lang.bind(this, this._updated))
            };
            this._accessPoints.push(apObj);
        }
    },

    _updated: function(ap) {
        if (ap.strength > this.bestAP.strength)
            this.bestAP = ap;

        this._signalIcon.icon_name = this._getIcon();
        let strStrengh = String(this.bestAP.strength);
        strStrengh = strStrengh + '%';
        this._labelStrength.set_text(strStrengh);
    },

    _getIcon: function() {
        if (this.bestAP.mode == NM80211Mode.ADHOC)
            return 'xsi-network-workgroup';
        else if (this.bestAP._secType != NMAccessPointSecurity.UNKNOWN &&
            this.bestAP._secType != NMAccessPointSecurity.NONE)
            return 'xsi-network-wireless-signal-' + signalToIcon(this.bestAP.strength) + '-secure';
        else
            return 'xsi-network-wireless-signal-' + signalToIcon(this.bestAP.strength);
    },

    updateAccessPoints: function(accessPoints) {
        for (let i = 0; i < this._accessPoints.length; i++) {
            let apObj = this._accessPoints[i];
            apObj.ap.disconnect(apObj.updateId);
            apObj.updateId = 0;
        }

        accessPoints = sortAccessPoints(accessPoints);
        this.bestAP = accessPoints[0];
        this._accessPoints = [ ];
        for (let i = 0; i < accessPoints; i++) {
            let ap = accessPoints[i];
            let apObj = {
                ap: ap,
                updateId: ap.connect('notify::strength', Lang.bind(this, this._updated))
            };
            this._accessPoints.push(apObj);
        }
    },

    destroy: function() {
        for (let i = 0; i < this._accessPoints.length; i++) {
            let apObj = this._accessPoints[i];
            apObj.ap.disconnect(apObj.updateId);
            apObj.updateId = 0;
        }

        PopupMenu.PopupBaseMenuItem.prototype.destroy.call(this);
    }
};

function NMWiredSectionTitleMenuItem() {
    this._init.apply(this, arguments);
}

NMWiredSectionTitleMenuItem.prototype = {
    __proto__: PopupMenu.PopupSwitchMenuItem.prototype,

    _init: function(label, params) {
        params = params || { };
        params.style_class = 'popup-subtitle-menu-item';
        PopupMenu.PopupSwitchMenuItem.prototype._init.call(this, label, false, params);
    },

    updateForDevice: function(device) {
        if (device) {
            this._device = device;
            this.setStatus(device.statusLabel);
            this.setToggleState(device.connected);
            // if this device is not controllable, hide the switch
            this._switch.actor.visible = device.controllable;
        } else {
            this.setStatus('');
            this._switch.actor.visible = false;
        }
    },

    activate: function(event) {
        PopupMenu.PopupSwitchMenuItem.prototype.activate.call(this, event);

        if (!this._device) {
            log('Section title activated when there is more than one device, should be non reactive');
            return;
        }

        let newState = this._switch.state;

        // Immediately reset the switch to false, it will be updated appropriately
        // by state-changed signals in devices (but fixes the VPN not being in sync
        // if the ActiveConnection object is never seen by libnm-glib)
        this._switch.setToggleState(false);

        if (newState)
            this._device.activate();
        else
            this._device.deactivate();
    }
};

function NMWirelessSectionTitleMenuItem() {
    this._init.apply(this, arguments);
}

NMWirelessSectionTitleMenuItem.prototype = {
    __proto__: PopupMenu.PopupSwitchMenuItem.prototype,

    _init: function(client, property, title, params) {
        params = params || { };
        params.style_class = 'popup-subtitle-menu-item';
        PopupMenu.PopupSwitchMenuItem.prototype._init.call(this, title, false, params);

        this._client = client;
        this._property = property + '_enabled';
        this._propertyHardware = property + '_hardware_enabled';
        this._setEnabledFunc = property + '_set_enabled';

        this._client.connect('notify::' + property + '-enabled', Lang.bind(this, this._propertyChanged));
        this._client.connect('notify::' + property + '-hardware-enabled', Lang.bind(this, this._propertyChanged));

        this._propertyChanged();
    },

    updateForDevice: function(device) {
        this._device = device;
        // we show the switch
        // - if there not just one device
        // - if the switch is off
        // - if the device is activated or disconnected
        if (device && this._softwareEnabled && this._hardwareEnabled) {
            this.setStatus(device.statusLabel);
            this._switch.actor.visible = device.controllable;
        } else {
            this.setStatus(null);
            this._switch.actor.show();
        }
    },

    activate: function(event) {
        PopupMenu.PopupSwitchMenuItem.prototype.activate.call(this, event);

        this._client[this._setEnabledFunc](this._switch.state);

        if (!this._device) {
            log('Section title activated when there is more than one device, should be non reactive');
            return;
        }

        let newState = this._switch.state;

        if (newState)
            this._device.activate();
        else
            this._device.deactivate();

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

    _propertyChanged: function() {
        this._softwareEnabled = this._client[this._property];
        this._hardwareEnabled = this._client[this._propertyHardware];

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

function NMDevice() {
    throw new TypeError('Instantanting abstract class NMDevice');
}

NMDevice.prototype = {
    _init: function(client, device, connections) {
        this.device = device;
        if (device) {
            this.device._delegate = this;
            this._stateChangedId = this.device.connect('state-changed', Lang.bind(this, this._deviceStateChanged));
        } else
            this._stateChangedId = 0;

        // protected
        this._client = client;
        this._connections = [ ];
        for (let i = 0; i < connections.length; i++) {
            if (!connections[i]._uuid)
                continue;
            if (!this.connectionValid(connections[i]))
                continue;
            // record the connection
            let obj = {
                connection: connections[i],
                name: connections[i]._name,
                uuid: connections[i]._uuid,
                timestamp: connections[i]._timestamp,
            };
            this._connections.push(obj);
        }
        this._connections.sort(this._connectionSortFunction);
        this._activeConnection = null;
        this._activeConnectionItem = null;
        this._autoConnectionItem = null;
        this._overflowItem = null;

        this._carrierChangedId = 0;
        this._firmwareChangedId = 0;
        this._firmwareMissingId = 0;

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

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

        this._createSection();
    },

    destroy: function() {
        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._carrierChangedId) {
            // see above for why this is needed
            GObject.Object.prototype.disconnect.call(this.device, this._carrierChangedId);
            this._carrierChangedId = 0;
        }
        if (this._firmwareChangedId) {
            GObject.Object.prototype.disconnect.call(this.device, this._firmwareChangedId);
            this._firmwareChangedId = 0;
        }
        if (this._firmwareMissingId) {
            GObject.Object.prototype.disconnect.call(this.device, this._firmwareMissingId);
            this._firmwareMissingId = 0;
        }

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

    deactivate: function() {
        this.device.disconnect_async(null, null);
    },

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

        // pick the most recently used connection and connect to that
        // or if no connections ever set, create an automatic one
        if (this._connections.length > 0) {
            this._client.activate_connection_async(this._connections[0].connection, this.device, null, null, null);
        } else if (this._autoConnectionName) {
            let connection = this._createAutomaticConnection();
            if (connection)
                this._client.add_and_activate_connection_async(connection, this.device, null, null, null);
        }
    },

    get connected() {
        return this.device.state == NM.DeviceState.ACTIVATED;
    },

    setActiveConnection: function(activeConnection) {
        if (activeConnection == this._activeConnection)
            // nothing to do
            return;

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

        this._activeConnection = activeConnection;

        this._clearSection();
        this._createSection();
    },

    checkConnection: function(connection) {
        let exists = this._findConnection(connection._uuid) != -1;
        let valid = this.connectionValid(connection);
        if (exists && !valid)
            this.removeConnection(connection);
        else if (!exists && valid)
            this.addConnection(connection);
    },

    addConnection: function(connection) {
        // record the connection
        let obj = {
            connection: connection,
            name: connection._name,
            uuid: connection._uuid,
            timestamp: connection._timestamp,
        };
        this._connections.push(obj);
        this._connections.sort(this._connectionSortFunction);

        this._clearSection();
        this._createSection();
    },

    removeConnection: function(connection) {
        if (!connection._uuid) {
            log('Cannot remove a connection without an UUID');
            return;
        }
        let pos = this._findConnection(connection._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._clearSection();
            this._createSection();
        }
    },

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

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

        return two.timestamp - one.timestamp;
    },

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

    get statusLabel(){
        switch(this.device.state) {
        case NM.DeviceState.DISCONNECTED:
        case NM.DeviceState.ACTIVATED:
            return null;
        case NM.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 NM.DeviceState.DEACTIVATING:
            return _("disconnecting...");
        case NM.DeviceState.PREPARE:
        case NM.DeviceState.CONFIG:
        case NM.DeviceState.IP_CONFIG:
        case NM.DeviceState.IP_CHECK:
        case NM.DeviceState.SECONDARIES:
            return _("connecting...");
        case NM.DeviceState.NEED_AUTH:
            /* Translators: this is for network connections that require some kind of key or password */
            return _("authentication required");
        case NM.DeviceState.UNAVAILABLE:
            // This state is actually a compound of various states (generically unavailable,
            // firmware missing, carrier not available), that are exposed by different properties
            // (whose state may or may not updated when we receive state-changed).
            if (!this._firmwareMissingId)
                this._firmwareMissingId = this.device.connect('notify::firmware-missing', Lang.bind(this, this._substateChanged));
            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");
            }
            if (this.device.capabilities & NM.DeviceCapabilities.CARRIER_DETECT) {
                if (!this._carrierChangedId)
                    this._carrierChangedId = this.device.connect('notify::carrier', Lang.bind(this, this._substateChanged));
                if (!this.carrier) {
                    /* Translators: this is for wired network devices that are physically disconnected */
                    return _("cable unplugged");
                }
            }
            /* 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 NM.DeviceState.FAILED:
            return _("connection failed");
        default:
            log('Device state invalid, is %d'.format(this.device.state));
            return 'invalid';
        }
    },

    get controllable(){
        // controllable for every state except unavailable or unmanaged
        if(this.device.state === NM.DeviceState.UNAVAILABLE || this.device.state === NM.DeviceState.UNMANAGED)
            return false;

        return true;
    },

    // protected
    _createAutomaticConnection: function() {
        throw new TypeError('Invoking pure virtual function NMDevice.createAutomaticConnection');
    },

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

    _clearSection: function() {
        // Clear everything
        this.section.removeAll();
        this._autoConnectionItem = null;
        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 >= NM.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) {
                    activeOffset = 0; // we no longer need to account for this
                    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);
            }
        } else if (this._autoConnectionName) {
            this._autoConnectionItem = new PopupMenu.PopupMenuItem(this._autoConnectionName);
            this._autoConnectionItem.connect('activate', Lang.bind(this, function() {
                let connection = this._createAutomaticConnection();
                if (connection)
                    this._client.add_and_activate_connection_async(connection, this.device, null, null, null);
            }));
            this.section.addMenuItem(this._autoConnectionItem);
        }
    },

    _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_async(connection, this.device, null, null, null);
        }));
        return item;
    },

    _createActiveConnectionItem: function() {
        let title;
        let active = this._activeConnection.connection;
        if (active) {
            title = active._name;
        } 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.setShowDot(true);
        this._activeConnectionItem.actor.add_style_class_name('popup-device-menu-item');
    },

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

        if (oldstate == NM.DeviceState.ACTIVATED) {
            this.emit('network-lost');
        }

        if (newstate == NM.DeviceState.FAILED) {
            this.emit('activation-failed', reason);
        }

        this._updateStatusItem();

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

    _updateStatusItem: function() {
        if (this._carrierChangedId) {
            // see above for why this is needed
            GObject.Object.prototype.disconnect.call(this.device, this._carrierChangedId);
            this._carrierChangedId = 0;
        }
        if (this._firmwareChangedId) {
            GObject.Object.prototype.disconnect.call(this.device, this._firmwareChangedId);
            this._firmwareChangedId = 0;
        }

        this.statusItem.setStatus(this.statusLabel);
        this.statusItem.setToggleState(this.connected);
        // if this device is not controllable, hide the switch
        this.statusItem._switch.actor.visible = this.controllable;
    },

    _substateChanged: function() {
        this.statusItem.setStatus(this.statusLabel);
        this.statusItem._switch.actor.visible = this.controllable;

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

    _getDescription: function() {
        let dev_product = this.device.get_product();
        let dev_vendor = this.device.get_vendor();
        if (!dev_product || !dev_vendor) {
            if (this.device.name) {
                return this.device.name;
            }
            else {
                switch (this.device.get_device_type()) {
                    case NM.DeviceType.ETHERNET: return _("Ethernet");
                    case NM.DeviceType.WIFI: return _("Wifi");
                    case NM.DeviceType.MODEM: return _("Modem");
                    case NM.DeviceType.BT: return _("Bluetooth");
                    case NM.DeviceType.WIREGUARD: return _("WireGuard");
                    default: return "";
                }
            }
        }

        let product = Util.fixupPCIDescription(dev_product);
        let vendor = Util.fixupPCIDescription(dev_vendor);
        let out = '';

        // Another quick hack; if all of the fixed up vendor string
        // is found in product, ignore the vendor.
        if (product.indexOf(vendor) == -1)
            out += vendor + ' ';
        out += product;

        return out;
    }
};
Signals.addSignalMethods(NMDevice.prototype);


function NMDeviceWired() {
    this._init.apply(this, arguments);
}

NMDeviceWired.prototype = {
    __proto__: NMDevice.prototype,

    _init: function(client, device, connections) {
        this._autoConnectionName = _("Auto Ethernet");
        this.category = NMConnectionCategory.WIRED;

        NMDevice.prototype._init.call(this, client, device, connections);
    },

    _createSection: function() {
        NMDevice.prototype._createSection.call(this);

        // 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
        if (this._connections.length <= 1)
            this.section.actor.hide();
        else
            this.section.actor.show();
    },

    _createAutomaticConnection: function() {
        let connection = new NM.SimpleConnection();
        connection._uuid = NM.utils_uuid_generate();
        connection.add_setting(new NM.SettingWired());
        let setting_conn = new NM.SettingConnection({
            uuid: connection._uuid,
            id: this._autoConnectionName,
            type: NM.SETTING_WIRED_SETTING_NAME,
            autoconnect: true
        });
        setting_conn.add_permission('user', GLib.get_user_name(), null);
        connection.add_setting(setting_conn);
        return connection;
    }
};

function NMDeviceModem() {
    this._init.apply(this, arguments);
}

NMDeviceModem.prototype = {
    __proto__: NMDevice.prototype,

    _init: function(client, device, connections) {
        let is_wwan = false;

        this._enabled = true;
        this.mobileDevice = null;
        this._connectionType = 'ppp';

        this._capabilities = device.current_capabilities;
        // Support new ModemManager1 devices
        if (device.udi.indexOf('/org/freedesktop/ModemManager1/Modem') == 0) {
            try {
                is_wwan = true;
                this.mobileDevice = new ModemManager.BroadbandModem(device.udi, device.current_capabilities);
                if (this._capabilities & NM.DeviceModemCapabilities.GSM_UMTS) {
                    this._connectionType = NM.SETTING_GSM_SETTING_NAME;
                } else if (this._capabilities & NM.DeviceModemCapabilities.LTE) {
                    this._connectionType = NM.SETTING_GSM_SETTING_NAME;
                } else if (this._capabilities & NM.DeviceModemCapabilities.CDMA_EVDO) {
                    this._connectionType = NM.SETTING_CDMA_SETTING_NAME;
                }
            }
            catch (e){
                global.logError(e);
            }
        } else if (this._capabilities & NM.DeviceModemCapabilities.GSM_UMTS) {
            is_wwan = true;
            this.mobileDevice = new ModemManager.ModemGsm(device.udi);
            this._connectionType = NM.SETTING_GSM_SETTING_NAME;
        } else if (this._capabilities & NM.DeviceModemCapabilities.CDMA_EVDO) {
            is_wwan = true;
            this.mobileDevice = new ModemManager.ModemCdma(device.udi);
            this._connectionType = NM.SETTING_CDMA_SETTING_NAME;
        } else if (this._capabilities & NM.DeviceModemCapabilities.LTE) {
            is_wwan = true;
            // FIXME: support signal quality
        }

        if (is_wwan) {
            this.category = NMConnectionCategory.WWAN;
            this._autoConnectionName = _("Auto broadband");
        } else {
            this.category = NMConnectionCategory.WIRED;
            this._autoConnectionName = _("Auto dial-up");
        }

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

        NMDevice.prototype._init.call(this, 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.statusLabel);
        }

        NMDevice.prototype.setEnabled.call(this, enabled);
    },

    get connected() {
        return this._enabled && this.device.state == NM.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;
        }

        NMDevice.prototype.destroy.call(this);
    },

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

    _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._operatorItem.actor.add_style_class_name('popup-device-menu-item');
        }

        NMDevice.prototype._createSection.call(this);
    },

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

        NMDevice.prototype._clearSection.call(this);
    },

    _createAutomaticConnection: function() {
        // Mobile wizard is too complex for Cinnamon UI and
        // is handled by the network panel
        Util.spawn(['cinnamon-settings', 'network',
                    'connect-3g', this.device.get_path()]);
        return null;
    }
};

function NMDeviceBluetooth() {
    this._init.apply(this, arguments);
}

NMDeviceBluetooth.prototype = {
    __proto__: NMDevice.prototype,

    _init: function(client, device, connections) {
        this._autoConnectionName = this._makeConnectionName(device);
        device.connect('notify::name', Lang.bind(this, this._updateAutoConnectionName));

        this.category = NMConnectionCategory.WWAN;

        NMDevice.prototype._init.call(this, client, device, connections);
    },

    _createAutomaticConnection: function() {
        let connection = new NM.SimpleConnection();
        connection._uuid = NM.utils_uuid_generate();
        connection.add_setting(new NM.SettingBluetooth());
        connection.add_setting(new NM.SettingConnection({
            uuid: connection._uuid,
            id: this._autoConnectionName,
            type: NM.SETTING_BLUETOOTH_SETTING_NAME,
            autoconnect: false
        }));
        return connection;
    },

    _makeConnectionName: function(device) {
        let name = device.name;
        if (name)
            return _("Auto %s").format(name);
        else
            return _("Auto bluetooth");
    },

    _updateAutoConnectionName: function() {
        this._autoConnectionName = this._makeConnectionName(this.device);

        this._clearSection();
        this._createSection();
    }
};


// Not a real device, but I save a lot code this way
function NMDeviceVPN() {
    this._init.apply(this, arguments);
}

NMDeviceVPN.prototype = {
    __proto__: NMDevice.prototype,

    _init: function(client) {
        this._client = client
        this._autoConnectionName = null;
        this.category = NMConnectionCategory.VPN;

        this._activeConnections = [];

        NMDevice.prototype._init.call(this, client, null, []);
    },

    connectionValid: function(connection) {
        return connection._type == NM.SETTING_VPN_SETTING_NAME;
    },

    get empty() {
        return this._connections.length == 0;
    },

    get connected() {
        return this._activeConnections.length > 0;
    },

    setActiveConnections: function(activeConnections) {
        this._activeConnections = activeConnections || [];

        this._createSection();
        this.emit('active-connections-changed');
    },

    deactivate: function() {
        for (let ac of this._activeConnections)
            this._client.deactivate_connection(ac, null);

        this._activeConnections = []
    },

    statusLabel: null,
    controllable: true,

    _clearSection: function() {
        if (this.section && this.section.removeAll)
            this.section.removeAll();

        this._autoConnectionItem = null;
        this._overflowItem = null;

        for (let i = 0; i < this._connections.length; i++) {
            if (this._connections[i].item && this._connections[i].item.destroy)
                this._connections[i].item.destroy();

            this._connections[i].item = null;
        }
    },

    _updateConnectionItemView: function(item, connection, active) {
        item.label.text = connection._name  || _("Connected (private)");
        if (active) {
            item.setShowDot(true);
            item.actor.add_style_class_name('popup-device-menu-item');
        } else {
            item.setShowDot(false);
            item.actor.remove_style_class_name('popup-device-menu-item');
        }
    },

    _createSection: function() {
        for (let obj of this._connections) {
            if (!obj.item) {
                obj.item = new PopupMenu.PopupMenuItem(obj.name);
                this._updateConnectionItemView(obj.item, obj.connection);
                obj.item.connect('activate', Lang.bind(this, function() {
                    let activeConnection = this._activeConnections.find(ac => ac.connection === obj.connection);
                    if (activeConnection) {
                        this._client.deactivate_connection(activeConnection, null);
                    } else {
                        this._client.activate_connection_async(obj.connection, this.device, null, null, null);
                    }
                }));
                this.section.addMenuItem(obj.item);
            } else {
                this._updateConnectionItemView(obj.item, obj.connection, this._activeConnections.some(ac => ac.connection == obj.connection));
            }
        }
    },
};

function NMDeviceWIREGUARD() {
    this._init.apply(this, arguments);
}

NMDeviceWIREGUARD.prototype = {
    __proto__: NMDevice.prototype,

    _init: function(client, device, connections) {
        // Disable autoconnections
        this._autoConnectionName = null;
        this._client = client;
        this.category = NMConnectionCategory.WIREGUARD;
        this._type = NM.SETTING_WIREGUARD_SETTING_NAME;

        NMDevice.prototype._init.call(this, client, null, [ ]);

        // Tests:
        this.category = NMConnectionCategory.WIREGUARD;
        this._type = NM.SETTING_WIREGUARD_SETTING_NAME;
    },

    connectionValid: function(connection) {
        return connection._type == NM.SETTING_WIREGUARD_SETTING_NAME;
    },

    get empty() {
        return this._connections.length == 0;
    },

    get connected() {
        return !!this._activeConnection;
    },

    setActiveConnection: function(activeConnection) {
        if (activeConnection) {
            activeConnection._type = NM.SETTING_WIREGUARD_SETTING_NAME;
        }
        NMDevice.prototype.setActiveConnection.call(this, activeConnection);

        this.emit('active-connection-changed');
    },

    _shouldShowConnectionList: function() {
        return true;
    },

    deactivate: function() {
        if (this._activeConnection)
            this._client.deactivate_connection(this._activeConnection, null);
    },

    statusLabel: null,
    controllable: true
};


function NMDeviceWireless() {
    this._init.apply(this, arguments);
}

NMDeviceWireless.prototype = {
    __proto__: NMDevice.prototype,

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

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

        this._client = client;

        // breaking the layers with this, but cannot call
        // this.connectionValid until I have a device
        this.device = device;

        let validConnections = connections.filter(Lang.bind(this, function(connection) {
            return this.connectionValid(connection);
        }));
        let accessPoints = device.get_access_points() || [ ];
        for (let i = 0; i < accessPoints.length; i++) {
            // Access points are grouped by network
            let ap = accessPoints[i];

            if (ap.get_ssid() == null) {
                // hidden access point cannot be added, we need to know
                // the SSID and security details to connect
                // nevertheless, the access point can acquire a SSID when
                // NetworkManager connects to it (via nmcli or cinnamon-settings)
                ap._notifySsidId = ap.connect('notify::ssid', Lang.bind(this, this._notifySsidCb));
                continue;
            }

            let pos = this._findNetwork(ap);
            let obj;
            if (pos != -1) {
                obj = this._networks[pos];
                obj.accessPoints.push(ap);
            } else {
                obj = { ssid: ap.get_ssid(),
                        mode: ap.mode,
                        security: this._getApSecurityType(ap),
                        connections: [ ],
                        item: null,
                        accessPoints: [ ap ]
                      };
                obj.ssidText = ssidToLabel(obj.ssid);
                this._networks.push(obj);
            }

            // Check if some connection is valid for this AP
            for (let j = 0; j < validConnections.length; j++) {
                let connection = validConnections[j];
                if (ap.connection_valid(connection) &&
                    obj.connections.indexOf(connection) == -1) {
                    obj.connections.push(connection);
                }
            }
        }

        if (this.device.active_access_point) {
            let networkPos = this._findNetwork(this.device.active_access_point);

            if (networkPos == -1) // the connected access point is invisible
                this._activeNetwork = null;
            else
                this._activeNetwork = this._networks[networkPos];
        } else {
            this._activeNetwork = null;
        }
        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));

        NMDevice.prototype._init.call(this, client, device, validConnections);
    },

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

        NMDevice.prototype.destroy.call(this);
    },

    setEnabled: function(enabled) {
        if (enabled) {
            this.statusItem.actor.show();
            this.section.actor.show();
        } else {
            this.statusItem.actor.hide();
            this.section.actor.hide();
        }
    },

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

        // among all visible networks, pick the last recently used connection
        let best = null;
        let bestApObj = null;
        let bestTime = 0;
        for (let i = 0; i < this._networks.length; i++) {
            let apObj = this._networks[i];
            for (let j = 0; j < apObj.connections.length; j++) {
                let connection = apObj.connections[j];
                if (connection._timestamp > bestTime) {
                    best = connection;
                    bestTime = connection._timestamp;
                    bestApObj = apObj;
                }
            }
        }

        if (best) {
            for (let i = 0; i < bestApObj.accessPoints.length; i++) {
                let ap = bestApObj.accessPoints[i];
                if (ap.connection_valid(best)) {
                    this._client.activate_connection_async(best, this.device, ap.path, null, null);
                    break;
                }
            }
            return;
        }

        // XXX: what else to do?
        // for now, just pick a random network
        // (this function is called in a corner case anyway, that is, only when
        // the user toggles the switch and has more than one wireless device)
        if (this._networks.length > 0) {
            let connection = this._createAutomaticConnection(this._networks[0]);
            let accessPoints = sortAccessPoints(this._networks[0].accessPoints);
            this._client.add_and_activate_connection_async(connection, this.device, accessPoints[0].path, null, null);
        }
    },

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

        return two.accessPoints[0].strength - one.accessPoints[0].strength;
/**
        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 (!network.ssid.equal (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 apObj = this._networks[i];
            for (let j = 0; j < apObj.accessPoints.length; j++) {
                if (apObj.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;
    },

    _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 apObj;
        let needsupdate = false;

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

            apObj.accessPoints.push(accessPoint);
            if (apObj.item)
                apObj.item.updateAccessPoints(apObj.accessPoints);
        } else {
            apObj = { ssid: accessPoint.get_ssid(),
                      mode: accessPoint.mode,
                      security: this._getApSecurityType(accessPoint),
                      connections: [ ],
                      item: null,
                      accessPoints: [ accessPoint ]
                    };
            apObj.ssidText = ssidToLabel(apObj.ssid);
            needsupdate = true;
        }

        // 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) &&
                apObj.connections.indexOf(connection) == -1) {
                apObj.connections.push(connection);

                // this potentially changes the order
                needsupdate = true;
            }
        }

        if (needsupdate) {
            this._accessPointRemoved(device, accessPoint);

            if (pos != -1)
                this._networks.splice(pos, 1);

            if (this._networks.length == 0) {
                // only network in the list
                this._networks.push(apObj);
                this._clearSection();
                this._createSection();
                return;
            }

            // skip networks that should appear earlier
            let menuPos = this._activeConnectionItem ? 1 : 0;
            for (pos = 0;
                 pos < this._networks.length &&
                 this._networkSortFunction(this._networks[pos], apObj) < 0; ++pos) {
                if (this._networks[pos] != this._activeNetwork)
                    menuPos++;
            }

            // (re-)add the network
            this._networks.splice(pos, 0, apObj);

            if (this._shouldShowConnectionList()) {
                this._createNetworkItem(apObj, menuPos);
                if (menuPos < NUM_VISIBLE_NETWORKS && this._networks.length > NUM_VISIBLE_NETWORKS) {
                    for (; menuPos < NUM_VISIBLE_NETWORKS; ++pos) {
                        if (this._networks[pos] != this._activeNetwork)
                            menuPos++;
                    }
                    let item = this._networks[pos].item;
                    if (item && item._apObj) {
                        item.destroy();
                        item._apObj.item = null;

                        this._createNetworkItem(item._apObj, NUM_VISIBLE_NETWORKS);
                    }
                }
            }
        }
    },

    _accessPointRemoved: function(device, accessPoint) {
        let res = this._findExistingNetwork(accessPoint);

        if (res == null) {
            return;
        }

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

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

            if (apObj.item)
                apObj.item.destroy();

            if (this._overflowItem) {
                if (!apObj.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._apObj) {
                        item.destroy();
                        // clear the cycle, and allow the construction of the new item
                        item._apObj.item = null;

                        this._createNetworkItem(item._apObj, 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 if (apObj.item)
            apObj.item.updateAccessPoints(apObj.accessPoints);
    },

    _createAPItem: function(connection, accessPointObj, useConnectionName) {
        let item = new NMNetworkMenuItem(accessPointObj.accessPoints, useConnectionName ? connection._name : undefined);
        item._connection = connection;
        item.connect('activate', Lang.bind(this, function() {
            let accessPoints = sortAccessPoints(accessPointObj.accessPoints);
            for (let i = 0; i < accessPoints.length; i++) {
                if (accessPoints[i].connection_valid(connection)) {
                    this._client.activate_connection_async(connection, this.device, accessPoints[i].path, null, null);
                    break;
                }
            }
        }));
        return item;
    },

    _clearSection: function() {
        NMDevice.prototype._clearSection.call(this);

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

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

        this._connections.splice(pos, 1);

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

                    if (anyauto) {
                        // this potentially changes the sorting order
                        forceupdate = true;
                        break;
                    }
                    if (apObj.item) {
                        if (apObj.item instanceof PopupMenu.PopupSubMenuMenuItem) {
                            let items = apObj.item.menu.getMenuItems();
                            if (items.length == 2) {
                                // we need to update the connection list to convert this to a normal item
                                forceupdate = true;
                            } else {
                                for (let j = 0; j < items.length; j++) {
                                    if (items[j]._connection._uuid == connection._uuid) {
                                        items[j].destroy();
                                        break;
                                    }
                                }
                            }
                        } else {
                            apObj.item.destroy();
                            apObj.item = null;
                        }
                    }
                    break;
                }
            }
        }

        if (forceupdate || anyauto) {
            this._networks.sort(this._networkSortFunction);
            this._clearSection();
            this._createSection();
        }
    },

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

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

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

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

    _createActiveConnectionItem: function() {
        if (this._activeConnection.connection) {
            if (!this._activeNetwork) {
                if (this.device.active_access_point) {
                    let networkPos = this._findNetwork(this.device.active_access_point);
                    if (networkPos == -1) // the connected access point is invisible
                        this._activeNetwork = null;
                    else
                        this._activeNetwork = this._networks[networkPos];
                } else {
                    this._activeNetwork = null;
                }
            }

            if (this._activeNetwork)
                this._activeConnectionItem = new NMNetworkMenuItem(this._activeNetwork.accessPoints, undefined, { reactive: false });
            else
                this._activeConnectionItem = new PopupMenu.PopupImageMenuItem(this._activeConnection.connection._name, 'xsi-network-wireless-connected', { reactive: false });
        } else {
            // We cannot read the connection (due to ACL, or API incompatibility), but we still show signal if we have it
            if (this._activeNetwork)
                this._activeConnectionItem = new NMNetworkMenuItem(this._activeNetwork.accessPoints, undefined,
                                                                   { reactive: false });
            else
                this._activeConnectionItem = new PopupMenu.PopupImageMenuItem(_("Connected (private)"),
                                                                              'xsi-network-wireless-connected',
                                                                              { reactive: false });
        }
        this._activeConnectionItem.setShowDot(true);
        this._activeConnectionItem.actor.add_style_class_name('popup-device-menu-item');
    },

    _createAutomaticConnection: function(apObj) {
        let name;
        let ssid = NM.utils_ssid_to_utf8(apObj.ssid.get_data());
        if (ssid) {
            /* Translators: this the automatic wireless connection name (including the network name) */
            name = _("Auto %s").format(ssid);
        } else
            name = _("Auto wireless");

        let connection = new NM.SimpleConnection();
        connection.add_setting(new NM.SettingWireless());
        let setting_conn = new NM.SettingConnection({
            id: name,
            autoconnect: true, // NetworkManager will know to ignore this if appropriate
            uuid: NM.utils_uuid_generate(),
            type: NM.SETTING_WIRELESS_SETTING_NAME
        });
        if (default_to_private_connections(this._client)) {
            setting_conn.add_permission('user', GLib.get_user_name(), null);
            if (apObj.security == NMAccessPointSecurity.WPA2_PSK ||
                apObj.security == NMAccessPointSecurity.WPA_PSK) {
                connection.add_setting(new NM.SettingWirelessSecurity({
                    psk_flags: NM.SettingSecretFlags.AGENT_OWNED
                }));
            }
            if (apObj.security == NMAccessPointSecurity.WEP) {
                connection.add_setting(new NM.SettingWirelessSecurity({
                    wep_key_flags: NM.SettingSecretFlags.AGENT_OWNED
                }));
            }
        }
        connection.add_setting(setting_conn);
        return connection;
    },

    _createNetworkItem: function(apObj, position) {

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

        if(apObj.connections.length > 0) {
            if (apObj.connections.length == 1) {
                apObj.item = this._createAPItem(apObj.connections[0], apObj, false);
            } else {
                let title = apObj.ssidText;
                apObj.item = new PopupMenu.PopupSubMenuMenuItem(title);
                for (let i = 0; i < apObj.connections.length; i++)
                    apObj.item.menu.addMenuItem(this._createAPItem(apObj.connections[i], apObj, true));
            }
        } else {
            apObj.item = new NMNetworkMenuItem(apObj.accessPoints);
            apObj.item.connect('activate', Lang.bind(this, function() {
                let accessPoints = sortAccessPoints(apObj.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 cinnamon-settings
                    Util.spawn(['cinnamon-settings', 'network', 'connect-8021x-wifi',
                                this.device.get_path(), accessPoints[0].path]);
                } else {
                    let connection = this._createAutomaticConnection(apObj);
                    this._client.add_and_activate_connection_async(connection, this.device, accessPoints[0].path, null, null);
                }
            }));
        }
        apObj.item._apObj = apObj;

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

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

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

        let activeOffset = this._activeConnectionItem ? 1 : 0;

        for(let j = 0; j < this._networks.length; j++) {
            let apObj = this._networks[j];
            if (apObj == this._activeNetwork) {
                activeOffset = 0; // we no longer need to account for this
                continue;
            }

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

function NMMessageTraySource() {
    this._init();
}

NMMessageTraySource.prototype = {
    __proto__: MessageTray.Source.prototype,

    _init: function() {
        MessageTray.Source.prototype._init.call(this, _("Network Manager"));

        let icon = new St.Icon({ icon_name: 'xsi-network-transmit-receive',
                                 icon_type: St.IconType.SYMBOLIC,
                                 icon_size: this.ICON_SIZE
                               });
        this._setSummaryIcon(icon);
    }
};

function CinnamonNetworkApplet(metadata, orientation, panel_height, instance_id) {
    this._init(metadata, orientation, panel_height, instance_id);
}

CinnamonNetworkApplet.prototype = {
    __proto__: Applet.IconApplet.prototype,

    _init: function(metadata, orientation, panel_height, instance_id) {
        Applet.IconApplet.prototype._init.call(this, orientation, panel_height, instance_id);

        try {
            this.metadata = metadata;
            Main.systrayManager.registerTrayIconReplacement("network", metadata.uuid);
            Main.systrayManager.registerTrayIconReplacement("nm-applet", metadata.uuid);

            this.menuManager = new PopupMenu.PopupMenuManager(this);
            this.menu = new Applet.AppletPopupMenu(this, orientation);
            this.menuManager.addMenu(this.menu);

            this._currentIconName = undefined;
            this._setIcon('xsi-network-offline');

            this.settings = new Settings.AppletSettings(this, metadata.uuid, this.instance_id);
            this.settings.bind("keyOpen", "keyOpen", this._setKeybinding);
            this._setKeybinding();

            NM.Client.new_async(null, Lang.bind(this, this._clientGot));
        }
        catch (e) {
            global.logError(e);
        }
    },

    _setKeybinding() {
        Main.keybindingManager.addXletHotKey(this, "network-open", this.keyOpen, Lang.bind(this, this._openMenu));
    },

    _clientGot: function(obj, result) {
        try {
            this._client = NM.Client.new_finish(result);
            this._v1_28_0 = Util.version_exceeds(this._client.get_version(), "1.28.0");

            this._nm_proxy = null;

            if (!this._v1_28_0) {
                new NMDBus(Lang.bind(this, function(proxy, error) {
                    this._nm_proxy = proxy;
                }));
            }

            this._statusSection = new PopupMenu.PopupMenuSection();
            this._statusItem = new PopupMenu.PopupMenuItem('', { style_class: 'popup-inactive-menu-item', 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._devices = { };

            this._devices.wired = {
                section: new PopupMenu.PopupMenuSection(),
                devices: [ ],
                item: new NMWiredSectionTitleMenuItem(_("Wired"))
            };

            this._devices.wired.section.addMenuItem(this._devices.wired.item);
            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._makeToggleItem('wireless', _("Wireless"))
            };
            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: [ ],
                item: this._makeToggleItem('wwan', _("Mobile broadband"))
            };
            this._devices.wwan.section.addMenuItem(this._devices.wwan.item);
            this._devices.wwan.section.actor.hide();
            this.menu.addMenuItem(this._devices.wwan.section);
            this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

            this._devices.vpn = {
                section: new PopupMenu.PopupMenuSection(),
                device: new NMDeviceVPN(this._client),
                item: new NMWiredSectionTitleMenuItem(_("VPN Connections"))
            };
            this._devices.vpn.device.connect('active-connections-changed', Lang.bind(this, function() {
                this._devices.vpn.item.updateForDevice(this._devices.vpn.device);
            }));
            this._devices.vpn.item.updateForDevice(this._devices.vpn.device);
            this._devices.vpn.section.addMenuItem(this._devices.vpn.item);
            this._devices.vpn.section.addMenuItem(this._devices.vpn.device.section);
            this._devices.vpn.section.actor.hide();
            this.menu.addMenuItem(this._devices.vpn.section);
            this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

            this._devices.wireguard = {
                section: new PopupMenu.PopupMenuSection(),
                device: new NMDeviceWIREGUARD(this._client),
                item: new NMWiredSectionTitleMenuItem(_("WIREGUARD Connections"))
            };
            this._devices.wireguard.device.connect('active-connection-changed', Lang.bind(this, function() {
                this._devices.wireguard.item.updateForDevice(this._devices.wireguard.device);
            }));
            this._devices.wireguard.item.updateForDevice(this._devices.wireguard.device);
            this._devices.wireguard.section.addMenuItem(this._devices.wireguard.item);
            this._devices.wireguard.section.addMenuItem(this._devices.wireguard.device.section);
            this._devices.wireguard.section.actor.hide();
            this.menu.addMenuItem(this._devices.wireguard.section);
            this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

            this.menu.addSettingsAction(_("Network Settings"), 'network');
            this.menu.addAction(_("Network Connections"), Lang.bind(this, function() {
                Util.spawnCommandLine("nm-connection-editor");
            }));

            this.menu.connect("open-state-changed", Lang.bind(this, this._onMenuOpenStateChanged));

            this._periodicTimeoutId = 0;
            this._updateFrequencySeconds = 0;
            this._checkingConnectivity = false;
            this._lastConnectivityState = NM.ConnectivityState.UNKNOWN;

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

            this._mainConnection = null;

            // Device types
            this._dtypes = { };
            this._dtypes[NM.DeviceType.ETHERNET] = NMDeviceWired;
            this._dtypes[NM.DeviceType.WIFI] = NMDeviceWireless;
            this._dtypes[NM.DeviceType.MODEM] = NMDeviceModem;
            this._dtypes[NM.DeviceType.BT] = NMDeviceBluetooth;

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

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

            this._client.connect('notify::nm-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._updateIcon));
            this._client.connect('device-added', Lang.bind(this, this._deviceAdded));
            this._client.connect('device-removed', Lang.bind(this, this._deviceRemoved));
            this._client.connect('connection-added', Lang.bind(this, this._connectionAdded));
            this._client.connect('connection-removed', Lang.bind(this, this._connectionRemoved));

            this._inited = true;

            this._periodicUpdateIcon();

        }
        catch (e) {
            global.logError(e);
        }
    },

    _setIcon: function(name) {
        if (this._currentIconName !== name) {
            this.set_applet_icon_symbolic_name(name);
            this._currentIconName = name;
        }
    },

    on_applet_clicked: function(event) {
        this.menu.toggle();
    },

    _openMenu: function () {
        this.menu.toggle();
    },

    _ensureSource: function() {
        if (!this._source) {
            this._source = new NMMessageTraySource();
            this._source.connect('destroy', Lang.bind(this, function() {
                this._source = null;
            }));
            if (Main.messageTray) Main.messageTray.add(this._source);
        }
    },

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

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

        let item = this._devices[category].item;
        let section = this._devices[category].section;

        /* We don't want unmanaged entries to clutter the list, but we do need to
         * keep them around in case they become manageable and need to be made visible
         * later (some valid wifi hardware starts out initially unmanaged, for instance.)
         * We'll only count managed devices for evaluating what entries to display, and
         * when to show or hide section titles. */
        if (devices) {
            for (let i = 0; i < devices.length; i++) {
                if (devices[i].device.state == NM.DeviceState.UNMANAGED) {
                    continue;
                }

                vis_devices.push(devices[i]);
            }
        }

        if (vis_devices.length == 0) {
            section.actor.hide();
        } else {
            section.actor.show();
            if (vis_devices.length == 1) {
                devices.forEach(function(dev) {
                    dev.statusItem.actor.hide();
                });

                let dev = vis_devices[0];
                item.updateForDevice(dev);
            } else {
                devices.forEach(function(dev) {
                    dev.statusItem.actor.visible = (dev.device.state != NM.DeviceState.UNMANAGED);
                });
                // 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]);
        }
    },

    _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 icon = new St.Icon({ icon_name: iconName,
                                 icon_type: St.IconType.SYMBOLIC,
                                 icon_size: this._source.ICON_SIZE
                               });
        device._notification = new MessageTray.Notification(this._source, title, text,
                                                            { icon: icon });
        device._notification.setUrgency(urgency);
        device._notification.setTransient(true);
        device._notification.connect('destroy', function() {
            device._notification = null;
        });
        this._source.notify(device._notification);
    },

    _deviceAdded: function(client, device) {
        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);

            wrapper._activationFailedId = wrapper.connect('activation-failed', Lang.bind(this, 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',
                                      _("Connection failed"),
                                      _("Activation of network connection failed"),
                                     MessageTray.Urgency.HIGH);
            }));
            wrapper._deviceStateChangedId = wrapper.connect('state-changed', Lang.bind(this, function(dev) {
                this._syncSectionTitle(dev.category);
            }));
            wrapper._destroyId = wrapper.connect('destroy', function(wrapper) {
                wrapper.disconnect(wrapper._activationFailedId);
                wrapper.disconnect(wrapper._deviceStateChangedId);
                wrapper.disconnect(wrapper._destroyId);
            });
            let section = this._devices[wrapper.category].section;
            let devices = this._devices[wrapper.category].devices;

            section.addMenuItem(wrapper.section, 1);
            section.addMenuItem(wrapper.statusItem, 1);
            devices.push(wrapper);

            this._syncSectionTitle(wrapper.category);
        } else
            log('Unknown network device type, is ' + device.get_device_type());
    },

    _deviceRemoved: function(client, device) {
        if (!device._delegate) {
            log('Removing a network device that was not added');
            return;
        }

        let wrapper = device._delegate;
        wrapper.destroy();

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

        this._syncSectionTitle(wrapper.category);
    },

    _syncActiveConnections: function() {
        let closedConnections = [];
        let newActiveConnections = this._client.get_active_connections() || [];

        for (let a of this._activeConnections) {
            if (!newActiveConnections.includes(a))
                closedConnections.push(a);
        }

        for (let active of closedConnections) {
            if (active._primaryDevice) {
                if (active._type == NM.SETTING_VPN_SETTING_NAME)
                    this._devices.vpn.device.setActiveConnections([]);
                else
                    active._primaryDevice.setActiveConnection(null);

                active._primaryDevice = null;
            }

            if (active._inited) {
                active.disconnect(active._notifyStateId);
                active._inited = false;
            }
        }

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

        let activating = null;
        let activated = null;
        let default_ip4 = null;
        let default_ip6 = null;

        let vpnConnections = [];

        for (let a of this._activeConnections) {
            if (!a._inited) {
                a._notifyStateId = a.connect('notify::state', Lang.bind(this, this._notifyActivated));
                a._inited = true;
            }

            if (!a._type) {
                a._type = a.connection._type;
                a._section = this._ctypes[a._type];
                if (a._errorLogged) {
                    log('network applet: Found connection for active');
                    a._errorLogged = false;
                }
            }

            if (!a._section){
                // Do not take connections which section is undefined into account
                // For instance, in Mint 18, when the "vpn" is "activated", we sometimes see a "tun" act as the default ipv4 connection.
                continue;
            }

            if (a.state == NM.ActiveConnectionState.ACTIVATED) {
                if (!default_ip4) {
                    // We didn't find the default IPV4 device yet..
                    if (!default_ip6) {
                        // We didn't find the default IPV6 device either so consider this one.
                        activated = a;
                    }
                    else {
                        // We already found the default IPV6 device, so only use this one if it's the default IPV4 device
                        if (a['default']) {
                            activated = a;
                        }
                    }
                }
            }
            if (a.state == NM.ActiveConnectionState.ACTIVATING) {
                activating = a;
            }

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

            if (!a._primaryDevice) {
                if (a._type != NM.SETTING_VPN_SETTING_NAME && a._type != NM.SETTING_WIREGUARD_SETTING_NAME) {
                    // find a good device to be considered primary
                    a._primaryDevice = null;
                    let devices = a.get_devices() || [ ];
                    for (let j = 0; j < devices.length; j++) {
                        let d = devices[j];
                        if (d._delegate) {
                            a._primaryDevice = d._delegate;
                            break;
                        }
                    }
                } else {
                    if (a._type == NM.SETTING_VPN_SETTING_NAME)
                        a._primaryDevice = this._devices.vpn.device;
                    else {
                        a._primaryDevice = this._devices.wireguard.device;
                    }
                }

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

            if (a._primaryDevice) {
                if (a._type == NM.SETTING_VPN_SETTING_NAME) {
                    vpnConnections.push(a);
                } else {
                    a._primaryDevice.setActiveConnection(a);
                }
            }
        }

        if (this._devices.vpn && this._devices.vpn.device)
            this._devices.vpn.device.setActiveConnections(vpnConnections);

        this._mainConnection = activated || activating || default_ip4 || default_ip6 || null;
    },

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

        this._updateIcon();
    },

    _readConnections: function() {
        let connections = this._client.get_connections();
        for (let i = 0; i < connections.length; i++) {
            let connection = connections[i];
            if (connection._uuid) {
                // connection was already seen (for example because NetworkManager was restarted)
                continue;
            }
            connection._updatedId = connection.connect('changed', Lang.bind(this, this._updateConnection));

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

    _connectionAdded: function(client, connection) {
        if (connection._uuid) {
            // connection was already seen
            return;
        }

        connection._updatedId = connection.connect('changed', Lang.bind(this, this._updateConnection));

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

        this._updateIcon();
    },

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

        let section = connection._section;

        if (section == NMConnectionCategory.VPN) {
            this._devices.vpn.device.removeConnection(connection);
            if (this._devices.vpn.device.empty)
                this._devices.vpn.section.actor.hide();
        } else if (section != NMConnectionCategory.INVALID) {
            let devices = this._devices[section].devices;
            for (let i = 0; i < devices.length; i++)
                devices[i].removeConnection(connection);
        }

        connection._uuid = null;
        connection.disconnect(connection._updatedId);
    },

    _updateConnection: function(connection) {
        let connectionSettings = connection.get_setting_by_name(NM.SETTING_CONNECTION_SETTING_NAME);
        connection._type = connectionSettings.type;

        connection._section = this._ctypes[connection._type] || NMConnectionCategory.INVALID;
        connection._name = connectionSettings.id;
        connection._uuid = connectionSettings.uuid;
        connection._timestamp = connectionSettings.timestamp;

        let section = connection._section;

        if (connection._section == NMConnectionCategory.INVALID)
            return;
        if (section == NMConnectionCategory.VPN) {
            this._devices.vpn.device.checkConnection(connection);
            this._devices.vpn.section.actor.show();
        } else if (section == NMConnectionCategory.WIREGUARD) {
            this._devices.wireguard.device.checkConnection(connection);
            this._devices.wireguard.section.actor.show();
        } 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('wired');
        this._syncSectionTitle('wireless');
        this._syncSectionTitle('wwan');

        if (!this._devices.vpn.device.empty)
            this._devices.vpn.section.actor.show();
        if (!this._devices.wireguard.device.empty)
            this._devices.wireguard.section.actor.show();
    },

    _syncNMState: function() {
        if (!this._client.nm_running) {
            log('NetworkManager is not running, hiding...');
            this.menu.close();
            this.actor.hide();
            return;
        } else
            this.actor.show();

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

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

    _updateIcon: function() {
        let new_delay = DEFAULT_PERIODIC_UPDATE_FREQUENCY_SECONDS;

        try {
            this._syncActiveConnections();
            let mc = this._mainConnection;

            if (!mc) {
                this._setIcon('xsi-network-offline');
                this.set_applet_tooltip(_("No connection"));
            } else if (mc.state == NM.ActiveConnectionState.ACTIVATING) {
                new_delay = FAST_PERIODIC_UPDATE_FREQUENCY_SECONDS;
                switch (mc._section) {
                case NMConnectionCategory.WWAN:
                    this._setIcon('xsi-network-cellular-acquiring');
                    this.set_applet_tooltip(_("Connecting to the cellular network..."));
                    break;
                case NMConnectionCategory.WIRELESS:
                    this._setIcon('xsi-network-wireless-acquiring');
                    this.set_applet_tooltip(_("Connecting to the wireless network..."));
                    break;
                case NMConnectionCategory.WIRED:
                    this._setIcon('xsi-network-wired-acquiring');
                    this.set_applet_tooltip(_("Connecting to the wired network..."));
                    break;
                case NMConnectionCategory.VPN:
                    this._setIcon('xsi-network-vpn-acquiring');
                    this.set_applet_tooltip(_("Connecting to the VPN..."));
                    break;
                case NMConnectionCategory.WIREGUARD:
                    this._setIcon('xsi-network-vpn-acquiring');
                    this.set_applet_tooltip(_("Connecting to WIREGUARD..."));
                    break;
                default:
                    // fallback to a generic connected icon
                    // (it could be a private connection of some other user)
                    this._setIcon('xsi-network-wired-acquiring');
                    this.set_applet_tooltip(_("Connecting to the network..."));
                }
            } else {
                let dev;
                let limited_conn = false;
                // let limited_conn = this._correctStateForTunnel(this._client.get_connectivity()) !== NM.ConnectivityState.FULL;

                switch (mc._section) {
                case NMConnectionCategory.WIRELESS:
                    dev = mc._primaryDevice;
                    if (dev) {
                        let ap = dev.device.active_access_point;
                        let mode = dev.device.mode;
                        if (!ap) {
                            if (mode != NM80211Mode.ADHOC) {
                                log('An active wireless connection, in infrastructure mode, involves no access point?');
                                break;
                            }
                            this._setIcon('xsi-network-wireless-connected');
                            this.set_applet_tooltip(_("Connected to the wireless network"));
                        } else {
                            if (limited_conn) {
                                this._setIcon('xsi-network-wireless-no-route');
                                this.set_applet_tooltip(_("Wireless connection") + ": " + ssidToLabel(ap.get_ssid()) + " " + _("(Limited connectivity)"));
                            } else {
                                this._setIcon('xsi-network-wireless-signal-' + signalToIcon(ap.strength));
                                this.set_applet_tooltip(_("Wireless connection") + ": " + ssidToLabel(ap.get_ssid()) + " ("+ ap.strength +"%)");
                            }
                        }
                    } else {
                        log('Active connection with no primary device?');
                    }
                    break;
                case NMConnectionCategory.WIRED:
                    if (limited_conn) {
                        this._setIcon('xsi-network-wired-no-route');
                        this.set_applet_tooltip(_("Connected to the wired network") + " " + _("(Limited connectivity)"));

                    } else {
                        this._setIcon('xsi-network-wired');
                        this.set_applet_tooltip(_("Connected to the wired network"));
                    }
                    break;
                case NMConnectionCategory.WWAN:
                    dev = mc._primaryDevice;
                    if (!dev) {
                        log('Active connection with no primary device?');
                        break;
                    }
                    if (!dev.mobileDevice) {
                        // this can happen for bluetooth in PAN mode
                        this._setIcon('xsi-network-cellular-connected');
                        this.set_applet_tooltip(_("Connected to the cellular network"));
                        break;
                    }

                    if (limited_conn) {
                        this._setIcon('xsi-network-cellular-no-route');
                        this.set_applet_tooltip(_("Connected to the cellular network") + " " + _("(Limited connectivity)"));
                    } else {
                        this._setIcon('xsi-network-cellular-signal-' + signalToIcon(dev.mobileDevice.signal_quality));
                        this.set_applet_tooltip(_("Connected to the cellular network"));
                    }

                    break;
                case NMConnectionCategory.VPN:
                    // Should we indicate limited connectivity for VPNs like we do above? What if the connection is to
                    // a local machine? Need to test.
                    this._setIcon('xsi-network-vpn');
                    this.set_applet_tooltip(_("Connected to the VPN"));
                    break;
                case NMConnectionCategory.WIREGUARD:
                    // Should we indicate limited connectivity for WIREGUARDs like we do above? What if the connection is to
                    // a local machine? Need to test.
                    this._setIcon('xsi-network-vpn');
                    this.set_applet_tooltip(_("Connected to WIREGUARD"));
                    break;
                default:
                    // fallback to a generic connected icon
                    // (it could be a private connection of some other user)
                    this._setIcon('xsi-network-wired');
                    this.set_applet_tooltip(_("Connected to the network"));
                    break;
                }
            }
            if (this._devices.wireguard.item && this._devices.wireguard.item._switch.state) {
                this._setIcon('xsi-network-vpn');
                this.set_applet_tooltip(_("Connected to WIREGUARD"));
            }
            for (let i = 0; i < this._activeConnections.length; i++) {
                const a = this._activeConnections[i];
                if (a._section === NMConnectionCategory.VPN && a.state === NM.ActiveConnectionState.ACTIVATING) {
                    this._setIcon('xsi-network-vpn-acquiring');
                    this.set_applet_tooltip(_("Connecting to the VPN..."));
                    break;
                }
                else if (a._section === NMConnectionCategory.VPN && a.state === NM.ActiveConnectionState.ACTIVATED) {
                    let iconName = 'xsi-network-vpn';
                    if (mc._section == NMConnectionCategory.WIRELESS) {
                        const dev = mc._primaryDevice;
                        if (dev) {
                            const ap = dev.device.active_access_point;
                            iconName = 'xsi-network-wireless-signal-' + signalToIcon(ap.strength) + '-secure-symbolic';
                        }
                    }
                    this._setIcon(iconName);
                    this.set_applet_tooltip(_("Connected to the VPN"));
                    break;
                }
            }
        }
        catch (e) {
            global.logError(e);
        }

        if (this.menu.isOpen) {
            return FAST_PERIODIC_UPDATE_FREQUENCY_SECONDS;
        }

        return new_delay;
    },

    _periodicUpdateIcon: function() {
        let new_delay = this._updateIcon();
        this._lastConnectivityState = this._client.get_connectivity();

        // TODO: This can be unreliable with multiple interfaces, and libnm
        // can be flaky with its connectivity state - on one machine of mine,
        // I observed regular switching between full and limited connectivity,
        // when no noticeable change in my actual ability to reach the internet.
        // We might re-implement to be independent of libnm. For now just disable it.
        //
        // if (!this._checkingConnectivity) {
        //     // https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/476
        //     if (this._v1_28_0) {
        //         this._checkingConnectivity = true;
        //         this._client.check_connectivity_async(null, Lang.bind(this, this._connectivityCheckCallback));
        //     } else
        //     if (this._nm_proxy != null) {
        //         this._checkingConnectivity = true;
        //         this._nm_proxy.CheckConnectivityRemote(Lang.bind(this, this._proxyConnectivityCheckCallback));
        //     }
        // }

        if (this._updateFrequencySeconds != new_delay) {
            this._restartPeriodicUpdateTimer(new_delay);
        } else {
            return GLib.SOURCE_CONTINUE;
        }

        return GLib.SOURCE_REMOVE;
    },

    _correctStateForTunnel(state) {
        if (state !== NM.ConnectivityState.LIMITED) {
            return state;
        }

        // if our primary connection state is 'limited', check if there's a tunnel
        // connection active, and assume 'full' if so - this prevents reporting limited
        // connectivity when something like PIA vpn is active. This could end up reporting
        // a full connection when there isn't one, depending on what the tunnel is for,
        // but NM doesn't seem to provide any way to discern a relationship between a virtual
        // device (tunnel) and the physical device in use. The physical device is always returned
        // as the primary.

        if (this._activeConnections.some(con => con.get_connection_type() === "tun")) {
            return NM.ConnectivityState.FULL;
        }

        if (this._activeConnections.some(con => con.get_connection_type() === "wireguard")) {
            return NM.ConnectivityState.FULL;
        }
    },

    _proxyConnectivityCheckCallback(new_state) {
        let state = this._correctStateForTunnel(new_state[0]);

        if (state !== this._lastConnectivityState) {
            this._updateIcon();
        }

        this._checkingConnectivity = false;
    },

    _connectivityCheckCallback(source, result) {
        let new_state = NM.ConnectivityState.UNKNOWN;

        try {
            new_state = source.check_connectivity_finish(result);
        } catch (e) {
            log(e);
        }

        let state = this._correctStateForTunnel(new_state);

        if (state !== this._lastConnectivityState) {
            this._updateIcon();
        }

        this._checkingConnectivity = false;
    },

    _restartPeriodicUpdateTimer: function(new_delay) {
        if (this._periodicTimeoutId > 0) {
            Mainloop.source_remove(this._periodicTimeoutId);
        }

        this._updateFrequencySeconds = new_delay;

        this._periodicTimeoutId = Mainloop.timeout_add_seconds(this._updateFrequencySeconds, Lang.bind(this, this._periodicUpdateIcon));
    },

    _rescanAccessPoints: function() {
        try {
            let devices = this._devices.wireless.devices;

            for (let i = 0; i < devices.length; i++) {
                if (devices[i]._client.wireless_get_enabled()) {
                    devices[i].device.request_scan(null);
                }
            }
        } catch (gerror) {
            if (gerror.code == NM.DeviceError.NOTALLOWED) {
                // NM only allows a rescan every 10 seconds
                return;
            } else {
                log(gerror);
            }
        }
    },

    _onMenuOpenStateChanged: function(popup, open) {
        this._periodicUpdateIcon();

        if (open) {
            this._rescanAccessPoints();
        }
    },

    on_applet_removed_from_panel: function() {
        Main.systrayManager.unregisterTrayIconReplacement(this.metadata.uuid);
        Main.keybindingManager.removeXletHotKey(this, "network-open");
        if (this._periodicTimeoutId){
            Mainloop.source_remove(this._periodicTimeoutId);
        }
    },

};

function main(metadata, orientation, panel_height, instance_id) {
    return new CinnamonNetworkApplet(metadata, orientation, panel_height, instance_id);
}
