# -*- 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 several classes that represent Markdown-style statements 
#  that are parsed during a pre-scan of a source stream.

from Library.Parse.Semantic.SourceStatements import ASourceStatement
from Library.Parse.Lex.Languages import TokenType
from Library.Parse.SymbolTables import Scope
from Plugins.Content.Parse.Lex.Languages import MarkdownOpcode
from Plugins.Content.Parse.Symbols import ALinkIdentifier
from Local.Configuration import Site
from Library.String.Utilities import String
from Library.IO.XML.XMLTags import AnXMLTag, AnXMLWrapperTag
import os.path

## The format used to write the beginning of an XML tag to the output stream
mdxmlsStringRepresentation = "&lt;"
## The format used to write a mailto: link to the output string
mdxmlsMailtoSchema = "mailto:"
## The format used to write an ampersand to the output stream
mdasStringRepresentation = "&amp;"
## The formats used to write special characters to the stream
mdpsSpecialMarks = {
    "R":  "&reg;",
    "r":  "&reg;",
    "TM": "&trade;",
    "tm": "&trade;",
    "C":  "&copy;",
    "c":  "&copy;",
    "c$": "&cent;",
    "L":  "&pound;",
    "Y":  "&yen;",
    "E":  "&euro;",
    "SS": "&sect;",
    "ss": "&sect;",
    "o":  "&curren;"
}

## A pre-scanned Markdown statement
class APrescannedMarkdownStatement(ASourceStatement):
    ## Initializer
    def init(Self):
        result = ASourceStatement.init(Self)
        
        if result:
            # Establish private attributes
            # The engine driving the parser
            Self._Engine = None
            ## The data source that contains the posts being parsed
            Self._DataSource = None
                        
        return result
        
    ## Parse tokens from the source and output them, verbatim
    def parse(Self):
        Event = Self.generate("parse")
        if Event.handled():
            del Event
            return
            
        del Event
                
        Self._Output.write(Self._Parser.Token().literal())
        Self._Parser.NextToken()
    
    ## Search subclasses for the handler for the specified token.
    #
    #  This method takes advantage of the fact that Python links the 
    #  immediate subclasses of a class to their parent using an iterable object.
    #  The method iterates through those subclasses, calling
    #  APrescannedMarkdownStatement.handlerFor() on each one in an attempt to
    #  find a class that will handle the specified \p tokenCode.
    #
    #  If a class determines that it will handle the specified \p tokenCode, it
    #  creates an instance of itself and returns that instance instead of
    #  continuing the search.  That instance will be returned by this method,
    #  which otherwise returns \p None to indicate that no class was found to
    #  handle the specified \p tokenCode.
    @classmethod
    def subclassHandlerFor(Self, tokenCode, **params):
        Result = None
        
        # Query subclasses for one that will accept the token
        for Subclass in Self.__subclasses__():
            Result = Subclass.handlerFor(tokenCode, **params)
            if Result is not None:
                break
        
        return Result 
        
    ## Determine whether the class will handle the specified token.
    #
    #  Together with APrescannedMarkdownStatement.subclassHandlerFor(), this 
    #  method makes adding new handlers for Markdown tokens as easy as deriving 
    #  a class from APrescannedMarkdownStatement or its descendants.  
    #
    #  The base implementation of this method searches the immediate subclasses 
    #  of APrescannedMarkdownStatement, calling AMarkdownStatement.handlerFor() 
    #  on each one.  THOSE classes, in turn, first determine whether or not they 
    #  will handle \p tokenCode.  If not, they search their immediate subclasses 
    #  (by calling AMarkdownStatement.subclassHandlerFor() on themselves) -- and 
    #  so forth.
    #
    #  The base implementation of this method returns the result of call to
    #  APrescannedMarkdownStatement.subclassHandlerFor() on itself.  The return 
    #  value will either be an instance of the class that will handle the 
    #  specified \p tokenCode; or \p None to indicate that no class was found to 
    #  handle the specified token.
    #
    #  Descendant classes should override this method to add code that will
    #  determine whether or not the class will handle the specified token.
    #  Otherwise, the overriding method should return the result of a call to
    #  APrescannedMarkdownStatement.subclassHandlerFor() on its class.
    @classmethod
    def handlerFor(Self, tokenCode, **params):
        return Self.subclassHandlerFor(tokenCode, **params)           
    
    ## Determine whether the class will handle the specified token
    @classmethod
    def willHandle(Self, ThisParser, tokenCode):
        return True            
      
    @classmethod
    def ParsedFrom(Self, ThisParser):
        Result = None
        if ThisParser is not None:
            token = ThisParser.Token().code()
            
            # Query subclasses for one that will accept the token
            Result = Self.handlerFor(token, Parser = ThisParser)
                    
            if Result == None:
                # Output the token verbatim
                Result = Self(Parser = ThisParser)
                
        return Result  
        
## A link identifier statement
class AMarkdownLinkIdentifierStatement(APrescannedMarkdownStatement):
    ## Determine whether the class will handle the specified token
    @classmethod
    def handlerFor(Self, tokenCode, **params):
        if tokenCode == MarkdownOpcode.dlOpenBracket:
            return Self(**params)
            
        return Self.subclassHandlerFor(tokenCode, **params)
        
    ## Parse the URL from the statement
    def parseURL(Self):
        # Format is:
        #     <one or more tokens until the first space or EOL>
        result = ""
        
        while Self._Parser.Token().code() not in (TokenType.space,
            TokenType.endOfLine):
                if Self._Parser.Token().code() == TokenType.endOfStream:
                    break
                        
                result += Self._Parser.Token().literal()
                Self._Parser.NextToken()
            
        return result
        
    ## Parse the title from the statement
    def parseTitle(Self):
        # Format is:
        #    "title" - OR -
        #    'title' - OR -
        #    (title)
        
        result = ""
        
        if Self._Parser.Token().code() == TokenType.string:
            result = Self._Parser.Token().value()
            Self._Parser.NextToken()
                
        elif Self._Parser.Token().code() == MarkdownOpcode.dlOpenParen:
            Self._Parser.NextToken()
                
            # Collect the title text
            while Self._Parser.Token().code() != MarkdownOpcode.dlCloseParen:
                if Self._Parser.Token().code() == TokenType.endOfStream:
                    break
                        
                result += Self._Parser.Token().literal()
                Self._Parser.NextToken()
            
            # Skip the closing paren
            Self._Parser.NextToken()
        
        return result
        
    ## Parse attributes from the statement
    def parseAttributes(Self):
        # Format is:
        #    attribute="value" - OR -
        #    attribute='value' - OR -
        #    attribute=value
        # until two consecutive line breaks are encountered
        
        result = {}
        
        # Check for optional attributes before the end of the line
        while True:
            if Self._Parser.Token().code() == TokenType.endOfStream:
                break
                
            # Check for two line breaks in a row
            if Self._Parser.Token().code() == TokenType.endOfLine:
                Self._Output.write(Self._Parser.Token().literal())
                Tok = Self._Parser.PeekToken()
                if Tok.code() == TokenType.endOfLine:
                    Self._Parser.NextToken()
                    del Tok
                    break
                
                # Check for a new link
                elif Tok.code() == MarkdownOpcode.dlOpenBracket:
                    del Tok
                    break
                    
                del Tok
                    
            # Collect attributes
            attributeName = None
            attributeValue = None
            
            if Self._Parser.Token().code() == TokenType.identifier:
                # Store the attribute name
                attributeName = Self._Parser.Token().literal()
                
            elif Self._Parser.Token().code() == TokenType.string:
                # Store the attribute name
                attributeName = Self._Parser.Token().value()
                
            if attributeName is not None:
                # Check for the equals sign    
                Self._Parser.NextToken()                
                if Self._Parser.Token().code() == \
                    MarkdownOpcode.dlPostfixHeader:
                        Self._Parser.NextToken()
                        
                        # Get the attribute value
                        if Self._Parser.Token().code() == TokenType.string:
                            attributeValue = Self._Parser.Token().value()
                            
                        elif Self._Parser.Token().code() == TokenType.number:
                            attributeValue = Self._Parser.Token().value()
                            
                        elif Self._Parser.Token().code() == TokenType.identifier:
                            attributeValue = Self._Parser.Token().literal()
                            
                        attributeName = attributeName.lower()
                        if attributeName in Site.Posts.allowedAttributes:
                            result[attributeName] = attributeValue
                        
                        attributeName = None
                    
            Self._Parser.NextToken()
            
        return result
            
    ## Parse the statement
    def parse(Self):
        # Format is
        #     [id]: URL - OR -
        #     [id]: URL "title" - OR -
        #     [id]: URL 'title' - OR -
        #     [id]: URL (title) - OR -
        #     [id]: URL attributes... - OR -
        #     [id]: URL "title" attributes... - OR -
        #     [id]: URL 'title' attributes... - OR -
        #     [id]: URL (title) attributes...
        
        Event = Self.generate("parse")
        if Event.handled():
            del Event
            return
            
        del Event
                
        # If we are one tab or more from the beginning of the line, we cannot
        # have a link reference statement            
        if Self._Parser.PreviousToken().code() == TokenType.space:
            if len(Self._Parser.PreviousToken().literal()) >= \
              Self._Language.tabSize():
                  # Output the token verbatim
                  Self._Output.write(Self._Parser.Token().literal())
                  Self._Parser.NextToken()
                  return
                  
        # If we are not at the beginning of a line, we cannot have a link
        # reference statement
        elif Self._Parser.PreviousToken().code() != TokenType.endOfLine:
            # Output he token verbatim
            Self._Output.write(Self._Parser.Token().literal())
            Self._Parser.NextToken()
            return
        
        # Skip past the opening bracket
        Self._Parser.NextToken()
        
        # Store the link ID
        Self._value = ""
        while Self._Parser.Token().code() not in (TokenType.endOfLine,
            TokenType.endOfStream, MarkdownOpcode.dlEndIdentifier):
                Self._value += Self._Parser.Token().literal()
                Self._Parser.NextToken()
        
        # Check the next token to see if we have an actual link identifier
        # Self._Parser.NextToken()
        if Self._Parser.Token().code() == MarkdownOpcode.dlEndIdentifier:
            Self._Parser.NextToken()
            
            # Skip any space between the identifier closure and the URL
            while Self._Parser.Token().code() == TokenType.space:
                Self._Parser.NextToken()

            # Construct a new link identifier for the symbol table
            Link = ALinkIdentifier(Self._value)
                                
            # Retrieve the URL
            Link.url = Self.parseURL()
            
            # Check for an optional title
            if Self._Parser.Token().code() in \
                (TokenType.string, MarkdownOpcode.dlOpenParen):
                    Link.title = Self.parseTitle()
            
            # Check for additional optional link attributes
            if Self._Parser.Token().code() != TokenType.endOfLine:
                Link.attributes.update(Self.parseAttributes())
                
            # Insert the link
            Self._Parser.Symbols()[Scope.globals][Link.name()] = Link
            
        else:
            Self._Output.write("[{previous}{current}".format(
                previous=Self._value,
                current=Self._Parser.Token().literal()
            ))
            Self._Parser.NextToken()
        
## A class that handles XML tags in the source (by skipping them)
class AMarkdownXMLStatement(APrescannedMarkdownStatement):
    ## Determine whether the class will handle the specified token
    @classmethod
    def handlerFor(Self, tokenCode, **params):
        if tokenCode == MarkdownOpcode.dlBeginXMLTag:
            return Self(**params)
            
        return Self.subclassHandlerFor(tokenCode, **params)
            
    ## Parse the statement
    def parse(Self):
        Event = Self.generate("parse")
        if Event.handled():
            del Event
            return
            
        del Event
                
        # If the previous token was a backtick, we ignore the link
        if Self._Parser.PreviousToken().code() == MarkdownOpcode.dlBacktick:
            # Output the token 
            Self._Output.write(mdxmlsStringRepresentation)
            Self._Parser.NextToken()
            return
            
        # Get token following "<"
        Self._Parser.NextToken()
        
        # Check to see if this could be an embedded link by peeking at the
        # second token that follows "<" -- if a link, it should be ":" or "@"
        Tok = Self._Parser.PeekToken()        
        if Tok.code() in Self._Language.Rule("link/automatic"):
            # It looks like an embedded link, so grab it
            Tag = AnXMLWrapperTag("a")
            Tag["href"] = Self._Parser.Token().literal()
                
            Self._Parser.NextToken()
            
            while Self._Parser.Token().code() != MarkdownOpcode.opBlockquote:
                if Self._Parser.Token().code() == TokenType.endOfStream:
                    break
                    
                Tag["href"] += Self._Parser.Token().literal()
                Self._Parser.NextToken()
                
            Self._Parser.NextToken()

            # Output the link
            Tag.text = Tag["href"]
            if Tok.code() == MarkdownOpcode.dlAt:        
                # Obfuscate the e-mail address                                        
                Tag.text = String.obfuscate(Tag["href"])
                Tag["title"] = \
                  String.obfuscate(mdxmlsMailtoSchema + Tag["href"])
                Tag["href"] = \
                  String.obfuscate(mdxmlsMailtoSchema + Tag["href"])
                
            # Obfuscate a "mailto:" link
            elif mdxmlsMailtoSchema in Tag["href"]:
                Tag.text = String.obfuscate(Tag["href"])
                Tag["title"] = String.obfuscate(Tag["href"])
                Tag["href"] = String.obfuscate(Tag["href"])
                
            Tag.printTo(Self._Output)
            del Tag
                    
        else:
            Self._Output.write(mdxmlsStringRepresentation)
        
## A class that handles ampersands in the source
class AMarkdownAmpersandStatement(APrescannedMarkdownStatement):
    ## Determine whether the class will handle the specified token
    @classmethod
    def handlerFor(Self, tokenCode, **params):
        if tokenCode == MarkdownOpcode.opAmpersand:
            return Self(**params)
            
        return Self.subclassHandlerFor(tokenCode, **params)
            
    ## Parse the statement
    def parse(Self):
        Event = Self.generate("parse")
        if Event.handled():
            del Event
            return
            
        del Event
                
        Self._Output.write(mdasStringRepresentation)
        Self._Parser.NextToken()
        
## A class that handles parentheses in the source
class AMarkdownParenStatement(APrescannedMarkdownStatement):
    ## Determine whether the class will handle the specified token
    @classmethod
    def handlerFor(Self, tokenCode, **params):
        if tokenCode == MarkdownOpcode.dlOpenParen:
            return Self(**params)
            
        return Self.subclassHandlerFor(tokenCode, **params)
            
    ## Parse the statement
    def parse(Self):
        Event = Self.generate("parse")
        if Event.handled():
            del Event
            return
            
        del Event
                
        ## Get the next token
        Self._Parser.NextToken()
        
        # Look for special characters
        ch = Self._Parser.Token().literal()
        if ch in mdpsSpecialMarks.keys():
            Tok = Self._Parser.PeekToken()
            if Tok.code() == MarkdownOpcode.dlCloseParen:
                # Replace the character
                Self._Output.write(mdpsSpecialMarks[ch])
                Self._Parser.NextToken()
                    
            else:
                # Otherwise, output the tokens verbatim
                Self._Output.write(Self._Parser.PreviousToken().literal())
                Self._Output.write(Self._Parser.Token().literal())
                
        else:
            # Otherwise, output the tokens verbatim
            Self._Output.write(Self._Parser.PreviousToken().literal())
            Self._Output.write(Self._Parser.Token().literal())
            
        Self._Parser.NextToken()
        
## A class that handle line endings
class AMarkdownLineEnding(APrescannedMarkdownStatement):
    ## Determine whether the class will handle the specified token
    @classmethod
    def handlerFor(Self, tokenCode, **params):
        if tokenCode == TokenType.endOfLine:
            return Self(**params)
            
        return Self.subclassHandlerFor(tokenCode, **params)
            
    ## Parse the statement
    def parse(Self):
        Event = Self.generate("parse")
        if Event.handled():
            del Event
            return
            
        del Event
                
        # Output the token verbatim
        Self._Output.write(Self._Parser.Token().literal())
        
        if Self._Parser.line() == 1:
            return
            
        # Check for setext-style headers, which are delimited by "=" or "-"
        # IMMEDIATELY at the beginning of a line (but cannot occur on the very
        # first line)
        Self._Parser.NextToken()
        if Self._Parser.Token().code() in \
            Self._Language.Rule("line/header/postfix"):
                # Mark the line
                if Self._Parser.Token().code() == MarkdownOpcode.dlPostfixHeader:
                    Self._Parser.markPostfixHeaderLine(1)
                    
                elif Self._Parser.Token().code() == MarkdownOpcode.opUnorderedList2:
                    Self._Parser.markPostfixHeaderLine(2)
                    
                # Skip to the end of the line
                Self._Parser.skipTo(TokenType.endOfLine)

## A class that handles whitespace
class AMarkdownSpaceSpan(APrescannedMarkdownStatement):
    ## Determine whether the class will handle the specified token
    @classmethod
    def handlerFor(Self, tokenCode, **params):
        if tokenCode == TokenType.space:
            return Self(**params)
            
        return Self.subclassHandlerFor(tokenCode, **params)
            
    ## Parse the statement
    def parse(Self):
        Event = Self.generate("parse")
        if Event.handled():
            del Event
            return
            
        del Event
                
        # Convert the span into tabs
        span = Self._Parser.Token().literal()
        
        if len(span) >= Self._Language.tabSize():
            tabCount = int(len(span) / Self._Language.tabSize())            
            
            Self._Output.write("\t" * tabCount)
            tabCount *= Self._Language.tabSize()
            span = span[:-tabCount]
                
            if len(span) > 0:
                Self._Output.write(span)
                
        # Otherwise, write the span verbatim
        else:
            Self._Output.write(span)
            
        Self._Parser.NextToken()
                    
