"""
Name: PPDA.py
Author: Radovan Klembara

Represents parallel deep pushdown automaton.
"""

from Helpers import check_empty_intersection
from Helpers import list_to_str, check_string_over_set
from Automaton import Automaton
from Error import Error


class PPDAExpandRule:
    """
    Representation of expand rules of parallel deep pushdown automata.
    """
    def __init__(self, source_state: str, pushdown_symbols: [str], goal_state: str, goal_strings: [str]):
        """
        Creates rule of parallel deep pushdown automaton.

        :param source_state: Source state of rule.
        :param pushdown_symbols: List of source pushdown symbols.
        :param goal_state: Goal state.
        :param goal_strings: List of goal strings.
        """
        self.source_state = source_state
        self.pushdown_symbols = pushdown_symbols
        self.goal_state = goal_state
        self.goal_strings = goal_strings

    def __str__(self):
        """
        String representation of rule.

        :return: Rule as a string.
        """
        pushdown_symbols = self.pushdown_symbols[0]
        for symbol in self.pushdown_symbols[1:]:
            pushdown_symbols = pushdown_symbols + " " + symbol
        goal_strings = self.goal_strings[0]
        for string in self.goal_strings[1:]:
            goal_strings = goal_strings + " " + string
        return self.source_state + " " + pushdown_symbols + " : " + self.goal_state + " " + goal_strings

    def __hash__(self):
        """
        Computes hash of rule.

        :return: Hash value.
        """
        strings = list_to_str(self.goal_strings)
        return hash((self.source_state, list_to_str(self.pushdown_symbols), self.goal_state, strings))

    def __eq__(self, other):
        """
        Implementation of rule equality.

        :param other: Other instance of PPDAExtractRule.
        :return: True if self and other rule are same one rule.
        """
        if not isinstance(other, type(self)):
            return False
        same_states = self.source_state == other.source_state and self.goal_state == other.goal_state
        same_symbols = self.pushdown_symbols == other.pushdown_symbols and self.goal_strings == other.goal_strings
        return same_states and same_symbols

    def __gt__(self, other):
        """
        Implementation of rule greater compare operation.

        :param other: Other instance of PPDAExtractRule.
        :return: True if self and other rule are same one rule.
        """
        return str(self) < str(other)


def __get_ppda_rule_from_string__(string: str) -> PPDAExpandRule:
    """
    Creates deep pushdown rule from given correct string.

    :param string: String containing correct deep pushdown rule
    :return: Representation of parallel deep pushdown rule
    """
    left, right = string.split(" : ", 2)
    left = left.split(" ")
    right = right.split(" ")
    return PPDAExpandRule(left[0], left[1:], right[0], right[1:])


def __create_ppda_rules__(rules: str,
                          states: [str],
                          nonterminals: [str],
                          pushdown: [str],
                          depth: int) -> [PPDAExpandRule]:
    """
    Creates expansion rules of parallel deep pushdown automaton.

    :param rules: String representation of automaton rules.
    :return: List of automaton rules.
    """
    split_rules = rules.split(", ")
    rules = list(map(__get_ppda_rule_from_string__, split_rules))
    __check_ppda_rules__(rules, states, nonterminals, pushdown, depth)
    return rules


def __check_ppda_rules__(rules: [PPDAExpandRule],
                         states: [str],
                         nonterminals: [str],
                         pushdown_alphabet: [str],
                         depth: int):
    """
    Checks whether rules comply to automata definition.

    :param rules: Expansion rules of automaton.
    :param states: Defined automaton states.
    :param nonterminals: Defined automaton nonterminals.
    :param pushdown_alphabet: Defined automaton pushdown alphabet.
    :param depth: Defined depth of automaton.
    """
    for rule in rules:
        if check_empty_intersection([rule.source_state], states):
            print("Error: Rule uses unknown source state!")
            exit(Error.FORMAL)
        if len(rule.pushdown_symbols) > depth:
            print("Error: Rule has too many source pushdown symbols!")
            exit(Error.FORMAL)
        if len(rule.goal_strings) > depth:
            print("Error: Rule has too many goal strings!")
            exit(Error.FORMAL)
        if len(rule.goal_strings) != len(rule.pushdown_symbols):
            print("Error: Number of source pushdown symbols is different than number of goal strings!")
            exit(Error.FORMAL)
        for pushdown_symbol in rule.pushdown_symbols:
            if check_empty_intersection([pushdown_symbol], nonterminals):
                print("Error: Rule uses unknown source pushdown symbol!")
                exit(Error.FORMAL)
        if check_empty_intersection([rule.goal_state], states):
            print("Error: Rule uses unknown goal state!")
            exit(Error.FORMAL)
        for goal_string in rule.goal_strings:
            if not check_string_over_set(pushdown_alphabet, goal_string):
                print("Error: Rule uses unknown pushdown symbol in final string!")
                exit(Error.FORMAL)


class PPDA:
    """
    Representation of parallel deep pushdown automata.
    """
    def __init__(self,
                 depth: int,
                 states: [str],
                 input_alphabet: [str],
                 nonterminals: [str],
                 expand_rules: [PPDAExpandRule],
                 start_state: str,
                 start_pushdown: str,
                 final_states: [str]):
        """
        Creates parallel deep pushdown automaton.

        :param depth: Depth of automaton.
        :param states: Automaton states.
        :param input_alphabet: Input alphabet.
        :param nonterminals: Non-input alphabet.
        :param expand_rules: List of deep pushdown expand rules.
        :param start_state: Starting state.
        :param start_pushdown: Starting pushdown symbol.
        :param final_states: List of final states.
        """
        self.depth = depth
        self.states = states
        self.input_alphabet = input_alphabet
        self.nonterminals = nonterminals
        self.expand_rules = expand_rules
        self.start_state = start_state
        self.start_pushdown = start_pushdown
        self.final_states = final_states
        self.states.sort()
        self.input_alphabet.sort()
        self.nonterminals.sort()
        self.expand_rules.sort()
        self.final_states.sort()


    @classmethod
    def from_input(cls, automaton: Automaton, start_pushdown_symbol: str, rules: str):
        """
        Creates parallel deep pushdown automata from basic automaton and string representing expansion rules.

        :param start_pushdown_symbol: Starting pushdown symbol.
        :param automaton: Parallel automaton representation.
        :param rules: String representation of expansion rules.
        :return: Parallel deep pushdown automaton.
        """
        pushdown = automaton.input_alphabet + automaton.nonterminals
        start_symbol = start_pushdown_symbol.replace("\n", "")
        if check_empty_intersection(automaton.nonterminals, [start_symbol]):
            print("Error: Unknown start pushdown symbol!")
            exit(Error.FORMAL)
        return PPDA(
            automaton.depth,
            automaton.states,
            automaton.input_alphabet,
            automaton.nonterminals,
            __create_ppda_rules__(rules, automaton.states, automaton.nonterminals, pushdown, automaton.depth),
            automaton.start_state,
            start_symbol,
            automaton.final_states
        )

    def __str__(self):
        """
        Creates string representation of parallel deep pushdown automata.

        :return: Parallel deep pushdown automaton as string.
        """
        states = list_to_str(self.states)
        input_alphabet = list_to_str(self.input_alphabet)
        pushdown_alphabet = input_alphabet + " " + list_to_str(self.nonterminals)
        alphabets = '\n' + input_alphabet + '\n' + pushdown_alphabet + '\n'
        single_elemets = self.start_state + '\n' + self.start_pushdown + '\n'
        final = list_to_str(self.final_states)
        rules = ""
        for rule in self.expand_rules:
            rules = rules + str(rule) + '\n'
        return str(self.depth) + '\n' + states + alphabets + single_elemets + final + '\n' + rules[:len(rules) - 1]
