/* sensors.js
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

/* exported SensorManager */

const { Gio, GLib, Shell } = imports.gi;

const ExtensionUtils = imports.misc.extensionUtils;
const { EventEmitter } = imports.misc.signals;

const _ = ExtensionUtils.gettext;

const DEFAULT_REFRESH_INTERVAL = 600;

function getStringFromArray(data) {
    return new TextDecoder().decode(data).trim();
}

function getCommandLineString(command) {
    try {
        return getStringFromArray(GLib.spawn_command_line_sync(command)[1]).split('\n');
    } catch (e) {
        return null;
    }
}

function getFileContent(fileName, asString) {
    if (!GLib.file_test(fileName, GLib.FileTest.EXISTS)) return null;
    try {
        let output = getStringFromArray(GLib.file_get_contents(fileName)[1]);
        return asString ? _(output.split('\n')[0]) : parseInt(output) / 1000;
    } catch (e) {
        return null;
    }
}

class SensorInfo extends EventEmitter {
    constructor(...params) {
        super();
        this._init(...params);
    }

    _init(sorted) {
        this.children = [];
        this._sorted = sorted;
        this._sources = [];
        this._appSystem = Shell.AppSystem.get_default();
        this._appSystem.connectObject(
            'app-state-changed', () => this._checkSensor(),
            'installed-changed', () => this._checkSensor(), this);
        this._checkSensor();
    }

    _checkSensor() {
        let sources = this.getSources();
        if (this._sources.length !== sources.length) {
            this._sources = sources;
            this.emit('changed::sensor');
        }
    }

    destroy() {
        this._appSystem.disconnectObject(this);
    }

    refresh() {
        this.children = [];
        this._sources.forEach(source => {
            let sourceOutput = this.getSourceOutput(source);
            if (sourceOutput.length > 0) this.children = this.children.concat(sourceOutput);
        });
        if (this._sorted) this.children.sort((a, b) => GLib.utf8_collate(a.label, b.label));
        this.emit('updated::sensor');
    }

    get available() {
        return this.children.length > 0;
    }

    get average() {
        let counter = 0;
        let summary = 0;
        this.children.forEach(child => {
            counter ++;
            summary += child['current'];
        });
        return counter > 0 ? summary / counter : 0;
    }

    get highest() {
        let current = 0;
        this.children.forEach(child => {
            if (child['current'] > current) current = child['current'];
        });
        return current;
    }

    get warning() {
        let result = false;
        this.children.forEach(child => {
            if (child['warning']) result = true;
        });
        return result;
    }
}

class SensorHDDInfo extends SensorInfo {
    _init() {
        super._init(true);
        this._volumeMonitor = Gio.VolumeMonitor.get();
        this._volumeMonitor.connectObject(
            'drive-changed', () => this.refresh(),
            'drive-connected', () => this.refresh(),
            'drive-disconnected', () => this.refresh(),
            'mount-added', () => this.refresh(),
            'mount-changed', () => this.refresh(),
            'mount-removed', () => this.refresh(),
            'volume-added', () => this.refresh(),
            'volume-changed', () => this.refresh(),
            'volume-removed', () => this.refresh(), this);
    }

    destroy() {
        this._volumeMonitor.disconnectObject(this);
        super.destroy();
    }

    getSourceOutput(source) {
        let sourceOutput = [];
        switch (source.id) {
        case 'UDisksCTL':
            let linesStatus = getCommandLineString(source.cmd+' status');
            linesStatus.forEach(status => {
                let device = '/dev/'+status.trim().split(' ').pop();
                let linesInfoBlockDevice = getCommandLineString(source.cmd+' info --block-device='+device);
                linesInfoBlockDevice.forEach(infoBlockDevice => {
                    if (infoBlockDevice.indexOf('Drive:') >= 0) {
                        let drive = infoBlockDevice.split('Drive:')[1].split(/'/)[1].split('/')[5];
                        let model;
                        let current = 0;
                        let linesInfoDrive = getCommandLineString(source.cmd+' info --drive="'+drive+'"');
                        linesInfoDrive.forEach(infoDrive => {
                            if (infoDrive.indexOf('Model:') >= 0) model = infoDrive.split(':')[1].trim();
                            if (infoDrive.indexOf('SmartTemperature:') >= 0) current = parseFloat(infoDrive.split(':')[1]);
                        });
                        if (current > 0) sourceOutput.push({
                            'label': model+'  »%s«'.format(device),
                            'current': current - 273.15,
                            'maximum': null,
                            'critical': null,
                            'warning': false,
                        });
                    }
                });
            });
            break;
        default: // Nothing to do
        }
        return sourceOutput;
    }

    getSources() {
        let sources = [];
        let command = GLib.find_program_in_path('udisksctl');
        if (command) sources.push({
            'id': 'UDisksCTL',
            'cmd': command,
        });
        return sources;
    }
}

class SensorHWMInfo extends SensorInfo {
    _init() {
        super._init(false);
    }

    getSourceOutput(source) {
        let sourceOutput = [];
        let name = getFileContent(source+'name', true);
        if (name) for (let i = 1; ; i++) {
            let current = getFileContent(source+'temp'+i+'_input', false);
            if (!current && i > 8) break;
            if (!current) continue;
            let label = getFileContent(source+'temp'+i+'_label', true);
            let maximum = getFileContent(source+'temp'+i+'_max', false);
            let critical = getFileContent(source+'temp'+i+'_crit', false);
            switch (name) {
            case 'acpitz':
                label = _('ACPI Thermal Zone %s').format(i);
                break;
            case 'atk0110':
                label = 'ASUS '+name.toUpperCase()+'  »%s«'.format(_(label));
                break;
            case 'coretemp': // All Intel Core family
                label = _('Processor - Intel  »%s«').format(_(label));
                break;
            case 'k8temp': // AMD Athlon64/FX or Opteron CPUs
                let core = i < 2 ? 1 : 0;
                let place = i == 2 || i === 4 ? 1 : 0;
                label = _('Processor - AMD K8  »Core %s/%s«').format(core, place);
                break;
            case 'k10temp': // AMD Family 10h/11h/12h/14h/15h/16h
                let k10Input = getFileContent(source+'temp2_input', false);
                if (k10Input) {
                    current = k10Input;
                    let k10Label = getFileContent(source+'temp2_label', true);
                    if (k10Label) label = k10Label;
                    i++;
                }
                label = label ? _('Processor - AMD K10  »%s«').format(label) : _('Processor - AMD K10');
                break;
            case 'thinkpad':
                label = _('ThinkPad  »%s«').format(label || i);
                break;
            case 'amdgpu':  // AMD Radeon GPUs
            case 'nouveau': // NV43+
                label = _('Graphic card  »%s«').format(name);
                break;
            default: // Unknown
                //TODO: Add unknow sensors
                label = name+'  »%s«'.format(label || i);
            }
            let warning = maximum && current > maximum * 0.9 || critical && current > critical * 0.8;
            sourceOutput.push({
                'label': label,
                'current': current,
                'maximum': maximum,
                'critical': critical,
                'warning': warning,
            });
        }
        return sourceOutput;
    }

    getSources() {
        let sources = [];
        for (let i = 0; ; i++) {
            let source = '/sys/class/hwmon/hwmon'+i+'/';
            if (GLib.file_test(source+'name', GLib.FileTest.EXISTS)) {
                sources.push(source);
            } else if (GLib.file_test(source+'device/name', GLib.FileTest.EXISTS)) {
                sources.push(source+'device/');
            } else {
                if (i > 1) break;
            }
        }
        return sources;
    }
}

class SensorVGAInfo extends SensorInfo {
    _init() {
        super._init(true);
    }

    getSourceOutput(source) {
        let sourceOutput = [];
        switch (source.id) {
        case 'ATIConfig':
        case 'NVidiaSMI':
            let current = 0;
            let label = null;
            let lines = getCommandLineString(source['Command']);
            lines.forEach(line => {
                if (line.indexOf('Default Adapter') >= 0) label = line.split(' - ')[1];
                if (line.indexOf('Product Name') >= 0) label = line.split(' : ')[1];
                if (line.indexOf('Temperature') >= 0) current = parseFloat(line.split(/ - | : /)[1]);
                if (label !== null && current > 0) {
                    sourceOutput.push({
                        'label': label,
                        'current': current,
                        'maximum': null,
                        'critical': null,
                        'warning': false,
                    });
                    current = 0;
                    label = null;
                }
            });
            break;
        default: // Nothing to do
        }
        return sourceOutput;
    }

    getSources() {
        let sources = [];
        let command = GLib.find_program_in_path('aticonfig');
        if (command) sources.push({
            'id': 'ATIConfig',
            'cmd': command+' --odgt',
        });
        command = GLib.find_program_in_path('nvidia-smi');
        if (command) sources.push({
            'id': 'NVidiaSMI',
            'cmd': command+' -q -a',
        });
        return sources;
    }
}

var SensorManager = class extends EventEmitter {
    constructor() {
        super();
        this._init();
    }

    _init() {
        this.sensor = {
            'hdd': new SensorHDDInfo(),
            'hwm': new SensorHWMInfo(),
            'vga': new SensorVGAInfo(),
        };
        this._refreshTimeoutId = 0;
        this.setRefreshInterval(DEFAULT_REFRESH_INTERVAL);
        for (let sensor in this.sensor) {
            this.sensor[sensor].connectObject('changed::sensor', () => this._refreshSensors(), this);
        }
        this._appSystem = Shell.AppSystem.get_default();
        this._appSystem.connectObject(
            'app-state-changed', () => this._refreshSensors(),
            'installed-changed', () => this._refreshSensors(), this);
    }

    _refreshSensors() {
        for (let sensor in this.sensor) {
            this.sensor[sensor].refresh();
        }
        this.emit('updated::sensors');
    }

    destroy() {
        if (this._refreshTimeoutId !== 0) GLib.source_remove(this._refreshTimeoutId);
        this._refreshTimeoutId = 0;
        this._appSystem.disconnectObject(this);
        for (let sensor in this.sensor) {
            this.sensor[sensor].disconnectObject(this);
            this.sensor[sensor].destroy();
        }
    }

    setRefreshInterval(seconds) {
        if (this._refreshTimeoutId !== 0) GLib.source_remove(this._refreshTimeoutId);
        this._refreshTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, seconds, () => {
            this._refreshTimeoutId = 0;
            this._refreshSensors();
            return GLib.SOURCE_REMOVE;
        });
        this._refreshSensors();
    }

    get available() {
        for (let sensor in this.sensor) {
            if (this.sensor[sensor].available) return true;
        }
        return false;
    }

    get average() {
        let counter = 0;
        let summary = 0;
        for (let sensor in this.sensor) {
            this.sensor[sensor].children.forEach(child => {
                counter ++;
                summary += child['current'];
            });
        }
        return counter > 0 ? summary / counter : 0;
    }

    get highest() {
        let current = 0;
        for (let sensor in this.sensor) {
            this.sensor[sensor].children.forEach(child => {
                if (child['current'] > current) current = child['current'];
            });
        }
        return current;
    }

    get warning() {
        let result = false;
        for (let sensor in this.sensor) {
            this.sensor[sensor].children.forEach(child => {
                if (child['warning']) result = true;
            });
        }
        return result;
    }
};
