#
# (c) 2008-2020 Matthew Shaw
#

import sys
import os
import re
import logging

import nelly

from .scanner import Scanner
from .program import Program
from .types   import *


class Parser(object):
    def __init__(self, include_dirs=[]):
        self.include_dirs = include_dirs + [ os.path.join(nelly.root, 'grammars') ]
        self.pwd = []

        # setup the scanner based on the regular expressions
        self.scanner = Scanner(os.path.join(nelly.root, 'rules.lex'))

        # container for the compiled program
        self.program = Program()

        self.tokens_stack = []

        self.groups_stack = []
        self.group_stack  = []
        self.groups = None
        self.group  = None

    def Parse(self, grammarFile):
        grammar = grammarFile.read()

        self.pwd.append(os.path.dirname(grammarFile.name))

        logging.debug('Parsing %s (%d bytes)', grammarFile.name, len(grammar))

        self.tokens = self.scanner.Scan(grammar)

        # keep a reference to the tokens for when included files are parsed
        self.tokens_stack.append(self.tokens)

        # iterate over all the tokens
        while self.tokens:
            (token,value,line,col) = self.tokens.Next()

            # handle all the top-level tokens
            if 'nonterminal' == token:
                if value.startswith('::'):
                    value = value[2:]
                self._nonterminal(Types.NONTERMINAL, value)
            elif 'varterminal' == token:
                if value.startswith('::'):
                    value = value[2:]
                self._nonterminal(Types.VARTERMINAL, value)
            elif 'include' == token:
                self._include()
            elif 'start_python_code' == token:
                if r'<%pre' == value:
                    self.program.preamble.append(self._python_code('pre'))
                elif r'<%post' == value:
                    self.program.postscript.append(self._python_code('post'))
                else:
                    raise nelly.error('Please specify pre or post in code section')
            elif 'start_comment' == token:
                self._comment()
            else:
                raise nelly.error('Unhandled %s %s at %d:%d', token, repr(value), line, col)

        self.tokens_stack.pop()
        return self.program

    def _nonterminal(self, _type, name):
        # create a new container and add it to the program
        nonterminal = Nonterminal(_type, name)
        self.program.nonterminals[name] = nonterminal

        (token,value,line,col) = self.tokens.Next()

        # parse any optional arguments for the non-terminal
        if 'lparen' == token:
            while True:
                (token,value,line,col) = self.tokens.Next()
                if 'rparen' == token:
                    break
                elif 'comma' == token:
                    continue
                elif 'option' == token:
                    nonterminal.options.append(value)
                    if value == 'start':
                        self.program.start.append(name)
                elif 'decorator' == token:
                    nonterminal.decorators.append(value[1:])
                else:
                    raise nelly.error('Unknown option: %s %s', token, value)
            (token,value,line,col) = self.tokens.Next()

        if 'colon' != token:
            raise nelly.error('Parse error, missing colon at line %d, column %d', line, col)

        # parse zero or more expressions until a semicolon is found
        self._expressions('pipe', 'semicolon', nonterminal)

    def _expressions(self, delimiter, sentinel, nonterminal):
        (token,value,line,col) = self.tokens.Peek()
        expression = Expression((line,col))

        while self.tokens:
            (token,value,line,col) = self.tokens.Next()

            if sentinel == token:
                nonterminal.expressions.append(expression)
                break
            elif delimiter == token:
                nonterminal.expressions.append(expression)
                expression = Expression((line,col))
            elif 'lparen' == token:
                anonterminal = Nonterminal(Types.ANONYMOUS)
                expression.Statement(Types.ANONYMOUS, anonterminal)
                self._expressions('pipe', 'rparen', anonterminal)
            elif token in ['start_single_quote', 'start_double_quote', 'start_triple_quote']:
                quote = self._quote()
                expression.Statement(Types.TERMINAL, quote)
            elif token in ['start_single_bytes', 'start_double_bytes', 'start_triple_bytes']:
                byte_quote = self._quote()
                expression.Statement(Types.TERMINAL, byte_quote)
            elif 'nonterminal' == token:
                expression.Statement(Types.NONTERMINAL, value)
            elif 'varterminal' == token:
                expression.Statement(Types.VARTERMINAL, value)
            elif 'backref' == token:
                expression.Statement(Types.BACKREFERENCE, value)
            elif 'function' == token:
                functerminal = Nonterminal(Types.ANONYMOUS)
                self._expressions('comma', 'rparen', functerminal)
                expression.Statement(Types.FUNCTION, value[1:], functerminal)
            elif 'reference' == token:
                expression.Statement(Types.REFERENCE, value[1:])
            elif 'constant' == token:
                expression.Statement(Types.TERMINAL, value)
            elif 'start_python_code' == token:
                expression.code = self._python_code(nonterminal.name)
            elif 'lbracket' == token:
                try:
                    expression.Operation(Types.SLICE, self._slice())
                except IndexError:
                    raise nelly.error('Applying slice to nothing at line %d, column %d', line, col)
            elif 'lcurley' == token:
                try:
                    expression.Operation(Types.RANGE, self._range())
                except IndexError:
                    raise nelly.error('Applying range to nothing at line %d, column %d', line, col)
            elif 'langle' == token:
                expression.Weight(self._weight())
            elif 'empty' == token:
                pass
            else:
                raise nelly.error('Unhandled token "%s" at line %d, column %d', token, line, col)

    def _quote(self):
        # this will always be the quoted value
        (token,value,line,col) = self.tokens.Next()
        # this will always be the terminal quote
        self.tokens.Next()
        return value

    #
    # Slice a string
    #
    def _slice(self):
        front = None
        back  = None
        start = False

        (token,value,line,col) = self.tokens.Next()
        if 'constant' == token:
            front = value
            start = True
            (token,value,line,col) = self.tokens.Next()

        if 'rbracket' == token:
            if False == start:
                raise nelly.error('Empty slice at line %d, column %d', line, col)
            return (front,front+1)

        elif 'colon' != token:
            raise nelly.error('Missing colon at line %d, column %d', line, col)

        (token,value,line,col) = self.tokens.Next()
        if 'constant' == token:
            back = value
            (token,value,line,col) = self.tokens.Next()
        elif 'rbracket' != token:
            raise nelly.error('Missing ] at line %d, column %d', line, col)

        return (front,back)

    #
    # Repeat a range
    #
    def _range(self):
        lower = 0
        upper = 0

        (token,value,line,col) = self.tokens.Next()
        if 'constant' != token:
            raise nelly.error('Missing range at line %d, column %d', line, col)

        lower = value
        upper = value

        (token,value,line,col) = self.tokens.Next()
        if 'rcurley' == token:
            return (lower,upper)
        elif 'comma' != token:
            raise nelly.error('Missing comma at line %d, column %d', line, col)

        (token,value,line,col) = self.tokens.Next()
        if 'constant' == token:
            upper = value
        else:
            raise nelly.error('Missing range at line %d, column %d', line, col)

        (token,value,line,col) = self.tokens.Next()
        if 'rcurley' != token:
            raise nelly.error('Missing } at line %d, column %d', line, col)

        if lower > upper:
            lower,upper = upper,lower

        return (lower,upper)

    def _weight(self):
        (token,value,line,col) = self.tokens.Next()
        if 'constant' != token:
            raise nelly.error('Missing weight at line %d, column %d', line, col)
        (token,ignore,line,col) = self.tokens.Next()
        if 'rangle' != token:
            raise nelly.error('Missing > at %d, column %d', line, col)
        return value

    #
    # Compile the Python into a code object
    #
    def _python_code(self, name):
        (token,value,line,col) = self.tokens.Next()

        values = [s for s in value.split('\n') if s.strip()] or ['']

        # save the whitepsace of the first line
        ws = re.compile(r'\s*').match(values[0]).group()

        # check indentation
        if [s for s in values if not s.startswith(ws)]:
            raise nelly.error('Bad indentation in code block at line %d, column %d', line, col)

        # strip and rejoin the code
        codeblock = '\n'.join(s[len(ws):] for s in values)

        # eat the end_python_code token
        self.tokens.Next()

        try:
            return compile(codeblock, '<'+name+'>', 'exec')
        except SyntaxError as e:
            raise nelly.error('%d: %s: %s', e.lineno, e.msg, repr(e.text))

    #
    # Include other BNF files
    #
    def _include(self):
        (token,value,line,col) = self.tokens.Next()

        # file names are quoted
        if token not in ['start_single_quote', 'start_double_quote', 'start_triple_quote']:
            raise nelly.error('quoted file path expected')

        # get the quoted value
        path = self._quote()

        # try opening the file in each include directory, ignore errors
        content = None
        for include_dir in self.pwd[-1:] + self.include_dirs:
            try:
                fullpath = os.path.join(include_dir, path)
                content = open(fullpath, 'r')
                logging.debug('Including file %s', repr(fullpath))
                break
            except:
                continue

        # if no file was found, throw an error
        if None == content:
            raise nelly.error('Could not load file %s', repr(path))

        # ignore empty file
        if not content:
            return

        # compile it inline
        self.Parse(content)
        self.pwd.pop()

        # restore the current tokens
        self.tokens = self.tokens_stack[-1]

    #
    # Multi-line comments
    #
    def _comment(self):
        # consume and disregard the tokens
        while True:
            (token,value,line,col) = self.tokens.Next()
            if 'start_comment' == token:
                self._comment()
            if 'end_comment' == token:
                return

[((23, 31, 23, 68), 'os.path.join', 'os.path.join', ({(23, 44, 23, 54): 'nelly.root', (23, 56, 23, 67): '"""rules.lex"""'}, {}), "(nelly.root, 'rules.lex')", False, 'import os\n'), ((38, 24, 38, 57), 'os.path.dirname', 'os.path.dirname', ({(38, 40, 38, 56): 'grammarFile.name'}, {}), '(grammarFile.name)', False, 'import os\n'), ((103, 18, 103, 92), 'nelly.error', 'nelly.error', ({(103, 30, 103, 80): '"""Parse error, missing colon at line %d, column %d"""', (103, 82, 103, 86): 'line', (103, 88, 103, 91): 'col'}, {}), "('Parse error, missing colon at line %d, column %d', line, col)", False, 'import nelly\n'), ((211, 18, 211, 79), 'nelly.error', 'nelly.error', ({(211, 30, 211, 67): '"""Missing range at line %d, column %d"""', (211, 69, 211, 73): 'line', (211, 75, 211, 78): 'col'}, {}), "('Missing range at line %d, column %d', line, col)", False, 'import nelly\n'), ((226, 18, 226, 79), 'nelly.error', 'nelly.error', ({(226, 30, 226, 67): '"""Missing range at line %d, column %d"""', (226, 69, 226, 73): 'line', (226, 75, 226, 78): 'col'}, {}), "('Missing range at line %d, column %d', line, col)", False, 'import nelly\n'), ((230, 18, 230, 75), 'nelly.error', 'nelly.error', ({(230, 30, 230, 63): '"""Missing } at line %d, column %d"""', (230, 65, 230, 69): 'line', (230, 71, 230, 74): 'col'}, {}), "('Missing } at line %d, column %d', line, col)", False, 'import nelly\n'), ((240, 18, 240, 80), 'nelly.error', 'nelly.error', ({(240, 30, 240, 68): '"""Missing weight at line %d, column %d"""', (240, 70, 240, 74): 'line', (240, 76, 240, 79): 'col'}, {}), "('Missing weight at line %d, column %d', line, col)", False, 'import nelly\n'), ((243, 18, 243, 70), 'nelly.error', 'nelly.error', ({(243, 30, 243, 58): '"""Missing > at %d, column %d"""', (243, 60, 243, 64): 'line', (243, 66, 243, 69): 'col'}, {}), "('Missing > at %d, column %d', line, col)", False, 'import nelly\n'), ((259, 18, 259, 95), 'nelly.error', 'nelly.error', ({(259, 30, 259, 83): '"""Bad indentation in code block at line %d, column %d"""', (259, 85, 259, 89): 'line', (259, 91, 259, 94): 'col'}, {}), "('Bad indentation in code block at line %d, column %d', line, col)", False, 'import nelly\n'), ((280, 18, 280, 58), 'nelly.error', 'nelly.error', ({(280, 30, 280, 57): '"""quoted file path expected"""'}, {}), "('quoted file path expected')", False, 'import nelly\n'), ((19, 45, 19, 81), 'os.path.join', 'os.path.join', ({(19, 58, 19, 68): 'nelly.root', (19, 70, 19, 80): '"""grammars"""'}, {}), "(nelly.root, 'grammars')", False, 'import os\n'), ((187, 22, 187, 81), 'nelly.error', 'nelly.error', ({(187, 34, 187, 69): '"""Empty slice at line %d, column %d"""', (187, 71, 187, 75): 'line', (187, 77, 187, 80): 'col'}, {}), "('Empty slice at line %d, column %d', line, col)", False, 'import nelly\n'), ((191, 18, 191, 79), 'nelly.error', 'nelly.error', ({(191, 30, 191, 67): '"""Missing colon at line %d, column %d"""', (191, 69, 191, 73): 'line', (191, 75, 191, 78): 'col'}, {}), "('Missing colon at line %d, column %d', line, col)", False, 'import nelly\n'), ((198, 18, 198, 75), 'nelly.error', 'nelly.error', ({(198, 30, 198, 63): '"""Missing ] at line %d, column %d"""', (198, 65, 198, 69): 'line', (198, 71, 198, 74): 'col'}, {}), "('Missing ] at line %d, column %d', line, col)", False, 'import nelly\n'), ((220, 18, 220, 79), 'nelly.error', 'nelly.error', ({(220, 30, 220, 67): '"""Missing comma at line %d, column %d"""', (220, 69, 220, 73): 'line', (220, 75, 220, 78): 'col'}, {}), "('Missing comma at line %d, column %d', line, col)", False, 'import nelly\n'), ((289, 27, 289, 58), 'os.path.join', 'os.path.join', ({(289, 40, 289, 51): 'include_dir', (289, 53, 289, 57): 'path'}, {}), '(include_dir, path)', False, 'import os\n'), ((255, 13, 255, 31), 're.compile', 're.compile', ({(255, 24, 255, 30): '"""\\\\s*"""'}, {}), "('\\\\s*')", False, 'import re\n'), ((99, 26, 99, 76), 'nelly.error', 'nelly.error', ({(99, 38, 99, 61): '"""Unknown option: %s %s"""', (99, 63, 99, 68): 'token', (99, 70, 99, 75): 'value'}, {}), "('Unknown option: %s %s', token, value)", False, 'import nelly\n'), ((68, 26, 68, 83), 'nelly.error', 'nelly.error', ({(68, 38, 68, 82): '"""Please specify pre or post in code section"""'}, {}), "('Please specify pre or post in code section')", False, 'import nelly\n'), ((151, 26, 151, 99), 'nelly.error', 'nelly.error', ({(151, 38, 151, 87): '"""Applying slice to nothing at line %d, column %d"""', (151, 89, 151, 93): 'line', (151, 95, 151, 98): 'col'}, {}), "('Applying slice to nothing at line %d, column %d', line, col)", False, 'import nelly\n'), ((156, 26, 156, 99), 'nelly.error', 'nelly.error', ({(156, 38, 156, 87): '"""Applying range to nothing at line %d, column %d"""', (156, 89, 156, 93): 'line', (156, 95, 156, 98): 'col'}, {}), "('Applying range to nothing at line %d, column %d', line, col)", False, 'import nelly\n'), ((162, 22, 162, 97), 'nelly.error', 'nelly.error', ({(162, 34, 162, 78): '"""Unhandled token "%s" at line %d, column %d"""', (162, 80, 162, 85): 'token', (162, 87, 162, 91): 'line', (162, 93, 162, 96): 'col'}, {}), '(\'Unhandled token "%s" at line %d, column %d\', token, line, col)', False, 'import nelly\n')]