"""
This module contains logic for parsing ls output. It attempts to handle
output when selinux is enabled or disabled and also skip "bad" lines.
"""
import logging

log = logging.getLogger(__name__)
PERMBITS = set("-+.dlbcpsrwxtT")


def parse_path(path):
    """
    Convert possible symbolic link into a source -> target pair.

    Args:
        path (str): The path portion of an ls output line.

    Returns:
        A (path, link) tuple where path is always populated and link is a non
        empty string if the original path is a symoblic link.
    """
    path, _, link = path.partition(" -> ")
    return path, link


def parse_non_selinux(parts):
    """
    Parse part of an ls output line that isn't selinux.

    Args:
        parts (list): A four element list of strings representing the initial
            parts of an ls line after the permission bits. The parts are link
            count, owner, group, and everything else.

    Returns:
        A dict containing links, owner, group, date, and name. If the line
        represented a device, major and minor numbers are included.  Otherwise,
        size is included. If the raw name was a symbolic link, link is
        included.
    """
    links, owner, group, last = parts
    result = {
        "links": int(links),
        "owner": owner,
        "group": group,
    }

    # device numbers only go to 256.
    # If a comma is in the first four characters, the next two elements are
    # major and minor device numbers. Otherwise, the next element is the size.
    if "," in last[:4]:
        major, minor, rest = last.split(None, 2)
        result["major"] = int(major.rstrip(","))
        result["minor"] = int(minor)
    else:
        size, rest = last.split(None, 1)
        result["size"] = int(size)

    # The date part is always 12 characters regardless of content.
    result["date"] = rest[:12]

    # Jump over the date and the following space to get the path part.
    path, link = parse_path(rest[13:])
    result["name"] = path
    if link:
        result["link"] = link

    return result


def parse_selinux(parts):
    """
    Parse part of an ls output line that is selinux.

    Args:
        parts (list): A four element list of strings representing the initial
            parts of an ls line after the permission bits. The parts are owner
            group, selinux info, and the path.

    Returns:
        A dict containing owner, group, se_user, se_role, se_type, se_mls, and
        name. If the raw name was a symbolic link, link is always included.

    """

    owner, group = parts[:2]
    selinux = parts[2].split(":")
    lsel = len(selinux)
    path, link = parse_path(parts[-1])
    result = {
        "owner": owner,
        "group": group,
        "se_user": selinux[0],
        "se_role": selinux[1] if lsel > 1 else None,
        "se_type": selinux[2] if lsel > 2 else None,
        "se_mls": selinux[3] if lsel > 3 else None,
        "name": path
    }
    if link:
        result["link"] = link
    return result


def parse(lines, root):
    """
    Parses a list of lines from ls into dictionaries representing their
    components.

    Args:
        lines (list): A list of lines generated by ls.
        root (str): The directory name to be used for ls output stanzas that
            don't have a name.

    Returns:
        A dictionary representing the ls output. It's keyed by the path
        containing each ls stanza.
    """
    lines = iter(lines)
    results = {}
    # we process until StopIteration exception is thrown by the line iterator.
    while True:
        try:
            # get a line and skip blanks
            line = next(lines)
            while not(line):
                line = next(lines)
            try:
                # an ls stanza begins with an optional directory name followed
                # by a total number of entries in the directory and then the
                # list of entries. Stanzas are separated by blank lines.
                name = None
                total = None
                if line.endswith(":"):
                    name = line[:-1]
                    line = next(lines).strip()
                if line.startswith("total"):
                    total = int(line.split()[1])
                    line = next(lines).strip()

                # If we didn't get a name or a total, it's not a valid stanza
                # start, so just start over.
                if not name and not total:
                    continue

                # handle unnamed ls stanzas
                name = name or root

                # keep track of directories, files, special entries like block
                # and character devices, and a master list of all entries
                # regardless of kind.
                dirs = []
                ents = {}
                files = []
                specials = []

                # make sure we have a good line and its first character is one
                # of the valid permission bits.
                while line and line[0] in PERMBITS:
                    # break the line into initial parts
                    parts = line.split(None, 4)

                    # Get the permissions and validate they're all valid bits.
                    # Otherwise, our single character check in the loop was
                    # fooled, and we should skip the line as invalid.
                    perms = parts[0]
                    if len(perms) < 10:
                        line = next(lines).strip()
                        continue

                    # First character of the perms is the type of entry:
                    # directory, regular file, etc. The rest are the actual
                    # permission bits.
                    typ = perms[0]
                    entry = {
                        "type": typ,
                        "perms": perms[1:]
                    }

                    # If the first character of the second part of the line is
                    # a digit, then it's a link count and indicates we're not
                    # dealing with selinux. Otherwise, it's an owner, and we
                    # are dealing with selinux.
                    if parts[1][0].isdigit():
                        rest = parse_non_selinux(parts[1:])
                    else:
                        rest = parse_selinux(parts[1:])

                    # Update our entry and put it into the correct buckets
                    # based on its type.
                    entry.update(rest)
                    entry["raw_entry"] = line
                    entry["dir"] = name
                    nm = entry["name"]
                    ents[nm] = entry
                    if typ not in "bcd":
                        files.append(nm)
                    elif typ == "d":
                        dirs.append(nm)
                    elif typ in "bc":
                        specials.append(nm)
                    try:
                        line = next(lines).strip()
                    except StopIteration:
                        break

                # We're either at the end of input or a blank line, so we wrap
                # up this stanza and save it.
                if total is None:
                    total = len(ents)

                result = {
                    "name": name,
                    "total": total,
                    "entries": ents,
                    "files": files,
                    "dirs": dirs,
                    "specials": specials
                }
                results[result["name"]] = result

            # If we're at the end of input, blow all the way out.
            except StopIteration:
                raise

            # Something exploded. Just log it and try to move on.
            except:
                line = next(lines)
                log.info("Failed to parse: %s" % line)
        except StopIteration:
            break
    return results
