import logging
import os
import pwd
import collections


# Initialize a logger.
logger = logging.getLogger(__name__)


def uid_to_name(uid):
    """
    Find the username associated with a user ID.

    :param uid: The user ID (an integer).
    :returns: The username (a string) or :data:`None` if :func:`pwd.getpwuid()`
              fails to locate a user for the given ID.
    """
    try:
        return pwd.getpwuid(uid).pw_name
    except Exception:
        return None


def find_processes():
    """
    Scan the numerical subdirectories of ``/proc`` for process information.
    """
    root = '/proc'
    num_processes = 0
    logger.debug("Scanning for process information in %r ..", root)
    for entry in os.listdir(root):
        if entry.isdigit():
            try:
                process = Process.from_path(os.path.join(root, entry))
                if process:
                    num_processes += 1
                    yield process
            except IOError:
                # Couldn't open the proc's files. Will just pass it.
                pass
    logger.debug("Finished scanning %r, found %i processes.", root, num_processes)


def parse_process_status(directory):
    """
    Read and tokenize a ``/proc/[pid]/stat`` file.

    :param directory: The absolute pathname of the numerical subdirectory of
                      ``/proc`` to get process information from (a string).
    :returns: A list of strings containing the tokenized fields or ``None`` if
              the ``/proc/[pid]/stat`` file disappears before it can be read
              (in this case a warning is logged).
    """
    with open(os.path.join(directory, 'stat')) as handle:
        contents = handle.read()

    if contents:
        before_comm, _, remainder = contents.partition('(')
        comm, _, after_comm = remainder.rpartition(')')
        # Combine the tokenized fields into a list of strings. All of the
        # fields except `comm' are integers or a single alphabetic
        # character (the state field) so using str.split() is okay here.
        fields = before_comm.split()
        fields.append(comm)
        fields.extend(after_comm.split())
        return fields


def parse_process_cmdline(directory):
    """
    Read and tokenize a ``/proc/[pid]/cmdline`` file.

    :param directory: The absolute pathname of the numerical subdirectory of
                      ``/proc`` to get process information from (a string).
    :returns: A list of strings containing the tokenized command line.
    """
    with open(os.path.join(directory, 'cmdline')) as handle:
        contents = handle.read()

    contents = contents.strip('\0')

    return contents.split('\0') if contents else []


class Process(object):

    """
    Process information based on ``/proc/[pid]/stat`` and similar files.

    :class:`Process` objects are constructed using
    :func:`find_processes()` and :func:`Process.from_path()`. You
    shouldn't be using the :class:`Process` constructor directly unless you
    know what you're doing.

    The :class:`Process` class extends :class:`~proc.unix.UnixProcess` which means
    all of the process manipulation supported by :class:`~proc.unix.UnixProcess`
    is also supported by :class:`Process` objects.

    **Comparison to official /proc documentation**

    Quite a few of the instance properties of this class are based on (and
    named after) fields extracted from ``/proc/[pid]/stat``. The following
    table lists these properties and the *zero based index* of the
    corresponding field in ``/proc/[pid]/stat``:

    ====================  =====
    Property              Index
    ====================  =====
    :attr:`pid`            0
    :attr:`comm`           1
    :attr:`state`          2
    :attr:`ppid`           3
    :attr:`pgrp`           4
    :attr:`session`        5
    :attr:`starttime`     21
    :attr:`vsize`         22
    :attr:`rss`           23
    ====================  =====

    As you can see from the indexes in the table above quite a few fields from
    ``/proc/[pid]/stat`` are not currently exposed by :class:`Process`
    objects. In fact ``/proc/[pid]/stat`` contains 44 fields! Some of these
    fields are no longer maintained by the Linux kernel and remain only for
    backwards compatibility (so exposing them is not useful) while other fields
    are not exposed because I didn't consider them relevant to a Python API. If
    your use case requires fields that are not yet exposed, feel free to
    suggest additional fields to expose in the issue tracker.

    The documentation on the properties of this class quotes from and
    paraphrases the text in `man 5 proc`_ so if things are unclear and you're
    feeling up to it, dive into the huge manual page for clarifications :-).

    .. _man 5 proc: http://linux.die.net/man/5/proc
    """

    @classmethod
    def from_path(cls, directory):
        """
        Construct a process information object from a numerical subdirectory of ``/proc``.

        :param directory: The absolute pathname of the numerical subdirectory
                          of ``/proc`` to get process information from (a
                          string).
        :returns: A process information object or ``None`` (in case the process
                  ends before its information can be read).

        This class method is used by :func:`find_processes()` to construct
        :class:`Process` objects. It's exposed as a separate method because
        it may sometimes be useful to call directly. For example:

        >>> from proc.core import Process
        >>> Process.from_path('/proc/self')
        Process(pid=1468,
                comm='python',
                state='R',
                ppid=21982,
                pgrp=1468,
                session=21982,
                vsize=40431616,
                rss=8212480,
                cmdline=['python'],
                exe='/home/peter/.virtualenvs/proc/bin/python')
        """
        fields = parse_process_status(directory)
        if fields:
            return cls(directory, fields)

    @classmethod
    def from_pid(cls, pid):
        """
        Construct a process information object based on a process ID.

        :param pid: The process ID (an integer).
        :returns: A process information object or ``None`` (in case the process
                  ends before its information can be read).
        """
        return cls.from_path(os.path.join('/proc', str(pid)))

    def __init__(self, proc_tree, stat_fields):
        # Initialize the superclass.
        super(Process, self).__init__()
        # Initialize instance variables.
        self.proc_tree = proc_tree
        self.stat_fields = stat_fields
        self.__cmdline = None
        self.__rss = None

    @property
    def cmdline(self):
        if self.__cmdline:
            return self.__cmdline

        self.__cmdline = parse_process_cmdline(self.proc_tree)
        return self.__cmdline

    @property
    def comm(self):
        return self.stat_fields[1]

    @property
    def command_line(self):
        return self.cmdline

    @property
    def environ(self):
        variables = {}
        with open(os.path.join(self.proc_tree, 'environ')) as handle:
            contents = handle.read()
        if contents:
            for token in contents.split('\0'):
                name, _, value = token.partition('=')
                if name:
                    variables[name] = value
        return variables

    def exe(self):
        return os.readlink(os.path.join(self.proc_tree, 'exe'))

    @property
    def is_alive(self):
        stat_fields = parse_process_status(self.proc_tree)
        return bool(stat_fields and stat_fields[2] != 'Z')

    @property
    def pid(self):
        return int(self.stat_fields[0])

    @property
    def ppid(self):
        return int(self.stat_fields[3])

    @property
    def rss(self):
        if self.__rss:
            return self.__rss
        self.__rss = int(self.stat_fields[23]) * os.sysconf('SC_PAGESIZE')
        return self.__rss

    @property
    def session(self):
        return int(self.stat_fields[5])

    @property
    def state(self):
        return self.stat_fields[2]

    @property
    def status_fields(self):
        fields = {}
        with open(os.path.join(self.proc_tree, 'status')) as handle:
            for line in handle:
                name, _, value = line.partition(':')
                fields[name] = value.strip()
        return fields

    @property
    def user(self):
        return uid_to_name(self.user_ids.real) if self.user_ids else None

    @property
    def user_ids(self):
        return self._parse_ids('Uid')

    @property
    def vsize(self):
        return int(self.stat_fields[22])

    def _parse_ids(self, field_name):
        """Helper for :attr:`user_ids` and :attr:`group_ids`."""
        raw_value = self.status_fields.get(field_name, '')
        parsed_values = [int(n) for n in raw_value.split()]
        if len(parsed_values) >= 4:
            return OwnerIDs(*parsed_values[:4])


class OwnerIDs(collections.namedtuple('OwnerIDs', 'real, effective, saved, fs')):
    pass