#
# Copyright (C) 2018 Marko Myllynen <myllynen@redhat.com>
#
# 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 USDT JVM threads module """

# pylint: disable=invalid-name, too-many-instance-attributes

from ctypes import c_int
from os import path

from bcc import BPF, USDT

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

from modules.pcpbcc import PCPBCCBase

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

#
# PCP BCC PMDA constants
#
MODULE = 'usdt_jvm_threads'
BASENS = 'usdt.jvm.threads.'
units_count = pmUnits(0, 0, 1, 0, 0, PM_COUNT_ONE)

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

        self.pid = None
        self.proc_filter = None
        self.proc_refresh = proc_refresh

        self.stats = None
        self.jvm_path = "/etc/alternatives/jre/lib/server/libjvm.so"
        self.usdt_contexts = []

        for opt in self.config.options(MODULE):
            if opt == 'jvm_path':
                self.jvm_path = self.config.get(MODULE, opt)
            if opt == 'process':
                self.proc_filter = self.config.get(MODULE, opt)
                self.update_pids(self.get_proc_info(self.proc_filter))

        if not self.proc_filter:
            # https://github.com/iovisor/bcc/issues/1774
            raise RuntimeError("Process filter is mandatory.")

        self.log("Initialized.")

    def metrics(self):
        """ Get metric definitions """
        name = BASENS
        self.items = (
            # Name - reserved - type - semantics - units - help
            (name + 'start', None, PM_TYPE_U64, PM_SEM_COUNTER, units_count, 'thread starts'),
            (name + 'stop', None, PM_TYPE_U64, PM_SEM_COUNTER, units_count, 'thread stops'),
        )
        return False, self.items

    def reset_cache(self):
        """ Reset internal cache """
        self.stats = [0, 0]

    def undef_cache(self):
        """ Undefine internal cache """
        self.stats = None

    def compile(self):
        """ Compile BPF """
        try:
            if not self.pid and self.proc_filter and not self.proc_refresh:
                raise RuntimeError("No process to trace found.")

            if not self.bpf_text:
                with open(path.dirname(__file__) + '/../' + bpf_src) as src:
                    self.bpf_text = src.read()

            if not self.pid and self.proc_filter and self.proc_refresh:
                self.log("No process to trace found, activation postponed.")
                return

            self.usdt_contexts = []
            usdt = USDT(pid=self.pid)
            usdt.enable_probe("thread__start", "trace_jvm_thread_start")
            usdt.enable_probe("thread__stop", "trace_jvm_thread_stop")
            self.usdt_contexts.append(usdt)

            if self.debug:
                self.log("BPF to be compiled:\n" + self.bpf_text.strip())

            self.reset_cache()
            self.bpf = BPF(text=self.bpf_text, usdt_contexts=self.usdt_contexts)
            self.log("Compiled.")
        except Exception as error: # pylint: disable=broad-except
            self.bpf = None
            self.undef_cache()
            self.err(str(error))
            self.err("Module NOT active!")
            raise

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

        self.stats[0] = self.bpf["stats"][c_int(0)].value
        self.stats[1] = self.bpf["stats"][c_int(1)].value

    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]
