import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import St from 'gi://St'

import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as Util from 'resource:///org/gnome/shell/misc/util.js';

import * as Sensors from './sensors.js';

import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';

import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js';
import * as Values from './values.js';
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
import * as MenuItem from './menuItem.js';

let vitalsMenu;

var VitalsMenuButton = GObject.registerClass({
    GTypeName: 'VitalsMenuButton',
}, class VitalsMenuButton extends PanelMenu.Button {
    _init(extensionObject) {
        super._init(Clutter.ActorAlign.FILL);

        this._extensionObject = extensionObject;
        this._settings = extensionObject.getSettings();

        this._sensorIcons = {
            'temperature' : { 'icon': 'temperature-symbolic.svg' },
                'voltage' : { 'icon': 'voltage-symbolic.svg' },
                    'fan' : { 'icon': 'fan-symbolic.svg' },
                 'memory' : { 'icon': 'memory-symbolic.svg' },
              'processor' : { 'icon': 'cpu-symbolic.svg' },
                 'system' : { 'icon': 'system-symbolic.svg' },
                'network' : { 'icon': 'network-symbolic.svg',
                           'icon-rx': 'network-download-symbolic.svg',
                           'icon-tx': 'network-upload-symbolic.svg',
                           'icon-ad': '../flags/1x1/ad.svg',
                           'icon-ae': '../flags/1x1/ae.svg',
                           'icon-af': '../flags/1x1/af.svg',
                           'icon-ag': '../flags/1x1/ag.svg',
                           'icon-ai': '../flags/1x1/ai.svg',
                           'icon-al': '../flags/1x1/al.svg',
                           'icon-am': '../flags/1x1/am.svg',
                           'icon-ao': '../flags/1x1/ao.svg',
                           'icon-ar': '../flags/1x1/ar.svg',
                           'icon-at': '../flags/1x1/at.svg',
                           'icon-au': '../flags/1x1/au.svg',
                           'icon-aw': '../flags/1x1/aw.svg',
                           'icon-ax': '../flags/1x1/ax.svg',
                           'icon-az': '../flags/1x1/az.svg',
                           'icon-ba': '../flags/1x1/ba.svg',
                           'icon-bb': '../flags/1x1/bb.svg',
                           'icon-bd': '../flags/1x1/bd.svg',
                           'icon-be': '../flags/1x1/be.svg',
                           'icon-bf': '../flags/1x1/bf.svg',
                           'icon-bg': '../flags/1x1/bg.svg',
                           'icon-bh': '../flags/1x1/bh.svg',
                           'icon-bi': '../flags/1x1/bi.svg',
                           'icon-bj': '../flags/1x1/bj.svg',
                           'icon-bl': '../flags/1x1/bl.svg',
                           'icon-bm': '../flags/1x1/bm.svg',
                           'icon-bn': '../flags/1x1/bn.svg',
                           'icon-bo': '../flags/1x1/bo.svg',
                           'icon-bq': '../flags/1x1/bq.svg',
                           'icon-br': '../flags/1x1/br.svg',
                           'icon-bs': '../flags/1x1/bs.svg',
                           'icon-bt': '../flags/1x1/bt.svg',
                           'icon-bv': '../flags/1x1/bv.svg',
                           'icon-bw': '../flags/1x1/bw.svg',
                           'icon-by': '../flags/1x1/by.svg',
                           'icon-bz': '../flags/1x1/bz.svg',
                           'icon-ca': '../flags/1x1/ca.svg',
                           'icon-cc': '../flags/1x1/cc.svg',
                           'icon-cd': '../flags/1x1/cd.svg',
                           'icon-cf': '../flags/1x1/cf.svg',
                           'icon-cg': '../flags/1x1/cg.svg',
                           'icon-ch': '../flags/1x1/ch.svg',
                           'icon-ci': '../flags/1x1/ci.svg',
                           'icon-ck': '../flags/1x1/ck.svg',
                           'icon-cl': '../flags/1x1/cl.svg',
                           'icon-cm': '../flags/1x1/cm.svg',
                           'icon-cn': '../flags/1x1/cn.svg',
                           'icon-co': '../flags/1x1/co.svg',
                           'icon-cr': '../flags/1x1/cr.svg',
                           'icon-cu': '../flags/1x1/cu.svg',
                           'icon-cv': '../flags/1x1/cv.svg',
                           'icon-cw': '../flags/1x1/cw.svg',
                           'icon-cx': '../flags/1x1/cx.svg',
                           'icon-cy': '../flags/1x1/cy.svg',
                           'icon-cz': '../flags/1x1/cz.svg',
                           'icon-de': '../flags/1x1/de.svg',
                           'icon-dj': '../flags/1x1/dj.svg',
                           'icon-dk': '../flags/1x1/dk.svg',
                           'icon-dm': '../flags/1x1/dm.svg',
                           'icon-do': '../flags/1x1/do.svg',
                           'icon-dz': '../flags/1x1/dz.svg',
                           'icon-ec': '../flags/1x1/ec.svg',
                           'icon-ee': '../flags/1x1/ee.svg',
                           'icon-eg': '../flags/1x1/eg.svg',
                           'icon-eh': '../flags/1x1/eh.svg',
                           'icon-er': '../flags/1x1/er.svg',
                           'icon-es': '../flags/1x1/es.svg',
                           'icon-et': '../flags/1x1/et.svg',
                           'icon-eu': '../flags/1x1/eu.svg',
                           'icon-fi': '../flags/1x1/fi.svg',
                           'icon-fj': '../flags/1x1/fj.svg',
                           'icon-fk': '../flags/1x1/fk.svg',
                           'icon-fm': '../flags/1x1/fm.svg',
                           'icon-fo': '../flags/1x1/fo.svg',
                           'icon-fr': '../flags/1x1/fr.svg',
                           'icon-ga': '../flags/1x1/ga.svg',
                           'icon-gb': '../flags/1x1/gb.svg',
                           'icon-gd': '../flags/1x1/gd.svg',
                           'icon-ge': '../flags/1x1/ge.svg',
                           'icon-gf': '../flags/1x1/gf.svg',
                           'icon-gg': '../flags/1x1/gg.svg',
                           'icon-gh': '../flags/1x1/gh.svg',
                           'icon-gi': '../flags/1x1/gi.svg',
                           'icon-gl': '../flags/1x1/gl.svg',
                           'icon-gm': '../flags/1x1/gm.svg',
                           'icon-gn': '../flags/1x1/gn.svg',
                           'icon-gp': '../flags/1x1/gp.svg',
                           'icon-gq': '../flags/1x1/gq.svg',
                           'icon-gr': '../flags/1x1/gr.svg',
                           'icon-gs': '../flags/1x1/gs.svg',
                           'icon-gt': '../flags/1x1/gt.svg',
                           'icon-gu': '../flags/1x1/gu.svg',
                           'icon-gw': '../flags/1x1/gw.svg',
                           'icon-gy': '../flags/1x1/gy.svg',
                           'icon-hk': '../flags/1x1/hk.svg',
                           'icon-hm': '../flags/1x1/hm.svg',
                           'icon-hn': '../flags/1x1/hn.svg',
                           'icon-hr': '../flags/1x1/hr.svg',
                           'icon-ht': '../flags/1x1/ht.svg',
                           'icon-hu': '../flags/1x1/hu.svg',
                           'icon-id': '../flags/1x1/id.svg',
                           'icon-ie': '../flags/1x1/ie.svg',
                           'icon-il': '../flags/1x1/il.svg',
                           'icon-im': '../flags/1x1/im.svg',
                           'icon-in': '../flags/1x1/in.svg',
                           'icon-io': '../flags/1x1/io.svg',
                           'icon-iq': '../flags/1x1/iq.svg',
                           'icon-ir': '../flags/1x1/ir.svg',
                           'icon-is': '../flags/1x1/is.svg',
                           'icon-it': '../flags/1x1/it.svg',
                           'icon-je': '../flags/1x1/je.svg',
                           'icon-jm': '../flags/1x1/jm.svg',
                           'icon-jo': '../flags/1x1/jo.svg',
                           'icon-jp': '../flags/1x1/jp.svg',
                           'icon-ke': '../flags/1x1/ke.svg',
                           'icon-kg': '../flags/1x1/kg.svg',
                           'icon-kh': '../flags/1x1/kh.svg',
                           'icon-ki': '../flags/1x1/ki.svg',
                           'icon-km': '../flags/1x1/km.svg',
                           'icon-kn': '../flags/1x1/kn.svg',
                           'icon-kp': '../flags/1x1/kp.svg',
                           'icon-kr': '../flags/1x1/kr.svg',
                           'icon-kw': '../flags/1x1/kw.svg',
                           'icon-ky': '../flags/1x1/ky.svg',
                           'icon-kz': '../flags/1x1/kz.svg',
                           'icon-la': '../flags/1x1/la.svg',
                           'icon-lb': '../flags/1x1/lb.svg',
                           'icon-lc': '../flags/1x1/lc.svg',
                           'icon-li': '../flags/1x1/li.svg',
                           'icon-lk': '../flags/1x1/lk.svg',
                           'icon-lr': '../flags/1x1/lr.svg',
                           'icon-ls': '../flags/1x1/ls.svg',
                           'icon-lt': '../flags/1x1/lt.svg',
                           'icon-lu': '../flags/1x1/lu.svg',
                           'icon-lv': '../flags/1x1/lv.svg',
                           'icon-ly': '../flags/1x1/ly.svg',
                           'icon-ma': '../flags/1x1/ma.svg',
                           'icon-mc': '../flags/1x1/mc.svg',
                           'icon-md': '../flags/1x1/md.svg',
                           'icon-me': '../flags/1x1/me.svg',
                           'icon-mf': '../flags/1x1/mf.svg',
                           'icon-mg': '../flags/1x1/mg.svg',
                           'icon-mh': '../flags/1x1/mh.svg',
                           'icon-mk': '../flags/1x1/mk.svg',
                           'icon-ml': '../flags/1x1/ml.svg',
                           'icon-mm': '../flags/1x1/mm.svg',
                           'icon-mn': '../flags/1x1/mn.svg',
                           'icon-mo': '../flags/1x1/mo.svg',
                           'icon-mp': '../flags/1x1/mp.svg',
                           'icon-mq': '../flags/1x1/mq.svg',
                           'icon-mr': '../flags/1x1/mr.svg',
                           'icon-ms': '../flags/1x1/ms.svg',
                           'icon-mt': '../flags/1x1/mt.svg',
                           'icon-mu': '../flags/1x1/mu.svg',
                           'icon-mv': '../flags/1x1/mv.svg',
                           'icon-mw': '../flags/1x1/mw.svg',
                           'icon-mx': '../flags/1x1/mx.svg',
                           'icon-my': '../flags/1x1/my.svg',
                           'icon-mz': '../flags/1x1/mz.svg',
                           'icon-na': '../flags/1x1/na.svg',
                           'icon-nc': '../flags/1x1/nc.svg',
                           'icon-ne': '../flags/1x1/ne.svg',
                           'icon-nf': '../flags/1x1/nf.svg',
                           'icon-ng': '../flags/1x1/ng.svg',
                           'icon-ni': '../flags/1x1/ni.svg',
                           'icon-nl': '../flags/1x1/nl.svg',
                           'icon-no': '../flags/1x1/no.svg',
                           'icon-np': '../flags/1x1/np.svg',
                           'icon-nr': '../flags/1x1/nr.svg',
                           'icon-nu': '../flags/1x1/nu.svg',
                           'icon-nz': '../flags/1x1/nz.svg',
                           'icon-om': '../flags/1x1/om.svg',
                           'icon-pa': '../flags/1x1/pa.svg',
                           'icon-pe': '../flags/1x1/pe.svg',
                           'icon-pf': '../flags/1x1/pf.svg',
                           'icon-pg': '../flags/1x1/pg.svg',
                           'icon-ph': '../flags/1x1/ph.svg',
                           'icon-pk': '../flags/1x1/pk.svg',
                           'icon-pl': '../flags/1x1/pl.svg',
                           'icon-pm': '../flags/1x1/pm.svg',
                           'icon-pn': '../flags/1x1/pn.svg',
                           'icon-pr': '../flags/1x1/pr.svg',
                           'icon-ps': '../flags/1x1/ps.svg',
                           'icon-pt': '../flags/1x1/pt.svg',
                           'icon-pw': '../flags/1x1/pw.svg',
                           'icon-py': '../flags/1x1/py.svg',
                           'icon-qa': '../flags/1x1/qa.svg',
                           'icon-re': '../flags/1x1/re.svg',
                           'icon-ro': '../flags/1x1/ro.svg',
                           'icon-rs': '../flags/1x1/rs.svg',
                           'icon-ru': '../flags/1x1/ru.svg',
                           'icon-rw': '../flags/1x1/rw.svg',
                           'icon-sa': '../flags/1x1/sa.svg',
                           'icon-sb': '../flags/1x1/sb.svg',
                           'icon-sc': '../flags/1x1/sc.svg',
                           'icon-sd': '../flags/1x1/sd.svg',
                           'icon-se': '../flags/1x1/se.svg',
                           'icon-sg': '../flags/1x1/sg.svg',
                           'icon-sh': '../flags/1x1/sh.svg',
                           'icon-si': '../flags/1x1/si.svg',
                           'icon-sj': '../flags/1x1/sj.svg',
                           'icon-sk': '../flags/1x1/sk.svg',
                           'icon-sl': '../flags/1x1/sl.svg',
                           'icon-sm': '../flags/1x1/sm.svg',
                           'icon-sn': '../flags/1x1/sn.svg',
                           'icon-so': '../flags/1x1/so.svg',
                           'icon-sr': '../flags/1x1/sr.svg',
                           'icon-ss': '../flags/1x1/ss.svg',
                           'icon-st': '../flags/1x1/st.svg',
                           'icon-sv': '../flags/1x1/sv.svg',
                           'icon-sx': '../flags/1x1/sx.svg',
                           'icon-sy': '../flags/1x1/sy.svg',
                           'icon-sz': '../flags/1x1/sz.svg',
                           'icon-tc': '../flags/1x1/tc.svg',
                           'icon-td': '../flags/1x1/td.svg',
                           'icon-tf': '../flags/1x1/tf.svg',
                           'icon-tg': '../flags/1x1/tg.svg',
                           'icon-th': '../flags/1x1/th.svg',
                           'icon-tj': '../flags/1x1/tj.svg',
                           'icon-tk': '../flags/1x1/tk.svg',
                           'icon-tl': '../flags/1x1/tl.svg',
                           'icon-tm': '../flags/1x1/tm.svg',
                           'icon-tn': '../flags/1x1/tn.svg',
                           'icon-to': '../flags/1x1/to.svg',
                           'icon-tr': '../flags/1x1/tr.svg',
                           'icon-tt': '../flags/1x1/tt.svg',
                           'icon-tv': '../flags/1x1/tv.svg',
                           'icon-tw': '../flags/1x1/tw.svg',
                           'icon-tz': '../flags/1x1/tz.svg',
                           'icon-ua': '../flags/1x1/ua.svg',
                           'icon-ug': '../flags/1x1/ug.svg',
                           'icon-um': '../flags/1x1/um.svg',
                           'icon-us': '../flags/1x1/us.svg',
                           'icon-uy': '../flags/1x1/uy.svg',
                           'icon-uz': '../flags/1x1/uz.svg',
                           'icon-va': '../flags/1x1/va.svg',
                           'icon-vc': '../flags/1x1/vc.svg',
                           'icon-ve': '../flags/1x1/ve.svg',
                           'icon-vg': '../flags/1x1/vg.svg',
                           'icon-vi': '../flags/1x1/vi.svg',
                           'icon-vn': '../flags/1x1/vn.svg',
                           'icon-vu': '../flags/1x1/vu.svg',
                           'icon-wf': '../flags/1x1/wf.svg',
                           'icon-ws': '../flags/1x1/ws.svg',
                           'icon-xk': '../flags/1x1/xk.svg',
                           'icon-ye': '../flags/1x1/ye.svg',
                           'icon-yt': '../flags/1x1/yt.svg',
                           'icon-za': '../flags/1x1/za.svg',
                           'icon-zm': '../flags/1x1/zm.svg',
                           'icon-zw': '../flags/1x1/zw.svg'
                },
                'storage' : { 'icon': 'storage-symbolic.svg' },
                'battery' : { 'icon': 'battery-symbolic.svg' },
                    'gpu' : { 'icon': 'gpu-symbolic.svg' }
        }

        // list with the prefixes for the according themes, the index of each
        // item must match the index on the combo box
        this._sensorsIconPathPrefix = ['/icons/original/', '/icons/gnome/'];

        this._warnings = [];
        this._sensorMenuItems = {};
        this._hotLabels = {};
        this._hotItems = {};
        this._groups = {};
        this._widths = {};
        this._numGpus = 1;
        this._newGpuDetected = false;
        this._newGpuDetectedCount = 0;
        this._last_query = new Date().getTime();

        this._sensors = new Sensors.Sensors(this._settings, this._sensorIcons);
        this._values = new Values.Values(this._settings, this._sensorIcons);
        this._menuLayout = new St.BoxLayout({
            vertical: false,
            clip_to_allocation: true,
            x_align: Clutter.ActorAlign.START,
            y_align: Clutter.ActorAlign.CENTER,
            reactive: true,
            x_expand: true,
            style_class: 'vitals-panel-menu'
        });

        this._drawMenu();
        this.add_child(this._menuLayout);
        this._settingChangedSignals = [];
        this._refreshTimeoutId = null;

        this._addSettingChangedSignal('update-time', this._updateTimeSettingChanged.bind(this));
        this._addSettingChangedSignal('position-in-panel', this._positionInPanelChanged.bind(this));
        this._addSettingChangedSignal('menu-centered', this._positionInPanelChanged.bind(this));
        this._addSettingChangedSignal('icon-style', this._iconStyleChanged.bind(this));

        let settings = [ 'use-higher-precision', 'alphabetize', 'hide-zeros',
                         'fixed-widths', 'hide-icons', 'unit',
                         'memory-measurement', 'include-public-ip', 'network-public-ip-interval',
                         'network-public-ip-show-flag', 'network-speed-format', 'storage-measurement',
                         'include-static-info', 'include-static-gpu-info' ];

        for (let setting of Object.values(settings))
            this._addSettingChangedSignal(setting, this._redrawMenu.bind(this));

        // add signals for show- preference based categories
        for (let sensor in this._sensorIcons)
            this._addSettingChangedSignal('show-' + sensor, this._showHideSensorsChanged.bind(this));

        this._initializeMenu();

        // start off with fresh sensors
        this._querySensors();

        // start monitoring sensors
        this._initializeTimer();
    }

    _initializeMenu() {
        // display sensor categories
        for (let sensor in this._sensorIcons) {
            // groups associated sensors under accordion menu
            if (sensor in this._groups) continue;

            //handle gpus separately.
            if (sensor === 'gpu') continue;

            this._initializeMenuGroup(sensor, sensor);
        }

        for (let i = 1; i <= this._numGpus; i++)
            this._initializeMenuGroup('gpu#' + i, 'gpu', (this._numGpus > 1 ? ' ' + i : ''));

        // add separator
        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

        let item = new PopupMenu.PopupBaseMenuItem({
            reactive: false,
            style_class: 'vitals-menu-button-container'
        });

        let customButtonBox = new St.BoxLayout({
            style_class: 'vitals-button-box',
            vertical: false,
            clip_to_allocation: true,
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
            reactive: true,
            x_expand: true
        });

        // custom round refresh button
        let refreshButton = this._createRoundButton('view-refresh-symbolic', _('Refresh'));
        refreshButton.connect('clicked', (self) => {
            // force refresh by clearing history
            this._sensors.resetHistory();
            this._values.resetHistory(this._numGpus);

            // make sure timer fires at next full interval
            this._updateTimeChanged();

            // refresh sensors now
            this._querySensors();
        });
        customButtonBox.add_child(refreshButton);

        // custom round monitor button
        let monitorButton = this._createRoundButton('org.gnome.SystemMonitor-symbolic', _('System Monitor'));
        monitorButton.connect('clicked', (self) => {
            this.menu._getTopMenu().close();
            Util.spawn(this._settings.get_string('monitor-cmd').split(" "));
        });
        customButtonBox.add_child(monitorButton);

        // custom round preferences button
        let prefsButton = this._createRoundButton('preferences-system-symbolic', _('Preferences'));
        prefsButton.connect('clicked', (self) => {
            this.menu._getTopMenu().close();
            this._extensionObject.openPreferences();
        });
        customButtonBox.add_child(prefsButton);

        // now add the buttons to the top bar
        item.actor.add_child(customButtonBox);

        // add buttons
        this.menu.addMenuItem(item);

        // query sensors on menu open
        this._menuStateChangeId = this.menu.connect('open-state-changed', (self, isMenuOpen) => {
            if (isMenuOpen) {
                // make sure timer fires at next full interval
                this._updateTimeChanged();

                // refresh sensors now
                this._querySensors();
            }
        });
    }

    _initializeMenuGroup(groupName, optionName, menuSuffix = '', position = -1) {
        this._groups[groupName] = new PopupMenu.PopupSubMenuMenuItem(_(this._ucFirst(groupName) + menuSuffix), true);
        this._groups[groupName].icon.gicon = Gio.icon_new_for_string(this._sensorIconPath(groupName));

        // hide menu items that user has requested to not include
        if (!this._settings.get_boolean('show-' + optionName))
            this._groups[groupName].actor.hide();

        if (!this._groups[groupName].status) {
            this._groups[groupName].status = this._defaultLabel();
            this._groups[groupName].actor.insert_child_at_index(this._groups[groupName].status, 4);
            this._groups[groupName].status.text = _('No Data');
        }

        if(position == -1) this.menu.addMenuItem(this._groups[groupName]);
        else this.menu.addMenuItem(this._groups[groupName], position);
    }

    _createRoundButton(iconName) {
        let button = new St.Button({
            style_class: 'message-list-clear-button button vitals-button-action'
        });

        button.child = new St.Icon({
            icon_name: iconName
        });

        return button;
    }

    _removeMissingHotSensors(hotSensors) {
        for (let i = hotSensors.length - 1; i >= 0; i--) {
            let sensor = hotSensors[i];

            // make sure default icon (if any) stays visible
            if (sensor == '_default_icon_') continue;

            // removes sensors that are no longer available
            if (!this._sensorMenuItems[sensor]) {
                hotSensors.splice(i, 1);
                this._removeHotItem(sensor);
            }
        }

        return hotSensors;
    }

    _saveHotSensors(hotSensors) {
        // removes any sensors that may not currently be available
        hotSensors = this._removeMissingHotSensors(hotSensors);

        this._settings.set_strv('hot-sensors', hotSensors.filter(
            function(item, pos) {
                return hotSensors.indexOf(item) == pos;
            }
        ));
    }

    _initializeTimer() {
        // used to query sensors and update display
        let update_time = this._settings.get_int('update-time');
        this._refreshTimeoutId = GLib.timeout_add_seconds(
            GLib.PRIORITY_DEFAULT,
            update_time,
            (self) => {
                // always query sensors (for panel display when hot)
                this._querySensors();
                return GLib.SOURCE_CONTINUE;
            }
        );
    }

    _createHotItem(key, value, gicon) {
        let item = new St.BoxLayout({
            style_class: 'vitals-panel-item',
        });
        this._hotItems[key] = item;
        this._menuLayout.add_child(item);

        if (!this._settings.get_boolean('hide-icons') || key == '_default_icon_') {
            let icon = this._defaultIcon(key);
            if (gicon) icon.gicon = gicon;
            item.add_child(icon);
        }

        // don't add a label when no sensors are in the panel
        if (key == '_default_icon_') return;

        let label = new St.Label({
            style_class: 'vitals-panel-label',
            text: (value)?value:'\u2026', // ...
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER
        });
        // attempt to prevent ellipsizes
        label.get_clutter_text().ellipsize = 0;
        // keep track of label for removal later
        this._hotLabels[key] = label;
        // prevent "called on the widget"  "which is not in the stage" errors by adding before width below
        item.add_child(label);

        // support for fixed widths #55, save label (text) width
        this._widths[key] = label.get_clutter_text().width;
    }

    _showHideSensorsChanged(self, sensor) {
        this._sensors.resetHistory();

        const sensorName = sensor.substr(5);
        if(sensorName === 'gpu') {
            for(let i = 1; i <= this._numGpus; i++)
                this._groups[sensorName + '#' + i].visible = this._settings.get_boolean(sensor);
        } else
            this._groups[sensorName].visible = this._settings.get_boolean(sensor);
    }

    _positionInPanelChanged() {
        this.container.get_parent().remove_child(this.container);
        let position = this._positionInPanel();

        // allows easily addressable boxes
        let boxes = {
            left: Main.panel._leftBox,
            center: Main.panel._centerBox,
            right: Main.panel._rightBox
        };

        // update position when changed from preferences
        boxes[position[0]].insert_child_at_index(this.container, position[1]);
    }

    _redrawDetailsMenuIcons() {
        // updates the icons on the 'details' menu, the one
        // you have to click to appear
        this._sensors.resetHistory();
        for (const sensor in this._sensorIcons) {
            if (sensor == "gpu") continue;
            this._groups[sensor].icon.gicon = Gio.icon_new_for_string(this._sensorIconPath(sensor));
        }

        // gpu's are indexed differently, handle them here
        const gpuKeys = Object.keys(this._groups).filter(key => key.startsWith("gpu#"));
        gpuKeys.forEach((gpuKey) => {
            this._groups[gpuKey].icon.gicon = Gio.icon_new_for_string(this._sensorIconPath("gpu"));
        });
    }

    _iconStyleChanged() {
        this._redrawDetailsMenuIcons();
        this._redrawMenu();
    }

    _removeHotItems(){
        for (let key in this._hotItems) {
            this._removeHotItem(key);
        }
    }

    _removeHotItem(key) {
        if (key in this._hotItems) {
            this._hotItems[key].destroy();
            delete this._hotItems[key];
            delete this._hotLabels[key];
            delete this._widths[key];
        }
    }

    _redrawMenu() {
        this._removeHotItems();

        for (let key in this._sensorMenuItems) {
            if (key.includes('-group')) continue;
            this._sensorMenuItems[key].destroy();
            delete this._sensorMenuItems[key];
        }

        this._drawMenu();
        this._sensors.resetHistory();
        this._values.resetHistory(this._numGpus);
        this._querySensors();
    }

    _drawMenu() {
        // grab list of selected menubar icons
        let hotSensors = this._settings.get_strv('hot-sensors');
        for (let key of Object.values(hotSensors)) {
            // fixes issue #225 which started when _max_ was moved to the end
            if (key == '__max_network-download__') key = '__network-rx_max__';
            if (key == '__max_network-upload__') key = '__network-tx_max__';

            this._createHotItem(key);
        }
    }

    _destroyTimer() {
        // invalidate and reinitialize timer
        if (this._refreshTimeoutId != null) {
            GLib.Source.remove(this._refreshTimeoutId);
            this._refreshTimeoutId = null;
        }
    }

    _updateTimeSettingChanged() {
        this._destroyTimer();
        this._initializeTimer();
    }

    _updateTimeChanged() {
        this._destroyTimer();
        this._initializeTimer();
    }

    _addSettingChangedSignal(key, callback) {
        this._settingChangedSignals.push(this._settings.connect('changed::' + key, callback));
    }

    _updateDisplay(label, value, type, key) {
        // update sensor value in menubar
        if (this._hotLabels[key]) {
            this._hotLabels[key].set_text(value);

            // support for fixed widths #55
            if (this._settings.get_boolean('fixed-widths')) {
                // grab text box width and see if new text is wider than old text
                let width2 = this._hotLabels[key].get_clutter_text().width;
                if (width2 > this._widths[key]) {
                    this._hotLabels[key].set_width(width2);
                    this._widths[key] = width2;
                }
            }
        }

        // have we added this sensor before?
        let item = this._sensorMenuItems[key];
        if (item) {
            // update sensor value in the group
            item.value = value;
        } else if (type.includes('-group')) {
            // update text next to group header
            let group = type.split('-')[0];
            if (this._groups[group]) {
                this._groups[group].status.text = value;
                this._sensorMenuItems[type] = this._groups[group];
            }
        } else {
            // add item to group for the first time
            let sensor = { 'label': label, 'value': value, 'type': type }
            this._appendMenuItem(sensor, key);
        }
    }

    _appendMenuItem(sensor, key) {
        let split = sensor.type.split('-');
        let type = split[0];
        let icon = (split.length == 2)?'icon-' + split[1]:'icon';
        let gicon = Gio.icon_new_for_string(this._sensorIconPath(type, icon));

        let item = new MenuItem.MenuItem(gicon, key, sensor.label, sensor.value, this._hotLabels[key]);
        item.connect('toggle', (self) => {
            let hotSensors = this._settings.get_strv('hot-sensors');

            if (self.checked) {
                // add selected sensor to panel
                hotSensors.push(self.key);
                this._createHotItem(self.key, self.value, self.gicon);
            } else {
                // remove selected sensor from panel
                hotSensors.splice(hotSensors.indexOf(self.key), 1);
                this._removeHotItem(self.key);
            }

            if (hotSensors.length <= 0) {
                // add generic icon to panel when no sensors are selected
                hotSensors.push('_default_icon_');
                this._createHotItem('_default_icon_');
            } else {
                let defIconPos = hotSensors.indexOf('_default_icon_');
                if (defIconPos >= 0) {
                    // remove generic icon from panel when sensors are selected
                    hotSensors.splice(defIconPos, 1);
                    this._removeHotItem('_default_icon_');
                }
            }

            // this code is called asynchronously - make sure to save it for next round
            this._saveHotSensors(hotSensors);
        });

        this._sensorMenuItems[key] = item;
        let i = Object.keys(this._sensorMenuItems[key]).length;

        // alphabetize the sensors for these categories
        if (this._settings.get_boolean('alphabetize')) {
            let menuItems = this._groups[type].menu._getMenuItems();
            for (i = 0; i < menuItems.length; i++)
                // use natural sort order for system load, etc
                if (menuItems[i].label.localeCompare(item.label, undefined, { numeric: true, sensitivity: 'base' }) > 0)
                    break;
        }

        this._groups[type].menu.addMenuItem(item, i);
    }

    _defaultLabel() {
        return new St.Label({
               y_expand: true,
                y_align: Clutter.ActorAlign.CENTER
        });
    }

    _defaultIcon(key) {
        let split = key.replaceAll('_', ' ').trim().split(' ')[0].split('-');
        let type = split[0];

        let icon = new St.Icon({
          style_class: 'system-status-icon vitals-panel-icon-' + type,
            reactive: true
        });

        // second condition prevents crash due to issue #225, which started when _max_ was moved to the end
        // don't use the default system icon if the type is a gpu; use the universal gpu icon instead
        if (type == 'default' || (!(type in this._sensorIcons) && !type.startsWith('gpu'))) {
            icon.gicon = Gio.icon_new_for_string(this._sensorIconPath('system'));
        } else { // support for hide icons #80
            let iconObj = (split.length == 2)?'icon-' + split[1]:'icon';
            icon.gicon = Gio.icon_new_for_string(this._sensorIconPath(type, iconObj));
        }

        return icon;
    }

    _sensorIconPath(sensor, icon = 'icon') {
        // If the sensor is a numbered gpu, use the gpu icon. Otherwise use whatever icon associated with the sensor name.
        let sensorKey = sensor;
        if(sensor.startsWith('gpu')) sensorKey = 'gpu';

        const iconPathPrefixIndex = this._settings.get_int('icon-style');
        return this._extensionObject.path + this._sensorsIconPathPrefix[iconPathPrefixIndex] + this._sensorIcons[sensorKey][icon];
    }

    _ucFirst(string) {
        if(string.startsWith('gpu')) return 'Graphics';
        return string.charAt(0).toUpperCase() + string.slice(1);
    }

    _positionInPanel() {
        let alignment = '';
        let gravity = 0;
        let arrow_pos = 0;

        switch (this._settings.get_int('position-in-panel')) {
            case 0: // left
                alignment = 'left';
                gravity = -1;
                arrow_pos = 1;
                break;
            case 1: // center
                alignment = 'center';
                gravity = -1;
                arrow_pos = 0.5;
                break;
            case 2: // right
                alignment = 'right';
                gravity = 0;
                arrow_pos = 0;
                break;
            case 3: // far left
                alignment = 'left';
                gravity = 0;
                arrow_pos = 1;
                break;
            case 4: // far right
                alignment = 'right';
                gravity = -1;
                arrow_pos = 0;
                break;
        }

        let centered = this._settings.get_boolean('menu-centered')
        if (centered) arrow_pos = 0.5;

        // set arrow position when initializing and moving vitals
        this.menu._arrowAlignment = arrow_pos;

        return [alignment, gravity];
    }

    _querySensors() {
        // figure out last run time
        let now = new Date().getTime();
        let dwell = (now - this._last_query) / 1000;
        this._last_query = now;

        this._sensors.query((label, value, type, format) => {
            let typeKey = type.replace('-group', '');
            if (/^network-(?!rx$|tx$)/.test(typeKey)) typeKey = 'network';
            let key = '_' + typeKey + '_' + label.replace(' ', '_').toLowerCase() + '_';

            // if a sensor is disabled, gray it out
            if (key in this._sensorMenuItems) {
                this._sensorMenuItems[key].setSensitive((value!='disabled'));

                // don't continue below, last known value is shown
                if (value == 'disabled') return;
            }

            // add/initialize any gpu groups that we haven't added yet
            if (typeKey.startsWith('gpu') && typeKey !== 'gpu#1') {
                const split = typeKey.split('#');
                if(split.length == 2 && this._numGpus < parseInt(split[1])) {
                    // occasionally two lines from nvidia-smi will be read at once
                    // so we only actually update the number of gpus if we have recieved multiple lines at least 3 times in a row
                    // i.e. we make sure that mutiple queries have detected a new gpu back-to-back
                    if(this._newGpuDetectedCount < 2) {
                        this._newGpuDetected = true;
                        return;
                    }

                    this._numGpus = parseInt(split[1]);
                    this._newGpuDetectedCount = 0;
                    this._newGpuDetected = false;
                    // change label for gpu 1 from "Graphics" to "Graphics 1" since we have multiple gpus now
                    this._groups['gpu#1'].label.text = this._ucFirst('gpu#1') + ' 1';
                    for(let i = 2; i <= this._numGpus; i++)
                        if(!('gpu#' + i in this._groups))
                            this._initializeMenuGroup('gpu#' + i, 'gpu', ' ' + i, Object.keys(this._groups).length);
                }
            }

            let items = this._values.returnIfDifferent(dwell, label, value, type, format, key);
            for (let item of items) {
                if (item.type.startsWith('network-') && item.type.length == 10 && item.type != 'network-rx' && item.type != 'network-tx') {
                    // Geo / flags: stable key (no country in key); type stays network-<cc> for icon-us etc.
                    const stem = item.type.slice('network-'.length);
                    let flagGIcon = Gio.icon_new_for_string(this._sensorIconPath('network', 'icon-' + stem));
                    if (this._hotItems[item.key] && !this._settings.get_boolean('hide-icons')) {
                        // change icon in menu bar
                        let icon = this._hotItems[item.key].get_first_child();
                        if (icon instanceof St.Icon)
                        icon.gicon = flagGIcon;
                    }
                    // change icon in dropdown
                    let menuRow = this._sensorMenuItems[item.key];
                    if (menuRow) menuRow.gicon = flagGIcon;
                }

                this._updateDisplay(_(item.label), item.value, item.type, item.key);
            }
        }, dwell);

        //if a new gpu has been detected during the last query, then increment the amount of times we've detected a new gpu
        if(this._newGpuDetected) this._newGpuDetectedCount++;
        else this._newGpuDetectedCount = 0;
        this._newGpuDetected = false;

        if (this._warnings.length > 0) {
            this._notify('Vitals', this._warnings.join("\n"), 'folder-symbolic');
            this._warnings = [];
        }
    }

    _notify(msg, details, icon) {
        let source = new MessageTray.Source('MyApp Information', icon);
        Main.messageTray.add(source);
        let notification = new MessageTray.Notification(source, msg, details);
        notification.setTransient(true);
        source.notify(notification);
    }

    destroy() {
        this._destroyTimer();
        this._sensors.destroy();

        for (let signal of Object.values(this._settingChangedSignals))
            this._settings.disconnect(signal);

        super.destroy();
    }
});

export default class VitalsExtension extends Extension {
    enable() {
        vitalsMenu = new VitalsMenuButton(this);
        let position = vitalsMenu._positionInPanel();
        Main.panel.addToStatusArea('vitalsMenu', vitalsMenu, position[1], position[0]);
    }

    disable() {
        vitalsMenu.destroy();
        vitalsMenu = null;
    }
}
