from graphviz import Digraph
import ply.lex as lex
import ply.yacc as yacc
import copy
import tempfile
import string

class Transition:

    def __init__(self, epsilon=False, dot=False, character_class=None,
                 negation_character_class=None):
        if not epsilon and not dot and character_class is None and negation_character_class is None:
            raise ValueError('Must set one of the Transition constructor arguments')
        self.epsilon = epsilon
        self.dot = dot
        self.character_class = frozenset(character_class) if character_class is not None else None
        self.negation_character_class = frozenset(negation_character_class) if negation_character_class is not None else None

    def __repr__(self):
        return 'Transition(epsilon={}, dot={}, character_class={}, negation_character_class={})'.format(
            self.epsilon, self.dot,
            None if self.character_class is None else ('[' + ''.join(self.character_class) + ']'),
            None if self.negation_character_class is None else ('[' + ''.join(self.negation_character_class) + ']'))

    def is_epsilon(self):
        return self.epsilon

    def is_dot(self):
        return self.dot

    def has_character_set(self):
        return self.character_class is not None

    def add_character(self, c):
        if self.is_epsilon():
            raise RuntimeError('Cannot call add_character on epsilon transition')
        if self.is_dot():
            return DOT_TRANSITION

        new_transition = copy.deepcopy(self)

        if new_transition.character_class is None:
            if c in new_transition.negation_character_class:
                new_transition.negation_character_class = new_transition.negation_character_class.difference([c])
        else:
            new_transition.character_class = new_transition.character_class.union([c])

        return new_transition

    def get_characters(self):
        if not self.has_character_set():
            raise RuntimeError('Cannot call get_characters on transition that does not have character set')
        return self.character_class

    def has_negation_character_set(self):
        return self.negation_character_class is not None

    def get_negation_characters(self):
        if not self.has_negation_character_set():
            raise RuntimeError('Cannot call get_negation_characters on transition that does not have negation character set')

        return self.negation_character_class

    def accepts_character(self, char):
        if self.epsilon:
            raise RuntimeError('Should not call acceps_character on epsilon transition')
        if self.dot:
            return True
        if self.character_class is not None:
            return char in self.character_class
        return char not in self.negation_character_class

    def get_char(self):
        if self.epsilon:
            return ''
        if self.dot:
            return 'a'
        if self.character_class is not None:
            return next(iter(self.character_class))
        # For negation first try all ASCII printable, then move to unicode
        for i in string.ascii_letters:
            if i not in self.negation_character_class:
                return i
        i = 0
        # TODO Need to output unicode properly
        while True:
            if chr(i) not in self.negation_character_class:
                return chr(i)

    def __eq__(self, other):
        if other is None:
            return False
        if (self.epsilon and other.epsilon) or (self.dot and other.dot):
            return True
        if self.epsilon or other.epsilon or self.dot or other.dot:
            return False

        if self.character_class is None:
            if other.character_class is None:
                return self.negation_character_class == other.negation_character_class
            else:
                return False
        else:
            if other.character_class is None:
                return False
            else:
                return self.character_class == other.character_class

    def __ne__(self, other):
        return not self.__eq__(other)

    def get_accepting_char(self):
        if self.epsilon:
            raise RuntimeError('Should not call get_nonaccepting_char on epsilon transition')

        if self.dot:
            return 'a'

        if self.character_class:
            return next(iter(self.character_class))
        # For non-accepting character class first try all ASCII printable, then move to unicode
        for i in string.printable:
            if i not in self.negation_character_class:
                return i
        i = 0
        while True:
            # TODO Need to output unicode properly
            if chr(i) not in self.negation_character_class:
                return chr(i)

    def get_nonaccepting_char(self):
        if self.epsilon:
            raise RuntimeError('Should not call get_nonaccepting_char on epsilon transition')

        if self.dot:
            return None

        if self.negation_character_class:
            return next(iter(self.negation_character_class))
        # For accepting character class first try all ASCII printable, then move to unicode
        for i in string.printable:
            if i not in self.character_class:
                return i
        i = 0
        while True:
            # TODO Need to output unicode properly
            if chr(i) not in self.character_class:
                return chr(i)

    def union(self, other):
        if self.epsilon:
            return other
        if other.epsilon:
            return self

        # No longer need to worry about epsilon

        if self.dot or other.dot:
            return DOT_TRANSITION

        # No longer need to worry about dot

        if self.character_class is None:
            if other.character_class is None:
                union_characters = self.negation_character_class & other.negation_character_class
                # fallthrough
            else:
                union_characters = self.negation_character_class - other.character_class
                # fallthrough
        else:
            if other.character_class is None:
                union_characters = other.negation_character_class - self.character_class
                # fallthrough
            else:
                return Transition(character_class=self.character_class | other.character_class)

        if len(union_characters) == 0:
            return DOT_TRANSITION

        return Transition(negation_character_class=union_characters)


    def intersection(self, other):
        if self.epsilon and other.epsilon:
            return EPSILON_TRANSITION
        if self.epsilon or other.epsilon:
            return None

        # No longer need to worry about epsilon

        if self.dot and other.dot:
            return DOT_TRANSITION

        if self.dot:
            return other
        if other.dot:
            return self

        # No longer need to worry about dot

        if self.character_class is None:
            if other.character_class is None:
                # bother negations are set
                return Transition(
                    negation_character_class=
                    self.negation_character_class | other.negation_character_class)
            else:
                intersecting_characters = other.character_class - self.negation_character_class
                # fallthrough
        else:
            if other.character_class is None:
                intersecting_characters = self.character_class - other.negation_character_class
                # fallthrough
            else:
                intersecting_characters = self.character_class & other.character_class
                # fallthrough

        if len(intersecting_characters) == 0:
            return None
        return Transition(character_class=intersecting_characters)

    @staticmethod
    def convert_set_to_character_class_string(character_class):
        string = ''
        sorted_list = sorted(character_class)

        i = 0
        range_start = None
        for i in range(len(sorted_list)):
            if range_start is None:
                if i + 2 < len(sorted_list):
                    if ord(sorted_list[i]) + 2 == ord(sorted_list[i+1]) + 1 == ord(sorted_list[i+2]):
                        range_start = sorted_list[i]
                    else:
                        string += sorted_list[i]
                else:
                    string += sorted_list[i]
            else:
                if i == len(sorted_list) - 1:
                    string += range_start + '-' + sorted_list[i]
                else:
                    if ord(sorted_list[i]) + 1 == ord(sorted_list[i+1]):
                        pass
                    else:
                        string += range_start + '-' + sorted_list[i]
                        range_start = None

        string = string.replace('\t', '\\t')
        string = string.replace('\n', '\\n')
        string = string.replace('\r', '\\r')
        return string


    def __str__(self):
        if self.epsilon:
            return '\u03B5'
        if self.dot:
            return '.'
        elif self.character_class is not None:
            character_range = Transition.convert_set_to_character_class_string(self.character_class)
            if len(character_range) == 1:
                return character_range
            else:
                return '[' + character_range + ']'
        else:
            return '[^' + Transition.convert_set_to_character_class_string(self.negation_character_class) + ']'


EPSILON_TRANSITION = Transition(epsilon=True)
DOT_TRANSITION = Transition(dot=True)

class Automata:
    def __init__(self, states, initial_state, transitions, accepting_states,
                 start_anchor, end_anchor, deterministic):
        self.states = states
        self.initial_state = initial_state
        self.transitions = transitions
        self.accepting_states = accepting_states
        self.start_anchor = start_anchor
        self.end_anchor = end_anchor
        self.deterministic = deterministic

    def __copy__(self):
        cls = self.__class__
        result = cls.__new__(cls)
        result.states = copy.copy(self.states)
        result.initial_state = self.initial_state
        result.accepting_states = copy.copy(self.accepting_states)
        result.start_anchor = self.start_anchor
        result.end_anchor = self.end_anchor
        result.deterministic = self.deterministic
        result.transitions = {}

        for s, value in self.transitions.items():
            new_state_transitions = {}
            result.transitions[s] = new_state_transitions
            for d, t in value.items():
                new_state_transitions[d] = t

        return result


    @staticmethod
    def stringify_node(n):
        if isinstance(n, frozenset):
            string = '{'
            for s in sorted(n):
                string += str(s) + ', '
            return string[:-2] + '}'
        else:
            return str(n)

    def show_graph(self):
        g = Digraph("Automata Graph", filename=tempfile.mktemp(), format='png')
        g.attr('node', label="")

        for n in self.states:
            string = Automata.stringify_node(n)
            if n in self.accepting_states and n != self.initial_state:
                g.node(string, string, shape='doublecircle')
            elif n not in self.accepting_states and n == self.initial_state:
                g.node(string, string, shape='octagon')
            elif n in self.accepting_states and n == self.initial_state:
                g.node(string, string, shape='doubleoctagon')
            else:
                g.node(string, string, shape='circle')

        for s, transitions in self.transitions.items():
            for d, t in transitions.items():
                g.edge(Automata.stringify_node(s),
                       Automata.stringify_node(d),
                       str(t).replace('\\', '\\\\'))
        g.view(cleanup=True)

    def prune(self):
        self.remove_redundant_states()
        self.remove_unreachable_states()
        self.remove_dead_states()


    def reachable_states(self, i):
        reachable = set([i])
        boundary = set([i])

        while len(boundary) > 0:
            s = boundary.pop()
            for d in self.transitions.get(s, {}).keys():
                if d not in reachable:
                    reachable.add(d)
                    boundary.add(d)
        return reachable

    def remove_state(self, state):
        if state in self.accepting_states:
            self.accepting_states.remove(state)
        if state in self.transitions:
            del self.transitions[state]
        self.states.remove(state)
        for s in self.states:
            if state in self.transitions.get(s, {}):
                del self.transitions[s][state]

    def remove_unreachable_states(self):
        reachable_states = self.reachable_states(self.initial_state)

        unreachable_states = self.states - reachable_states
        for u in unreachable_states:
            self.remove_state(u)

    def dead_state(self, state):
        visited = set()
        boundary = set()

        boundary.add(state)
        visited.add(state)
        while len(boundary) > 0:
            state = boundary.pop()
            if state in self.accepting_states:
                return False
            for d in self.transitions.get(state, {}):
                if d not in visited:
                    visited.add(d)
                    boundary.add(d)
        return True

    def remove_dead_states(self):
        for s in copy.deepcopy(self.states):
            if s != self.initial_state and self.dead_state(s):
                self.remove_state(s)

    def redundant_states(self, s1, s2):
        if len(self.transitions.get(s1, {})) != len(self.transitions.get(s2, {})):
            return None
        for d1, t1 in self.transitions.get(s1, {}).items():
            if d1 not in self.transitions[s2]:
                return None
            t2 = self.transitions[s2][d1]
            if t1 != t2:
                return None

        s1_accepting = s1 in self.accepting_states
        s2_accepting = s2 in self.accepting_states

        s1_init = s1 == self.initial_state
        s2_init = s2 == self.initial_state

        acceptings_equal = s1_accepting == s2_accepting

        if acceptings_equal:
            return s1 if s2_init else s2
        return None

    def _remove_redundant_states(self, state):
        if state not in self.states:
            return
        transitions = self.transitions.get(state, {})
        keys = list(transitions.keys())

        for i, d1 in enumerate(keys[:-1]):
            for d2 in keys[i+1:]:
                if transitions.get(d1, None) != transitions.get(d2, None):
                    continue
                if d1 not in self.states or d2 not in self.states:
                    continue
                to_remove = self.redundant_states(d1, d2)
                if to_remove is not None:
                    to_keep = d1 if to_remove == d2 else d2
                    if to_remove in self.accepting_states:
                        self.accepting_states.remove(to_remove)
                    if to_remove in self.transitions:
                        del self.transitions[to_remove]
                    self.states.remove(to_remove)

                    for s in self.states:
                        if to_remove in self.transitions.get(s, {}):
                            remove_transition = self.transitions[s][to_remove]
                            if to_keep in self.transitions.setdefault(s, {}):
                                self.transitions[s][to_keep] = self.transitions[s][to_keep].union(remove_transition)
                            else:
                                self.transitions[s][to_keep] = remove_transition
                            del self.transitions[s][to_remove]
                    if to_remove == state:
                        return

    def remove_redundant_states(self):
        for s in copy.deepcopy(self.states):
            self._remove_redundant_states(s)

    def _depth_first_search(self, state, epsilon_visited, position, input_string, entire_string):
        # epsilon_visited is used to track which states we got to via only
        # epsilon transitions to prevent infinite recursion since they do not consume a character

        for dest, transition in self.get_transitions(state).items():
            if not transition.is_epsilon():
                continue
            if dest in epsilon_visited:
                continue
            epsilon_visited.add(dest)
            if self._depth_first_search(dest, epsilon_visited, position, input_string, entire_string):
                return True

        if entire_string:
            if position == len(input_string):
                return self.is_accepting_state(state)
        else:
            if self.is_accepting_state(state):
                return True
            elif position == len(input_string):
                return False

        char = input_string[position]

        for dest, transition in self.get_transitions(state).items():
            if transition.is_epsilon():
                continue
            if transition.accepts_character(char):
                if self._depth_first_search(dest, set(), position + 1, input_string, entire_string):
                    return True
        return False

    # TODO need to modify to take into account epsilon transitions
    def find_shortest_path(self):
        queue = [(self.initial_state, [])]
        visited = set()
        while len(queue) > 0:
            node, transitions_list = queue.pop(0)
            if self.is_accepting_state(node):
                return transitions_list
            for dest, transition in self.get_transitions(node).items():
                if dest in visited:
                    continue
                new_transitions_list = copy.deepcopy(transitions_list)
                new_transitions_list.append(transition)
                queue.append((dest, new_transitions_list))
                visited.add(dest)
        return None

    def match_string(self, input_string, entire_string):
        return self._depth_first_search(self.initial_state, set(), 0, input_string, entire_string)

    def traverse(self, string):
        """Returns a list of states that are reachable given the input string"""
        boundary = set([(self.initial_state, string)])
        visited = set()
        results = set()

        while len(boundary) > 0:
            current, string = boundary.pop()
            for dest, transition in self.get_transitions(current).items():
                if len(string) == 0:
                    results.add(current)
                if transition.is_epsilon():
                    if (dest, string) not in visited:
                        boundary.add((dest, string))
                elif len(string) > 0 and transition.accepts_character(string[0]):
                    if (dest, string[1:]) not in visited:
                        boundary.add((dest, string[1:]))
        return results

    def is_accepting_state(self, state):
        return state in self.accepting_states

    def get_transitions(self, state):
        return self.transitions.get(state, {})

    def set_accepting_states(self, states):
        self.accepting_states = set(states)

    def has_accepting_states(self):
        return len(self.accepting_states) != 0

    def get_accepting_states(self):
        return self.accepting_states

    # Taken from PySimpleAutomata and modified to support epsilon
    def intersection(self, other):
        states = set()
        initial_state = (self.initial_state, other.initial_state)
        accepting_states = set()
        transitions = {}

        states.add(initial_state)

        boundary = set()
        boundary.add(initial_state)
        transitions[initial_state] = {}
        while len(boundary) != 0:
            current_state = (state_1, state_2) = boundary.pop()
            if (self.is_accepting_state(state_1) and
                other.is_accepting_state(state_2)):
                accepting_states.add(current_state)

            state_1_transitions = []

            for (d1, t1) in self.transitions.get(state_1, {}).items():
                if t1.is_epsilon():
                    next_state = (d1, state_2)
                    transitions[current_state][next_state] =  EPSILON_TRANSITION
                    if next_state not in states:
                        states.add(next_state)
                        boundary.add(next_state)
                        transitions[next_state] = {}
                        if self.is_accepting_state(d1) and other.is_accepting_state(state_2):
                            accepting_states.add(next_state)
                else:
                    state_1_transitions.append((d1, t1))

            for (d2, t2) in other.transitions.get(state_2, {}).items():
                if t2.is_epsilon():
                    next_state = (state_1, d2)
                    transitions[current_state][next_state] =  EPSILON_TRANSITION
                    if next_state not in states:
                        states.add(next_state)
                        boundary.add(next_state)
                        transitions[next_state] = {}
                        if self.is_accepting_state(state_1) and other.is_accepting_state(d2):
                            accepting_states.add(next_state)
                else:
                    for d1, t1 in state_1_transitions:
                        intersection_transition = t1.intersection(t2)
                        if intersection_transition is None:
                            continue
                        next_state = (d1, d2)
                        transitions[current_state][next_state] = intersection_transition
                        if next_state not in states:
                            states.add(next_state)
                            boundary.add(next_state)
                            transitions[next_state] = {}
                            if self.is_accepting_state(d1) and other.is_accepting_state(d2):
                                accepting_states.add(next_state)

        return Automata(states, initial_state, transitions, accepting_states, False, False, False)

    def get_states(self):
        return self.states

    def copy_states(self):
        return copy.copy(self.states)

    def copy_transitions(self):
        new_transitions = {}
        for s, values in self.transitions.items():
            new_source = {}
            new_transitions[s] = new_source
            for d, t in values.items():
                new_source[d] = t
        return new_transitions

    def get_new_state(self):
        state = len(self.states)
        while state in self.states:
            state += 1
        return state

    def has_end_anchor(self):
        return self.end_anchor

    def set_initial_state(self, state):
        self.initial_state = state

    def _find_negation_accepting_string(self, state, string, visited):
        union_transition = None
        if state in negation_dfa['accepting_states']:
            return string

        visited.add(state)

        for d, t in negation_dfa['transitions'].get(state, {}).items():
            if union_transition is None:
                union_transition = t
            else:
                union_transition = union_transition.union(t)

        if union_transition is not None:
            c = union_transition.get_nonaccepting_char()
            if c is not None:
                return string + c

        for d, t in negation_dfa['transitions'].get(state, {}).items():
            if d in visited:
                continue
            s = RegexParser._find_negation_accepting_string(
                negation_dfa, d, string + t.get_accepting_char(), visited)
            if s is not None:
                return s
            if union_transition is None:
                union_transition = t
            else:
                union_transition = union_transition.union(t)

        return None

    def convert_to_dfa(self):
        dfa = {}
        dfa['initial_state'] = frozenset([self.initial_state])
        states = {dfa['initial_state']}
        boundary = {dfa['initial_state']}
        transitions = {}
        dfa['accepting_states'] = set()
        if self.initial_state in self.accepting_states:
            dfa['accepting_states'].add(frozenset([self.initial_state]))

        while len(boundary) > 0:
            current = boundary.pop()
            transitions[current] = {}
            individual_characters = set()
            negation = False
            dests = {}
            negation_dest = set()
            dot_destinations = set()

            for c in current:
                for (d, t) in self.transitions.get(c, {}).items():
                    if t.is_epsilon():
                        raise ValueError('Cannot convert NFA with epsilon transitions to DFA')
                    if t.is_dot():
                        # When there is a dot there are transitions not in
                        # individual_characters, same as when there is a
                        # negation
                        dot_destinations.add(d)
                        negation = True
                    elif t.has_character_set():
                        individual_characters.update(t.get_characters())
                    else:
                        individual_characters.update(t.get_negation_characters())
                        negation = True

            if len(individual_characters) > 0:
                if negation:
                    negation_transition = Transition(negation_character_class=individual_characters)
                else:
                    negation_transition = None

                for i in individual_characters:
                    for c in current:
                        for (d, t) in self.transitions.get(c, {}).items():
                            if t.accepts_character(i):
                                dests.setdefault(i, set()).add(d)
                            if (negation_transition is not None) and (negation_transition.intersection(t) is not None):
                                negation_dest.add(d)

                if negation and len(negation_dest) > 0:
                    negation_dest = frozenset(negation_dest)
                    transitions[current][negation_dest] = negation_transition
                    if negation_dest not in states:
                        boundary.add(negation_dest)
                        states.add(negation_dest)
                        if len(negation_dest.intersection(self.accepting_states)) > 0:
                            dfa['accepting_states'].add(negation_dest)

                for i, dest_set in dests.items():
                    assert len(dest_set) > 0
                    dest_set = frozenset(dest_set)
                    if dest_set in transitions[current]:
                        transitions[current][dest_set] = transitions[current][dest_set].add_character(i)
                    else:
                        transitions[current][dest_set] = Transition(character_class=set(i))
                    if dest_set not in states:
                        boundary.add(dest_set)
                        states.add(dest_set)
                        if len(dest_set.intersection(self.accepting_states)) > 0:
                            dfa['accepting_states'].add(dest_set)

            elif len(dot_destinations) > 0:
                # if there are no individual characters but there are dot
                # destinations that means there were only dot transitions
                dot_destinations = frozenset(dot_destinations)
                transitions[current][dot_destinations] = DOT_TRANSITION
                if dot_destinations not in states:
                    boundary.add(dot_destinations)
                    states.add(dot_destinations)
                    if len(dot_destinations.intersection(self.accepting_states)) > 0:
                        dfa['accepting_states'].add(dot_destinations)

        dfa['states'] = states
        dfa['transitions'] = transitions

        for s in transitions.keys():
            for d, t in transitions[s].items():
                assert d in states
                assert len(d) > 0
        return Automata(dfa['states'], dfa['initial_state'], dfa['transitions'],
                        dfa['accepting_states'], self.start_anchor, self.end_anchor,
                        True)

    def negate(self):
        automata = copy.deepcopy(self)
        automata.accepting_states = automata.states - automata.accepting_states
        return automata

    def find_negation_accepting_string(self):
        boundary = []
        visited = set()
        negated_dfa = self.negate()
        boundary.append((negated_dfa.initial_state, ''))
        while len(boundary) > 0:
            union_transition = None
            state, string = boundary.pop(0)
            if state in negated_dfa.accepting_states:
                return string

            for d, t in negated_dfa.get_transitions(state).items():
                if union_transition is None:
                    union_transition = t
                else:
                    union_transition = union_transition.union(t)

            if union_transition is None:
                return string + 'a'
            else:
                c = union_transition.get_nonaccepting_char()
                if c is not None:
                    return string + c

            for d, t in negated_dfa.get_transitions(state).items():
                if d in visited:
                    continue
                visited.add(d)
                boundary.append((d, string + t.get_accepting_char()))
        return None



class RegexParser:

    def __init__(self, no_epsilon_transitions=False, dfa=False):
        lex.lex(module=self, debug=0)
        yacc.yacc(module=self, debug=0, write_tables=0)
        self.next_state = 0
        self.no_epsilon_transitions = no_epsilon_transitions
        self.dfa = dfa
        if dfa is True:
            self.no_epsilon_transitions = True

    def new_state(self):
        result = self.next_state
        self.next_state += 1
        return result

    def run(self, s):
        self.next_state = 0
        start_anchor = False
        end_anchor = False

        if len(s) > 0 and s[0] == '^':
            s = s[1:]
            start_anchor = True

        if len(s) > 1 and s[-1] == '$':
            if len(s) > 2:
                end_anchor = s[-2] != '\\'
            else:
                end_anchor = True

        if end_anchor:
            s = s[:-1]

        automata = yacc.parse(s)
        automata = Automata(automata['states'], automata['initial_state'], automata['transitions'],
                            automata['accepting_states'], start_anchor, end_anchor, False)
        automata.prune()
        if self.dfa:
            automata = automata.convert_to_dfa()

        return automata


    tokens = (
        'LITERAL',
        'ESCAPED_LITERAL',
        'DOT',
        'STAR',
        'STAR_QUESTION',
        'PLUS',
        'OPTIONAL',
        'LEFT_PAREN',
        'RIGHT_PAREN',
        'ALTERNATE',
        'CHARACTER_CLASS_LIST',
        'LEFT_CURLY_BRACKET',
        'RIGHT_CURLY_BRACKET',
        'NUMBER',
        'COMMA',
        'CATEGORY',
        'UNICODE_CHAR',
    )

    # Tokens

    t_LITERAL = r'.'
    t_ESCAPED_LITERAL = r'\\[dswDSW+?*^.]'
    t_DOT = r'\.'
    t_CHARACTER_CLASS_LIST = r'\[[^\[\]]*\]'
    t_STAR = r'\*'
    t_STAR_QUESTION = r'\*\?'
    t_PLUS = r'\+'
    t_OPTIONAL = r'\?'
    t_LEFT_PAREN = r'\('
    t_RIGHT_PAREN = r'\)'
    t_ALTERNATE = r'\|'
    t_LEFT_CURLY_BRACKET = r'\{'
    t_RIGHT_CURLY_BRACKET = r'\}'
    t_NUMBER = r'\d+'
    t_COMMA = r','
    t_CATEGORY = r'\\p\{\^?[a-zA-Z]+\}'
    t_UNICODE_CHAR = r'\\x\{[0-9a-fA-F]{4}\}'

    def t_error(self, t):
        raise ValueError("Illegal character '%s'" % t.value[0])

    # Parsing rules

    def p_exp(self, p):
        """
        exp : term ALTERNATE exp
            | term
        """
        if len(p) == 4:
            p[0] = self.alternate(p[1], p[3])
        else:
            p[0] = p[1]

    def p_term(self, p):
        """
        term : factor term
             | factor
        """
        if len(p) == 3:
            p[0] = self.concatenate(p[1], p[2])
        else:
            p[0] = p[1]

    def p_factor(self, p):
        """
        factor : primary STAR
               | primary STAR_QUESTION
               | primary PLUS
               | primary OPTIONAL
               | primary LEFT_CURLY_BRACKET NUMBER RIGHT_CURLY_BRACKET
               | primary LEFT_CURLY_BRACKET NUMBER COMMA RIGHT_CURLY_BRACKET
               | primary LEFT_CURLY_BRACKET NUMBER COMMA NUMBER RIGHT_CURLY_BRACKET
               | primary
        """
        if len(p) == 3:
            if p[2] == '*' or p[2] == '*?':
                p[0] = self.star(p[1])
            elif p[2] == '+':
                p[0] = self.plus(p[1])
            else: # p[2] == '?'
                p[0] = self.optional_repeat(p[1], 1)
        elif len(p) == 5:
            p[0] = self.repeat(p[1], int(p[3]))
        elif len(p) == 6:
            start = self.repeat(p[1], int(p[3]))
            p[0] = self.concatenate(start, self.star(self.duplicate(p[1])))
        elif len(p) == 7:
            minimum = int(p[3])
            maximum = int(p[5])
            start = self.repeat(p[1], int(p[3]))
            p[0] = self.concatenate(start, self.optional_repeat(
                self.duplicate(p[1]), maximum - minimum))
        else:
            p[0] = p[1]

    def collect_characters_in_class(self, character_class):
        if len(character_class) == 0:
            raise ValueError("Empty character class")

        characters = set()
        if character_class[0] == '-':
            characters.add(character_class[0])
            i = 1
        else:
            i = 0
        while i < len(character_class):
            if (i + 2 < len(character_class)) and (character_class[i+1] == '-'):
                if ord(character_class[i]) > ord(character_class[i + 2]):
                    raise ValueError('{} is less than {} in character class'.format(
                        character_class[i], character_class[i+2]))
                for c in range(ord(character_class[i]), ord(character_class[i + 2]) + 1):
                    characters.add(chr(c))
                i += 2
            else:
                characters.add(character_class[i])
                i += 1
        return characters

    def character_class(self, character_class):
        chars = self.collect_characters_in_class(character_class)
        return self.single_element_nfa(chars)

    def negation_character_class(self, character_class):
        chars = self.collect_characters_in_class(character_class)
        return self.single_element_negation_nfa(chars)

    def p_character_class_list(self, p):
        """
        character_class_list : CHARACTER_CLASS_LIST
        """
        character_class = p[1][1:-1]

        if len(character_class) == 0:
            raise ValueError("Empty character class")

        if character_class[0] == '^':
            character_class = character_class[1:]
            if len(character_class) == 0:
                raise ValueError("Empty character class")
            p[0] = self.negation_character_class(character_class)
        else:
            p[0] = self.character_class(character_class)

    def dot(self):
        initial_state = self.new_state()
        accepting_state = self.new_state()
        transitions = {}
        transitions[initial_state] = { accepting_state: DOT_TRANSITION }
        return {
            'states': set([initial_state, accepting_state]),
            'transitions': transitions,
            'initial_state': initial_state,
            'accepting_states': set([accepting_state]),
        }

    def digit(self):
        return self.single_element_nfa(set(string.digits))

    def non_digit(self):
        return self.single_element_negation_nfa(set(string.digits))

    def whitespace(self):
        return self.single_element_nfa(set(string.whitespace))

    def non_whitespace(self):
        return self.single_element_negation_nfa(set(string.whitespace))

    def word_character(self):
        return self.single_element_nfa(set(string.ascii_letters + string.digits + '_'))

    def non_word_character(self):
        return self.single_element_negation_nfa(set(string.ascii_letters + string.digits + '_'))

    def merge_transitions(self, t1, t2):
        result = {}
        # result = copy.deepcopy(t1)
        for s, transitions in t1.items():
            for (d, t) in transitions.items():
                self.add_transition(result, s, t, d)
        for s, transitions in t2.items():
            for (d, t) in transitions.items():
                self.add_transition(result, s, t, d)
        return result

    def duplicate(self, nfa):
        """Creates an indentical NFA with using new states"""
        state_mapping = {}
        new_states = set()
        for n in nfa['states']:
            state = self.new_state()
            state_mapping[n] = state
            new_states.add(state)

        new_transitions = {}
        for s, transitions in nfa['transitions'].items():
            for (d, t) in transitions.items():
                self.add_transition(new_transitions, state_mapping[s], t, state_mapping[d])

        return {
            'states': new_states,
            'transitions': new_transitions,
            'initial_state': state_mapping[nfa['initial_state']],
            'accepting_states': { state_mapping[s] for s in nfa['accepting_states'] },
        }

    def optional_repeat(self, nfa, number):
        if number == 0:
            raise ValueError('Repeat number cannot be zero')
        if self.no_epsilon_transitions:
            new_states = [self.new_state() for _ in range(number)]
            states = set(new_states)
            accepting_states = set(new_states)
            transitions = {}
            for i in range(number):
                duplicate_nfa = nfa if i == 0 else self.duplicate(nfa)
                states.update(duplicate_nfa['states'])
                accepting_states.update(duplicate_nfa['accepting_states'])
                transitions = self.merge_transitions(transitions, duplicate_nfa['transitions'])

                for d, t in duplicate_nfa['transitions'].get(duplicate_nfa['initial_state'], {}).items():
                    self.add_transition(transitions, new_states[i], t, d)
                    if d in duplicate_nfa['accepting_states'] and i != number - 1:
                        self.add_transition(transitions, new_states[i], t, new_states[i + 1])

                if i != number - 1:
                    for s in duplicate_nfa['states']:
                        for d, t in duplicate_nfa['transitions'].get(s, {}).items():
                            if d in duplicate_nfa['accepting_states']:
                                self.add_transition(transitions, s, t, new_states[i + 1])
            return {
                'states': states,
                'transitions': transitions,
                'initial_state': new_states[0],
                'accepting_states': accepting_states,
            }
        else:
            new_states = [self.new_state() for _ in range(number + 1)]
            states = set(new_states)
            transitions = {}

            for i in range(number):
                self.add_transition(transitions, new_states[i], EPSILON_TRANSITION, new_states[number])

                duplicate_nfa = nfa if i == 0 else self.duplicate(nfa)
                states.update(duplicate_nfa['states'])
                transitions = self.merge_transitions(transitions, duplicate_nfa['transitions'])

                self.add_transition(transitions, new_states[i], EPSILON_TRANSITION,
                                    duplicate_nfa['initial_state'])
                for s in duplicate_nfa['accepting_states']:
                    self.add_transition(transitions, s, EPSILON_TRANSITION, new_states[i+1])

            return {
                'states': states,
                'transitions': transitions,
                'initial_state': new_states[0],
                'accepting_states': set([new_states[number]]),
            }

    def repeat(self, nfa, number):
        single_nfa = nfa
        if number == 0:
            raise ValueError('Repeat number cannot be zero')
        else:
            for i in range(number - 1):
                nfa = self.concatenate(nfa, self.duplicate(single_nfa))
        return nfa


    category_sets = {
        # TODO: A  lot of these are incomplete but its a start
        # http://www.wellho.net/regex/javare.html is a good resource
        'Z': set(string.whitespace),
        'IsWhiteSpace': set(string.whitespace),
        'javaJavaIdentifierPart': set(string.ascii_letters).union(string.digits, {'_'}),
        'Alnum': set(string.ascii_letters).union(string.digits),
        'javaLetterOrDigit': set(string.ascii_letters).union(string.digits),
        'Alpha': set(string.ascii_letters),
        'ASCII': set(string.ascii_letters),
        'javaLetter': set(string.ascii_letters),
        'javaJavaIdentifierStart': set(string.ascii_letters).union({'_'}),
        'javaLowerCase': set(string.ascii_lowercase),
        'javaUpperCase': set(string.ascii_uppercase),
        'Lower': set(string.ascii_lowercase),
        'IsLowerCase': set(string.ascii_lowercase),
        'IsUpperCase': set(string.ascii_uppercase),
        'IsUpper': set(string.ascii_uppercase),
        'Upper': set(string.ascii_uppercase),
        'javaDigit': set(string.digits),
        'javaAlphabetic': set(string.ascii_letters),
        'Blank': {' ', '\t'},
        'Digit': set(string.digits),
        'javaWhitespace': set(string.whitespace),
        'Print': set(string.printable),
        'Punct': set(string.punctuation),
        'XDigit': set(string.hexdigits),
        'Space': set(string.whitespace),
        'Lu': set(string.ascii_uppercase),
        'IsLu': set(string.ascii_uppercase),
        'IsLetter': set(string.ascii_letters),
        'IsAlphabetic': set(string.ascii_letters),
        'Graph': set(string.printable),
    }
    def get_category_set(self, category):
        if category == 'all':
            return self.dot()
        if category in RegexParser.category_sets:
            return RegexParser.category_sets[category]
        else:
            raise ValueError('Unknown category {}'.format(category))

    def p_category(self, p):
        """
        category : CATEGORY
        """
        category = p[1][3:-1]
        if len(category) == 0:
            raise ValueError('Empty category')
        if category[0] == '^':
            category = category[1:]
            if len(category) == 0:
                raise ValueError('Empty category')
            p[0] = self.single_element_negation_nfa(self.get_category_set(category))
        else:
            p[0] = self.single_element_nfa(self.get_category_set(category))

    def p_unicode_char(self, p):
        """
        unicode_char : UNICODE_CHAR
        """
        p[0] = self.single_element_nfa({chr(int(p[1][3:-1], 16))})

    def p_primary(self, p):
        """
        primary : LEFT_PAREN exp RIGHT_PAREN
                | character_class_list
                | category
                | unicode_char
                | LITERAL
                | ESCAPED_LITERAL
                | DOT
                | NUMBER
        """
        if len(p) == 4: # LEFT_PAREN exp RIGHT_PAREN
            p[0] = p[2]
        elif type(p[1]) == dict: # character_class_list | category | unicode_char
            p[0] = p[1]
        else: # LITERAL | ESCAPED_LITERAL | DOT
            if len(p[1]) == 2 and p[1][0] == '\\':
                if p[1] == '\d':
                    p[0] = self.digit()
                elif p[1] == '\D':
                    p[0] = self.non_digit()
                elif p[1] == '\s':
                    p[0] = self.whitespace()
                elif p[1] == '\S':
                    p[0] = self.non_whitespace()
                elif p[1] == '\w':
                    p[0] = self.word_character()
                elif p[1] == '\W':
                    p[0] = self.non_word_character()
                else:
                    p[0] = self.single_element_nfa({p[1][1]})
            elif p[1] == '.':
                p[0] = self.dot()
            else: # NUMBER that has no special meaning
                nfa = self.single_element_nfa({p[1][0]})
                for c in p[1][1:]:
                    nfa = self.concatenate(nfa, self.single_element_nfa({c}))
                p[0] = nfa

    def p_error(self, p):
        if p:
            raise ValueError("Syntax error at '%s'" % p.value)
        else:
            raise ValueError("Syntax error at EOF")

    def plus(self, nfa):
        new_state = self.new_state()
        transitions = copy.deepcopy(nfa['transitions'])
        if self.no_epsilon_transitions:
            for s in nfa['states']:
                if s == nfa['initial_state']:
                    for d, t in nfa['transitions'].get(s, {}).items():
                        self.add_transition(transitions, new_state, t, d)
                        if d in nfa['accepting_states']:
                            self.add_transition(transitions, new_state, t, new_state)
                for d, t in nfa['transitions'].get(s, {}).items():
                    if d in nfa['accepting_states']:
                        self.add_transition(transitions, s, t, new_state)
        else:
            self.add_transition(transitions, new_state, EPSILON_TRANSITION,
                                nfa['initial_state'])
            for a in nfa['accepting_states']:
                self.add_transition(transitions, a, EPSILON_TRANSITION, new_state)
        return {
            'states': nfa['states'].union([new_state]),
            'transitions': transitions,
            'initial_state': nfa['initial_state'],
            'accepting_states': set([new_state]),
        }

    def star(self, nfa):
        new_state = self.new_state()
        transitions = copy.deepcopy(nfa['transitions'])
        if self.no_epsilon_transitions:
            for s in nfa['states']:
                if s == nfa['initial_state']:
                    for d, t in nfa['transitions'].get(s, {}).items():
                        self.add_transition(transitions, new_state, t, d)
                        if d in nfa['accepting_states']:
                            self.add_transition(transitions, new_state, t, new_state)
                for d, t in nfa['transitions'].get(s, {}).items():
                    if d in nfa['accepting_states']:
                        self.add_transition(transitions, s, t, new_state)
        else:
            self.add_transition(transitions, new_state, EPSILON_TRANSITION,
                                nfa['initial_state'])
            for a in nfa['accepting_states']:
                self.add_transition(transitions, a, EPSILON_TRANSITION, new_state)
        return {
            'states': nfa['states'].union([new_state]),
            'transitions': transitions,
            'initial_state': new_state,
            'accepting_states': set([new_state]),
        }

    def alternate(self, left, right):
        if self.no_epsilon_transitions:
            new_state = self.new_state()
            transitions = {**left['transitions'], **right['transitions']}
            states = left['states'].union(right['states'], [new_state])
            accepting_states = left['accepting_states'].union(right['accepting_states'])

            if (left['initial_state'] in left['accepting_states'] or
                right['initial_state'] in right['accepting_states']):
                accepting_states.add(new_state)

            for d, t in left['transitions'].get(left['initial_state'], {}).items():
                self.add_transition(transitions, new_state, t, d)

            for d, t in right['transitions'].get(right['initial_state'], {}).items():
                self.add_transition(transitions, new_state, t, d)
            return {
                'states': states,
                'transitions': transitions,
                'initial_state': new_state,
                'accepting_states': accepting_states,
            }
        else:
            new_start_state = self.new_state()
            transitions = self.merge_transitions(left['transitions'], right['transitions'])
            self.add_transition(transitions, new_start_state, EPSILON_TRANSITION,
                                left['initial_state'])
            self.add_transition(transitions, new_start_state, EPSILON_TRANSITION,
                                right['initial_state'])
            return {
                'states': left['states'].union(right['states'], [new_start_state]),
                'transitions': transitions,
                'initial_state': new_start_state,
                'accepting_states': left['accepting_states'].union(right['accepting_states'])
            }

    def optional(self, nfa):
        self.optional_repeat(nfa, 1)

    def add_transition(self, transitions, source, transition, destination):
        if destination in transitions.setdefault(source, {}):
            old_transition = transitions[source][destination]
            transitions[source][destination] = old_transition.union(transition)
        else:
            transitions.setdefault(source, {})[destination] = transition

    def concatenate(self, left, right):
        transitions = self.merge_transitions(left['transitions'], right['transitions'])
        if self.no_epsilon_transitions:
            for s in left['states']:
                if s in left['accepting_states']:
                    for d, t in right['transitions'].get(right['initial_state'], {}).items():
                        self.add_transition(transitions, s, t, d)
                else:
                    for d, t in left['transitions'].get(s, {}).items():
                        if d in left['accepting_states']:
                            self.add_transition(transitions, s, t, right['initial_state'])
        else:
            for a in left['accepting_states']:
                self.add_transition(transitions, a, EPSILON_TRANSITION, right['initial_state'])
        return {
            'states': left['states'].union(right['states']),
            'transitions': transitions,
            'initial_state': left['initial_state'],
            'accepting_states': right['accepting_states'],
        }

    def single_element_nfa(self, transition_set):
        initial_state = self.new_state()
        accepting_state = self.new_state()
        transitions = {}
        transitions[initial_state] = {
            accepting_state: Transition(character_class=transition_set)
        }
        return {
            'states': set([initial_state, accepting_state]),
            'transitions': transitions,
            'initial_state': initial_state,
            'accepting_states': set([accepting_state]),
        }

    def single_element_negation_nfa(self, transition_set):
        initial_state = self.new_state()
        accepting_state = self.new_state()
        transitions = {}
        transitions[initial_state] = {
            accepting_state: Transition(negation_character_class=transition_set)
        }
        return {
            'states': set([initial_state, accepting_state]),
            'transitions': transitions,
            'initial_state': initial_state,
            'accepting_states': set([accepting_state]),
        }
