from enum import Enum, auto

from famapy.core.transformations import TextToModel

from famapy.metamodels.pysat_metamodel.models.pysat_model import PySATModel


class LogicOperator(Enum):
    NOT = auto()
    AND = auto()
    OR = auto()


class CNFNotation(Enum):
    LOGICAL = {LogicOperator.NOT: '¬', LogicOperator.AND: '∧', LogicOperator.OR: '∨'}
    JAVA = {LogicOperator.NOT: '!', LogicOperator.AND: '&&', LogicOperator.OR: '||'}
    SHORT = {LogicOperator.NOT: '-', LogicOperator.AND: '&', LogicOperator.OR: '|'}
    TEXTUAL = {LogicOperator.NOT: 'not', LogicOperator.AND: 'and', LogicOperator.OR: 'or'}


def identify_notation(cnf_formula: str) -> CNFNotation:
    for notation in CNFNotation:
        for symbol in notation.value.values():
            symbol_pattern = ' ' + symbol + ' '
            if symbol_pattern in cnf_formula:
                return notation
    raise Exception("This should have happend. \
            The CNF file is corrupted or does not adhere to the format") 


class CNFReader(TextToModel):
    """
    Read a CNF formula as a string representing a feature model.

    The expected format is the generated by FeatureIDE when exporting the model as CNF (.txt).
    That generates a file with the CNF formula in four different notations:
        Logical Symbols:
            (A) ∧ (¬B ∨ C) ∧ ...
        Textual Symbols:
            (A) and (not B or C) and ...
        Java Symbols:
            (A) && (!B || C) && ...
        Short Symbols:
            (A) & (-B | C) & ...
    This class is able to read any of these notations, but only one notation,
    so the .txt file should be modified to include only 
    one of those notations by removing the others.
    """

    @staticmethod
    def get_source_extension() -> str:
        return 'txt'

    def __init__(self, path: str):
        self._path = path
        self.counter = 1
        self.destination_model = PySATModel()
        self.cnf = self.destination_model.cnf

    def transform(self) -> PySATModel:
        self._read_clauses()
        return self.destination_model

    def _add_feature(self, feature_name):
        if feature_name not in self.destination_model.variables.keys():
            self.destination_model.variables[feature_name] = self.counter
            self.destination_model.features[self.counter] = feature_name
            self.counter += 1

    def _read_cnf_formula(self) -> str:
        """It assumes the CNF formula is defined in one line in the file."""
        with open(self._path, encoding='utf-8') as file:
            cnf_formula = file.readline()
        return cnf_formula

    def _read_clauses(self):
        cnf_formula = self._read_cnf_formula()
        cnf_notation = identify_notation(cnf_formula)

        and_symbol_pattern = ' ' + cnf_notation.value[LogicOperator.AND] + ' '
        clauses = list(map(lambda c: c[1:len(c) - 1], cnf_formula.split(and_symbol_pattern)))  
        # Remove initial and final parenthesis

        # Remove final parenthesis of last clause (because of the possible end of line: '\n')
        if ')' in clauses[len(clauses) - 1]:
            clauses[len(clauses) - 1] = clauses[len(clauses) - 1][:-1]  

        for _c in clauses:
            tokens = _c.split(' ')
            tokens = list(filter(lambda t: t != cnf_notation.value[LogicOperator.OR], tokens))
            logic_not = False
            cnf_clause = []
            for feature in tokens:
                if feature == cnf_notation.value[LogicOperator.NOT]:
                    logic_not = True
                elif feature.startswith(cnf_notation.value[LogicOperator.NOT]):
                    feature = feature.replace(cnf_notation.value[LogicOperator.NOT], '', 1)
                    self._add_feature(feature)
                    cnf_clause.append(-1 * self.destination_model.variables[feature])
                else:
                    self._add_feature(feature)
                    if logic_not:
                        cnf_clause.append(-1 * self.destination_model.variables[feature])
                    else:
                        cnf_clause.append(self.destination_model.variables[feature])
                    logic_not = False
            self.destination_model.add_constraint(cnf_clause)

    def get_cnf_formula(self, cnf_output_syntax: CNFNotation = CNFNotation.JAVA) -> str:
        cnf_formula = self._read_cnf_formula()
        cnf_notation = identify_notation(cnf_formula)

        if cnf_output_syntax == cnf_notation:
            return cnf_formula
        # Translate AND operators
        symbol_pattern = ' ' + cnf_notation.value[LogicOperator.AND] + ' '
        new_symbol = ' ' + cnf_output_syntax.value[LogicOperator.AND] + ' '
        cnf_formula = cnf_formula.replace(symbol_pattern, new_symbol)

        # Translate OR operators
        symbol_pattern = ' ' + cnf_notation.value[LogicOperator.OR] + ' '
        new_symbol = ' ' + cnf_output_syntax.value[LogicOperator.OR] + ' '
        cnf_formula = cnf_formula.replace(symbol_pattern, new_symbol)

        # Translate NOT operators (this is more complex because 
        # the symbol may be part of a feature's name)
        if cnf_notation == CNFNotation.TEXTUAL:
            symbol_pattern = cnf_notation.value[LogicOperator.NOT] + ' '
            new_symbol = cnf_output_syntax.value[LogicOperator.NOT]
            cnf_formula = cnf_formula.replace(symbol_pattern, new_symbol)
        elif cnf_output_syntax == CNFNotation.TEXTUAL:
            symbol_pattern = ' ' + cnf_notation.value[LogicOperator.NOT]
            new_symbol = ' ' + cnf_output_syntax.value[LogicOperator.NOT] + ' '
            cnf_formula = cnf_formula.replace(symbol_pattern, new_symbol)

            symbol_pattern = '(' + cnf_notation.value[LogicOperator.NOT]
            new_symbol = '(' + cnf_output_syntax.value[LogicOperator.NOT] + ' '
            cnf_formula = cnf_formula.replace(symbol_pattern, new_symbol)
        else:
            symbol_pattern = ' ' + cnf_notation.value[LogicOperator.NOT]
            new_symbol = ' ' + cnf_output_syntax.value[LogicOperator.NOT]
            cnf_formula = cnf_formula.replace(symbol_pattern, new_symbol)

            symbol_pattern = '(' + cnf_notation.value[LogicOperator.NOT]
            new_symbol = '(' + cnf_output_syntax.value[LogicOperator.NOT]
            cnf_formula = cnf_formula.replace(symbol_pattern, new_symbol)
        return cnf_formula