"""
Lsof - command ``/usr/sbin/lsof``
=================================

This parser reads the output of the ``/usr/sbin/lsof`` command and makes each
line available as a dictionary keyed on the fields in the lsof output (with
names in upper case).

Because of the large quantity of output from this command, this class is based
on the ``Scannable`` parser class.  There are several ways to use this:

* If you simply want to know whether a search matched, use the ``any`` method.
* If you want all lines that match, use the ``collect`` method.

The way these scanner functions work is:

1. You provide a function, which returns True if a match is found, and an
   attribute name.
2. The parser runs every scanner function across every line of data read and
   successfully parsed.
3. The attribute then contains the result of the match (True or False in the
   case of the ``any`` method, or the list of matching rows in the case of
   the ``collect`` method.

As an easier way of finding all the lines that match by key=value pairs, use
the ``collect_keys`` method, giving the name of the scanner attribute to set
and one or more 'key=value' pairs in the method call.  This then returns the
list of rows for which the data in all those keywords matched the respective
given values.  (**Note**: the ``SIZE/OFF`` column is searched for using the
key ``SIZE_OFF`` - see example below)

Sample output::

    COMMAND     PID  TID           USER   FD      TYPE             DEVICE  SIZE/OFF       NODE NAME
    systemd       1                root  cwd       DIR              253,1      4096        128 /
    systemd       1                root  rtd       DIR              253,1      4096        128 /
    systemd       1                root  txt       REG              253,1   1230920    1440410 /usr/lib/systemd/systemd
    systemd       1                root  mem       REG              253,1     37152  135529970 /usr/lib64/libnss_sss.so.2
    abrt-watc  8619                root    0r      CHR                1,3       0t0       4674 /dev/null
    wpa_suppl   641                root    0r      CHR                1,3       0t0       4674 /dev/null
    polkitd     642             polkitd    0u      CHR                1,3       0t0       4674 /dev/null
    polkitd     642             polkitd    1u      CHR                1,3       0t0       4674 /dev/null

Examples:

    >>> Lsof.any('systemd_commands', lambda x: 'systemd' in x['COMMAND'])
    >>> Lsof.collect('polkitd_user', lambda x: x['USER'] == 'polkitd')
    >>> Lsof.collect_keys('root_stdin', USER='root', FD='0r', SIZE_OFF='0t0')
    >>> l = shared[Lsof]
    >>> l.systemd_commands
    True
    >>> len(l.polkitd_user)
    2
    >>> l.polkitd_user[0]['DEVICE']
    '1,3'
    >>> len(l.root_stdin)
    2
    >>> l.root_stdin[0]['COMMAND']
    'abrt-watc'

"""
from insights.core import CommandParser, Scannable
from insights.core.exceptions import SkipComponent
from insights.core.filters import add_filter
from insights.core.plugins import parser
from insights.specs import Specs

add_filter(Specs.lsof, ['COMMAND'])


@parser(Specs.lsof)
class Lsof(CommandParser, Scannable):
    """
    A parser for the output of ``/usr/sbin/lsof`` - determines the column
    widths from the first row and then puts the data in each row into a
    dictionary keyed on the column name and found by the locations of each
    column.  Leading and trailing spaces are stripped from data.
    """

    def _calc_indexes(self, line):
        self.headings = [c.strip() for c in line.split(None)]
        self.indexes = {}
        for heading in self.headings:
            index = line.index(heading)
            # (start, end)
            self.indexes[heading] = (index, index + len(heading))

    def _start(self, content):
        """
        Consumes lines from content until the HEADER is found and processed.
        Returns an iterator over the remaining lines.
        """
        start = len(content)
        for n, line in enumerate(content):
            if 'COMMAND ' in line:
                start = n + 1
                break

        if start >= len(content):
            raise SkipComponent

        content = iter(content[start:])

        self._calc_indexes(line)
        return content

    def _parse_line(self, line):
        """
        Given a line, returns a dictionary for that line. Requires _start to be
        called first.
        """
        # Split NAME seperately as it can have extra whitespaces
        command = line[self.indexes['NAME'][0]:].strip()
        rest = line[:self.indexes['NAME'][0]]
        rdict = dict.fromkeys(self.headings, '')
        rowsplit = [i.strip() for i in rest.split(None, len(self.headings) - 2)]
        if len(rowsplit) < len(self.headings) - 1:
            rowsplit = iter(rowsplit)
            for heading in self.headings[:-1]:
                # Use value if (start, end) index of heading is not empty
                if line[slice(*self.indexes[heading])].strip():
                    rdict[heading] = next(rowsplit, '')
        else:
            rdict = dict(zip(self.headings, rowsplit))
        rdict['NAME'] = command
        return rdict

    def parse(self, content):
        """
        Parse the content for the entire input file.
        """
        for line in self._start(content):
            yield self._parse_line(line)

    @classmethod
    def collect_keys(cls, result_key, **kwargs):
        """
        Store a list of lines having keyword=value matches in the given
        attribute name.

        Keyword argument names that exist as column names in the data are
        searched for in the lines of the file and stored in the ``result_key``
        attribute.  All columns must match (i.e. this is a bolean AND search).

        Call this class method before using the class data.

        Examples:
            collect_keys('root_block_devs', USER='root', TYPE='BLK')
        """
        def scanner(self, obj):
            # Have to set the attribute so it exists, even if it has no rows
            if not hasattr(self, result_key):
                setattr(self, result_key, [])
            # Minor hack - search for 'SIZE/OFF' as 'SIZE_OFF'.
            if 'SIZE_OFF' in kwargs:
                kwargs['SIZE/OFF'] = kwargs['SIZE_OFF']
            # Filter the keywords for the list of column names requested.
            for key in [k for k in kwargs if k in obj]:
                # If we have a keyword whose value doesn't match the given
                # item, then skip out now.
                if obj[key] != kwargs[key]:
                    return
            # OK, save the item now.
            getattr(self, result_key).append(obj)

        cls._scan(result_key, scanner)
