/*
  Copyright (c) 2018, Chris Monahan <chris@corecoding.com>

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the GNOME nor the names of its contributors may be
      used to endorse or promote products derived from this software without
      specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import * as SubProcessModule from './helpers/subprocess.js';
import * as FileModule from './helpers/file.js';
import { gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js';
import NM from 'gi://NM';

let GTop, hasGTop = true;
try {
    ({default: GTop} = await import('gi://GTop'));
} catch (err) {
    log(err);
    hasGTop = false;
};

export const Sensors = GObject.registerClass({
    GTypeName: 'Sensors',
}, class Sensors extends GObject.Object {
    _init(settings, sensorIcons) {
        this._settings = settings;
        this._sensorIcons = sensorIcons;

        this.resetHistory();

        this._last_processor = { 'core': {}, 'speed': [] };

        this._settingChangedSignals = [];
        this._addSettingChangedSignal('show-gpu', this._reconfigureNvidiaSmiProcess.bind(this));
        this._addSettingChangedSignal('update-time', this._reconfigureNvidiaSmiProcess.bind(this));
        this._addSettingChangedSignal('network-public-ip-interval', () => {this._next_public_ip_check = 0;});
        this._addSettingChangedSignal('network-public-ip-provider', () => {this._next_public_ip_check = 0;});
        //this._addSettingChangedSignal('include-static-gpu-info', this._reconfigureNvidiaSmiProcess.bind(this));

        this._gpu_drm_vendors = null;
        this._gpu_drm_indices = null;
        this._nvidia_smi_process = null;
        this._nvidia_labels = [];
        this._bad_split_count = 0;

        this._frameMonitorSignalId = 0;
        this._frameMonitorLastTime = 0;
        this._frameMonitorFrameCount = 0;
        this._frameMonitorAccTime = 0;
        this._frameMonitorCurrentHz = 0;

        if (hasGTop) {
            this.storage = new GTop.glibtop_fsusage();
            this._storageDevice = '';
            this._findStorageDevice();

            this._lastRead = 0;
            this._lastWrite = 0;
        }
    }

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

    _refreshIPAddress(callback) {
        const provider = this._settings.get_int('network-public-ip-provider');
        let url;
        if (provider === 1)
            url = 'https://api.myip.com';
        else if (provider === 2)
            url = 'https://api.ipify.org?format=json';
        else
            url = 'https://ipv4.corecoding.com';

        new FileModule.File(url).read().then(contents => {
            let obj = JSON.parse(contents);
            let cc = '';
            let ip = '';
            if (provider === 1) {
                cc = (obj && typeof obj['cc'] === 'string') ? obj['cc'].trim().toLowerCase() : '';
                if (cc === 'xx') cc = ''; // MyIP.com uses XX when country is unknown; not a real flag
                ip = (obj && typeof obj['ip'] === 'string') ? obj['ip'].trim() : '';
            } else if (provider === 2) {
                ip = (obj && typeof obj['ip'] === 'string') ? obj['ip'].trim() : '';
            } else {
                cc = (obj && typeof obj['countryCode'] === 'string') ? obj['countryCode'].trim().toLowerCase() : '';
                ip = (obj && typeof obj['IPv4'] === 'string') ? obj['IPv4'].trim() : '';
            }
            const showFlag = this._settings.get_boolean('network-public-ip-show-flag');
            let typeOut = (showFlag && /^[a-z]{2}$/.test(cc)) ? ('network-' + cc) : 'network';
            this._returnValue(callback, 'Public IP', ip, typeOut, 'string');
        }).catch(err => { });
    }

    _findStorageDevice() {
        new FileModule.File('/proc/mounts').read("\n").then(lines => {
            for (let line of lines) {
                let loadArray = line.trim().split(/\s+/);
                if (loadArray[1] == this._settings.get_string('storage-path')) {
                    this._storageDevice = loadArray[0];
                    break;
                }
            }
        }).catch(err => { });
    }

    query(callback, dwell) {
        if (!this._hardware_detected) {
            // we could set _hardware_detected in discoverHardwareMonitors, but by
            // doing it here, we guarantee avoidance of race conditions
            this._hardware_detected = true;
            this._discoverHardwareMonitors(callback);
        }

        for (let sensor in this._sensorIcons) {
            if (this._settings.get_boolean('show-' + sensor)) {
                if (sensor == 'temperature' || sensor == 'voltage' || sensor == 'fan') {
                    // for temp, volt, fan, we have a shared handler
                    this._queryTempVoltFan(callback, sensor);
                } else {
                    // directly call queryFunction below
                    let method = '_query' + sensor[0].toUpperCase() + sensor.slice(1);
                    this[method](callback, dwell);
                }
            }
        }
    }

    _queryTempVoltFan(callback, type) {
        for (let label in this._tempVoltFanSensors[type]) {
            let sensor = this._tempVoltFanSensors[type][label];

            new FileModule.File(sensor['path']).read().then(value => {
                this._returnValue(callback, label, value, type, sensor['format']);
            }).catch(err => {
                this._returnValue(callback, label, 'disabled', type, sensor['format']);
            });
        }
    }

    _queryMemory(callback) {
        // check memory info
        new FileModule.File('/proc/meminfo').read().then(lines => {
            let values = '', total = 0, avail = 0, swapTotal = 0, swapFree = 0, cached = 0, memFree = 0;

            if (values = lines.match(/MemTotal:(\s+)(\d+) kB/)) total = values[2];
            if (values = lines.match(/MemAvailable:(\s+)(\d+) kB/)) avail = values[2];
            if (values = lines.match(/SwapTotal:(\s+)(\d+) kB/)) swapTotal = values[2];
            if (values = lines.match(/SwapFree:(\s+)(\d+) kB/)) swapFree = values[2];
            if (values = lines.match(/Cached:(\s+)(\d+) kB/)) cached = values[2];
            if (values = lines.match(/MemFree:(\s+)(\d+) kB/)) memFree = values[2];

            let used = total - avail
            let utilized = used / total;
            let swapUsed = swapTotal - swapFree
            let swapUtilized = swapUsed / swapTotal;

            this._returnValue(callback, 'Usage', utilized, 'memory', 'percent');
            this._returnValue(callback, 'memory', utilized, 'memory-group', 'percent');
            this._returnValue(callback, 'Physical', total, 'memory', 'memory');
            this._returnValue(callback, 'Available', avail, 'memory', 'memory');
            this._returnValue(callback, 'Allocated', used, 'memory', 'memory');
            this._returnValue(callback, 'Cached', cached, 'memory', 'memory');
            this._returnValue(callback, 'Free', memFree, 'memory', 'memory');
            this._returnValue(callback, 'Swap Total', swapTotal, 'memory', 'memory');
            this._returnValue(callback, 'Swap Free', swapFree, 'memory', 'memory');
            this._returnValue(callback, 'Swap Used', swapUsed, 'memory', 'memory');
            this._returnValue(callback, 'Swap Usage', swapUtilized, 'memory', 'percent');
        }).catch(err => { });
    }

    _queryProcessor(callback, dwell) {
        let columns = ['user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq', 'steal', 'guest', 'guest_nice'];

        // check processor usage
        new FileModule.File('/proc/stat').read("\n").then(lines => {
            let statistics = {};

            for (let line of lines) {
                let reverse_data = line.match(/^(cpu\d*\s)(.+)/);
                if (reverse_data) {
                    let cpu = reverse_data[1].trim();

                    if (!(cpu in statistics))
                        statistics[cpu] = {};

                    if (!(cpu in this._last_processor['core']))
                        this._last_processor['core'][cpu] = 0;

                    let stats = reverse_data[2].trim().split(' ').reverse();
                    for (let column of columns)
                        statistics[cpu][column] = parseInt(stats.pop());
                }
            }

            let cores = Object.keys(statistics).length - 1;

            for (let cpu in statistics) {
                let total = statistics[cpu]['user'] + statistics[cpu]['nice'] + statistics[cpu]['system'];

                // make sure we have data to report
                if (this._last_processor['core'][cpu] > 0) {
                    let delta = (total - this._last_processor['core'][cpu]) / dwell;

                    // /proc/stat provides overall usage for us under the 'cpu' heading
                    if (cpu == 'cpu') {
                        delta = delta / cores;
                        this._returnValue(callback, 'processor', delta / 100, 'processor-group', 'percent');
                        this._returnValue(callback, 'Usage', delta / 100, 'processor', 'percent');
                    } else {
                        this._returnValue(callback, _('Core %d').format(cpu.substr(3)), delta / 100, 'processor', 'percent');
                    }
                }

                this._last_processor['core'][cpu] = total;
            }

            // if frequency scaling is enabled, gather cpu-freq values
            if (!this._processor_uses_cpu_info) {
                for (let core = 0; core <= cores; core++) {
                    new FileModule.File('/sys/devices/system/cpu/cpu' + core + '/cpufreq/scaling_cur_freq').read().then(value => {
                        this._last_processor['speed'][core] = parseInt(value);
                    }).catch(err => { });
                }
            }
        }).catch(err => { });

        // if frequency scaling is disabled, use cpuinfo for speed
        if (this._processor_uses_cpu_info) {
            // grab CPU frequency
            new FileModule.File('/proc/cpuinfo').read("\n").then(lines => {
                let freqs = [];
                for (let line of lines) {
                    // grab megahertz
                    let value = line.match(/^cpu MHz(\s+): ([+-]?\d+(\.\d+)?)/);
                    if (value) freqs.push(parseFloat(value[2]));
                }

                let sum = freqs.reduce((a, b) => a + b);
                let hertz = (sum / freqs.length) * 1000 * 1000;
                this._returnValue(callback, 'Frequency', hertz, 'processor', 'hertz');

                let max_hertz = freqs.reduce((a, b) => Math.max(a, b)) * 1000 * 1000;
                this._returnValue(callback, 'Max frequency', max_hertz, 'processor', 'hertz');
                let min_hertz = freqs.reduce((a, b) => Math.min(a, b)) * 1000 * 1000;
                this._returnValue(callback, 'Min frequency', min_hertz, 'processor', 'hertz');
            }).catch(err => { });
        // if frequency scaling is enabled, cpu-freq reports
        } else if (Object.values(this._last_processor['speed']).length > 0) {
            let sum = this._last_processor['speed'].reduce((a, b) => a + b);
            let hertz = (sum / this._last_processor['speed'].length) * 1000;
            this._returnValue(callback, 'Frequency', hertz, 'processor', 'hertz');
            let max_hertz = this._last_processor['speed'].reduce((a, b) => Math.max(a, b)) * 1000;
            this._returnValue(callback, 'Max frequency', max_hertz, 'processor', 'hertz');
            let min_hertz = this._last_processor['speed'].reduce((a, b) => Math.min(a, b)) * 1000;
            this._returnValue(callback, 'Min frequency', min_hertz, 'processor', 'hertz');
        }
    }

    _querySystem(callback) {
        // check load average
        new FileModule.File('/proc/sys/fs/file-nr').read("\t").then(loadArray => {
            this._returnValue(callback, 'Open Files', loadArray[0], 'system', 'string');
        }).catch(err => { });

        // check load average
        new FileModule.File('/proc/loadavg').read(' ').then(loadArray => {
            let proc = loadArray[3].split('/');

            this._returnValue(callback, 'Load 1m', loadArray[0], 'system', 'load');
            this._returnValue(callback, 'system', loadArray[0], 'system-group', 'load');
            this._returnValue(callback, 'Load 5m', loadArray[1], 'system', 'load');
            this._returnValue(callback, 'Load 15m', loadArray[2], 'system', 'load');
            this._returnValue(callback, 'Threads Active', proc[0], 'system', 'string');
            this._returnValue(callback, 'Threads Total', proc[1], 'system', 'string');
        }).catch(err => { });

        // check uptime
        new FileModule.File('/proc/uptime').read(' ').then(upArray => {
            this._returnValue(callback, 'Uptime', upArray[0], 'system', 'uptime');

            let cores = Object.keys(this._last_processor['core']).length - 1;
            if (cores > 0)
                this._returnValue(callback, 'Process Time', upArray[0] - upArray[1] / cores, 'processor', 'uptime');
        }).catch(err => { });
    }

    _queryNetwork(callback, dwell) {
        // check network speed
        let directions = ['tx', 'rx'];
        let netbase = '/sys/class/net/';

        new FileModule.File(netbase).list().then(interfaces => {
            for (let iface of interfaces) {
                for (let direction of directions) {
                    // lo tx and rx are the same
                    if (iface == 'lo' && direction == 'rx') continue;

                    new FileModule.File(netbase + iface + '/statistics/' + direction + '_bytes').read().then(value => {
                        // issue #217 - don't include 'lo' traffic in Maximum calculations in values.js
                        // by not using network-rx or network-tx
                        let name = iface + ((iface == 'lo')?'':' ' + direction);

                        let type = 'network' + ((iface=='lo')?'':'-' + direction);
                        this._returnValue(callback, name, value, type, 'storage');
                    }).catch(err => { });
                }
            }
        }).catch(err => { });

        // some may not want public ip checking
        if (this._settings.get_boolean('include-public-ip')) {
            // check the public ip every hour or when waking from sleep
            if (this._next_public_ip_check <= 0) {
                let intervalMinutes = this._settings.get_int('network-public-ip-interval');
                this._next_public_ip_check = intervalMinutes * 60;

                this._refreshIPAddress(callback);
            }

            this._next_public_ip_check -= dwell;
        }

        // wireless interface statistics
        new FileModule.File('/proc/net/wireless').read("\n", true).then(lines => {
            // wireless has two headers - first is stripped in helper function
            lines.shift();

            // if multiple wireless device, we use the last one
            for (let line of lines) {
                let netArray = line.trim().split(/\s+/);
                let quality_pct = netArray[2].substr(0, netArray[2].length-1) / 70;
                let signal = netArray[3].substr(0, netArray[3].length-1);

                this._returnValue(callback, 'WiFi Link Quality', quality_pct, 'network', 'percent');
                this._returnValue(callback, 'WiFi Signal Level', signal, 'network', 'string');
            }
        }).catch(err => { });
    }

    _queryStorage(callback, dwell) {
        // display zfs arc status, if available
        new FileModule.File('/proc/spl/kstat/zfs/arcstats').read().then(lines => {
            let values = '', target = 0, maximum = 0, current = 0;

            if (values = lines.match(/c(\s+)(\d+)(\s+)(\d+)/)) target = values[4];
            if (values = lines.match(/c_max(\s+)(\d+)(\s+)(\d+)/)) maximum = values[4];
            if (values = lines.match(/size(\s+)(\d+)(\s+)(\d+)/)) current = values[4];

            // ZFS statistics
            this._returnValue(callback, 'ARC Target', target, 'storage', 'storage');
            this._returnValue(callback, 'ARC Maximum', maximum, 'storage', 'storage');
            this._returnValue(callback, 'ARC Current', current, 'storage', 'storage');
        }).catch(err => { });

        // check disk performance stats
        new FileModule.File('/proc/diskstats').read("\n").then(lines => {
            for (let line of lines) {
                let loadArray = line.trim().split(/\s+/);
                if ('/dev/' + loadArray[2] == this._storageDevice) {
                    var read = (loadArray[5] * 512);
                    var write = (loadArray[9] * 512);
                    this._returnValue(callback, 'Read total', read, 'storage', 'storage');
                    this._returnValue(callback, 'Write total', write, 'storage', 'storage');
                    this._returnValue(callback, 'Read rate', (read - this._lastRead) / dwell, 'storage', 'storage');
                    this._returnValue(callback, 'Write rate', (write - this._lastWrite) / dwell, 'storage', 'storage');
                    this._lastRead = read;
                    this._lastWrite = write;
                    break;
                }
            }
        }).catch(err => { });

        // skip rest of stats if gtop not available
        if (!hasGTop) return;

        GTop.glibtop_get_fsusage(this.storage, this._settings.get_string('storage-path'));

        let total = this.storage.blocks * this.storage.block_size;
        let avail = this.storage.bavail * this.storage.block_size;
        let free = this.storage.bfree * this.storage.block_size;
        let used = total - free;
        let reserved = (total - avail) - used;
        let freePercent = 0;
        let usedPercent = 0;
        if (total > 0) {
          freePercent = Math.round((free / total) * 100);
          usedPercent = Math.round((used / total) * 100);
        }

        this._returnValue(callback, 'Total', total, 'storage', 'storage');
        this._returnValue(callback, 'Used', used, 'storage', 'storage');
        this._returnValue(callback, 'Reserved', reserved, 'storage', 'storage');
        this._returnValue(callback, 'Free', avail, 'storage', 'storage');
        this._returnValue(callback, 'Used %', usedPercent + '%', 'storage', 'string');
        this._returnValue(callback, 'Free %', freePercent + '%', 'storage', 'string');
        this._returnValue(callback, 'storage', avail, 'storage-group', 'storage');
    }

    _queryBattery(callback) {
        let battery_slot = this._settings.get_int('battery-slot');

        // create a mapping of indices to battery paths (from prefs.ui)
        const BATTERY_PATHS = {
            0: 'BAT0',
            1: 'BAT1',
            2: 'BAT2',
            3: 'BATT',
            4: 'CMB0',
            5: 'CMB1',
            6: 'CMB2',
            7: 'macsmc-battery'
        };

        // uevent has all necessary fields, no need to read individual files
        let battery_path = '/sys/class/power_supply/' + BATTERY_PATHS[battery_slot] + '/uevent';
        new FileModule.File(battery_path).read("\n").then(lines => {
            let output = {};
            for (let line of lines) {
                let split = line.split('=');
                output[split[0].replace('POWER_SUPPLY_', '')] = split[1];
            }

            if ('STATUS' in output) {
                this._returnValue(callback, 'State', output['STATUS'], 'battery', '');
            }

            if ('CYCLE_COUNT' in output) {
                this._returnValue(callback, 'Cycles', output['CYCLE_COUNT'], 'battery', '');
            }

            if ('VOLTAGE_NOW' in output) {
                this._returnValue(callback, 'Voltage', output['VOLTAGE_NOW'] / 1000, 'battery', 'in');
            }

            if ('CAPACITY_LEVEL' in output) {
                this._returnValue(callback, 'Level', output['CAPACITY_LEVEL'], 'battery', '');
            }

            if ('CAPACITY' in output) {
                this._returnValue(callback, 'Percentage', output['CAPACITY'] / 100, 'battery', 'percent');
            }

            if ('VOLTAGE_NOW' in output && 'CURRENT_NOW' in output && (!('POWER_NOW' in output))) {
                output['POWER_NOW'] = (output['VOLTAGE_NOW'] * output['CURRENT_NOW']) / 1000000;
            }

            if ('POWER_NOW' in output) {
                const powerValue = (
                    parseFloat(output['POWER_NOW']) * (output['STATUS'] === 'Discharging' ? -1 : 1)
                );
                this._returnValue(callback, 'Power Rate', powerValue, 'battery', 'watt');
                this._returnValue(callback, 'battery', powerValue, 'battery-group', 'watt');
            }

            if ('CHARGE_FULL' in output && 'VOLTAGE_MIN_DESIGN' in output && (!('ENERGY_FULL' in output))) {
                output['ENERGY_FULL'] = (output['CHARGE_FULL'] * output['VOLTAGE_MIN_DESIGN']) / 1000000;
            }

            if ('ENERGY_FULL' in output) {
                this._returnValue(callback, 'Energy (full)', output['ENERGY_FULL'], 'battery', 'watt-hour');
            }

            if ('CHARGE_FULL_DESIGN' in output && 'VOLTAGE_MIN_DESIGN' in output && (!('ENERGY_FULL_DESIGN' in output))) {
                output['ENERGY_FULL_DESIGN'] = (output['CHARGE_FULL_DESIGN'] * output['VOLTAGE_MIN_DESIGN']) / 1000000;
            }

            if ('ENERGY_FULL_DESIGN' in output) {
                this._returnValue(callback, 'Energy (design)', output['ENERGY_FULL_DESIGN'], 'battery', 'watt-hour');

                if ('ENERGY_FULL' in output) {
                    this._returnValue(callback, 'Capacity', (output['ENERGY_FULL'] / output['ENERGY_FULL_DESIGN']), 'battery', 'percent');
                }
            }

            if ('VOLTAGE_MIN_DESIGN' in output && 'CHARGE_NOW' in output && (!('ENERGY_NOW' in output))) {
                output['ENERGY_NOW'] = (output['VOLTAGE_MIN_DESIGN'] * output['CHARGE_NOW']) / 1000000;
            }

            if ('ENERGY_NOW' in output) {
                this._returnValue(callback, 'Energy (now)', output['ENERGY_NOW'], 'battery', 'watt-hour');
            }

            if ('ENERGY_FULL' in output && 'ENERGY_NOW' in output && 'POWER_NOW' in output && output['POWER_NOW'] !== 0 && 'STATUS' in output && (output['STATUS'] == 'Charging' || output['STATUS'] == 'Discharging')) {

                let timeLeft = 0;

                // two different formulas depending on if we are charging or discharging
                if (output['STATUS'] == 'Charging') {
                    timeLeft = ((output['ENERGY_FULL'] - output['ENERGY_NOW']) / output['POWER_NOW']);
                } else {
                    timeLeft = (output['ENERGY_NOW'] / Math.abs(output['POWER_NOW']));
                }

                // don't process Infinity values
                if (timeLeft !== Infinity) {
                    if (this._battery_charge_status != output['STATUS']) {
                        // clears history due to state change
                        this._battery_time_left_history = [];

                        // clear time left history when laptop goes in and out of charging
                        this._battery_charge_status = output['STATUS'];
                    }

                    // add latest time left estimate to our history
                    this._battery_time_left_history.push(parseInt(timeLeft * 3600));

                    // keep track of last 15 time left estimates by erasing the first
                    if (this._battery_time_left_history.length > 10)
                        this._battery_time_left_history.shift();

                    // sum up and create average of our time left history
                    let sum = this._battery_time_left_history.reduce((a, b) => a + b);
                    let avg = sum / this._battery_time_left_history.length;

                    // use time left history to update screen
                    this._returnValue(callback, 'Time left', parseInt(avg), 'battery', 'runtime');
                }
            } else {
                this._returnValue(callback, 'Time left', output['STATUS'], 'battery', '');
            }
        }).catch(err => { });
    }

    _initFrameMonitor() {
        if (this._frameMonitorSignalId) return;
        this._frameMonitorLastTime = 0;
        this._frameMonitorFrameCount = 0;
        this._frameMonitorAccTime = 0;
        this._frameMonitorCurrentHz = 0;
        this._frameMonitorSignalId = global.stage.connect('after-paint', () => {
            this._onAfterPaint();
        });
    }

    _destroyFrameMonitor() {
        if (this._frameMonitorSignalId) {
            global.stage.disconnect(this._frameMonitorSignalId);
            this._frameMonitorSignalId = 0;
        }
        this._frameMonitorLastTime = 0;
        this._frameMonitorCurrentHz = 0;
    }

    _onAfterPaint() {
        const now = GLib.get_monotonic_time();

        if (this._frameMonitorLastTime === 0) {
            this._frameMonitorLastTime = now;
            return;
        }

        const delta = now - this._frameMonitorLastTime;
        this._frameMonitorLastTime = now;

        this._frameMonitorFrameCount++;
        this._frameMonitorAccTime += delta;

        if (this._frameMonitorAccTime >= 500000) {
            this._frameMonitorCurrentHz = this._frameMonitorFrameCount / (this._frameMonitorAccTime / 1000000);
            this._frameMonitorFrameCount = 0;
            this._frameMonitorAccTime = 0;
        }
    }

    _queryGpu(callback) {
        if (this._frameMonitorCurrentHz > 0)
            this._returnValue(callback, 'Refresh Rate', this._frameMonitorCurrentHz, 'gpu#1', 'hertz');

        if (!this._nvidia_smi_process) {
            // no nvidia-smi, so we use sysfs DRM if any cards was discovered
            if (!this._gpu_drm_indices){
                if (this._frameMonitorCurrentHz > 0)
                    this._returnValue(callback, 'Refresh Rate', this._frameMonitorCurrentHz, 'gpu#1-group', 'hertz');
                this._disableGpuLabels(callback);
                return;
            } else {
                this._readGpuDrm(callback);
                return;
            }
        }

        this._nvidia_smi_process.read('\n').then(lines => {
            /// for debugging multi-gpu on systems with only one gpu
            /// duplicates the first gpu's data 3 times, for 4 total gpus
            ///if(lines.length == 0) return;
            ///for(let _gpuNum = 1; _gpuNum <= 3; _gpuNum++)
            ///    lines.push(lines[0]);

            for (let i = 0; i < lines.length; i++) {
                this._parseNvidiaSmiLine(callback, lines[i], i + 1, lines.length > 1);
            }

            // if we've already updated the static info during the last parse, then stop doing so.
            // this is so the _parseNvidiaSmiLine function won't return static info anymore
            // and the nvidia-smi commmand won't be queried for static info either
            if(!this._nvidia_static_returned) {
                this._nvidia_static_returned = true;
                //reconfigure the process to stop querying static info
                this._reconfigureNvidiaSmiProcess();
            }
        }).catch(err => {
            this._disableGpuLabels(callback);
            this._terminateNvidiaSmiProcess();
        });
    }

    _parseNvidiaSmiLine(callback, csv, gpuNum, multiGpu) {
        const expectedSplitLength = 19;
        let csv_split = csv.split(',');

        // occasionally the nvidia-smi command can get cut off before it can be fully read, thus the parse function only gets part of a line
        // hence we count the number of bad splits and only terminate the process after a few bad splits in a row
        // this prevents anomalous readings from terminating the process
        if (csv_split.length < expectedSplitLength) {
            this._bad_split_count++;
            //if we've had 2 bad splits/reads in a row, try to restart the process
            if (this._bad_split_count == 2) this._reconfigureNvidiaSmiProcess();
            //if we still get a bad read after that, then it's not an anomaly; terminate the process
            else if (this._bad_split_count >= 3) this._terminateNvidiaSmiProcess();
            return;
        }
        this._bad_split_count = 0;

        let [
            label,
            fan_speed_pct,
            temp_gpu, temp_mem,
            mem_total, mem_used, mem_reserved, mem_free,
            util_gpu, util_mem, util_encoder, util_decoder,
            clock_gpu, clock_mem, clock_encode_decode,
            power, power_avg,
            link_gen_current, link_width_current
        ] = csv_split;

        const staticNames = [
            'temp_limit', 'power_limit',
            'link_gen_max', 'link_width_max',
            'addressing_mode',
            'driver_version', 'vbios', 'serial',
            'domain_num', 'bus_num', 'device_num', 'device_id', 'sub_device_id'
        ];
        let staticInfo = {};

        // if we have queried static info this time around, populate our static info object
        if(csv_split.length == (expectedSplitLength + staticNames.length)){
            for(let i = 0; i < staticNames.length; i++) {
                //set the static info to a default (0) if it's undefined
                const value = csv_split[expectedSplitLength + i];
                staticInfo[staticNames[i]] = (typeof value !== "undefined") ? value : 0;
            }
        }

        const typeName = 'gpu#' + gpuNum;
        const globalLabel = 'GPU' + (multiGpu ? ' ' + gpuNum : '');
        const memTempValid = !isNaN(parseInt(temp_mem));

        this._returnGpuValue(callback, 'Graphics', parseInt(util_gpu) * 0.01, typeName + '-group', 'percent');

        this._returnGpuValue(callback, 'Name', label, typeName, '');

        this._returnGpuValue(callback, globalLabel, parseInt(fan_speed_pct) * 0.01, 'fan', 'percent');
        this._returnGpuValue(callback, 'Fan', parseInt(fan_speed_pct) * 0.01, typeName, 'percent');

        this._returnGpuValue(callback, globalLabel, parseInt(temp_gpu) * 1000, 'temperature', 'temp');
        this._returnGpuValue(callback, 'Temperature', parseInt(temp_gpu) * 1000, typeName, 'temp');
        this._returnGpuValue(callback, 'Memory Temperature', parseInt(temp_mem) * 1000, typeName, 'temp', memTempValid);
        this._returnStaticGpuValue(callback, 'Temperature Limit', parseInt(staticInfo['temp_limit']) * 1000, typeName, 'temp');

        this._returnGpuValue(callback, 'Memory Usage', parseInt(mem_used) / parseInt(mem_total), typeName, 'percent');
        this._returnGpuValue(callback, 'Memory Total', parseInt(mem_total) * 1000, typeName, 'memory');
        this._returnGpuValue(callback, 'Memory Used', parseInt(mem_used) * 1000, typeName, 'memory');
        this._returnGpuValue(callback, 'Memory Reserved', parseInt(mem_reserved) * 1000, typeName, 'memory');
        this._returnGpuValue(callback, 'Memory Free', parseInt(mem_free) * 1000, typeName, 'memory');

        this._returnGpuValue(callback, 'Memory Utilization', parseInt(util_mem) * 0.01, typeName, 'percent');
        this._returnGpuValue(callback, 'Utilization', parseInt(util_gpu) * 0.01, typeName, 'percent');
        this._returnGpuValue(callback, 'Encoder Utilization', parseInt(util_encoder) * 0.01, typeName, 'percent');
        this._returnGpuValue(callback, 'Decoder Utilization', parseInt(util_decoder) * 0.01, typeName, 'percent');

        this._returnGpuValue(callback, 'Frequency', parseInt(clock_gpu) * 1000 * 1000, typeName, 'hertz');
        this._returnGpuValue(callback, 'Memory Frequency', parseInt(clock_mem) * 1000 * 1000, typeName, 'hertz');
        this._returnGpuValue(callback, 'Encoder/Decoder Frequency', parseInt(clock_encode_decode) * 1000 * 1000, typeName, 'hertz');

        //this._returnGpuValue(callback, 'Encoder Sessions', parseInt(encoder_sessions), typeName, 'string');

        this._returnGpuValue(callback, 'Power', power, typeName, 'watt-gpu');
        this._returnGpuValue(callback, 'Average Power', power_avg, typeName, 'watt-gpu');
        this._returnStaticGpuValue(callback, 'Power Limit', parseInt(staticInfo['power_limit']), typeName, 'watt-gpu');

        this._returnGpuValue(callback, 'Link Speed', link_gen_current + 'x' + link_width_current, typeName, 'pcie');
        this._returnStaticGpuValue(callback, 'Maximum Link Speed', staticInfo['link_gen_max'] + 'x' + staticInfo['link_width_max'], typeName, 'pcie');

        this._returnStaticGpuValue(callback, 'Addressing Mode', staticInfo['addressing_mode'], typeName, 'string');

        this._returnStaticGpuValue(callback, 'Driver Version', staticInfo['driver_version'], typeName, 'string');
        this._returnStaticGpuValue(callback, 'vBIOS Version', staticInfo['vbios'], typeName, 'string');
        this._returnStaticGpuValue(callback, 'Serial Number', staticInfo['serial'], typeName, 'string');

        this._returnStaticGpuValue(callback, 'Domain Number', staticInfo['domain_num'], typeName, 'string');
        this._returnStaticGpuValue(callback, 'Bus Number', staticInfo['bus_num'], typeName, 'string');
        this._returnStaticGpuValue(callback, 'Device Number', staticInfo['device_num'], typeName, 'string');
        this._returnStaticGpuValue(callback, 'Device ID', staticInfo['device_id'], typeName, 'string');
        this._returnStaticGpuValue(callback, 'Sub Device ID', staticInfo['sub_device_id'], typeName, 'string');
    }

    _readGpuDrm(callback){
        const multiGpu = this._gpu_drm_indices.length > 1;
        const unit = this._settings.get_int('memory-measurement') ? 1000 : 1024;
        for (let z = 0; z < this._gpu_drm_indices.length; z++ ) {
            let i = this._gpu_drm_indices[z];
            const typeName = 'gpu#' + i;
            const vendor = this._gpu_drm_vendors[z];

            // AMD
            if(vendor === "0x1002") {
                // read GPU usage and create group lebel for card
                new FileModule.File('/sys/class/drm/card'+i+'/device/gpu_busy_percent').read().then(value => {
                    // create group
                    this._returnGpuValue(callback, 'Graphics', parseInt(value) * 0.01, typeName + '-group', 'percent');
                    this._returnGpuValue(callback, 'Vendor', "AMD", typeName, 'string');
                    this._returnGpuValue(callback, 'Usage', parseInt(value) * 0.01, typeName, 'percent');
                }).catch(err => {
                    // nothing to do, keep old value displayed
                });
                new FileModule.File('/sys/class/drm/card'+i+'/device/mem_info_vram_used').read().then(value => {
                    this._returnGpuValue(callback, 'Memory Used', parseInt(value) / unit, typeName, 'memory');
                }).catch(err => {
                    // nothing to do, keep old value displayed
                });
                new FileModule.File('/sys/class/drm/card'+i+'/device/mem_info_vram_total').read().then(value => {
                    this._returnGpuValue(callback, 'Memory Total', parseInt(value) / unit, typeName, 'memory');
                }).catch(err => {
                    // nothing to do, keep old value displayed
                });
            } else {
                // for other vendors only show basic card info
                let vendorName = null;
                switch (vendor){
                    case '0x10DE': vendorName = 'NVIDIA'; break; // should be never used as nvidia-smi should be preferred
                    case '0x13B5': vendorName = 'ARM'; break;
                    case '0x5143': vendorName = 'Qualcomm'; break;
                    case '0x8086': vendorName = 'Intel'; break;
                    default: vendorName = "Unknown " + vendor;
                }
                this._returnGpuValue(callback, 'Graphics', vendorName, typeName + '-group', 'string');
            }
        }
    }

    _disableGpuLabels(callback) {
        for (let labelObj of this._nvidia_labels)
            this._returnValue(callback, labelObj.label, 'disabled', labelObj.type, labelObj.format);
    }

    _returnStaticGpuValue(callback, label, value, type, format) {
        //if we've already tried to return existing static info before or if the option isn't enabled, then do nothing.
        if (this._nvidia_static_returned || !this._settings.get_boolean('include-static-gpu-info'))
            return;

        //we don't need to disable static info labels, so just use ordinary returnValue function
        this._returnValue(callback, label, value, type, format);
    }

    _returnGpuValue(callback, label, value, type, format, display = true) {
        if(!display) return;

        if(format !== "string" && (value === 'N/A' || value === '[N/A]' || isNaN(value))) return;

        let nvidiaLabel = {'label': label, 'type': type, 'format': format};
        if (!this._nvidia_labels.includes(nvidiaLabel))
            this._nvidia_labels.push(nvidiaLabel);

        this._returnValue(callback, label, value, type, format);
    }

    _returnValue(callback, label, value, type, format) {
        // don't return if value is not a number - will revisit later
        //if (isNaN(value)) return;
        callback(label, value, type, format);
    }

    _discoverHardwareMonitors(callback) {
        this._tempVoltFanSensors = { 'temperature': {}, 'voltage': {}, 'fan': {} };

        let hwbase = '/sys/class/hwmon/';

        // process sensor_types now so it is not called multiple times below
        let sensor_types = {};

        if (this._settings.get_boolean('show-temperature'))
            sensor_types['temp'] = 'temperature';

        if (this._settings.get_boolean('show-voltage'))
            sensor_types['in'] = 'voltage';

        if (this._settings.get_boolean('show-fan'))
            sensor_types['fan'] = 'fan';

        // a little informal, but this code has zero I/O block
        new FileModule.File(hwbase).list().then(files => {
            for (let file of files) {
                // grab name of sensor
                new FileModule.File(hwbase + file + '/name').read().then(name => {
                    // are we dealing with a CPU?
                    if (name == 'coretemp') {
                        // determine which processor (socket) we are dealing with
                        new FileModule.File(hwbase + file + '/temp1_label').read().then(prefix => {
                            this._processTempVoltFan(callback, sensor_types, prefix, hwbase + file, file);
                        }).catch(err => {
                            // this shouldn't be necessary, but just in case temp1_label doesn't exist
                            // attempt to fix #266
                            this._processTempVoltFan(callback, sensor_types, name, hwbase + file, file);
                        });
                    } else {
                        // not a CPU, process all other sensors
                        this._processTempVoltFan(callback, sensor_types, name, hwbase + file, file);
                    }
                }).catch(err => {
                    new FileModule.File(hwbase + file + '/device/name').read().then(name => {
                        this._processTempVoltFan(callback, sensor_types, name, hwbase + file + '/device', file);
                    }).catch(err => { });
                });
            }
        }).catch(err => { });

        // does this system support cpu scaling? if so we will use it to grab Frequency and Boost below
        new FileModule.File('/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq').read().then(value => {
            this._processor_uses_cpu_info = false;
        }).catch(err => { });

        // is static CPU information enabled?
        if (this._settings.get_boolean('include-static-info')) {
            // grab static CPU information
            new FileModule.File('/proc/cpuinfo').read("\n").then(lines => {
                let vendor_id = '';
                let bogomips = '';
                let sockets = {};
                let cache = '';

                for (let line of lines) {
                    let value = '';

                    // grab cpu vendor
                    if (value = line.match(/^vendor_id(\s+): (\w+.*)/)) vendor_id = value[2];

                    // grab bogomips
                    if (value = line.match(/^bogomips(\s+): (\d*\.?\d*)$/)) bogomips = value[2];

                    // grab processor count
                    if (value = line.match(/^physical id(\s+): (\d+)$/)) sockets[value[2]] = 1;

                    // grab cache
                    if (value = line.match(/^cache size(\s+): (\d+) KB$/)) cache = value[2];
                }

                this._returnValue(callback, 'Vendor', vendor_id, 'processor', 'string');
                this._returnValue(callback, 'Bogomips', bogomips, 'processor', 'string');
                this._returnValue(callback, 'Sockets', Object.keys(sockets).length, 'processor', 'string');
                this._returnValue(callback, 'Cache', cache, 'processor', 'memory');
            }).catch(err => { });

            // grab static CPU information
            new FileModule.File('/proc/version').read(' ').then(kernelArray => {
                this._returnValue(callback, 'Kernel', kernelArray[2], 'system', 'string');
            }).catch(err => { });
        }

        // Launch nvidia-smi subprocess if nvidia querying is enabled
        this._reconfigureNvidiaSmiProcess();
        this._discoverGpuDrm();
        this._initFrameMonitor();
    }

    _discoverGpuDrm() {
        // use DRM only if nvidia-smi is not used
        if (this._settings.get_boolean('show-gpu') && this._nvidia_smi_process == null) {
            // try to discover up to 10 cards starting from index 0
            for(let i = 0; i < 10 ; i++){
                new FileModule.File('/sys/class/drm/card'+i+'/device/vendor').read().then(value => {
                    if(!this._gpu_drm_indices){
                        this._gpu_drm_indices = [];
                        this._gpu_drm_vendors = [];
                    }
                    this._gpu_drm_indices.push(i);
                    this._gpu_drm_vendors.push(value);
                }).catch(err => { });
            }
        } else {
            this._gpu_drm_vendors = null;
            this._gpu_drm_indices = null;
        }
    }

    // The nvidia-smi subprocess will keep running and print new sensor data to stdout every
    // `update_time` seconds. _queryNvidiaSmi() will be called at roughly the same interval and
    // read from the subprocess's stdout to get new sensor data.

    // Regarding "keeping main process & sub process in sync", there are two possible scenarios:
    // - For some reason, nvidia-smi prints at a somewhat higher frequency than we call
    //   _queryNvidiaSmi() to read data. This is okay, eventually one call to _queryNvidiaSmi()
    //   will read two sensor data updates in a single call.
    // - For some reason, _queryNvidiaSmi() is called at a somewhat higher frequency than
    //   nvidia-smi prints data. This is the more likely scenario with user actions triggering
    //   additional reads. This eventually triggers an "IO PENDING" error while attempting to
    //   read, because the previous async read is still waiting. To solve this, the subprocess
    //   module simply ignores PENDING errors. After ignoring the error, the earlier read will
    //   eventually return and sensor data will be updated, so this scenario is handled correctly.

    // Generally speaking, the call to _queryNvidiaSmi() and nvidia-smi's printing to stdout do
    // not happen at the same time. So the async call in _queryNvidiaSmi() will usually have to
    // wait up to `update_time` seconds before getting any results and reporting them through the
    // callback.
    _reconfigureNvidiaSmiProcess() {
        if (this._settings.get_boolean('show-gpu')) {
            this._terminateNvidiaSmiProcess();

            try {
                let update_time = this._settings.get_int('update-time');
                let query_interval = Math.max(update_time, 1);
                let command = [
                    'nvidia-smi',
                    '--query-gpu=name,' +
                    'fan.speed,' +
                    'temperature.gpu,temperature.memory,' +
                    'memory.total,memory.used,memory.reserved,memory.free,' +
                    'utilization.gpu,utilization.memory,utilization.encoder,utilization.decoder,' +
                    'clocks.gr,clocks.mem,clocks.video,' +
                    'power.draw.instant,power.draw.average,' +
                    'pcie.link.gen.gpucurrent,pcie.link.width.current,' +
                    (!this._nvidia_static_returned && this._settings.get_boolean('include-static-gpu-info') ?
                        'temperature.gpu.tlimit,' +
                        'power.limit,' +
                        'pcie.link.gen.max,pcie.link.width.max,'   +
                        'addressing_mode,'+
                        'driver_version,vbios_version,serial,' +
                        'pci.domain,pci.bus,pci.device,pci.device_id,pci.sub_device_id,'
                    : ''),
                    '--format=csv,noheader,nounits',
                    '-l', query_interval.toString()
                ];

                this._nvidia_smi_process = new SubProcessModule.SubProcess(command);
            } catch(e) {
                // proprietary nvidia driver not installed
                this._terminateNvidiaSmiProcess();
            }
        } else {
            this._terminateNvidiaSmiProcess();
        }
    }

    _terminateNvidiaSmiProcess() {
        if (this._nvidia_smi_process) {
            this._nvidia_smi_process.terminate();
            this._nvidia_smi_process = null;
        }
    }

    _processTempVoltFan(callback, sensor_types, name, path, file) {
        let sensor_files = [ 'input', 'label' ];

        // grab files from directory
        new FileModule.File(path).list().then(files2 => {
            let trisensors = {};

            // loop over files from directory
            for (let file2 of Object.values(files2)) {
                // simple way of processing input and label (from above)
                for (let key of Object.values(sensor_files)) {
                    // process toggled on sensors from extension preferences
                    for (let sensor_type in sensor_types) {
                        if (file2.substr(0, sensor_type.length) == sensor_type && file2.substr(-(key.length+1)) == '_' + key) {
                            let key2 = file + file2.substr(0, file2.indexOf('_'));

                            if (!(key2 in trisensors)) {
                                trisensors[key2] = {
                                    'type': sensor_types[sensor_type],
                                  'format': sensor_type,
                                   'label': path + '/name'
                                };
                            }

                            trisensors[key2][key] = path + '/' + file2;
                        }
                    }
                }
            }

            for (let obj of Object.values(trisensors)) {
                if (!('input' in obj))
                    continue;

                new FileModule.File(obj['input']).read().then(value => {
                    let extra = (obj['label'].indexOf('_label')==-1) ? ' ' + obj['input'].substr(obj['input'].lastIndexOf('/')+1).split('_')[0] : '';

                    if (value > 0 || !this._settings.get_boolean('hide-zeros') || obj['type'] == 'fan') {
                        new FileModule.File(obj['label']).read().then(label => {
                            this._addTempVoltFan(callback, obj, name, label, extra, value);
                        }).catch(err => {
                            let tmpFile = obj['label'].substr(0, obj['label'].lastIndexOf('/')) + '/name';
                            new FileModule.File(tmpFile).read().then(label => {
                                this._addTempVoltFan(callback, obj, name, label, extra, value);
                            }).catch(err => { });
                        });
                    }
                }).catch(err => { });
            }
        }).catch(err => { });
    }

    _addTempVoltFan(callback, obj, name, label, extra, value) {
        // prepend module that provided sensor data
        if (name != label) label = name + ' ' + label;

        //if (label == 'nvme Composite') label = 'NVMe';
        //if (label == 'nouveau') label = 'Nvidia';

        label = label + extra;

        // in the future we will read /etc/sensors3.conf
        if (label == 'acpitz temp1') label = 'ACPI Thermal Zone';
        if (label == 'pch_cannonlake temp1') label = 'Platform Controller Hub';
        if (label == 'iwlwifi_1 temp1') label = 'Wireless Adapter';
        if (label == 'Package id 0') label = 'Processor 0';
        if (label == 'Package id 1') label = 'Processor 1';
        label = label.replace('Package id', 'CPU');

        let types = [ 'temperature', 'voltage', 'fan' ];
        for (let type of types) {
            // check if this label already exists
            if (label in this._tempVoltFanSensors[type]) {
                for (let i = 2; i <= 9; i++) {
                    // append an incremented number to end
                    let new_label = label + ' ' + i;

                    // if new label is available, use it
                    if (!(new_label in this._tempVoltFanSensors[type])) {
                        label = new_label;
                        break;
                    }
                }
            }
        }

        // update screen on initial build to prevent delay on update
        this._returnValue(callback, label, value, obj['type'], obj['format']);

        this._tempVoltFanSensors[obj['type']][label] = {
          'format': obj['format'],
            'path': obj['input']
        };
    }

    resetHistory() {
        this._next_public_ip_check = 0;
        this._hardware_detected = false;
        this._nvidia_static_returned = false;
        this._processor_uses_cpu_info = true;
        this._battery_time_left_history = [];
        this._battery_charge_status = '';
        this._nvidia_labels = [];
        this._bad_split_count = 0;
        this._frameMonitorLastTime = 0;
        this._frameMonitorFrameCount = 0;
        this._frameMonitorAccTime = 0;
    }

    destroy() {
        this._destroyFrameMonitor();
        this._terminateNvidiaSmiProcess();

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