"""Produces memory usage information"""
import gc
import objgraph
import os
import resource
import subprocess
import tempfile
import zlib
from StringIO import StringIO
from typing import Optional  # noqa

from autopush.gcdump import Stat

from cffi import FFI


# cffi's API mode is preferable but it would assume jemalloc is always
# available (and we LD_PRELOAD it)
ffi = FFI()
ffi.cdef("""
int malloc_info(int options, FILE *stream);
void malloc_stats_print(void (*write_cb) (void *, const char *),
                        void *cbopaque, const char *opts);
""")
lib = ffi.dlopen(None)


def memusage(do_dump_rpy_heap=True, do_objgraph=True):
    # type: (Optional[bool], Optional[bool]) -> str
    """Returning a str of memory usage stats"""
    def trap_err(func, *args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:  # pragma: nocover
            # include both __str/repr__, sometimes one's useless
            buf.writelines([func.__name__, ': ', repr(e), ': ', str(e)])

    buf = StringIO()
    rusage = trap_err(resource.getrusage, resource.RUSAGE_SELF)
    buf.writelines([repr(rusage), '\n\n'])
    trap_err(pmap_extended, buf)
    trap_err(jemalloc_stats, buf)
    trap_err(glibc_malloc_info, buf)
    if hasattr(gc, 'get_stats'):
        buf.writelines(['\n\n', gc.get_stats(), '\n\n'])
    if do_dump_rpy_heap:
        # dump rpython's heap before objgraph potentially pollutes the
        # heap with its heavy workload
        trap_err(dump_rpy_heap, buf)
    trap_err(get_stats_asmmemmgr, buf)
    buf.write('\n\n')
    if do_objgraph:
        trap_err(objgraph.show_most_common_types, limit=0, file=buf)
    return buf.getvalue()


def pmap_extended(stream):
    """Write pmap (w/ the most extended stats supported) to stream"""
    pid = str(os.getpid())
    # -XX/-X are recent linux only
    ex_args = ['XX', 'X', 'x']
    while True:
        cmd = ['pmap', '-' + ex_args.pop(0), pid]
        try:
            pmap = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
        except subprocess.CalledProcessError:  # pragma: nocover
            if not ex_args:
                raise
        else:
            stream.writelines([' '.join(cmd[:2]), '\n', pmap, '\n\n'])
            break


def dump_rpy_heap(stream):  # pragma: nocover
    """Write PyPy's gcdump to the specified stream"""
    if not hasattr(gc, '_dump_rpy_heap'):
        # not PyPy
        return

    with tempfile.NamedTemporaryFile('wb') as fp:
        gc._dump_rpy_heap(fp.fileno())
        try:
            fpsize = os.stat(fp.name).st_size
        except OSError:
            pass
        else:
            stream.write("{} size: {}\n".format(fp.name, fpsize))
        stat = Stat()
        stat.summarize(fp.name, stream=None)
    stat.load_typeids(zlib.decompress(gc.get_typeids_z()).split("\n"))
    stream.write('\n\n')
    stat.print_summary(stream)


def get_stats_asmmemmgr(stream):  # pragma: nocover
    """Write PyPy's get_stats_asmmemmgr to the specified stream

    (The raw memory currently used by the JIT backend)

    """
    try:
        import pypyjit
    except ImportError:
        # not PyPy or no jit?
        return

    stream.write('\n\nget_stats_asmmemmgr: ')
    stream.write(repr(pypyjit.get_stats_asmmemmgr()))
    stream.write('\n')


def glibc_malloc_info(stream):
    """Write glib malloc's malloc_info(3)"""
    with tempfile.NamedTemporaryFile('wb+') as fp:
        if not lib.malloc_info(0, fp.file):
            fp.seek(0)
            stream.writelines(fp.readlines())


def jemalloc_stats(stream):  # pragma: nocover
    """Write jemalloc's malloc_stats_print()"""
    try:
        malloc_stats_print = lib.malloc_stats_print
    except AttributeError:
        # not using jemalloc
        return
    malloc_stats_print(_jemalloc_write_cb, ffi.new_handle(stream), ffi.NULL)
    stream.write('\n')


@ffi.callback("void (*write_cb) (void *, const char *)")
def _jemalloc_write_cb(handle, msg):  # pragma: nocover
    """Callback for jemalloc's malloc_stats_print

    Writes to a Python stream passed via the cbopaque pointer

    """
    stream = ffi.from_handle(handle)
    stream.write(ffi.string(msg))