"""
Functions and variables to access to Linux proc directory.

Constant:

   - PAGE_SIZE: size of a memory page
"""
from os import readlink, listdir
from resource import getpagesize
from ptrace.tools import timestampUNIX
from datetime import timedelta

PAGE_SIZE = getpagesize()


class ProcError(Exception):
    """
    Linux proc directory error.
    """
    pass


def openProc(path):
    """
    Open a proc entry in read only mode.
    """
    filename = "/proc/%s" % path
    try:
        return open(filename)
    except IOError as err:
        raise ProcError("Unable to open %r: %s" % (filename, err))


def readProc(path):
    """
    Read the content of a proc entry.
    Eg. readProc("stat") to read /proc/stat.
    """
    with openProc(path) as procfile:
        return procfile.read()


def readProcessProc(pid, key):
    """
    Read the content of a process entry in the proc directory.
    Eg. readProcessProc(pid, "status") to read /proc/pid/status.
    """
    try:
        filename = "/proc/%s/%s" % (pid, key)
        with open(filename) as proc:
            return proc.read()
    except IOError as err:
        raise ProcError("Process %s doesn't exist: %s" % (pid, err))


class ProcessState(object):
    """
    Processus state. Attributes:
    - state (str): process status ('R', 'S', 'T', ...)
    - program (str): program name
    - pid (int): process identifier
    - ppid (int): parent process identifier
    - pgrp (int): process group
    - session (int): session identifier
    - tty_nr (int): tty number
    - tpgid (int)
    - utime (int): user space time (jiffies)
    - stime (int): kernel space time (jiffies)
    - starttime (int): start time
    """
    STATE_NAMES = {
        "R": "running",
        "S": "sleeping",
        "D": "disk",
        "Z": "zombie",
        "T": "traced",
        "W": "paging",
    }

    def __init__(self, stat):
        # pid (program) ... => "pid (program", "..."
        part, stat = stat.rsplit(')', 1)
        self.pid, self.program = part.split('(', 1)
        self.pid = int(self.pid)

        # "state ..." => state, "..."
        stat = stat.split()
        self.state = stat[0]
        stat = [int(item) for item in stat[1:]]

        # Read next numbers
        self.ppid = stat[0]
        self.pgrp = stat[1]
        self.session = stat[2]
        self.tty_nr = stat[3]
        self.tpgid = stat[4]
        self.utime = stat[10]
        self.stime = stat[11]
        self.starttime = stat[18]


def readProcessStat(pid):
    """
    Read the process state ('stat') as a ProcessState object.
    """
    stat = readProcessProc(pid, 'stat')
    return ProcessState(stat)


def readProcessStatm(pid):
    """
    Read the process memory status ('statm') as a list of integers.
    Values are in bytes (and not in pages).
    """
    statm = readProcessProc(pid, 'statm')
    statm = [int(item) * PAGE_SIZE for item in statm.split()]
    return statm


def readProcessProcList(pid, key):
    """
    Read a process entry as a list of strings.
    """
    data = readProcessProc(pid, key)
    if not data:
        # Empty file: empty list
        return []
    data = data.split("\0")
    if not data[-1]:
        del data[-1]
    return data


def readProcessLink(pid, key):
    """
    Read a process link.
    """
    try:
        filename = "/proc/%s/%s" % (pid, key)
        return readlink(filename)
    except OSError as err:
        raise ProcError("Unable to read proc link %r: %s" % (filename, err))


def readProcesses():
    """
    Read all processes identifiers. The function is a generator,
    use it with: ::

       for pid in readProcesses(): ...
    """
    for filename in listdir('/proc'):
        try:
            yield int(filename)
        except ValueError:
            # Filename is not an integer (e.g. "stat" from /proc/stat)
            continue


def readProcessCmdline(pid, escape_stat=True):
    """
    Read the process command line. If escape_stat is True, format program name
    with "[%s]" if the process has no command line, e.g. "[khelper]".
    """
    # Try /proc/42/cmdline
    try:
        cmdline = readProcessProcList(pid, 'cmdline')
        if cmdline:
            return cmdline
    except ProcError:
        pass

    # Try /proc/42/stat
    try:
        stat = readProcessStat(pid)
        program = stat.program
        if escape_stat:
            program = "[%s]" % program
        return [program]
    except ProcError:
        return None


def searchProcessesByName(process_name):
    """
    Find all processes matching the program name pattern.
    Eg. pattern "ssh" will find the program "/usr/bin/ssh".

    This function is a generator yielding the process identifier,
    use it with: ::

       for pid in searchProcessByName(pattern):
          ...
    """
    suffix = '/' + process_name
    for pid in readProcesses():
        cmdline = readProcessCmdline(pid)
        if not cmdline:
            continue
        program = cmdline[0]
        if program == process_name or program.endswith(suffix):
            yield pid


def searchProcessByName(process_name):
    """
    Function similar to searchProcessesByName() but only return the identifier
    of the first matching process. Raise a ProcError if there is no matching
    process.
    """
    for pid in searchProcessesByName(process_name):
        return pid
    raise ProcError("Unable to find process: %r" % process_name)


def getUptime():
    """
    Get the system uptime as a datetime.timedelta object.
    """
    uptime = readProc('uptime')
    uptime = uptime.strip().split()
    uptime = float(uptime[0])
    return timedelta(seconds=uptime)


def getSystemBoot():
    """
    Get the system boot date as a datetime.datetime object.
    """
    if getSystemBoot.value is None:
        stat_file = openProc('stat')
        for line in stat_file:
            if not line.startswith("btime "):
                continue
            seconds = int(line[6:])
            btime = timestampUNIX(seconds, True)
            getSystemBoot.value = btime
            break
        stat_file.close()
        if getSystemBoot.value is None:
            raise ProcError("Unable to read system boot time!")
    return getSystemBoot.value


getSystemBoot.value = None
