#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#
# SPDX-FileCopyrightText: 2012 Eike Hein <hein@kde.org>


# The overall format of this script's output. The format of the individual
# elements referenced here is specified further below.
output_format = "Sysinfo for '$host': Running $kde $distro $kernel, $cpu, $ram, $storage, $procs, $uptime"

# The formats for the individual elements referenced in output_format.
format_strings = {
    'host'    : '$hostname',
    'distro'  : 'on $name $version',
    'kernel'  : 'powered by $name $version',
    'kde'     : '$mode $product $version', # $mode is 'inside' or 'against'; $product is 'KDE Plasma' or 'KDE Frameworks'
    'cpu'     : 'CPU: $model at $mhz MHz',
    'ram'     : 'RAM: $used/$total MB', # Also available: $free.
    'storage' : 'Storage: $used/$total GB', # Also available: $free.
    'procs'   : '$count procs',
    'uptime'  : '${hours}h up'
}


# ===== Do not change anything below this line. =====

import abc
import os
import re
import socket
import string
import subprocess
import sys

try:
    import konversation.dbus
except ImportError:
    sys.exit("This script is intended to be run from within Konversation.")

if sys.hexversion < 0x02070000:
    import konversation.i18n
    konversation.i18n.init()
    err = i18n("The sysinfo script requires Python %1 or higher.", '2.7')
    konversation.dbus.error(err)
    sys.exit(err)


sensors = list()

class MetaSensor(abc.ABCMeta):
    def __init__(cls, name, *rest):
        global sensors

        try:
            if cls.__base__ == Sensor:
                sensors.append(cls)
                cls.template = string.Template(format_strings[name])
        except NameError:
            pass

        return abc.ABCMeta.__init__(cls, name, *rest)

# This is for papering over the syntax differences for specifying a
# metaclass in Python 2 and 3.
Shunt = MetaSensor('Shunt', (object,), {})

class Sensor(Shunt):
    __metaclass__ = MetaSensor

    def __init__(self):
        self.data = dict()
        self.gather()

    @abc.abstractmethod
    def gather(self):
        pass

    def format(self):
        substituted = self.template.safe_substitute(self.data)

        for match in self.template.pattern.findall(substituted):
            substituted = substituted.replace('${' + match[2] + '}', '').replace('$' + match[1], '')

        return substituted

class host(Sensor):
    def gather(self):
        self.data['hostname'] = socket.gethostname()

class kde(Sensor):
    def gather(self):
        kde = 'KDE_FULL_SESSION' in os.environ and 'KDE_SESSION_VERSION' in os.environ

        self.data['mode'] = ('against', 'inside')[kde]
        self.data['product'] = ('KDE Frameworks', 'KDE Plasma')[kde]

        if kde and os.environ['KDE_SESSION_VERSION'] == '3':
            cmd = ['kde-config']
        elif kde and os.environ['KDE_SESSION_VERSION'] == '4':
            cmd = ['kde4-config']
        elif kde and (os.environ['KDE_SESSION_VERSION'] == '5' or os.environ['KDE_SESSION_VERSION'] == '6'):
            cmd = ['plasmashell']
        else:
            cmd = ['kconfig_compiler_kf6']

        cmd.append('--version')

        try:
            version = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode()
        except (OSError, subprocess.CalledProcessError):
            self.data['version'] = "6"
            return

        for line in version.splitlines():
            if line.startswith('KDE'):
                self.data['version'] = line.split(':')[1].split()[0]
                break
            elif line.startswith('plasmashell ') or line.startswith('kconfig_compiler '):
                self.data['version'] = line.split()[-1]
                break

class distro(Sensor):
    def gather(self):
        if os.path.exists('/etc/os-release'):
            with open('/etc/os-release', 'rb') as osrelease:
                lines = (line.strip() for line in osrelease)
                lines = (line for line in lines if line)
                for line in lines:
                    key, value = line.decode('utf-8', 'replace').split('=', 1)
                    if key == 'NAME':
                        self.data['name'] = value.strip(string.whitespace + '"')
                    elif key == 'VERSION':
                        self.data['version'] = value.strip(string.whitespace + '"')
        else:
            try:
                lsbrelease = subprocess.check_output(['lsb_release', '-i', '-r'], stderr=subprocess.STDOUT)

                for line in lsbrelease.decode().splitlines():
                    key, value = line.split(':', 1)
                    if key == 'Distributor ID':
                        self.data['name'] = value.strip()
                    elif key == 'Release':
                        self.data['version'] = value.strip()
            except (OSError, subprocess.CalledProcessError):
                pass

    def format(self):
        return Sensor.format(self) if 'name' in self.data else ''

class kernel(Sensor):
    def gather(self):
        uname = os.uname()
        self.data['name'] = uname[0]
        self.data['version'] = uname[2]

class cpu(Sensor):
    def gather(self):
        curfreqs = set()

        self.data['model'] = 'Unknown model'

        with open('/proc/cpuinfo', 'r') as cpuinfo:
            for line in (line for line in cpuinfo if len(line.strip()) > 1):
                key, value = line.split(':', 1)
                if key.strip() == 'model name':
                    self.data['model'] = value.strip()
                elif key.strip() == 'cpu MHz':
                    curfreqs.add(int(float(value.strip())))

        if not curfreqs:
            try:
                dir = '/sys/devices/system/cpu/'

                for cpu in (cpu for cpu in os.listdir(dir) if re.match('cpu[0-9]+', cpu)):
                    with open(os.path.join(dir, cpu, 'cpufreq/scaling_cur_freq'), 'r') as curfreqf:
                        curfreqs.add(int(curfreqf.read())/1000)
            except IOError:
                self.data['mhz'] = 'unknown'
                return

        curfreqs = sorted(curfreqs)

        if len(curfreqs) > 1:
            curfreq = '{0}-{1}'.format(curfreqs[0], curfreqs[-1])
        else:
            curfreq = curfreqs[0]

        maxfreq = 0

        try:
            dir = '/sys/devices/system/cpu/'

            for cpu in (cpu for cpu in os.listdir(dir) if re.match('cpu[0-9]+', cpu)):
                with open(os.path.join(dir, cpu, 'cpufreq/cpuinfo_max_freq'), 'r') as maxfreqf:
                    new = int(maxfreqf.read())
                    maxfreq = new if new > maxfreq else maxfreq

            maxfreq = int(maxfreq / 1000)
        except IOError:
            pass

        if curfreqs[0] < maxfreq:
            self.data['mhz'] = '{0}/{1}'.format(curfreq, maxfreq)
        else:
            self.data['mhz'] = curfreq

class ram(Sensor):
    def gather(self):
        with open('/proc/meminfo', 'r') as meminfo:
            for line in meminfo:
                key, value = line.split(':', 1)
                if key == 'MemTotal':
                    self.data['total'] = int(value.split()[0])
                elif key == 'MemFree':
                    self.data['free'] = int(value.split()[0])

            self.data['used'] = self.data['total'] - self.data['free']

            for (key, value) in self.data.items():
                self.data[key] = int(value / 1024)

class storage(Sensor):
    def gather(self):
        try:
            env = dict(LC_ALL='C')
            df = subprocess.check_output(['df', '-lP'], stderr=subprocess.STDOUT, env=env).decode()
        except subprocess.CalledProcessError as e:
            df = e.output.decode()

        volumes = {line.split()[0] : line.split()[1:4] for line in df.splitlines() if line.startswith('/dev')}

        for (key, index) in zip(['total', 'used', 'free'], range(2)):
            self.data[key] = int(sum(int(volume[index]) for volume in volumes.values()) / 1048576)

class procs(Sensor):
    def gather(self):
        self.data['count'] = len([pid for pid in os.listdir('/proc') if pid.isdigit()])

class uptime(Sensor):
    def gather(self):
        with open('/proc/uptime', 'r') as uptimef:
            self.data['hours'] = round(float(uptimef.read().split()[0]) / 3600, 2)

if __name__ == '__main__':
    sensors = {cls.__name__ : cls().format() for cls in sensors}

    output = string.Template(output_format).safe_substitute(sensors)
    output = ' '.join(output.split()) # Simplify whitespace.

    report = konversation.dbus.say if konversation.dbus.target else konversation.dbus.info
    report(output)
