import sys

from core.levConfig import *
from core.classNode import Node
from core.classNameSpace import *

# A class for parsing modules
class Parser:
    #Parser constructor
    def __init__(self,p_lexer, p_namespace):
        # Expect lexer
        self.lex = p_lexer
        self.current_name_space = None # current section namespace
        self.module_name_space  = None # the namespace of module
        # Parser properties
        self.root      = Node()     # first node is AST
        self.node      = self.root  # current node
        self.token     = None       # current token
        self.tokens    = []         # tokens from one statement
        self.index     = -1         # current token index

        # Internal states
        self.region         = None  # current region type [import,type, V...]

        # todo cleanup
        # self.createChild    = False # we create first child
        # self.selectParent   = False # we do not use first parent

        #progress bar in regular mode (console)
        self.progress = 0  # progress barr
        self.scount   = 0  # source lines
        self.position = 0  # current position

        #if composition is defined, identifiers become members
        self.composition = None
        self.comp_stack  = []

        # indentation attributes
        self.expect_indent    = 0 # next token indent level

        # internal stack lists
        self.region_stack  = []  # stack of regions

        # most recent created identifier
        self.identifier = None

        # internal flags for consumer to know what to do
        self.start_child  =False;
        self.start_parent =False;

    # get last token of previous line
    def get_line(self, n):
        print(n)
        for t in reversed(self.tokens):
            print(t.lineIndex, t.cargo)
            if t.lineIndex<=n:
               c_token = t
               break
        if c_token:
           return c_token
        else:
            self.error("Can't find line %d !" % n)

    # find back a previous token n in statement
    def back(self, n=1):
        if self.index - n >=0 :
            c_token = self.tokens[self.index-n]
            return c_token
        else:
            return None

    # find current token == self.token
    def current(self):
        if self.index >=0 and self.index < len(self.tokens):
            c_token = self.tokens[self.index]
            return c_token
        else:
            return None

    # find ahead token in statement
    def ahead(self, n=1):
        if len(self.tokens) > self.index + n:
            c_token = self.tokens[self.index + n]
            return c_token
        else:
            return None

    # error message when token is present
    def error(self,msg):
        if self.token:
            self.token.abort(msg)
        else:
            raise ParserError(msg)

    # back error is using previous token
    def eback(self, msg, n=1):
        c_token = self.tokens[-n]
        if c_token:
            c_token.abort(msg)
        else:
            self.token.abort(msg)

    # previous line token error at the end of line
    def elast(self, msg):
        c_token = None
        line = self.token.lineIndex
        for t in reversed(self.tokens):
            if t.lineIndex<line:
               c_token = t
               break
        if c_token:
            c_token.abort(msg)
        else:
            self.token.abort(msg)

    # create a new region context
    def start_region(self, name=None):
        if self.region:
           self.region_stack.append(self.region)
        if name:
            self.region = name
        else:
            self.region = self.token.cargo

    # close a region context
    def close_region(self, name=None):
        if name and name != self.region:
            raise ParserError('Unexpected region:"%s" current region is "%s"' % (name,self.region))
        if len(self.region_stack)>0:
           self.region = self.region_stack.pop()
        else:
           self.region = None

    # check if region current region is ending,
    # region is ending when indentation is reduced or EOF
    def ending_region(self, name=None):
        if self.token.type == EOF:
            return True
        if self.under_indent():
            return True
        if name and self.region:
            return self.region != name


    # create a nameSpace based on a section type and section name
    # the name-space can have parent and owner is the section object
    # when section is a procedure or function must have owner
    def create_namespace(self, ns_section, ns_name=""):
        if ns_section=="module":
            # avoid duplicate name-spaces for modules
            # a module is not parsed two times even if imported
            if ns_name in config.symbolTable:
                self.error("Spacename: " + ns_name + " is duplicated!")
            elif ns_name in config.syntaxTree:
                self.error("Spacename: " + ns_name + " is duplicated!")
            else:
                # create new namespace into symbol table
                self.module_name_space  = NameSpace(None, ns_section, ns_name)
                self.current_name_space = self.module_name_space
                self.module = ns_name
                # add new created objects to "config"
                config.symbolTable[ns_name]  = self.current_name_space
                config.syntaxTree[ns_name]   = self.node
        elif self.current_name_space:
            # this is not a module so it can be a section
            # parent namespace exist. It is the current namespace
            if ns_name == "":
               # anonymous blocks get automatic name: module_name.line_number
               ns_name = self.module + '_' + str(self.token.lineIndex)
            # create child namespace and set it as current namespace
            self.current_name_space = NameSpace(self.current_name_space, ns_section, ns_name)
        else:
            raise ParserError("Undiefined program namespace!")

    # select parent nameSpace and make it current
    # we use parent_namespace every time we exit from a section or a local scope (let)
    def parent_namespace(self):
        assert self.current_name_space.parent, "Internal error: Parent namespace not found!"
        self.current_name_space = self.current_name_space.parent

    # select any name-space and make this namespace current
    def use_namespace(self, ns_identifier):
        # the namespace is push to stack
        #search for a child name-space
        ns_name = ns_identifier.name
        if ns_name in self.current_name_space.children:
            nameSpace = self.current_name_space.children[ns_name]
        elif ns_identifier.owner.name in config.symbolTable:
            moduleSpace = config.symbolTable[ns_identifier.owner.name]
            if ns_name in moduleSpace.children:
              nameSpace = moduleSpace.children[ns_name]
            else:
              self.error('Namespace "%s" not found!' % ns_name)
        else:
            self.error('Namespace "%s" not found!' %ns_name)
        return nameSpace


    # check for identifier existence
    def find_identifier(self, i_name):
        """
        if not self.check_directive('type:library'):
            assert self.token.isElement(IDENTIFIER), ('Expect identifier, got: "%s"' % self.token.cargo)
        elif not self.token.isElement(IDENTIFIER):
            assert self.token.isOneOf(setBuiltIn), ('Expect built-in, got: "%s"' % self.token.cargo)
        """
        # try to find identifier as in local scope or parent scope
        result = self.current_name_space.find_identifier(i_name)
        # try to find identifier as import from another module
        if not result:
            ## todo establish qualifier
            result = self.module_name_space.find_imported(None, i_name)
        if not result:
            self.token.abort('Undefined identifier:"%s" in namespace:"%s" ' % (i_name,self.current_name_space.name))
        return result

    # find identifier member of composite type
    def find_member(self, identifier, m_name=None):
        assert self.token.isElement(IDENTIFIER), ('Expect identifier, got: "%s"' % self.token.cargo)
        if m_name:
            member = identifier.get_member(m_name)
        else:
            member = identifier.get_member(self.token.cargo)
        if not member:
            self.token.abort('Undefined member:"%s" for identifier:"%s" ' % (self.token.cargo, name))
        return member

    # set a composition (record, tuple)
    def set_composition(self, identifier):
        if identifier.type not in setComposition:
            self.error("Identifier is not a composition!"+identifier.name)
        if self.composition:
            self.comp_stack.append(identifier)
        self.composition = identifier


    # end composition (record, tuple)
    def end_composition(self):
        if len(self.comp_stack)>0:
            self.composition = self.comp_stack.pop()
        else:
            self.composition = None

    # create an identifier depending on set_composition
    # name can be None and then is generated by addMember
    def add_identifier(self, p_name):
        if self.composition:
            identifier = self.composition.add_member(p_name)
        else:
            if p_name:
                name=p_name
            else:
                name=self.token.cargo
            # check if an identifier is re-defined
            if name in self.current_name_space.identifiers:
                redef=self.current_name_space.identifiers[name]
                self.token.abort('Identifier name:"%s" already defined as: "%s"!' % (name, redef.category))
            # create different categories by region
            if self.region == 'params':
                identifier = self.current_name_space.add_parameter(name)
            elif self.region == 'type':
                identifier = self.current_name_space.add_identifier(name, CA_TYPE)
            elif self.region == 'constant':
                identifier = self.current_name_space.add_identifier(name, CA_CONSTANT)
            elif self.region == 'implement':
                identifier = self.current_name_space.add_identifier(name, CA_METHOD)
            else:
                # default region: a variable is created
                identifier = self.current_name_space.add_identifier(name, CA_VARIABLE)
        self.identifier = identifier
        return identifier

    # import the symbols from another module if is parsed
    def import_module(self, identifier, module_name, use_alias):
        moduleNameSpace = config.symbolTable[module_name]
        identifier.import_members(moduleNameSpace, use_alias)
        return None

    # create directive
    def add_directive(self, directive, value):
        if self.current_name_space:
           self.current_name_space.add_directive(directive, value)

    # check direct directive
    def has_directive(self, directive):
        if self.current_name_space:
            return self.current_name_space.has_directive(directive)
        else:
            return False

    # check all parent directives
    def check_directive(self, directive):
        found = False
        if self.current_name_space:
            ns = self.current_name_space
            while ns and not found:
                found = ns.has_directive(directive)
                ns = ns.parent
        return found

    # set start expression (increase AST level)
    def set_child(self):
        self.start_child=True;

    # move back to previous level in AST
    def set_parent(self):
        self.node = self.node.parent
        self.start_parent=True;


    # --------------------------------------------------------------------
    #  create a progress bar for each token
    # --------------------------------------------------------------------
    def progress(self):
       self.progress += 1
       new_position   = math.ceil(self.progress * console.con_with / self.scount)
       if new_position > self.position:
          print('#',end='',flush=True)
          self.position=new_position

    # --------------------------------------------------------------------
    # read tokens ahead one statement until ";" is found
    # --------------------------------------------------------------------
    def read_ahead(self):
        # remove older tokens
        self.tokens = []
        # ask for next token from lexer
        while True:
           token = next(self.lex)
           if token.isSymbol(';') or token.type==EOF:
              # end of statement ";" is skipped
              break
           else:
              self.tokens.append(token)
        # check if any token is read
        if len(self.tokens) == 0: return
        # assign first token as current token
        self.index = 0
        self.token = self.tokens[0]
        #display all tokens for current statement
        if config.mode == "trace":
           for token in self.tokens:
               print(token.show(align=True))
        sys.stdout.flush()
        #display the progress bar
        if config.mode=="regular" and config.driver=="console":
           self.progress()


    # --------------------------------------------------------------------
    # store current token in AST Node and read next token
    # --------------------------------------------------------------------
    def consume(self):
        # decide how to add token to AST
        if self.token.isSymbol(';'):
            # In this case we do not add a node
            self.start_child   = False
            self.start_parent  = False
            # Statement end --> reset node
            self.node = self.root
        elif self.start_parent:
            self.node = self.node.addNode(self.token)
            self.start_parent  = False
        elif self.start_child:
            self.node = self.node.addChild(self.token)
            self.start_child = False;
        else:
            self.node = self.node.addNode(self.token)
        # get next token from buffer
        self.index += 1
        if self.index >= len(self.tokens):
           self.read_ahead()
        else:
           self.token = self.tokens[self.index]

        # node created for previous token
    #end read_more

    #Read token cargo
    def cargo(self):
        return self.token.cargo;

    #increase indentation level
    def indent(self, delta=1):
        self.expect_indent+=(2 * delta)

    #reduce indentation level
    def reduce(self, delta=1):
        if self.expect_indent>0:
           self.expect_indent-=(2 * delta)

    #check indent level for child elements
    def check_indent(self):
        assert self.token, "Internal error: Token not found"
        if self.token.indent > -1:
           if self.token.indent != self.expect_indent:
              self.error('Expect indent: %d used: %d' % (self.expect_indent, self.token.indent))

    #check if indent is less then expected indent
    def under_indent(self):
        assert self.token, "Internal error: Token not found"
        if self.token.indent > -1:
           return (self.token.indent < self.expect_indent)
        else:
           return False

    #check if indent is over the expected indent
    def over_indent(self):
        assert self.token, "Internal error: Token not found"
        if self.token.indent > -1:
           return (self.token.indent > self.expect_indent)
        else:
           return False

    # check if indent is same with expected indent
    def same_indent(self):
        assert self.token, "Internal error: Token not found"
        if self.token.indent > -1:
           return (self.token.indent == self.expect_indent)
        else:
            return False

    # verify if current token is a symbol
    def expect_symbol(self, argSymbol):
        if not self.token.isSymbol(argSymbol):
            self.error('Expected symbol: "%s" after "%s"' % (argSymbol, self.token.cargo))

    # verify type of current token
    def expect_element(self, argTokenType):
        if not self.token.isElement(argTokenType):
            self.error('Expect "%s" found "%s"' % (argTokenType, self.token.type))

    # verify if current token is an identifier
    def expect_identifier(self, p_string):
        if not self.token.isIdentifier(p_string):
            self.error('Expect "%s" found "%s"' % (p_string, self.token.cargo))


    # verify if current token is a keyword
    def expect_keyword(self, expectedKeyword):
        if not self.token.isKeyword(expectedKeyword):
            self.error('Expected keyword:"%s" found: "%s"' % (expectedKeyword, self.token.cargo))

    # verify if current token is member of a set
    def expect_oneof(self, argTokenSet):
        if not self.token.isOneOf(argTokenSet):
            self.error('Expected one of: %s found "%s"' % (sorted(argTokenSet), self.token.cargo))


    # Set type for an identifier
    def set_identifier_type(self, identifier, p_type):
        # verify type to be composition
        if identifier.category == CA_TYPE and p_type not in setComposition:
            self.error("You can't redefine a native type!")
        else:
            identifier.set_type(p_type)

    # Determine type category
    def get_expression_type(self, identifier):
        x_type = identifier.get_type()
        # corespondent variable type
        if x_type in {"integer", "real", "decimal", "rational", "number"}:
           x_type = NUMERIC
        return x_type

    # Determine type category
    def get_element_type(self, identifier):
        # base type
        x_type = identifier.of_type;
        # corespondent variable type
        if x_type in {"integer", "real", "decimal", "rational", "number"}:
            x_type = NUMERIC
        return x_type

    # ----------------------------------------------
    # Check if expression match the identifier type
    # ----------------------------------------------
    def type_match(self, v_type, e_type):
        if v_type not in ExpressionType:
            self.error("Unexpected variable type:"+str(v_type))
        if e_type not in ExpressionType:
            self.error("Unexpected expression type:" + str(e_type))
        # do type match?
        if v_type == e_type:
            return True
        else:
            return False

# END OF CLASS PARSER