"""
PhraseStructure:

Provides the PhraseStructure class.

"""

import random
from .phrase_structure_symbol import PhraseStructureSymbol

class PhraseStructure:
    """
    The PhraseStructure class handles the relationshis between PhraseStructureSymbols.
    It also can generate symbol strings randomly, and check if a symbol string fits
    the grammar specified. This should be your interface with instances of
    PhraseStructureSymbol, as directly accessing the PhraseStructureSymbol can mess
    things up.

    Functions:
    - __init__(self):
        Initializes an instance of the class.

    - add_symbol(self, symbol, is_terminal):
        Creates a new PhraseStructureSymbol instance, with the symbol parameter
        specifying the data to be used to generate symbol strings and to reference
        when setting up phrase-structure rules. The symbol parameter should never be None,
        as this is used as the root of the phrase-structure tree. Returns -1 if symbol
        has already been added.

    - add_terminal_rule(self, parent_symbol, daughter_symbol, generate_new_symbol=False):
        Creates a new rule, assuming the parent_symbol and daughter_symbol have already
        been added. If they have not, and generate_new_symbol is left False, this
        function will return -1 and not add the rule. If generate_new_symbol is set
        to True, then the daughter symbol will be created if it does not already exist
        in the symbol set. 

        Because grammars in this class follow Chomsky Normal Form,
        the daughter symbol must be terminal. If the daughter symbol is not terminal,
        then the function will return -2. If the daughter symbol is created by this
        class, it will be initialized to be terminal, and will not permit the user
        to add rules with that daughter symbol as a parent.

    - add_intermediate_rule(self, parent_symbol, daughter_symbol1, daughter_symbol2, generate_new_symbol=False):
        Creates a new rule, assuming the parent_symbol has already been added.
        If generate_new_symbol is False (which is the default argument) and
        the daughter symbols have not already been added, then this function
        will return -1.

        Because grammars in this class follow Chomsky Normal Form, the daughter
        symbols must not be terminal. If either daughter symbol is terminal,
        then the function will return -2, and no rule will be added.
        If generate_new_symbol is set to True, and one or both of the daughter
        symbols have not been added, then those symbols which have been added
        will be created and initialized with their is_terminal property set
        to False.

    - add_rule(self, parent_symbol, daughter_symbol, daughter_symbol2=None, generate_new_symbol=False):
        Adds a phrase-structure rule. If daughter_symbol2 is None, then a
        terminal rule will be added. If daughter_symbol2 is not None, then an
        intermediate rule will be added.

        Returns -1 if either symbol has not been added yet, and generate_new_symbol is set to False.

        Returns -2 if either: (a) only one symbol has been specified as a daughter, yet that symbol
                                  is not terminal, or
                              (b) two symbols have been specified as daughters, yet at least one
                                  of those symbols are terminal.

        Returns -3 if the parent symbol is terminal

        Returns -4 if the parent symbol has not yet been added.

    - list_unterminalized_symbols(self):
        Gives a list of symbol reference labels (the data used to add the symbol) which have
        no terminal progeny (when these symbols are used in the generator process,
        they will not be able to be terminalized, and as a result will break the process.)

    - remove_unterminalized_symbols(self):
        Removes all symbols which are listed by list_unterminalized_symbol.

    - remove_symbol(self, symbol):
        Removes a given symbol, all the rules mapping to that symbol, and all the symbols which
        are only mapped to by the given symbol to be removed. Also removes references to any
        symbol which is removed in the process.

    - prune(self):
        Removes all symbols which do not have parents leading to the root symbol (None).

    - generate_symbol_string(self, depth=4):
        Generates a tuple of random length which follows the phrase-structure, but does so
        randomly.

    - fits_grammar(self, symbol_string):
        Uses the CYK algorithm to determine whether the symbol string (a tuple) can be
        generated by the grammar.


    Variables:
    - symbols(dict): A dictionary which maps symbol reference labels (the data stored
                     in the nodes of each symbol), with the matching symbol which
                    is referenced by that symbol.

    """
    def __init__(self):
        """
        Initializes an instance of the PhraseStructure class.

        Parameters:
            None

        Returns:
            None
        """
        self.symbols = {None: PhraseStructureSymbol(None, is_terminal=False)}


    def add_symbol(self, symbol, is_terminal):
        """
        Creates a new PhraseStructureSymbol instance, with the symbol parameter
        specifying the data to be used to generate symbol strings and to reference
        when setting up phrase-structure rules. The symbol parameter should never be None,
        as this is used as the root of the phrase-structure tree. Returns -1 if symbol
        has already been added.

        Parameters:
            - symbol(any type): The value which is used to refer to the data (rules, parents)
                                stored for this symbol, as well as the value that is outputted
                                in a symbol string generated from this phrase-structure (assuming
                                the symbol is terminal. If is_terminal is set to False, then
                                this symbol cannot be outputted in a generated symbol string

            - is_terminal(bool): Determines whether this symbol can have daughter symbols,
                                 (whether the symbol can be used as a parent when adding rules).
                                 Also determines if the symbol can be outputted in a symbol string.

        Returns:
            int: -1 if the symbol already exists. 0 otherwise.
        """

        if symbol in self.symbols:
            return -1
        self.symbols[symbol] = PhraseStructureSymbol(symbol, is_terminal)
        return 0

    def add_terminal_rule(self, parent_symbol, daughter_symbol, generate_new_symbol=False):
        """
        Creates a new rule, assuming the parent_symbol and daughter_symbol have already
        been added. If they have not, and generate_new_symbol is left False, this
        function will return -1 and not add the rule. If generate_new_symbol is set
        to True, then the daughter symbol will be created if it does not already exist
        in the symbol set. 

        Because grammars in this class follow Chomsky Normal Form,
        the daughter symbol must be terminal. If the daughter symbol is not terminal,
        then the function will return -2. If the daughter symbol is created by this
        class, it will be initialized to be terminal, and will not permit the user
        to add rules with that daughter symbol as a parent.

        Parameters:
            - parent_symbol(any type): The symbol reference label of the parent
                                       PhraseStructureSymbol in which to establish the rule.

            - daughter_symbol(any type): The symbol reference label of the symbol which is
                                         routed to by the parent in the rule set.

            - generate_new_symbol(bool): Determines whether the daughter symbol should be
                                         added to the symbol set, in the case that the daughter
                                         symbol has not been added yet.

        Returns:
            int: -1 if the daughter symbol has not been added and generate_new_symbol is False.
                 -2 if the daughter symbol is not terminal.
                 -3 if the parent symbol is terminal
                 -4 if the parent symbol is not defined
                 0 otherwise.
        """
        if parent_symbol not in self.symbols:
            return -4
        if self.symbols[parent_symbol].terminal():
            return -3
        if daughter_symbol not in self.symbols:
            if not generate_new_symbol:
                return -1
            self.symbols[daughter_symbol] = PhraseStructureSymbol(daughter_symbol, is_terminal=True)
        if not self.symbols[daughter_symbol].terminal():
            return -2
        self.symbols[daughter_symbol].add_parent(parent_symbol)
        self.symbols[parent_symbol].add_terminal_rule(daughter_symbol)
        return 0


    def add_intermediate_rule(self, parent_symbol, daughter_symbol1, daughter_symbol2, generate_new_symbol=False):
        """
        Creates a new rule, assuming the parent_symbol has already been added.
        If generate_new_symbol is False (which is the default argument) and
        the daughter symbols have not already been added, then this function
        will return -1.

        Because grammars in this class follow Chomsky Normal Form, the daughter
        symbols must not be terminal. If either daughter symbol is terminal,
        then the function will return -2, and no rule will be added.
        If generate_new_symbol is set to True, and one or both of the daughter
        symbols have not been added, then those symbols which have been added
        will be created and initialized with their is_terminal property set
        to False.

        Parameters:
            - parent_symbol(any type): The symbol reference label of the parent
                                       PhraseStructureSymbol in which to establish
                                       this rule.

            - daughter_symbol1(any type): The symbol reference label of the symbol
                                          which is the first of the two symbols
                                          to replace the parent symbol in
                                          symbol string generation. Also adds the
                                          parent symbol to the daughter's list
                                          of parents. Must be non-terminal node,
                                          by CNF convention.

            - daughter_symbol2(any type): See daughter symbol1.

            - generate_new_symbol(bool): Default value is False. Determines whether
                                         this function should create a
                                         PhraseStructureSymbol instance for the daughter
                                         symbols, if they are not already created.
                                         If set to True, the daughter symbols will be
                                         set to be intermediate, meaning their
                                         is_terminal value is False, and they will
                                         not be able to store rules or route to other
                                         symbols downstream.

        Returns:
            int: -1 if a daughter symbol has not been added yet, and generate_new_symbol=False
                 -2 if either daughter is terminal
                 -3 if parent symbol is terminal
                 -4 if the parent has not been added.
                 0 otherwise
        """
        if parent_symbol not in self.symbols:
            return -4
        if self.symbols[parent_symbol].terminal():
            return -3
        if daughter_symbol1 in self.symbols:
            if self.symbols[daughter_symbol1].terminal():
                return -2
        if daughter_symbol2 in self.symbols:
            if self.symbols[daughter_symbol2].terminal():
                return -2
        if not generate_new_symbol:
            if daughter_symbol1 not in self.symbols or daughter_symbol2 not in self.symbols:
                return -1
        else:
            if daughter_symbol1 not in self.symbols:
                self.add_symbol(daughter_symbol1, is_terminal=False)
            if daughter_symbol2 not in self.symbols:
                self.add_symbol(daughter_symbol2, is_terminal=False)
        self.symbols[daughter_symbol1].add_parent(parent_symbol)
        self.symbols[daughter_symbol2].add_parent(parent_symbol)
        self.symbols[parent_symbol].add_intermediate_rule(daughter_symbol1, daughter_symbol2)
        return 0

    def add_rule(self, parent_symbol, daughter_symbol, daughter_symbol2=None, generate_new_symbol=False, use_root_for_second_symbol=False):
        """
        Adds a phrase-structure rule. If daughter_symbol2 is None, then a
        terminal rule will be added. If daughter_symbol2 is not None, then an
        intermediate rule will be added. This is primarily a convenience function,
        prefer using add_terminal_rule and add_intermediate_rule when you can.

        Parameters:
            - parent_symbol(any type): The symbol reference denoting the symbol which will
                                       store the rule, which will be replaced by the daughter symbol(s)
                                       in symbol string generation, and which will be added as a parent
                                       in the daughter symbol(s).

            - daughter_symbol(any type): The symbol reference denoting the symbol which will
                                         be referenced under the parent's rule, and which will
                                         store the parent symbol as a parent.

            - daughter_symbol2(any type): Optional. The symbol reference denoting the second daughter
                                          in the rule. If None, no second symbol will be used and
                                          the rule will be intermediate (the daughters must have
                                          is_terminal set to False). To add the root (whose symbol is None)
                                          as the second daughter, set the use_root_for_second_symbol to
                                          True.

            - generate_new_symbol(bool): Optional. If the daughter symbol(s) are not yet added,
                                         add them to the phrase-structure. Otherwise, the function
                                         will return -1 without creating the rule (in the case
                                         that the daughter symbol(s) are not yet defined).

            - use_root_for_second_symbol(bool): Optional. Sets the second symbol to be the root.
                                                Returns -5 if daughter_symbol2 is set to anything
                                                other than None.
        Returns:
            int: -1 if either symbol has not been added yet, and generate_new_symbol is set to False.
                 -2 if either: (a) only one symbol has been specified as a daughter, yet that symbol
                                  is not terminal, or
                               (b) two symbols have been specified as daughters, yet at least one
                                  of those symbols are terminal.
                 -3 if the parent symbol is terminal
                 -4 if the parent symbol has not yet been added.
                 -5 if daughter_symbol2 is not None and use_root_for_second_symbol is True
        """
        if use_root_for_second_symbol:
            if daughter_symbol2 is not None:
                return -5
            return self.add_intermediate_rule(parent_symbol, daughter_symbol, daughter_symbol2)
        if daughter_symbol2 is None:
            return self.add_terminal_rule(parent_symbol, daughter_symbol, generate_new_symbol)
        return self.add_intermediate_rule(parent_symbol, daughter_symbol, daughter_symbol2)


    def list_unterminalized_symbols(self):
        """
        Gives a set of symbol reference labels (the data used to add the symbol) which have
        no terminal progeny (when these symbols are used in the generator process,
        they will not be able to be terminalized, and as a result will break the process.)

        Parameters:
            None

        Returns:
            set: The list of symbol references whose symbols do not have terminal progeny.
                 (No symbol can be generated by this symbol, and it will cause a failure
                 when attempting to generate a symbol string).
        """
        unterminalized_symbols = set()
        raise NotImplementedError
        pass


    def remove_unterminalized_symbols(self):
        raise NotImplementedError


    def remove_symbol(self, symbol):
        raise NotImplementedError


    def prune(self):
        raise NotImplementedError


    def generate_symbol_string(self, depth=4, brute_force=12):
        symbol_string = [None]
        # First, we will iterate rule replacements to the depth specified.
        for _ in range(depth):
            new_symbol_string = []
            for symbol in symbol_string:
                if not self.symbols[symbol].terminal():
                    for subsymbol in random.sample(list(self.symbols[symbol].get_rules()), 1)[0]:
                        new_symbol_string.append(subsymbol)
                else:
                    new_symbol_string.append(symbol)
            symbol_string = new_symbol_string
        # Then, we will (desperately) look for a terminalization of the remaining symbols.
        for _ in range(brute_force):
            # We set the totally_terminalized value to True, then set it to False if a
            # non-terminal symbol is found.
            totally_terminalized = True
            for symbol in symbol_string:
                if not self.symbols[symbol].terminal():
                    totally_terminalized = False
                    break
            if totally_terminalized:
                break
            new_symbol_string = []
            for symbol in symbol_string:
                if not self.symbols[symbol].terminal():
                    terminal_rules = self.symbols[symbol].get_terminal_rules()
                    if len(terminal_rules) == 0:
                        subrule = random.sample(list(self.symbols[symbol].get_rules()), 1)[0]
                        new_symbol_string.append(subrule[0])
                        new_symbol_string.append(subrule[1])
                    else:
                        new_symbol_string.append(random.sample(list(terminal_rules), 1)[0][0])
                else:
                    new_symbol_string.append(symbol)
            symbol_string = new_symbol_string
        return symbol_string

    def fits_grammar(self, symbol_string):
        raise NotImplementedError
