#
# Copyright (C) 2018 Marko Myllynen <myllynen@redhat.com>
# Based on the cachestat BCC tool by Brendan Gregg:
# https://github.com/iovisor/bcc/blob/master/tools/cachestat.py
#
# 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.
#
""" PCP BCC PMDA cachestat module """

# pylint: disable=invalid-name, line-too-long, too-many-locals

from bcc import BPF

from pcp.pmapi import pmUnits
from cpmapi import PM_TYPE_U64, PM_SEM_COUNTER, PM_COUNT_ONE, PM_SPACE_MBYTE
from cpmda import PMDA_FETCH_NOVALUES

from modules.pcpbcc import PCPBCCBase

#
# BPF program
#
bpf_src = "modules/cachestat.bpf"

#
# PCP BCC PMDA constants
#
MODULE = 'cachestat'
METRIC = 'mem.vmstat.'
units_count = pmUnits(0, 0, 1, 0, 0, PM_COUNT_ONE)
units_mbyte = pmUnits(1, 0, 0, PM_SPACE_MBYTE, 0, 0)

#
# PCP BCC Module
#
class PCPBCCModule(PCPBCCBase):
    """ PCP BCC cachestat module """
    def __init__(self, config, log, err, _):
        """ Constructor """
        PCPBCCBase.__init__(self, MODULE, config, log, err)

        self.stats = [0, 0, 0, 0, 0, 0]

        self.log("Initialized.")

    def metrics(self):
        """ Get metric definitions """
        name = METRIC
        self.items = (
            # Name - reserved - type - semantics - units - help
            (name + 'total', None, PM_TYPE_U64, PM_SEM_COUNTER, units_count, 'page cache access total'),
            (name + 'misses', None, PM_TYPE_U64, PM_SEM_COUNTER, units_count, 'page cache misses'),
            (name + 'hits', None, PM_TYPE_U64, PM_SEM_COUNTER, units_count, 'page cache hits'),
            (name + 'dirtied', None, PM_TYPE_U64, PM_SEM_COUNTER, units_count, 'pages dirtied (writes)'),
            (name + 'buffers', None, PM_TYPE_U64, PM_SEM_COUNTER, units_mbyte, 'page cache buffers'),
            (name + 'cached', None, PM_TYPE_U64, PM_SEM_COUNTER, units_mbyte, 'page cache cached'),
        )
        return False, self.items

    def compile(self):
        """ Compile BPF """
        try:
            self.bpf = BPF(src_file=bpf_src)
            self.bpf.attach_kprobe(event="add_to_page_cache_lru", fn_name="do_count")
            self.bpf.attach_kprobe(event="mark_page_accessed", fn_name="do_count")
            self.bpf.attach_kprobe(event="account_page_dirtied", fn_name="do_count")
            self.bpf.attach_kprobe(event="mark_buffer_dirty", fn_name="do_count")
            self.log("Compiled.")
        except Exception as error: # pylint: disable=broad-except
            self.bpf = None
            self.err(str(error))
            self.err("Module NOT active!")
            raise

    @staticmethod
    def get_meminfo():
        """ Helper to get data from /proc/meminfo """
        result = {}
        for line in open("/proc/meminfo"):
            k = line.split(":", 3)
            v = k[1].split()
            result[k[0]] = int(v[0])
        return result

    def refresh(self):
        """ Refresh BPF data """
        if self.bpf is None:
            return

        mpa, mbd, apcl, apd = 0, 0, 0, 0

        counts = self.bpf["counts"]

        for k, v in counts.items():
            event = self.bpf.ksym(k.ip)
            # Compat: bcc < 0.6
            event = event if isinstance(event, str) else event.decode("ASCII")
            val = v.value
            if event == "mark_page_accessed":
                mpa = val
            elif event == "mark_buffer_dirty":
                mbd = val
            elif event == "add_to_page_cache_lru":
                apcl = val
            elif event == "account_page_dirtied":
                apd = val

        counts.clear()

        total = max(0, (mpa - mbd))
        misses = max(0, (apcl - apd))
        hits = total - misses
        if hits < 0:
            hits = 0
            misses = total
        # For BCC cachestat compatibility
        mem = self.get_meminfo()
        cached = int(int(mem["Cached"]) / 1024)
        buff = int(int(mem["Buffers"]) / 1024)
        res = [total, misses, hits, mbd, buff, cached]

        for i in range(len(self.items)):
            self.stats[i] += res[i]

        return

    def bpfdata(self, item, inst):
        """ Return BPF data as PCP metric value """
        try:
            return [self.stats[item], 1]
        except Exception: # pylint: disable=broad-except
            return [PMDA_FETCH_NOVALUES, 0]
