import typing
import libsbml

from mathml.operator.basic import *
from mathml.operator.const import *
from mathml.operator.func import *
from mathml.operator.logical import *
from mathml.operator.relational import *


class MathMLParser:
    """
    This class is an AST parser of a MathML formule. It's able to parse a complete formula according 
    to :ref:`SBML ASTNode Type <http://model.caltech.edu/software/libsbml/5.18.0/docs/formatted/python-api/classlibsbml_1_1_a_s_t_node.html#a30a289823b56bfe39eefc3d5cf9db1da>`

    Attributes
    ----------
    __formula : str
        A string mathematical formula

    __ast_node : libsbml.ASTNode
        The ASTNode root for the AST of the formula

    __species : Dict[str, Specie]
        A dictionary of species

    __parameters : Dict[str, Parameter]
        A dictionary of parameters

    __compartments: Dic[str, Compartment] 
    
    __for_assignment : bool
        True if the math formula is for an assignment rule
    
    __for_function : bool
        True if the math formula is a function definition

    __BASIC_OPERATOR_DICT : Dict[int, BasicOperator]
        A mapping from an ASTNode type cose (an int) to a class 
        representing a basic operator

    __FUNCTION_OPERATOR_DICT : Dict[int, FunctionOperator]
        A mapping from an ASTNode type cose (an int) to a class 
        representing a functional operator

    __RELATIONAL_OPERATOR_DICT : Dict[int, Union[BasicRelational, FunctionOperator, BasicLogical]]
        A mapping from an ASTNode type cose (an int) to a class 
        representing a relational operator

    __LOGICAL_OPERATOR_DICT : Dict[int, Union[BasicLogical, Any]]
        A mapping from an ASTNode type cose (an int) to a class 
        representing a logical operator

    __CONSTANT_OPERATOR_DICT : Dict[int, BasicConstant]
        A mapping from an ASTNode type cose (an int) to a class 
        representing a constant value name
    """

    __BASIC_OPERATOR_DICT = {
        libsbml.AST_PLUS   : Plus,
        libsbml.AST_MINUS  : Minus,
        libsbml.AST_TIMES  : Times,
        libsbml.AST_DIVIDE : Divide
    }

    __FUNCTION_OPERATOR_DICT = {
        libsbml.AST_FUNCTION           : FunctionOperator,
        libsbml.AST_FUNCTION_FACTORIAL : FunctionOperator,
        libsbml.AST_POWER              : Power,
        libsbml.AST_FUNCTION_COS       : Cos,
        libsbml.AST_FUNCTION_COSH      : Cosh,
        libsbml.AST_FUNCTION_COT       : Cot,
        libsbml.AST_FUNCTION_COTH      : Coth,
        libsbml.AST_FUNCTION_CSC       : Csc,
        libsbml.AST_FUNCTION_CSCH      : Csch,
        libsbml.AST_FUNCTION_SEC       : Sec,
        libsbml.AST_FUNCTION_SECH      : Sech,
        libsbml.AST_FUNCTION_SIN       : Sin,
        libsbml.AST_FUNCTION_SINH      : Sinh,
        libsbml.AST_FUNCTION_TAN       : Tan,
        libsbml.AST_FUNCTION_TANH      : Tanh,
        libsbml.AST_FUNCTION_ARCCOS    : Acos,
        libsbml.AST_FUNCTION_ARCCOSH   : Acosh,
        libsbml.AST_FUNCTION_ARCCOT    : Acot,
        libsbml.AST_FUNCTION_ARCCOTH   : Acoth,
        libsbml.AST_FUNCTION_ARCCSC    : Acsc,
        libsbml.AST_FUNCTION_ARCCSCH   : Acsch,
        libsbml.AST_FUNCTION_ARCSIN    : Asin,
        libsbml.AST_FUNCTION_ARCSINH   : Asinh,
        libsbml.AST_FUNCTION_ARCSEC    : Asec,
        libsbml.AST_FUNCTION_ARCSECH   : Asech,
        libsbml.AST_FUNCTION_ARCTAN    : Atan,
        libsbml.AST_FUNCTION_ARCTANH   : Atanh,
        libsbml.AST_FUNCTION_ABS       : Abs,
        libsbml.AST_FUNCTION_CEILING   : Ceiling,
        libsbml.AST_FUNCTION_FLOOR     : Floor,
        libsbml.AST_FUNCTION_LN        : Ln,
        libsbml.AST_FUNCTION_LOG       : Log,
        libsbml.AST_FUNCTION_EXP       : Exp,
        libsbml.AST_FUNCTION_MAX       : Max,
        libsbml.AST_FUNCTION_MIN       : Min,
        libsbml.AST_FUNCTION_PIECEWISE : Piecewise,
        libsbml.AST_FUNCTION_REM       : Rem,
        libsbml.AST_FUNCTION_ROOT      : Root
    }

    __RELATIONAL_OPERATOR_DICT = {
        libsbml.AST_RELATIONAL_GT  : Gt,
        libsbml.AST_RELATIONAL_GEQ : Geq,
        libsbml.AST_RELATIONAL_LT  : Lt,
        libsbml.AST_RELATIONAL_LEQ : Leq,
        libsbml.AST_RELATIONAL_EQ  : Eq,
        libsbml.AST_RELATIONAL_NEQ : Neq
    }

    __LOGICAL_OPERATOR_DICT = {
        libsbml.AST_LOGICAL_AND     : And,
        libsbml.AST_LOGICAL_OR      : Or,
        libsbml.AST_LOGICAL_NOT     : Not,
        libsbml.AST_LOGICAL_XOR     : Xor,
        libsbml.AST_LOGICAL_IMPLIES : Implies
    }

    __CONSTANT_OPERATOR_DICT = {
        libsbml.AST_CONSTANT_E     : E,
        libsbml.AST_CONSTANT_PI    : Pi,
        libsbml.AST_CONSTANT_TRUE  : TrueConstant,
        libsbml.AST_CONSTANT_FALSE : FalseConstant,
        libsbml.AST_NAME_TIME      : Time,
        libsbml.AST_NAME_AVOGADRO  : Avogadro
    }

    def __init__(self, formula       : str,
                       species       : dict,
                       parameters    : dict,
                       compartments  : dict = dict(),
                       reactions     : dict = dict(),
                       for_assignment: bool = False,
                       for_function  : bool = False,
                       for_event     : bool = False,
                       reaction_name : str = ""
                ):

        settings = libsbml.L3ParserSettings()
        settings.setParseCollapseMinus(False)
        settings.setParseLog(libsbml.L3P_PARSE_LOG_AS_LN)

        self.__formula  = formula                                                      # A string mathematical formula
        self.__ast_node = libsbml.parseL3FormulaWithSettings(self.__formula, settings) # The ASTNode root for the AST of the formula

        self.__species      = species      # A dictionary of species
        self.__parameters   = parameters   # A dictionary of parameters
        self.__compartments = compartments # A dictionary of compartments
        self.__reactions    = reactions    # A dictionary of reactions

        self.__for_assignment = for_assignment # True if the math formula is for an assignment rule
        self.__for_function   = for_function   # True if the math formula is a function definition
        self.__for_event      = for_event      # True if the math formula is an event assignment

        self.__reaction_name  = reaction_name # The name of the reaction (if is different from "")

    def __str__(self) -> str:
        return obj2str(self)

    @classmethod
    def parse_mathml(cls, node          : libsbml.ASTNode, 
                          species       : dict, 
                          parameters    : dict,
                          compartments  : dict,
                          reactions     : dict,
                          for_assignment: bool,
                          for_event     : bool,
                          reaction_name : str = ""
                    ) -> str:
        """
        A class method which execute a recursive parsing of all the AST
        and return a string representing the relative math formula, well formatted.

        Parameters
        ----------
        cls : MathMLParser
            a MathMLParser instance

        node : libsbml.ASTNode
            an ASTNode object 

        species : typing.Dict[str, Specie]
            A dictionary of species

        parameters : typing.Dict[str, Parameter]
            A dictionary of parameters
    
        for_assignment : bool
            True if the math formula is for an assignment rule

        Returns
        -------
        str
            The relative math formula
        """
        # Recursively getting the formula for each child of the current ASTNode
        args = [cls.parse_mathml(node.getChild(i), species, parameters, compartments, reactions, for_assignment, for_event, reaction_name) for i in range(node.getNumChildren())]

        # If the current node is a basic operator, returns an instance of the relative class
        if node.getType() in cls.__BASIC_OPERATOR_DICT:

            if node.isUMinus():
                return f"(- {str(args[-1])})"

            return cls.__BASIC_OPERATOR_DICT[node.getType()](*args)
        
        # If the current node is a functional operator, returns an instance of the relative class
        if node.getType() in cls.__FUNCTION_OPERATOR_DICT:
            
            # If it's a user defined function
            if node.getType() == libsbml.AST_FUNCTION or node.getType() == libsbml.AST_FUNCTION_FACTORIAL:
                return FunctionOperator(node.getName(), *args)
            
            return cls.__FUNCTION_OPERATOR_DICT[node.getType()](*args)
        
        # If the current node is a relational operator, returns an instance of the relative class
        if node.getType() in cls.__RELATIONAL_OPERATOR_DICT:
            return cls.__RELATIONAL_OPERATOR_DICT[node.getType()](*args)

        # If the current node is a logical operator, returns an instance of the relative class
        if node.getType() in cls.__LOGICAL_OPERATOR_DICT:
            return cls.__LOGICAL_OPERATOR_DICT[node.getType()](*args)

        # If the current node is a constant operator, returns an instance of the relative class
        if node.getType() in cls.__CONSTANT_OPERATOR_DICT:
            return cls.__CONSTANT_OPERATOR_DICT[node.getType()](*args)

        # Otherwise is a number
        if node.isInteger():
            return node.getValue()
        
        if node.isReal():
            return node.getReal()

        # Otherwise is a specie, or a compartment or a parameter 
        if node.getName() in species:
            specie = species[node.getName()]

            if for_assignment:
                if specie.has_only_substance_units:
                    return specie._Specie__amount_name
                return Divide(specie._Specie__amount_name, specie._Specie__compartment.name)

            if for_event:
                if specie.has_only_substance_units:
                    return Pre(specie._Specie__amount_name)

                return Divide(Pre(specie._Specie__amount_name), Pre(specie._Specie__compartment.name))

            return Divide(specie._Specie__amount_name, specie._Specie__compartment.name)

        if reaction_name.upper() + "_" + node.getName() + "_local" in parameters:
            return parameters[reaction_name.upper() + "_" + node.getName() + "_local"].local_name

        if not node.getName() in parameters and node.getName() == "t":
            return Time()

        if node.getName() in parameters and for_event:
            if not parameters[node.getName()].is_constant:
                return Pre(node.getName())

        if node.getName() in compartments:
            if for_event and not compartments[node.getName()].is_constant:
                return Pre(node.getName())

        if node.getName() in reactions:
            return reactions[node.getName()].formula

        return node.getName()

    @property
    def for_assignment(self) -> bool:
        """ Return the value of the __for_assignment attribute """
        return self.__for_assignment

    @for_assignment.setter 
    def for_assignment(self, new_value: bool) -> None:
        """ Set a new value for the __for_assignment attribute """
        self.__for_assignment = new_value

    def parse(self) -> typing.Union[str, typing.Tuple[typing.List[str], str]]:
        """
        Execute the parsing and return a string, if the parsing wasn't
        for a function definition, otherwise returns a list of strings.

        Returns
        -------
        Union[str, Tuple[List[str], str]]
            A string or a list of strings.
        """
        if not self.__for_function:
            return self.parse_mathml(
                self.__ast_node, self.__species, self.__parameters, 
                self.__compartments, self.__reactions, self.__for_assignment, 
                self.__for_event, self.__reaction_name
            )

        inputs  = [self.__ast_node.getChild(i).getName() for i in range(self.__ast_node.getNumChildren() - 1)]
        formula = self.parse_mathml(self.__ast_node.getChild(self.__ast_node.getNumChildren() - 1), dict(), dict(), dict(), dict(), False, False)

        return (inputs, formula)