# Copyright 2014-2015, Tresys Technology, LLC
#
# This file is part of SETools.
#
# SETools is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 2.1 of
# the License, or (at your option) any later version.
#
# SETools is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with SETools.  If not, see
# <http://www.gnu.org/licenses/>.
#
from itertools import product
from collections import namedtuple

from . import exception
from . import qpol
from . import symbol

truth_table_row = namedtuple("truth_table_row", ["values", "result"])


def boolean_factory(policy, name):
    """Factory function for creating Boolean statement objects."""

    if isinstance(name, Boolean):
        assert name.policy == policy
        return name
    elif isinstance(name, qpol.qpol_bool_t):
        return Boolean(policy, name)

    try:
        return Boolean(policy, qpol.qpol_bool_t(policy, str(name)))
    except ValueError:
        raise exception.InvalidBoolean("{0} is not a valid Boolean".format(name))


def condexpr_factory(policy, name):
    """Factory function for creating conditional expression objects."""

    if not isinstance(name, qpol.qpol_cond_t):
        raise TypeError("Conditional expressions cannot be looked up.")

    return ConditionalExpr(policy, name)


class Boolean(symbol.PolicySymbol):

    """A Boolean."""

    @property
    def state(self):
        """The default state of the Boolean."""
        return bool(self.qpol_symbol.state(self.policy))

    def statement(self):
        """The policy statement."""
        return "bool {0} {1};".format(self, str(self.state).lower())


class ConditionalExpr(symbol.PolicySymbol):

    """A conditional policy expression."""

    _cond_expr_val_to_text = {
        qpol.QPOL_COND_EXPR_NOT: "!",
        qpol.QPOL_COND_EXPR_OR: "||",
        qpol.QPOL_COND_EXPR_AND: "&&",
        qpol.QPOL_COND_EXPR_XOR: "^",
        qpol.QPOL_COND_EXPR_EQ: "==",
        qpol.QPOL_COND_EXPR_NEQ: "!="}

    _cond_expr_val_to_precedence = {
        qpol.QPOL_COND_EXPR_NOT: 5,
        qpol.QPOL_COND_EXPR_OR: 1,
        qpol.QPOL_COND_EXPR_AND: 3,
        qpol.QPOL_COND_EXPR_XOR: 2,
        qpol.QPOL_COND_EXPR_EQ: 4,
        qpol.QPOL_COND_EXPR_NEQ: 4}

    def __contains__(self, other):
        for expr_node in self.qpol_symbol.expr_node_iter(self.policy):
            expr_node_type = expr_node.expr_type(self.policy)

            if expr_node_type == qpol.QPOL_COND_EXPR_BOOL and other == \
                    boolean_factory(self.policy, expr_node.get_boolean(self.policy)):
                return True

        return False

    def __str__(self):
        # qpol representation is in postfix notation.  This code
        # converts it to infix notation.  Parentheses are added
        # to ensure correct expressions, though they may end up
        # being overused.  Set previous operator at start to the
        # highest precedence (NOT) so if there is a single binary
        # operator, no parentheses are output
        stack = []
        prev_op_precedence = self._cond_expr_val_to_precedence[qpol.QPOL_COND_EXPR_NOT]
        for expr_node in self.qpol_symbol.expr_node_iter(self.policy):
            expr_node_type = expr_node.expr_type(self.policy)

            if expr_node_type == qpol.QPOL_COND_EXPR_BOOL:
                # append the boolean name
                nodebool = boolean_factory(
                    self.policy, expr_node.get_boolean(self.policy))
                stack.append(str(nodebool))
            elif expr_node_type == qpol.QPOL_COND_EXPR_NOT:  # unary operator
                operand = stack.pop()
                operator = self._cond_expr_val_to_text[expr_node_type]
                op_precedence = self._cond_expr_val_to_precedence[expr_node_type]

                # NOT is the highest precedence, so only need
                # parentheses if the operand is a subexpression
                if isinstance(operand, list):
                    subexpr = [operator, "(", operand, ")"]
                else:
                    subexpr = [operator, operand]

                stack.append(subexpr)
                prev_op_precedence = op_precedence
            else:
                operand1 = stack.pop()
                operand2 = stack.pop()
                operator = self._cond_expr_val_to_text[expr_node_type]
                op_precedence = self._cond_expr_val_to_precedence[expr_node_type]

                if prev_op_precedence > op_precedence:
                    # if previous operator is of higher precedence
                    # no parentheses are needed.
                    subexpr = [operand1, operator, operand2]
                else:
                    subexpr = ["(", operand1, operator, operand2, ")"]

                stack.append(subexpr)
                prev_op_precedence = op_precedence

        return self.__unwind_subexpression(stack)

    def __unwind_subexpression(self, expr):
        ret = []

        # do a string.join on sublists (subexpressions)
        for i in expr:
            if isinstance(i, list):
                ret.append(self.__unwind_subexpression(i))
            else:
                ret.append(i)

        return ' '.join(ret)

    @property
    def booleans(self):
        """The set of Booleans in the expression."""
        bools = set()

        for expr_node in self.qpol_symbol.expr_node_iter(self.policy):
            expr_node_type = expr_node.expr_type(self.policy)

            if expr_node_type == qpol.QPOL_COND_EXPR_BOOL:
                bools.add(boolean_factory(self.policy, expr_node.get_boolean(self.policy)))

        return bools

    def evaluate(self, **kwargs):
        """
        Evaluate the expression with the stated boolean values.

        Keyword Parameters:
        Each keyword parameter name corresponds to a boolean name
        in the expression

        Return:     bool
        """
        bools = sorted(self.booleans)

        if sorted(kwargs.keys()) != bools:
            raise ValueError("Boolean values not set correctly.")

        stack = []
        for expr_node in self.qpol_symbol.expr_node_iter(self.policy):
            expr_node_type = expr_node.expr_type(self.policy)

            if expr_node_type == qpol.QPOL_COND_EXPR_BOOL:
                nodebool = boolean_factory(self.policy, expr_node.get_boolean(self.policy))
                stack.append(kwargs[nodebool])
            elif expr_node_type == qpol.QPOL_COND_EXPR_NOT:
                operand = stack.pop()
                operator = self._cond_expr_val_to_text[expr_node_type]
                stack.append(not operand)
            else:
                operand1 = stack.pop()
                operand2 = stack.pop()
                operator = self._cond_expr_val_to_text[expr_node_type]
                if operator == "||":
                    stack.append(operand1 or operand2)
                elif operator == "&&":
                    stack.append(operand1 and operand2)
                elif operator == "^":
                    stack.append(operand1 ^ operand2)
                elif operator == "==":
                    stack.append(operand1 == operand2)
                else:  # not equal
                    stack.append(operand1 != operand2)

        return stack[0]

    def truth_table(self):
        """
        Generate a truth table for this expression.

        Return:     list

        List item:
        tuple:      values, result

        Tuple item:
        values:     Dictionary keyed on Boolean names
                    with each value being T/F.
        result:     Evaluation result for the expression
                    given the values.
        """
        bools = sorted(str(b) for b in self.booleans)

        truth_table = []

        # create a list of all combinations of T/F for each Boolean
        truth_list = list(product([True, False], repeat=len(bools)))

        for row in truth_list:
            values = {bools[i]: row[i] for i in range(len(bools))}
            truth_table.append(truth_table_row(values, self.evaluate(**values)))

        return truth_table

    def statement(self):
        raise exception.NoStatement
