"""
file:   mlspolicy.py
author: tllake
email:  thomas.l.lake@wmich.edu
description:
        Functions for converting a mls policy into a 
        set of datastructures representing the policy.
"""
from collections import namedtuple
from mlsparse import mlsparse, ParseException

ClearanceNode = namedtuple('ClearanceNode', ('parents', 'children'))
ResourceEntry = namedtuple('ResourceEntry', ('clearance', 'category', 'recursive'))
UserEntry = namedtuple('UserEntry', ('clearance', 'category'))

def prevcurrnext(l):
    """Given a list [x[1],x[2],...,x[n]] return 
    [(None,x[1],x[2]),(x[1],x[2],x[3]),...,(x[n-1],x[n],None)]
    """
    nl = [None]
    nl.extend(l)
    nl.append(None)
    return zip(*[nl[i:] for i in range(3)])

def singleton(thing):
    return set() if thing is None else {thing}

def makeclearancenode(parent=None, child=None):
    return ClearanceNode(singleton(parent), singleton(child))

def domrange(top, bottom, lattice):
    """Return all the elements of the lattice such that
    bottom <= element <= top
    """
    # base case, at top
    if bottom == top:
        return {top}
    members = set()
    for parent in lattice[bottom].parents:
        # between is the set of elements in lattice such that 
        # parent <= element <= top
        between = domrange(top, parent, lattice)
        if len(between) > 0:
            members.update(between)
    # members is non empty iff there is a path to top from bottom
    if len(members):
        members.add(bottom)
    return members

def mlspolicy(source, verbose=False):
    """Initialize a MLS policy representation.
    Args:
        source (file or filename): The MLS policy file
        verbose (bool): be verbose.

    Returns:
        clearance_lattice (dict): Dominance lattice.
            clearance_lattice['clearance'] gives a namedtuple with 
            fields parents (set) and children (set). 
            parents is the set of clearances which directly dominate 'clearance'.
            children the set of clearance which are directly dominated by 'clearance'.
        
        categories (set): The set of categories.
        
        resource_dict (dict): Dictionary containing assignments of clearances and 
            categories to resources. resource_dict['resource'] contains a set of 
            namedtuples with fields clearance, category, and recursive. 
            clearance and category are strings and recursive is a flag specifying
            if the clearances and categories should be applied recursively.

        user_dict (dict): Dictionary containing assignments of clearances and categories
            to users. user_dict['user'] contains a set of namedtuples
            with fields clearance and category.
    """
    #return clearance_lattice, category_set, resource_dict, user_dict 

    try:
        text = source.read()
    except AttributeError:
        with open(source, 'r') as f:
            text = f.read()
    stmts = mlsparse(text)

    clearance_stmts = [stmt for stmt in stmts if stmt[0] == 'clearances:']
    category_stmts = [stmt for stmt in stmts if stmt[0] == 'categories:']
    assign_stmts = [stmt for stmt in stmts if stmt[0] == 'assign:']
    user_stmts = [stmt for stmt in stmts if stmt[0] == 'users:']

    clearance_lattice = {}
    category_set = set()
    resource_dict = {}
    user_dict = {}
    
    # build the clearance lattice
    for stmt in clearance_stmts:
        domlist = stmt[1]
        for p, c, n in prevcurrnext(domlist):
            if verbose:
                if p is not None:
                    print 'adding {0} as parent of {1}'.format(p, c)
                if n is not None:
                    print 'adding {0} as child of {1}'.format(n, c)
            try:
                clearance_lattice[c].parents.update(singleton(p))
                clearance_lattice[c].children.update(singleton(n))
            except KeyError:
                clearance_lattice[c] = makeclearancenode(p, n)

    # add all the categories to category_set
    for stmt in category_stmts:
        categories = stmt[1]
        if verbose:
            print 'adding {{{0}}} to categories'.format(', '.join(categories))
        category_set.update(categories)
    
    # store the clearances and categories for each resource
    for stmt in assign_stmts:
        #print stmt
        if len(stmt) == 5:
            recursive = False
        elif len(stmt) == 6:
            recursive = True
        else:
            raise ParseException('Malformed assign statemet, {0}'.format(stmt))
        clearances = stmt[1]
        categories = stmt[3]
        resource = stmt[-1]
        
        # a clearence that looks like ['c1', '.', 'c2'] is a 
        # clearance range. Get all clearances 'c', such that 
        # 'c1' <= 'c' <= 'c2'
        if len(clearances) == 3 and clearances[1] == '.':
            bottom, _, top = clearances
            clearances = domrange(top, bottom, clearance_lattice)

        for clearance in clearances:
            for category in categories:
                if verbose:
                    print 'adding {0} to ({1}, {2}), recursive: {3}'.format(resource, clearance, category, recursive)
                resentry = ResourceEntry(clearance, category, recursive)
                try:
                    resource_dict[resource].add(resentry)
                except KeyError:
                    resource_dict[resource] = {resentry}
    
    # store the clearances and categories for each user
    for stmt in user_stmts:
        clearances = stmt[1]
        categories = stmt[3]
        users = stmt[4]
        
        # a clearence that looks like ['c1', '.', 'c2'] is a 
        # clearance range. Get all clearances 'c', such that 
        # 'c1' <= 'c' <= 'c2'
        if len(clearances) == 3 and clearances[1] == '.':
            bottom, _, top = clearances
            clearances = domrange(top, bottom, clearance_lattice)
        
        # store each (clearance, category) tuple for each user
        for user in users:
            for clearance in clearances:
                for category in categories:
                    if verbose:
                        print 'adding {0} to ({1}, {2})'.format(user, category, clearance)
                    userentry = UserEntry(clearance, category)
                    try:
                        user_dict[user].add(userentry)
                    except KeyError:
                        user_dict[user] = {userentry}     

    return clearance_lattice, category_set, resource_dict, user_dict 

if __name__ == '__main__':
    import sys
    if len(sys.argv) < 2:
        source = sys.stdin
    else:
        source = sys.argv[1]
    lattice, cats, res, user = mlspolicy(source, verbose=True)
    print 'CATS:', cats
    from mlslevels import showlevels
    showlevels(lattice, cats)

