import re
import string
import operator

#------------------------------------------------------------------------------
# Text Tokens
#------------------------------------------------------------------------------

class TextToken(object):
    """
    A single tokenized text element.

    TextTokens are effectively wrappers for individual regex matches found
    when tokenizing a text string (eg, a composition string). They provide
    location and type information for the token as it exists in the string.

    Besides being used to normalize and guide the parsing of a given
    string, TextTokens can be used for things like syntax highlighting.
    """

    def __init__(self, match):
        self.type  = match.lastgroup
        self.value = (str(match.group())).upper()
        self.span  = match.span()

    @property
    def index(self):
        return self.span[0]

#
# COVERAGE_TOKEN:
#   'A' | 'B' | 'C' | ... | 'Z'
#

# NOTE: this is now dynamically computed in parse(...)
#COVERAGE_TOKEN = r'(?P<COVERAGE_TOKEN>[A-Za-z])'
AGGREGATE_TOKEN = '*'

#
# LOGIC_TOKEN:
#   '|' | '^' | '&' | '-'
#

OR      = r'(?P<OR>\|)'
XOR     = r'(?P<XOR>\^)'
AND     = r'(?P<AND>\&)'
MINUS   = r'(?P<MINUS>-)'

#
# Misc Tokens
#

LPAREN  = r'(?P<LPAREN>\()'
RPAREN  = r'(?P<RPAREN>\))'
COMMA   = r'(?P<COMMA>\,)'
WS      = r'(?P<WS>\s+)'
UNKNOWN = r'(?P<UNKNOWN>.)'

TOKEN_DEFINITIONS = [OR, XOR, AND, MINUS, LPAREN, RPAREN, COMMA, WS, UNKNOWN]

#------------------------------------------------------------------------------
# AST Tokens
#------------------------------------------------------------------------------

class AstToken(object):
    """
    Base class for Abstract Syntax Tree (AST) Tokens.

    The Tokens subclassed from AstToken are used to build an abstract
    syntax tree representing a composition equation.

    Once generated, an AST can be logically evaluated by Lighthouse's
    director to compose a new coverage set described by the tree.
    """

    def __init__(self):
        self.nodes = []
        self.text_tokens = []

class TokenNull(AstToken):
    """
    AST Token indicating a NULL / empty composition.
    """

    def __init__(self):
        super(TokenNull, self).__init__()

class TokenLogicOperator(AstToken):
    """
    AST Token for a logical operator.

    eg: '|'
    """

    def __init__(self, logic_op, op1, op2=None):
        super(TokenLogicOperator, self).__init__()
        self.text_tokens = [logic_op]

        # logic operator
        self.operator = self.str2op(logic_op.value)

        # referenced operands
        self.op1 = op1
        self.op2 = op2

        # save the operand expressions as children
        self.nodes = [op1, op2]

    @staticmethod
    def str2op(op_char):
        if op_char == '|':
            return operator.or_
        if op_char == '&':
            return operator.and_
        if op_char == '^':
            return operator.xor
        if op_char == '-':
            return operator.sub
        raise ValueError("Unknown Operator")

class TokenCoverageSingle(AstToken):
    """
    AST Token for a single coverage reference.

    eg: 'A'
    """

    def __init__(self, coverage_single):
        super(TokenCoverageSingle, self).__init__()
        self.text_tokens = [coverage_single]

        # referenced coverage set
        self.symbol = coverage_single.value

#------------------------------------------------------------------------------
# AST Operations
#------------------------------------------------------------------------------

def ast_equal(first, second):
    """
    A fail-safe equality of the structure and contents of two AST.

    This is not a true (logical) equality check. Two AST's may evaluate to
    the same logical result, but have a slightly different structure which
    will trigger this check to return False.

    This is primarily used to check if a user specified AST has changed, and
    if we should probably re-evaluate the tree (composition).
    """

    # both trees are 'NULL' / empty AST
    if isinstance(first, TokenNull) and isinstance(second, TokenNull):
        return True

    # recursively evaluate the AST's
    return _ast_equal_recursive(first, second)

def _ast_equal_recursive(first, second):
    """
    The internal (recursive) AST evaluation routine.
    """

    #
    # if the left and right types are not identical at every step, the tree
    # is obviously different somehow
    #

    if type(first) != type(second):
        return False

    #
    # if both tokens are terminating / None, they are a match
    #

    if first == second == None:
        return True

    #
    # if the current node is a logic operator, we need to evaluate the
    # expressions that make up its input.
    #

    elif isinstance(first, TokenLogicOperator):
        if not _ast_equal_recursive(first.op1, second.op1):
            return False
        if not _ast_equal_recursive(first.op2, second.op2):
            return False
        return first.operator == second.operator

    #
    # if the current node is a coverage token, we need simply need
    # to compare its symbol.
    #

    elif isinstance(first, TokenCoverageSingle):
        return first.symbol == second.symbol

    #
    # unknown token? (this should never happen)
    #

    raise ValueError("Unknown token types, cannot compare them...")

#------------------------------------------------------------------------------
# Parsing
#------------------------------------------------------------------------------

class ParseError(SyntaxError):
    """
    Exception raised when composition parsing fails.

    A ParseError will provide some contextual information to how and why
    the parser failed. Information gleaned through the exception can still
    be consumed for user hints, syntax highlighting, or other uses.
    """

    def __init__(self, message, expected, error_token, parsed_tokens):
        super(ParseError, self).__init__(message)
        self.expected = expected
        self.error_token = error_token
        self.parsed_tokens = parsed_tokens

        if error_token == self.parsed_tokens[-1]:
            fail = self.parsed_tokens.pop()
            self.error_index = fail.span[0]
        else:
            self.error_index = self.parsed_tokens[-1].span[1]

    def __str__(self):
        return "%s: at %s, %s" % (self.__class__.__name__, self.error_token.span, self.msg)

#------------------------------------------------------------------------------
# Composition Parser
#------------------------------------------------------------------------------

class CompositionParser(object):
    """
    A simple recursive descent parser for Compositions.

    Heavily modified from:
      https://rockie-yang.gitbooks.io/python-cookbook/content/ch2/simple_parser.html

    #----------------------------------------------------------------------

    Below is the spec for the LL(1) 'Composition Grammar' that was designed
    to generically parse coverage composition equations form raw text.

    #----------------------------------------------------------------------

    COMPOSITION:
        EXPRESSION COMPOSITION_TAIL

    COMPOSITION_TAIL:
        LOGIC_TOKEN COMPOSITION | None

    EXPRESSION:
        '(' EXPRESSION ')' COMPOSITION_TAIL | COVERAGE COMPOSITION_TAIL

    COVERAGE_TOKEN:
        'A' | 'B' | 'C' | ... | 'Z' | AGGREGATE_TOKEN

    LOGIC_TOKEN:
        '&' | '|' | '^' | '-' | None

    """

    def parse(self, text, coverage_tokens):
        """
        Parse a string using the Composition Grammar.

        Returns an Abstract Syntax Tree (AST) of the parsed input.

        Raises ParseError on parse failure.
        """

        # if the string is *only* whitespace, return an empty, but valid parse
        if not text.strip():
            return ([], TokenNull())

        #
        # we used to parse [A-Za-z] as the COVERAGE_TOKEN, but that means we
        # would technically tokenize and construct trees with COVERAGE_TOKEN's
        # that have no matching (eg invalid) loaded coverage data.
        #
        # now we construct the COVERAGE_TOKEN regex just before parsing.
        # this enables us to tokenize/parse only the shorthand names that
        # reflect the state of loaded coverage
        #

        COVERAGE_TOKEN = r'(?P<COVERAGE_TOKEN>[%s])' % ''.join(coverage_tokens + [AGGREGATE_TOKEN])

        #
        # if there were any coverage tokens defined, then we definitely need
        # the constructed COVERAGE_TOKEN regex in our grammar list.
        #

        if coverage_tokens:
            TOKEN_REGEXES = [COVERAGE_TOKEN] + TOKEN_DEFINITIONS
        else:
            TOKEN_REGEXES = TOKEN_DEFINITIONS

        # build our master tokenizer regex pattern to parse the text stream
        master_pattern = re.compile('|'.join(TOKEN_REGEXES))

        # reset the parser's runtime variables
        self._parsed_tokens = []
        self.current_token  = None
        self.next_token     = None

        # tokenize the raw text stream
        self.tokens = self._generate_tokens(master_pattern, text)

        # initialize the parser state by bumping the parser onto the first token
        self._advance()

        # parse the token stream using the grammar defined by this class
        ast = self._COMPOSITION()

        # if there are any tokens remaining in the stream, the text is invalid
        if self.next_token:
            self._parse_error("Expected $$", TokenNull)

        # return the parsed tokens and generated AST
        return (self._parsed_tokens, ast)

    #--------------------------------------------------------------------------
    # Token Stream Operations
    #--------------------------------------------------------------------------

    def _advance(self):
        """
        Advance one token in the token stream.
        """
        self.current_token, self.next_token = self.next_token, next(self.tokens, None)

    def _accept(self, token_type):
        """
        Match and accept the lookahead token.
        """
        if self.next_token and self.next_token.type == token_type:
            self._advance()
            return True
        else:
            return False

    def _generate_tokens(self, regex_pattern, text):
        """
        Generate a TextToken stream using a given regex token pattern and text.
        """
        scanner = regex_pattern.scanner(text)
        for m in iter(scanner.match, None):
            token = TextToken(m)
            self._parsed_tokens.append(token)
            if token.type != 'WS': # ignore whitespace tokens
                yield token

    def _parse_error(self, message, expected):
        """
        Raises a ParseError, capturing elements of the parser state.
        """
        raise ParseError(message, expected, self.next_token, self._parsed_tokens)

    #--------------------------------------------------------------------------
    # Grammar Rules
    #--------------------------------------------------------------------------

    def _COMPOSITION(self):
        """
        COMPOSITION:
            EXPRESSION COMPOSITION_TAIL
        """
        expression = self._EXPRESSION()
        return self._COMPOSITION_TAIL(expression)

    def _COMPOSITION_TAIL(self, head):
        """
        COMPOSITION_TAIL:
            LOGIC_TOKEN COMPOSITION | None
        """

        #
        # LOGIC_TOKEN COMPOSITION
        #

        logic_op = self._LOGIC_TOKEN()
        if logic_op:
            composition = self._COMPOSITION()
            return TokenLogicOperator(logic_op, head, composition)

        #
        # None
        #

        # no COMPOSITION_TAIL to parse, simply return the leading expression
        return head

    def _EXPRESSION(self):
        """
        EXPRESSION:
            '(' EXPRESSION ')' COMPOSITION_TAIL | COVERAGE_TOKEN COMPOSITION_TAIL
        """

        #
        # ['(' EXPRESSION ')'] COMPOSITION_TAIL
        #

        if self._accept('LPAREN'):

            # parse left paren
            left_paren = self.current_token

            # parse the expression
            expression = self._EXPRESSION() # THESE ARE CHILDREN

            # parse the right paren
            if not self._accept('RPAREN'):
                self._parse_error("Expected RPAREN", TextToken)
            right_paren = self.current_token

            # inject parenthesis TextTokens into the expression
            expression.text_tokens.append(left_paren)
            expression.text_tokens.append(right_paren)

        #
        # [COVERAGE] COMPOSITION_TAIL
        #

        else:
            expression = self._COVERAGE_TOKEN()

        # ... [COMPOSITION_TAIL]
        return self._COMPOSITION_TAIL(expression)

    def _COVERAGE_TOKEN(self):
        """
        COVERAGE_TOKEN:
            'A' | 'B' | 'C' | ... | 'Z' | AGGREGATE_TOKEN
        """
        if self._accept("COVERAGE_TOKEN"):
            return TokenCoverageSingle(self.current_token)
        self._parse_error("Expected COVERAGE_TOKEN", TokenCoverageSingle)

    def _LOGIC_TOKEN(self):
        """
        LOGIC_TOKEN:
            '&' | '|' | '^' | '-' | None
        """
        if self._accept("OR")  or \
           self._accept("XOR") or \
           self._accept("AND") or \
           self._accept("MINUS"):
            return self.current_token
        return None
