# -*- coding: UTF-8 -*-
#    This file is part of arena.
#   
#    arena is free software: you can redistribute it and/or modify it
#    under the terms of the GNU General Public License as published by the
#    Free Software Foundation; either version 3 of the License or, at your
#    option, any later version.
#
#    arena is distributed in the hope that it will be useful, but
#    WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABLITY
#    or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
#    License for more details.
#
#    You should have received a copy of the GNU General Public License along
#    with arena.  If not, see <http://www.gnu.org/licenses/>. 

## @file
#  This file defines a class that parses a source stream.  The class constructs
#  its own scanner for that purpose. 

from Library.Base.Events import ALoggingEventfulObject
from Library.Parse.Errors import AFatalError, ASyntaxError, AHint, AWarning, \
  ANote
from Library.Parse.Lex.Languages import TokenType

## Events generated by a parser
prsrEvents = {
    ## Before a syntax error is logged
    "syntaxError": None,
    ## Before a fatal error is logged
    "fatalError": None,
    ## Before a hint is logged
    "hint": None,
    ## Before a warning is logged
    "warning": None,
    ## Before a note is logged
    "note": None,
    ## Before the current token is parsed
    "parseCurrentToken": None,
    ## Before a block of tokens is parsed
    "parseUntil": None,
    ## Before parsing begins
    "parse": None,
    ## Before peeking at the next token from the source scanner
    "peekToken": None,
    ## Before retrieving the next token from the source scanner
    "nextToken": None,
    ## Before skipping to a token
    "skipTo": None,
    ## Before resynchronizing
    "resyncTo": None,
    ## Before the parser output stream is changed
    "outputTo": None
}

## The error message used when no output stream is provided to the parser
perrNoOutput = "no output was provided to the parser"
## The error message used when an unexpected token is encountered
perrUnexpectedToken = "unexpected {} in source after {}"

## A parser. 
class AParser(ALoggingEventfulObject):
    ## Constructor    
    #
    #  In addition to the parameters expected by
    #  ALoggingEventfulObject.__init__(), this method expects that \p params
    #  will contain the following items:
    #  - Source -- A reference to the stream to be used for input.
    def __init__(Self, **params):
        ALoggingEventfulObject.__init__(Self, **params)
        
        if "Source" in Self._params:
            Self._Source = Self._params["Source"]
            del Self._params["Source"]
     
    ## Initializer
    def init(Self):
        result = ALoggingEventfulObject.init(Self)
        
        if result:
            # Establish private attributes
            # The source stream
            Self._Source = None
            # The scanner used to parse the source
            Self._Scanner = None
            # The current token retrieved from the source
            Self._Token = None
            # The previous token retrieved from the source
            Self._PreviousToken = None
            # The log used by the parser
            Self._Log = None
            # The output stream used by the parser
            Self._Output = None
            
            # Update the events generated by the class
            Self._events.update(prsrEvents)
            
        return result
    
    ## Destructor   
    def __del__(Self):
        # Destroy the token
        del Self._Token
        # Destroy the previous token
        del Self._PreviousToken
        # Destroy the scanner
        del Self._Scanner
        
        AnObject.__del__(Self)
    
    #  Log a syntax error.
    #
    #  This routine generates the \p syntaxError event.
    #
    #  If an event handler is specified for this event, it will receive an
    #  instance of AnEvent with the \p msg and \p params parameters passed to
    #  this method.  The handler should set Event.handled() to \p True in order 
    #  to prevent the error from being logged.
    def syntaxError(Self, msg, *params):
        result = None
        
        # Generate the event
        Event = Self.generate("syntaxError", msg=msg, params=params)
            
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
        
        result = Self.log(ASyntaxError(Self._Scanner, msg, *params), False)
        
        return result
     
    ## Log a fatal error.
    #
    #  If an event handler is specified for this event, it will receive an
    #  instance of AnEvent with the \p msg and \p params parameters passed to
    #  this method.  The handler should set Event.handled() to \p True in order 
    #  to prevent the error from being logged.    
    def fatalError(Self, msg, *params):
        result = None
        
        # Generate the event
        Event = Self.generate("fatalError", msg=msg, params=params)
        
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
        
        result = Self.log(AFatalError(Self._Scanner, msg, *params), False)
              
        return result
    
    ## Log a hint.
    #
    #  If an event handler is specified for this event, it will receive an
    #  instance of AnEvent with the \p msg and \p params parameters passed to
    #  this method.  The handler should set Event.handled() to \p True in order 
    #  to prevent the hint from being logged.     
    def hint(Self, msg, *params):
        result = None
        
        Event = Self.generate("hint", msg=msg, params=params)
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
        
        result = Self.log(AHint(Self._Scanner, msg, *params))
            
        return result
    
    ## Log a warning.   
    #
    #  If an event handler is specified for this event, it will receive an
    #  instance of AnEvent with the \p msg and \p params parameters passed to
    #  this method.  The handler should set Event.handled() to \p True in order 
    #  to prevent the warning from being logged. 
    def warn(Self, msg, *params):
        result = None
        
        Event = Self.generate("warning", msg=msg, params=params)
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
        
        result = Self.log(AWarning(Self._Scanner, msg, *params))
            
        return result
    
    ## Log a note.   
    #
    #  If an event handler is specified for this event, it will receive an
    #  instance of AnEvent with the \p msg and \p params parameters passed to
    #  this method.  The handler should set Event.handled() to \p True in order 
    #  to prevent the note from being logged.     
    def note(Self, msg, *params):
        result = None
        
        Event = Self.generate("note", msg=msg, params=params)
        if Event.handled():
            result = Event.result()
            del Event
            return result
        
        del Event
        
        result = Self.log(ANote(Self._Scanner, msg, *params))
        return result
                
    ## Parse the current token.
    #
    #  This method should be implemented by descendant classes.  It may parse
    #  one or more tokens and then return control to the calling method.
    #
    #  There is little need to call this method directly (from outside an
    #  implementation of this class), as it is called by AParser.parseUntil().
    def parseCurrentToken(Self):
        Event = Self.generate("parseCurrentToken")
        
        del Event
        
    ## Parse tokens until one of the specified tokens is encountered
    def parseUntil(Self, *theseTokens):
        Event = Self.generate("parseUntil", tokens=theseTokens)
        if Event.handled():
            del Event
            return
            
        del Event
        
        if len(theseTokens) == 0:
            return
        
        # We assume that the current token is NOT the token desired; otherwise,
        # why call this routine?    
        Self.NextToken()        
            
        # Parse tokens until one of the specified tokens is encountered.
        # Stop for the end of the input stream.
        while Self._Token.code() not in theseTokens:
            if Self._Token.code() == TokenType.endOfStream:
                break
                
            # Parse the next token
            Self.parseCurrentToken()                  
                
    ## Execute the parser.
    #    
    #  This method checks that the parser has a valid output stream.  If not,
    #  it raises an exception; otherwise it calls AParser.parseUntil() to parse
    #  tokens until the end of the stream is encountered.
    def parse(Self):
        result = 0
        
        Event = Self.generate("parse")
        if Event.handled():
            result = Event.result()
            del Event
            return result
        
        del Event
        
        if Self._Output is None:
            raise Self._Log.log(AFatalError(perrNoOutput), False)
        
        # Parse tokens until the end of the input stream
        Self.parseUntil(TokenType.endOfStream)    
        
        return 0
        
    ## Peek at the next token from the source.
    def PeekToken(Self):
        result = None
        
        Event = Self.generate("peekToken")
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
        
        result = Self._Scanner.PeekToken()
        
        return result
        
    ## Retrieve the next token from the source.
    def NextToken(Self):
        result = None
        
        Event = Self.generate("nextToken")
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
        
        # Store the old token before obtaining the new token
        if Self._PreviousToken is not None:
            del Self._PreviousToken
            
        Self._PreviousToken = Self._Token
        Self._Token = Self._Scanner.NextToken()
        
        result = Self._Token
        
        return result
        
    ## Retrieve the next token from the source only if the current
    #  token matches the specified token code.
    #    
    #  If /p muteCurrentToken is /p True, the /p silent property of the current
    #  token is set to /p True, which will prevent it from being written to
    #  the destination stream.
    def NextTokenIfMatch(Self, thisTokenCode, muteCurrentToken=False):
        if Self._Token is not None:
            if Self._Token.code() == thisTokenCode:
                Self._Token.silent = muteCurrentToken
                
                return Self.NextToken()
                
            else:
                # Flag an error because a specific token was expected
                Self._Log.log(ASyntaxError(Self._Scanner,
                  perrUnexpectedToken, Self._Token))
                  
        return None
        
    ## Skip tokens until one of the specified tokens is encountered.
    #    
    #  Tokens encountered along the way are muted, and so excluded from
    #  the parser output.
    def skipTo(Self, *theseTokenCodes):
        Event = Self.generate("skipTo", tokens=theseTokenCodes)
        if Event.handled():
            del Event
            return
            
        del Event
        
        if len(theseTokenCodes) == 0:
            return
            
        while Self._Token.code() not in theseTokenCodes:
            if Self._Token.code() == TokenType.endOfStream:
                break
                
            Self._Token.silent = True
            Self.NextToken()
        
    ## Resynchronize the parser to the specified opcode or opcodes.
    def resyncTo(Self, *theseTokenCodes):
        Event = Self.generate("resyncTo", tokens=theseTokenCodes)
        if Event.handled():
            del Event
            return
            
        del Event
        
        if len(theseTokenCodes) == 0:
            return
       
        # If the current token is not the one sought, we have a syntax error
        if Self._Token.code() not in theseTokenCodes:
            Self._Log.log(ASyntaxError(Self._Scanner, perrUnexpectedToken,
              Self._Token, Self._PreviousToken))
              
            # Skip tokens until the desired one is found
            Self.skipTo(*theseTokenCodes)

        # Flag an unexpected end of stream
        if (Self._Token.code() == TokenType.endOfStream) and \
          (TokenType.endOfStream not in theseTokenCodes):
            raise Self._Log.log(AFatalError(Self._Scanner,
              perrUnexpectedToken, Self._Token, Self._PreviousToken), False)
              
    # Properties ---------------------------------------------------------------
    ## Retrieve the scanner used by the parser.
    def Scanner(Self):
        return Self._Scanner
        
    ## Retrieve the most recent token retrieved from the scanner.
    def Token(Self):
        return Self._Token
        
    ## Retrieve the previous token retrieved from the scanner
    def PreviousToken(Self):
        return Self._PreviousToken
    
    ## Retrieve the log used by the parser.    
    def Log(Self):
        return Self._Log
     
    ## Retrieve the source stream used by the parser.
    def Source(Self):
        return Self._Source
        
    ## Retrieve the output stream used by the parser
    def Output(Self):
        return Self._Output
        
    ## Set the output stream used by the parser
    #
    #  Returns the previous output stream, if any.
    def OutputTo(Self, NewOutput):
        result = None
        
        Event = Self.generate("outputTo", NewOutput=NewOutput)
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
        
        # Retrieve the old output
        result = Self._Output
        
        # Set the new output
        if NewOutput is not None:
            Self._Output = NewOutput
            
        return result
        
    ## Retrieve the current line number from the scanner.
    def line(Self):
        if Self._Scanner is not None:
            return Self._Scanner.line()
            
        return 0
        
    ## Iterate through the tokens in the source.
    #    
    #  This routine is provided for convenience, so that the parser can be
    #  used in a <tt>for...in</tt> loop.
    def __iter__(Self):
        return iter(Self.NextToken, None)
        
