# ================================================================================
# Copyright (c) 2018 AT&T Intellectual Property. All rights reserved.
# ================================================================================
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============LICENSE_END=========================================================
#
# ECOMP is a trademark and service mark of AT&T Intellectual Property.

"""generic class to keep get real time info about the current process"""

import gc
import sys
import threading
import traceback
from functools import wraps

import psutil


def safe_operation(func):
    """safequard the function against any exception"""
    if not func:
        return

    @wraps(func)
    def wrapper(*args, **kwargs):
        """wrapper around the function"""
        try:
            return func(*args, **kwargs)
        except Exception as ex:
            return {type(ex).__name__ : str(ex)}
    return wrapper


class ProcessInfo(object):
    """static class to calculate process info"""
    _BIBYTES_SYMBOLS = ('KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB')
    _BIBYTES_VALS = {}
    _inited = False
    _lock = threading.Lock()

    @staticmethod
    def init():
        """init static constants"""
        if ProcessInfo._inited:
            return
        with ProcessInfo._lock:
            if ProcessInfo._inited:
                return

            for i, bibytes_symbol in enumerate(ProcessInfo._BIBYTES_SYMBOLS):
                ProcessInfo._BIBYTES_VALS[bibytes_symbol] = 1 << (i + 1) * 10
            ProcessInfo._BIBYTES_SYMBOLS = list(reversed(ProcessInfo._BIBYTES_SYMBOLS))
            ProcessInfo._inited = True

    @staticmethod
    def bytes_to_bibytes(byte_count):
        """converts byte count to human value in kibi-mebi-gibi-...-bytes"""
        if byte_count is None:
            return "unknown"
        if not byte_count or not isinstance(byte_count, int):
            return byte_count
        ProcessInfo.init()

        for bibytes_symbol in ProcessInfo._BIBYTES_SYMBOLS:
            bibytes_value = ProcessInfo._BIBYTES_VALS[bibytes_symbol]
            if byte_count >= bibytes_value:
                value = float(byte_count) / bibytes_value
                return '%.2f %s' % (value, bibytes_symbol)
        return "%s B" % byte_count

    @staticmethod
    @safe_operation
    def process_memory():
        """calculates the memory usage of the current process"""
        process = psutil.Process()
        with process.oneshot():
            return dict((k, ProcessInfo.bytes_to_bibytes(v))
                        for k, v in process.memory_full_info()._asdict().items())


    @staticmethod
    @safe_operation
    def virtual_memory():
        """calculates the virtual memory usage of the whole vm"""
        return dict((k, ProcessInfo.bytes_to_bibytes(v))
                    for k, v in psutil.virtual_memory()._asdict().items())


    @staticmethod
    @safe_operation
    def active_threads():
        """list of active threads"""
        return sorted([thr.name + "(" + str(thr.ident) + ")" for thr in threading.enumerate()])


    @staticmethod
    @safe_operation
    def thread_stacks():
        """returns the current threads with their stack"""
        thread_names = dict((thr.ident, thr.name) for thr in threading.enumerate())
        return [
            {
                "thread_id" : thread_id,
                "thread_name" : thread_names.get(thread_id),
                "thread_stack" : [
                    {
                        "filename" : filename,
                        "lineno" : lineno,
                        "function" : function_name,
                        "line" : line.strip() if line else None
                    }
                    for filename, lineno, function_name, line in traceback.extract_stack(stack)
                ]
            }
            for thread_id, stack in sys._current_frames().items()
        ]


    @staticmethod
    @safe_operation
    def gc_info(full=False):
        """gets info from garbage collector"""
        gc_info = {
            "gc_count" : str(gc.get_count()),
            "gc_threshold" : str(gc.get_threshold())
        }
        if gc.garbage:
            gc_info["gc_garbage"] = ([repr(stuck) for stuck in gc.garbage]
                                     if full else len(gc.garbage))
        return gc_info

    @staticmethod
    def get_all():
        """all info"""
        return {
            "active_threads" : ProcessInfo.active_threads(),
            "gc" : ProcessInfo.gc_info(full=True),
            "process_memory" : ProcessInfo.process_memory(),
            "virtual_memory" : ProcessInfo.virtual_memory(),
            "thread_stacks" : ProcessInfo.thread_stacks()
        }
