#!/usr/bin/env pmpython
#
# Copyright (C) 2014 Red Hat.
#
# 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.
#

""" Rewrite of pmgsys (originally a C++ application) in python

    Needs to handle the following options at some point:
    -h (host), -cpudelta (interval), -l (label), -v (verbose),
    -c (config), -zoom (factor), ... rest through to pmgadgets.
"""

from __future__ import print_function
from sys import argv
from pcp import pmapi
#from math import sqrt
import math
from cpmapi import PM_TYPE_U32

PCPARGS = ""	# for pmgadgets-launched child processes to use

# magic numbers, from original pmgsys algorithms
CPUDELTA = 0.5
LOADDELTA = 5
FONTASCENT = 7
DOLABEL = 1
HSPACE = 5
VSPACE = 2
CPUWIDTH = 30
CPUHEIGHT = 4
LOADWIDTH = CPUWIDTH
LOADHEIGHT = 4 * CPUHEIGHT + 3 * VSPACE
MEMWIDTH = CPUHEIGHT
MEMHEIGHT = LOADHEIGHT
DISKSIZE = 6
NETWIDTH = CPUWIDTH
NETHEIGHT = CPUHEIGHT

class Machine(object):
    def __init__(self, context):
        # pcp context
        self.context = context
        # hardware bits
        self.ncpu = 0
        self.ndisk = 0
        self.niface = 0
        self.ndiskmaps = 0
        self.memory = 0
        # external names
        self.cpus = []
        self.disks = []
        self.parts = []
        self.ifaces = []
        self.diskmaps = []

    def get_hinv(self):
        """ Extract counts of CPUs, disks, interfaces and memory size
        """
        hinv = ('hinv.ncpu', 'hinv.ndisk', 'hinv.ninterface', 'hinv.physmem')
        pmids = self.context.pmLookupName(hinv)
        descs = self.context.pmLookupDescs(pmids)
        result = self.context.pmFetch(pmids)
        hardware = [0, 0, 0, 0]
        for x in range(4):
            atom = self.context.pmExtractValue(
                                result.contents.get_valfmt(x),
                                result.contents.get_vlist(x, 0),
                                descs[x].contents.type, PM_TYPE_U32)
            hardware[x] = atom.ul
        context.pmFreeResult(result)
        self.ncpu = hardware[0]
        self.ndisk = hardware[1]
        self.niface = hardware[2]
        self.memory = hardware[3]

    def get_names(self):
        """ Extract names of CPUs, disks and network interfaces.
        """
        inst = ('kernel.percpu.cpu.user',               # expand CPU names
                'disk.dev.total',                       # expand disk names
                'disk.partitions.total',                # expand disk partition names
                'network.interface.total.bytes')        # expand network interface names
        pmids = self.context.pmLookupName(inst)
        descs = self.context.pmLookupDescs(pmids)
    
        (inst, self.cpus) = self.context.pmGetInDom(descs[0])
        (inst, self.disks) = self.context.pmGetInDom(descs[1])
        (inst, self.parts) = self.context.pmGetInDom(descs[2])
        (inst, self.ifaces) = self.context.pmGetInDom(descs[3])

    def details(self):
        print("CPUs: ", self.ncpu)
        print("CPU names: ", self.cpus)
        print("Disks: ", self.ndisk)
        print("Disk names: ", self.disks)
        print("Partition names: ", self.parts)
        print("Interfaces: ", self.niface)
        print("Interface names: ", self.ifaces)
        print("Memory: ", self.memory)

    def get_diskmaps(self):
        """ Produce disk -> partition mappings
            This means grouping "sda1 sda2 sda3 sdb1" into two mappings
            - "sda" -> (sda1, sda2, sda3) and "sdb" -> (sdb1); done via
            the disk.dev.total and disk.partitions.total metrics.
            (original: controller -> disk mappings, but ENODATA)
        """
        return 0

    def inventory(self):
        """ Wrap calls to getting counts and subsystem names
        """
        self.get_hinv()
        self.get_names()
        self.get_diskmaps()

    def gadgetize(self):
        """ Generate a pmgadgets configuration for this host
        """
        print("pmgadgets 1", end=' ')	# follow with command line
        for arg in argv:
            print("\"%s\"" % (arg), end=' ')
        print("")

        rows = 1
        ctiles = int((self.ncpu - 1) / 4 + 1)   # always at least one cpu
        ntiles = int((self.niface - 1) / 4 + 1)
        if ctiles > 3:
            cr = int(math.sqrt(ctiles))
            if cr > rows:
                rows = cr
        if ntiles > 3:
            nr = int(math.sqrt(ntiles))
            if nr > rows:
                rows = nr

        baseY = VSPACE
        if DOLABEL == 1:
            y = FONTASCENT + VSPACE
            hostname = self.context.pmGetContextHostName()
            print("_label %d %d \"%s\"" % (HSPACE, y, hostname))
            baseY += y
        baseX = maxX = HSPACE
        maxY = baseY

        print("_actions cpuActions (")
        print("    \"pmchart\"\t\t\"pmchart -c CPU%s\"" % (PCPARGS))
        print("    \"mpvis *\"\t\t\"mpvis%s\" _default" % (PCPARGS))
        # original had IRIX gr_top and gr_osview tools next;
        # perhaps some fine day we could implement these as
        # pmgadgets front-end tools (certainly the latter)
        print(")")

        y = baseY + FONTASCENT
        x = baseX
        print("_label %d %d \"CPU\"" % (x, y))
        print("    _actions cpuActions\n")
        # original: "these should match the colours in mpvis"
        print("_colourlist cpuColours (blue3 red3 yellow3 cyan3 green3)")

        # place the CPU bars
        y += VSPACE
        ccols = int((ctiles + rows - 1) / rows)

        cpu = 0
        for rc in range(0, self.ncpu):
            for ct in range(0, ccols):
                tc = 0
                while (cpu < self.ncpu and tc < 4):
                    print("_multibar %d %d %d %d" % (x, y, CPUWIDTH, CPUHEIGHT))
                    print("    _update %f" % CPUDELTA)
                    print("    _metrics (")
                    print("\tkernel.percpu.cpu.user[\"%s\"]" % (self.cpus[cpu]))
                    print("\tkernel.percpu.cpu.sys[\"%s\"]" % (self.cpus[cpu]))
                    print("\tkernel.percpu.cpu.intr[\"%s\"]" % (self.cpus[cpu]))
                    print("\tkernel.percpu.cpu.wait.total[\"%s\"]" % (self.cpus[cpu]))
                    print("\tkernel.percpu.cpu.idle[\"%s\"]" % (self.cpus[cpu]))
                    print("    )")
                    print("    _maximum 0.0\n")
                    print("    _colourlist cpuColours")
                    print("    _actions cpuActions\n")
                    cpu += 1
                    tc += 1
                    y += VSPACE + CPUHEIGHT

                if maxY < y:
                    maxY = y
                y -= (VSPACE + CPUHEIGHT) * tc
                x += HSPACE + CPUWIDTH

            y += (CPUHEIGHT + VSPACE) * 4 + VSPACE
            if (maxX < x):
                maxX = x
            x = baseX

        baseX += (HSPACE + CPUWIDTH) * ccols

        # The load gadget and its label
        print("_actions loadActions (")
        print("    \"pmchart *\"",)
        print("\t\"pmchart -c LoadAvg%s\" _default" % (PCPARGS))
        # original had IRIX gr_top here
        print(")")
        print("_label %d %d \"Load\"" % (baseX, baseY + FONTASCENT))
        print("    _actions loadActions")
        print

        y = VSPACE + baseY + FONTASCENT

        i = y + LOADHEIGHT
        if (i > maxY):
            maxY = i

        print("_bargraph %d %d %d %d" % (baseX, y, LOADWIDTH, LOADHEIGHT))
        print("    _update %f" % LOADDELTA)
        print("    _metric kernel.all.load[\"1 minute\"]")
        print("    _max 1.0")
        print("    _actions loadActions")

        # For more than one row, stack LoadAvg on top of Memory.
        #
        # Move baseX just after the right hand side of the memory, so
        # we don't have to do anything special for the netifs.  For the
        # sake of argument, consider total width occupied by memory
        # gauges equal to total width of loadavg graph
        if (rows > 1):
            y += LOADHEIGHT + VSPACE
            baseX += LOADWIDTH + HSPACE
        else:
            y += baseY
            baseX += LOADWIDTH * 2 + HSPACE * 2

        # The memory gadgets and their label (platform-specific!)
        x = baseX - LOADWIDTH - HSPACE
        y += FONTASCENT
        print("_label %d %d \"Mem\"\n" % (x, y))
        print("_colourlist memColours (cyan1 red yellow green)\n")
        y += VSPACE
        print("_multibar %d %d %d %d" % (x, y, MEMWIDTH, MEMHEIGHT))
        print("    _update 0.5")
        print("    _metrics (")
        print("\tmem.util.cached")
        print("\tmem.util.bufmem")
        print("\tmem.util.other")
        print("\tmem.util.free")
        print("    )")
        print("    _colourlist memColours")
        x += HSPACE + MEMWIDTH
        print("_bar %d %d %d %d" % (x, y, MEMWIDTH, MEMHEIGHT))
        print("    _metric swap.pagesout")
        print("    _vertical")

        # Check for the max horizontal offset
        i = y + MEMHEIGHT
        if (i > maxY):
            maxY = i

        # The network bars and their label
        print("_colourlist netColours (aquamarine orange)")
        print("_actions netActions (")
        print("    \"pmchart-packets *\"",)
        print("\t\"pmchart -c NetPackets%s\" _default" % (PCPARGS))
        print("    \"pmchart-bytes\"",)
        print("\t\t\"pmchart -c NetBytes%s\"" % (PCPARGS))
        # original had netstat within an xterm here, next
        print(")")

        y = baseY + FONTASCENT
        x = baseX

        print("_label %d %d \"Net\"" % (x, y))
        print("    _actions netActions\n")

        y += VSPACE

        ncols = int((ntiles + rows - 1) / rows)

        ni = 0
        while ni < self.niface:
            for nt in range(0, ncols):
                tc = 0
                while ni < self.niface and tc < 4:
                    print("_multibar %d %d %d %d" % (x, y, NETWIDTH, NETHEIGHT))
                    print("    _metrics (")
                    print("\tnetwork.interface.in.bytes[\"%s\"]" % (self.ifaces[ni]))
                    print("\tnetwork.interface.out.bytes[\"%s\"]" % (self.ifaces[ni]))
                    print("    )")
                    print("_colourlist netColours")
                    print("    _actions netActions")
                    y += NETHEIGHT + VSPACE

                    tc += 1
                    ni += 1

                if maxY < y:
                    maxY = y
                y -= (NETHEIGHT + VSPACE) * tc
                x += NETWIDTH + HSPACE
            if maxX < x:
                maxX = x
            y += (NETHEIGHT + VSPACE) * 4 + VSPACE
            x = baseX
        print

        # Disks
        dir = 1

        print("_actions diskActions (")
        print("    \"pmchart\"",)
        print("\t\t\"pmchart -c Disk%s\"" % (PCPARGS))
        print("    \"dkvis *\"",)
        print("\t\t\"dkvis%s\" _default" % (PCPARGS))
        print(")")

        x = HSPACE
        y = maxY + FONTASCENT + 2 * VSPACE
        print("_label %d %d \"Disk\"" % (x, y))
        print("    _actions diskActions\n")
        print("_legend diskLegend (")
        print("    _default green3")
        print("    15       yellow")
        print("    40       orange")
        print("    75       red")
        print(")")

        x += CPUWIDTH + HSPACE
        # this only works if FONTASCENT >= ledSize
        y -= DISKSIZE
        thickness = 4
        halfDiskSize = int((DISKSIZE - thickness) / 2)

        for i in range(0, self.ndiskmaps):
            mapping = self.diskmaps[i]
            for j in range(0, len(mappings)):
                if j > 0:
                    if oldX < x:	# moved to right
                        lx = x - VSPACE - 1
                        ly = y + halfDiskSize
                        lw = VSPACE + 2
                        lh = thickness
                    elif oldX > x:	# moved to left
                        lx = oldX - VSPACE - 1
                        ly = y + halfDiskSize
                        lw = VSPACE + 2
                        lh = thickness
                    else:		# moved down
                        lx = x + halfDiskSize
                        ly = oldY + DISKSIZE - 1
                        lw = thickness
                        lh = VSPACE + 2
                    print("_line %d %d %d %d" % (lx, ly, lw, lh))
                print("_led %d %d %d %d" % (x, y, DISKSIZE, DISKSIZE))
                print("    _metric disk.dev.total[\"%s\"]" % (mapping.name()))
                print("    _legend diskLegend")
                print("    _actions diskActions")
                oldX = x
                oldY = y
                xStep = dir * (DISKSIZE + VSPACE)	# use VSPACE (tighter packing)
                x += xStep
                if x > maxX - DISKSIZE or x <= HSPACE:
                    x -= xStep
                    y += DISKSIZE + VSPACE
                    dir = -dir


if __name__ == '__main__':
    context = pmapi.pmContext()
    machine = Machine(context)
    machine.inventory()
    # machine.details()
    machine.gadgetize()

