"""
Provides a `tokenize(code)` function which returns a collection of tokens.

It should follow this truth table, where "ws" means "whitespace" and an
empty CHAR column means "everything not handled elsewhere."

CHAR || INVERT | IDENTIFIER | OPERATOR | AND | OR | OPEN_PAREN | CLOSE_PAREN
============================================================================
ws   || 0      | 0          | 0        | 0   | 0  | 0          | 0
 &   || 0      | 0          | 1        | 1   | 0  | 0          | 0
 |   || 0      | 0          | 1        | 0   | 1  | 0          | 0
 (   || 0      | 0          | 1        | 0   | 0  | 1          | 0
 )   || 0      | 0          | 1        | 0   | 0  | 0          | 1
 !   || 1      | 1          | 1        | 0   | 0  | 0          | 1
     || 0      | 1          | 0        | 0   | 0  | 0          | 0
"""

from enum import Enum
import string


class TokenType(Enum):
    """The possible kinds of Tokens."""
    UNINITIALIZED = 0
    STATEMENT = 1
    IDENTIFIER = 2
    OPERATOR = 3


class OperatorType(Enum):
    """The possible kinds of Operator."""
    NONE = 0
    AND = 1
    OR = 2
    OPEN_PAREN = 3
    CLOSE_PAREN = 4


class Token:
    """
    The class representing a Token generated by the parser.
    - negated is only valid if type is IDENTIFIER.
    - operator_type is only valid if type is OPERATOR.
    """
    def __init__(self, type=None, value=None, negated=False, operator_type=None): # pylint: disable=redefined-builtin
        if type is None:
            self.type = TokenType.UNINITIALIZED
        else:
            self.type = type

        if value is None:
            self.value = ''
        else:
            self.value = value

        self.negated = negated

        if operator_type is None:
            self.operator_type = OperatorType.NONE
        else:
            self.operator_type = operator_type

    def __str__(self):
        if self.negated:
            return f'!{self.value}'
        else:
            return self.value

    def __repr__(self):
        if self.type == TokenType.OPERATOR:
            return f'<Token OPERATOR {self.operator_type.name}>'
        elif self.type == TokenType.IDENTIFIER:
            if self.negated:
                negated_symbol = '!'
            else:
                negated_symbol = ''
            return f'<Token IDENTIFIER {(negated_symbol + self.value)!r}>'
        else:
            return f'<Token type={self.type.name} value={self.value!r} ' + \
                   f'negated={self.negated} operator_type={self.operator_type.name}>'

def tokenize(code):  # pylint: disable=too-many-branches
    """Given code, returns a sequence of Tokens."""
    tokens = []
    token = Token()
    for c in code:  # pylint: disable=invalid-name
        is_whitespace = c in string.whitespace
        is_and    = (c == '&')
        is_or     = (c == '|')
        is_oparen = (c == '(')
        is_cparen = (c == ')')
        is_invert = (c == '!')
        is_ident  = not (is_whitespace or is_and or is_or or is_oparen or is_cparen)
        is_operator = is_and or is_or or is_oparen or is_cparen

        end_token_before = is_cparen or is_whitespace
        end_token_after = is_operator

        if end_token_before:
            if token.type != TokenType.UNINITIALIZED:
                tokens.append(token)
                token = Token()

        if is_whitespace:
            continue

        if is_ident or is_invert:
            token.type = TokenType.IDENTIFIER

        if is_invert:
            token.negated = is_invert

        # pylint: disable=redefined-variable-type
        if is_operator:
            token.type = TokenType.OPERATOR

        if is_and:
            token.operator_type = OperatorType.AND
        if is_or:
            token.operator_type = OperatorType.OR
        if is_oparen:
            token.operator_type = OperatorType.OPEN_PAREN
        if is_cparen:
            token.operator_type = OperatorType.CLOSE_PAREN
        # pylint: enable=redefined-variable-type

        if not is_invert:
            token.value += c

        if end_token_after:
            tokens.append(token)
            token = Token()

    if token.type != TokenType.UNINITIALIZED:
        tokens.append(token)

    return tokens
