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

Represents basic deep pushdown automaton.
"""

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


class PDAExpandRule:
    """
    Representation of expansion rules of basic deep pushdown automata.
    """
    def __init__(self, depth: int, source_state: str, pushdown_symbol: str, goal_state: str, goal_string: str):
        """
        Creates rule of deep pushdown automaton.

        :param depth: Depth of automaton.
        :param source_state: Source state.
        :param pushdown_symbol: Source pushdown symbol.
        :param goal_state: Goal state.
        :param goal_string: Goal string.
        """
        self.depth = depth
        self.source_state = source_state
        self.pushdown_symbol = pushdown_symbol
        self.goal_state = goal_state
        self.goal_string = goal_string

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

        :return: Rule as a string.
        """
        source = str(self.depth) + " " + self.source_state + " " + self.pushdown_symbol
        return source + " " + self.goal_state + " " + self.goal_string

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

        :return: Hash value.
        """
        return hash((self.depth, self.source_state, self.pushdown_symbol, self.goal_state, self.goal_string))

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

        :param other: Other instance of PDAExtractRule.
        :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_symbol == other.pushdown_symbol and self.goal_string == other.goal_string
        return self.depth == other.depth and same_states and same_symbols

    def __gt__(self, other):
        """
        Implementation of rule greater than 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 __create_pda_rules__(rules: str,
                         depth: int,
                         states: [str],
                         nonterminals: [str],
                         pushdown_alphabet: [str]) -> [PDAExpandRule]:
    """
    Creates expansion rules of basic deep pushdown automaton.

    :param rules: String representation of automaton rules.
    :return: List of automaton rules.
    """
    split_rules = rules.split(",")
    if len(split_rules) > 0:
        rules = [split_rules[0]]
        for rule in split_rules[1:]:
            rules.append(rule[1:])
        rules = list(map(__get_pda_rule_from_string__, rules))
        __check_pda_rules__(rules, depth, states, nonterminals, pushdown_alphabet)
        return rules
    else:
        return []


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

    :param rules: Expansion rules of automaton.
    :param depth: Defined depth of automaton.
    :param states: Defined automaton states.
    :param nonterminals: Defined automaton nonterminals.
    :param pushdown_alphabet: Defined automaton pushdown alphabet.
    """
    for rule in rules:
        if rule.depth > depth:
            print("Error: Rule uses depth deeper than is allowed by automata!")
            exit(Error.FORMAL)
        if check_empty_intersection([rule.source_state], states):
            print("Error: Rule uses unknown source state!")
            exit(Error.FORMAL)
        if check_empty_intersection([rule.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)
        if not check_string_over_set(pushdown_alphabet, rule.goal_string):
            print(pushdown_alphabet, rule.goal_string)
            print("Error: Rule uses unknown final pushdown symbol!")
            exit(Error.FORMAL)


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

    :param string: String containing correct deep pushdown rule
    :return: Representation of deep pushdown rule
    """
    result = string.split(" ")
    return PDAExpandRule(int(result[0]), result[1], result[2], result[3], result[4])


class PDA:
    """
    Representation of deep pushdown automata.
    """
    def __init__(self,
                 depth: int,
                 states: [str],
                 input_alphabet: [str],
                 nonterminals: [str],
                 expand_rules: [PDAExpandRule],
                 start_state: str,
                 start_pushdown: str,
                 final_states: [str]):
        """
        Creates 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 deep pushdown automata from basic automaton and string representing expansion rules.

        :param start_pushdown_symbol: Starting pushdown symbol.
        :param automaton: Basic automaton representation.
        :param rules: String representation of expansion rules.
        :return: 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 PDA(
            automaton.depth,
            automaton.states,
            automaton.input_alphabet,
            automaton.nonterminals,
            __create_pda_rules__(rules, automaton.depth, automaton.states, automaton.nonterminals, pushdown),
            automaton.start_state,
            start_symbol,
            automaton.final_states
        )

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

        :return: 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]
