# -*- coding: utf-8 -*-
###############################################################################
# Name: synxml.py                                                             #
# Purpose: Syntax Mode Xml Interface                                          #
# Author: Cody Precord <cprecord@editra.org>                                  #
# Copyright: (c) 2009 Cody Precord <staff@editra.org>                         #
# License: wxWindows License                                                  #
###############################################################################

""" Interface for extending language support through the use of xml files

@summary: EditraXml Implementation

"""

xml_spec = """
<editra version="1">
   <syntax language="Python" lexer="STC_LEX_PYTHON" id="ID_LANG_PYTHON">

      <associations value="py pyw"/>

      <keywordlist>
         <keywords value="0">
             if else elif for while in
         </keywords>
         <keywords value="1">
             str len setattr getattr
         </keywords>
      </keywordlist>

      <syntaxspeclist>
         <syntaxspec value="STC_P_DEFAULT" tag="default_style"/>
         <syntaxspec value="STC_P_WORD" tag="keyword_style"/>
      </syntaxspeclist>

      <propertylist>
         <property value="fold" enable="1"/>
         <property value="tab.timmy.whinge.level" enable="1"/>
      </propertylist>

      <commentpattern value="#"/>

      <featurelist>
         <feature method="AutoIndenter" source="myextension.py"/>
         <!-- <feature method="StyleText" source="myextension.py"/> -->
      </featurelist>

   </syntax>

</editra>

"""

__author__ = "Cody Precord <cprecord@editra.org>"
__svnid__ = "$Id: synxml.py 68814 2011-08-21 17:10:03Z CJP $"
__revision__ = "$Revision: 68814 $"

#----------------------------------------------------------------------------#
# Imports
import os
from xml import sax

# Workaround for building packages when wx is not available
try:
    import wx.stc as stc
except ImportError:
    pass

#----------------------------------------------------------------------------#
# Tag Definitions
EXML_START        = u"editra"
EXML_SYNTAX       = u"syntax"
EXML_KEYWORDLIST  = u"keywordlist"
EXML_KEYWORDS     = u"keywords"
EXML_SYNSPECLIST  = u"syntaxspeclist"
EXML_SYNTAXSPEC   = u"syntaxspec"
EXML_PROPERTYLIST = u"propertylist"
EXML_PROPERTY     = u"property"
EXML_COMMENTPAT   = u"commentpattern"
EXML_FEATURELIST  = u"featurelist"
EXML_FEATURE      = u"feature"
EXML_ASSOCIATIONS = u"associations"

# Attributes
EXML_ENABLE  = u"enable"
EXML_LANG    = u"language"
EXML_LEXER   = u"lexer"
EXML_METHOD  = u"method"
EXML_SOURCE  = u"source"
EXML_TAG     = u"tag"
EXML_VALUE   = u"value"
EXML_VERSION = u"version"
EXML_NAME    = u"name"
EXML_ID      = u"id"

#----------------------------------------------------------------------------#

class EditraXml(sax.ContentHandler):
    """Base class for all Editra syntax xml objects"""
    def __init__(self, path=None):
        sax.ContentHandler.__init__(self)

        # Attributes
        self.name   = u''            # Tag name
        self.level  = 0              # Set the level of the element
        self.indent = 3              # Indentation
        self.path = path
        self._ok = False             # Did the object load correctly

        self._context = None         # Current parse context
        self._reg_handler = dict()   # Registered parse handlers

    def __eq__(self, other):
        return self.GetXml() == other.GetXml()

    def __str__(self):
        return self.GetXml()

    #---- Internal Parser Api ----#

    def startElement(self, name, attrs):
        if self._context is not None:
            # Delegate to current context
            self._context.startElement(name, attrs)
        elif name in self._reg_handler:
            self._context = self._reg_handler.get(name)
            self._context.startElement(name, attrs)
        else:
            # Unknown tag
            # raise?
            pass

    def endElement(self, name):
        if self._context is not None:
            if self._context.Name == name:
                self._context = None
            else:
                # Delegate to registered handler
                self._context.endElement(name)
        else:
            # Unknown tag ending
            pass

    def characters(self, chars):
        if not chars.isspace():
            if self._context is not None:
                self._context.characters(chars)
            else:
                # No current context or context is self
                pass

    def GetXml(self):
        """Get the xml representation of this object
        @return: string

        """
        ident = self.GetIndentationStr()
        xml = ident + self.GetStartTag()
        xml += (self.GetSubElements() + os.linesep)
        xml += (ident + self.GetEndTag())
        return xml

    def GetSubElements(self):
        """Get the sub elements
        @return: string

        """
        xml = u''
        for handler in self.GetHandlers():
            handler.SetLevel(self.Level + 1)
            xml += (os.linesep + handler.GetXml())
        return xml

    def GetStartTag(self):
        """Get the opening tag
        @return: string

        """
        return u"<%s>" % self.name

    def GetEndTag(self):
        """Get the closing tag
        @return: string

        """
        return u"</%s>" % self.name

    def LoadFromDisk(self):
        """Load the object from from disk
        @precondition: path has been set
        @return: bool
        @todo: set error state on failed loads

        """
        assert self.path is not None, "Must SetPath before calling Load"
        try:
            sax.parse(self.path, self)
        except (sax.SAXException, OSError, IOError, UnicodeDecodeError):
            self._ok = False
            return False
        else:
            self._ok = True
            return True

    def LoadFromString(self, txt):
        """Load and initialize the object from an xml string
        @param txt: string

        """
        sax.parseString(txt, self)

    #---- External Api ----#

    @property
    def Context(self):
        return self._context

    @property
    def Indentation(self):
        return self.indent

    @property
    def Level(self):
        return self.level

    @property
    def Name(self):
        return self.name

    @property
    def Ok(self):
        return self.IsOk()

    def GetHandler(self, tag):
        """Get the handler associated with the given tag
        @param tag: string
        @return: EditraXml object or None

        """
        return self._reg_handler.get(tag, None)

    def GetHandlers(self):
        """Get all the handlers registered with this element
        @return: list of EditraXml objects

        """
        return self._reg_handler.values()

    def GetIndentation(self):
        """Get the indentation string
        @return: int

        """
        return self.indent

    def GetIndentationStr(self):
        """Get the indentation string taking level into consideration
        @return: string

        """
        return (self.indent * u" ") * self.level

    def GetLevel(self):
        """Get the level of this element
        @return: int

        """
        return self.level

    def GetName(self):
        """Get the tag name for the handler
        @return: string

        """
        return self.name

    def GetPath(self):
        """Get the xml files path
        @return: string

        """
        return self.path

    def IsOk(self):
        """Did the object load from file correctly?
        return: bool

        """
        return self._ok

    def RegisterHandler(self, handler):
        """Register a handler for a tag. Parsing will be delegated to the
        the registered handler until its end tag is encountered.
        @param handler: EditraXml instance

        """
        tag = handler.GetName()
        assert tag not in self._reg_handler, "%s already registered!" % tag
        handler.SetLevel(self.Level + 1)
        self._reg_handler[tag] = handler

    def SetIndentation(self, indent):
        """Set the indentation level
        @param indent: int

        """
        self.indent = indent

    def SetLevel(self, level):
        """Set the level of this element
        @param level: int

        """
        self.level = level

    def SetName(self, tag):
        """Set this handlers tag name used for identifying the open and
        end tags.
        @param tag: string

        """
        self.name = tag

    def SetPath(self, path):
        """Set the path to load this element from
        @param path: path

        """
        self.path = path

#----------------------------------------------------------------------------#

class SyntaxModeHandler(EditraXml):
    """Main Xml interface to extending filetype handling support"""
    def __init__(self, path=None):
        EditraXml.__init__(self, path)

        # Attributes
        self._start = False
        self._version = 0
        self.syntax = Syntax()

        # Setup
        self.SetName(EXML_START)
        self.SetIndentation(3)
        self.RegisterHandler(self.syntax)

    def startElement(self, name, attrs):
        if self._start:
            EditraXml.startElement(self, name, attrs)
        elif name == EXML_START:
            self._version = int(attrs.get(EXML_VERSION, 0))
            self._start = True

    def endElement(self, name):
        if name == EXML_START:
            self._start = False
        else:
            EditraXml.endElement(self, name)

    def GetStartTag(self):
        return u"<%s version=\"%s\">" % (self.GetName(), self.Version)

    #---- Get External Api ----#

    # Properties
    @property
    def CommentPattern(self):
        return self.GetCommentPattern()

    @property
    def FileExtensions(self):
        return self.GetFileExtensions()

    @property
    def Keywords(self):
        return self.GetKeywords()

    @property
    def LangId(self):
        return self.GetLangId()

    @property
    def Lexer(self):
        return self.GetLexer()

    @property
    def Properties(self):
        return self.GetProperties()

    @property
    def SyntaxSpec(self):
        return self.GetSyntaxSpec()

    @property
    def Version(self):
        return self._version

    # Getters
    def GetCommentPattern(self):
        """Get the comment pattern list
        @return: list of strings

        """
        return self.syntax.GetCommentPattern()

    def GetFileExtensions(self):
        """Get the list of associated file extensions
        @return: list of strings

        """
        return self.syntax.GetFileExtensions()

    def GetKeywords(self):
        """Get the keyword list
        @return: list of tuples [(idx, ['word', 'word2',]),]

        """
        kwxml = self.syntax.GetKeywordXml()
        return kwxml.GetKeywords()

    def GetFeatureFromXml(self, fet):
        """Get the callable associated with the given feature
        @param fet: string
        @return: string

        """
        fetxml = self.syntax.GetFeatureXml()
        return fetxml.GetFeature(fet)

    def GetSyntaxSpec(self):
        """Get the syntax spec
        @return: list of tuples [(style_id, "style_tag")]

        """
        spxml = self.syntax.GetSyntaxSpecXml()
        return spxml.GetStyleSpecs()

    def GetLangId(self):
        """Get the language id string
        @return: str "ID_LANG_"

        """
        return self.syntax.GetLangId()

    def GetLanguage(self):
        """Get the language name string
        @return: string

        """
        return self.syntax.GetLanguage()

    def GetLexer(self):
        """Get the lexer id
        @return: wx.stc.STC_LEX_

        """
        return self.syntax.GetLexer()

    def GetProperties(self):
        """Get the property defs
        @return: list of tuples [("fold", "1"),]

        """
        propxml = self.syntax.GetPropertiesXml()
        return propxml.GetProperties()

#----------------------------------------------------------------------------#

class Syntax(EditraXml):
    """Syntax definition for initializing a Scintilla Lexer"""
    def __init__(self):
        EditraXml.__init__(self)

        # Attributes
        self.language = u"Plain Text"
        self.langid = u"ID_LANG_TXT"
        self.lexstr = u"STC_LEX_NULL"
        self.lexer = stc.STC_LEX_NULL
        self.file_ext = list()

        # Sub Xml Objects
        self.keywords = KeywordList()
        self.synspec = SyntaxSpecList()
        self.props = PropertyList()
        self.features = FeatureList()
        self.comment = list()

        # Setup
        self.SetName(EXML_SYNTAX)
        self.RegisterHandler(self.keywords)
        self.RegisterHandler(self.synspec)
        self.RegisterHandler(self.props)
        self.RegisterHandler(self.features)

    def startElement(self, name, attrs):
        """Parse the Syntax Xml"""
        if name == EXML_COMMENTPAT:
            val = attrs.get(EXML_VALUE, '')
            tmp = val.split()
            # Trailing space may be significant for some comments
            if len(tmp) == 1:
                comment = [val,]
            else:
                comment = tmp
            self.comment = comment
        elif name == EXML_ASSOCIATIONS:
            self.file_ext = attrs.get(EXML_VALUE, '').split()
        elif name == self.Name:
            lang = attrs.get(EXML_LANG, u"Plain Text")
            langid = attrs.get(EXML_ID, u"ID_LANG_TXT")
            assert langid.startswith(u"ID_LANG_"), "id must start with ID_LANG_"
            lexer = attrs.get(EXML_LEXER, 'STC_LEX_NULL')
            lexval = getattr(stc, lexer, None)
            assert lexval is not None, "Invalid Lexer: %s" % lexer
            self.language = lang
            self.langid = langid
            self.lexstr = lexer
            self.lexer = lexval
        else:
            EditraXml.startElement(self, name, attrs)

    def GetStartTag(self):
        """Get the syntax opening tag and attributes"""
        return u"<%s %s=\"%s\" %s=\"%s\" %s=\"%s\">" % (self.Name, EXML_LANG, 
                                                        self.language, EXML_LEXER,
                                                        self.lexstr, EXML_ID,
                                                        self.langid)

    def GetSubElements(self):
        """Get the SubElements xml string
        @return: string

        """
        xml = EditraXml.GetSubElements(self)
        ident = self.GetIndentationStr() + (self.Indentation * u" ")
        xml += os.linesep
        cpat = u" ".join(self.GetCommentPattern())
        comment = u"<%s %s=\"%s\"/>" % (EXML_COMMENTPAT, EXML_VALUE, cpat.strip())
        xml += os.linesep
        xml += (ident + comment)
        xml += os.linesep
        fileext = u"<%s %s=\"%s\"/>" % (EXML_ASSOCIATIONS, EXML_VALUE, u" ".join(self.file_ext))
        xml += (ident + fileext)
        return xml

    #---- External Api ----#

    def GetCommentPattern(self):
        """Get the comment pattern
        @return: list of strings ["/*", "*/"]

        """
        return self.comment

    def GetFeatureXml(self):
        """Get the FeatureList xml object"""
        return self.features

    def GetFileExtensions(self):
        """Get the list of associated file extensions"""
        return self.file_ext

    def GetKeywordXml(self):
        """Get the Keyword Xml object"""
        return self.keywords

    def GetLanguage(self):
        """Get the language description/name
        @return: string

        """
        return self.language

    def GetLangId(self):
        """Get the language id
        @return: string

        """
        return self.langid

    def GetLexer(self):
        """Get the lexer to use for this language
        @return: stc.STC_LEX_FOO

        """
        return self.lexer

    def GetSyntaxSpecXml(self):
        """Get the Syntax Spec Xml object"""
        return self.synspec

    def GetPropertiesXml(self):
        """Get the properties xml object"""
        return self.props

#----------------------------------------------------------------------------#

class KeywordList(EditraXml):
    """Container object for all keyword sets"""
    def __init__(self):
        EditraXml.__init__(self)

        # Attributes
        self._current = None
        self._keywords = dict() # { index : word_list }

        # Setup
        self.SetName(EXML_KEYWORDLIST)

    def startElement(self, name, attrs):
        if name == EXML_KEYWORDS:
            idx = attrs.get(EXML_VALUE, None)
            assert idx is not None, "value attribute not set"
            idx = int(idx)
            assert idx not in self._keywords, "Duplicate index set %d" % idx
            self._keywords[idx] = list()
            self._current = self._keywords[idx]
        else:
            pass

    def endElement(self, name):
        if name == EXML_KEYWORDS:
            self._current = None

    def characters(self, chars):
        chars = chars.strip().split()
        if self._current is not None:
            if len(chars):
                self._current.extend(chars)

    def GetSubElements(self):
        """Get the keyword list elements"""
        xml = u""
        tag = u"<%s %s=" % (EXML_KEYWORDS, EXML_VALUE)
        tag += "\"%s\">"
        end = u"</%s>" % EXML_KEYWORDS
        ident = self.GetIndentationStr() + (self.Indentation * u" ")
        for key in sorted(self._keywords.keys()):
            xml += os.linesep + ident
            xml += (tag % key)
            xml += os.linesep + ident
            words = (self.Indentation * u" ") + u" ".join(self._keywords[key])
            xml += words
            xml += os.linesep + ident
            xml += end
        return xml

    #---- External Api ----#

    def GetKeywords(self):
        """Get the list of keyword strings
        @return: sorted list of tuples [(kw_idx, [word1, word2,])]

        """
        keys = sorted(self._keywords.keys())
        keywords = [ (idx, self._keywords[idx]) for idx in keys ]
        keywords.sort(key=lambda x: x[0])
        return keywords

    def GetKeywordList(self, idx):
        """Get the list of keywords associated with the given index
        @return: list of strings

        """
        return self._keywords.get(idx, None)

#----------------------------------------------------------------------------#

class SyntaxSpecList(EditraXml):
    """Container element for holding the syntax specification elements"""
    def __init__(self):
        EditraXml.__init__(self)

        # Attributes
        self._specs = list()

        # Setup
        self.SetName(EXML_SYNSPECLIST)

    def startElement(self, name, attrs):
        """Parse all syntaxspec elements in the list"""
        if name == EXML_SYNTAXSPEC:
            lid = attrs.get(EXML_VALUE, '')
            assert len(lid), "Style Id not specified"
            if lid.isdigit():
                style_id = int(lid)
            else:
                # Scintilla Value
                style_id = getattr(stc, lid, None)
                assert style_id is not None, "Invalid STC Value: %s" % lid
                assert isinstance(style_id, int), "Invalid ID: %s" % lid

            self._specs.append((style_id, attrs.get(EXML_TAG, 'default_style')))
        else:
            # Unknown Tag
            # Raise?
            pass

    def GetSubElements(self):
        """Get the xml for all the syntax spec elements"""
        xml = u""
        tag = u"<%s %s=" % (EXML_SYNTAXSPEC, EXML_VALUE)
        tag += ("\"%s\" " + EXML_TAG + "=\"%s\"/>")
        ident = self.GetIndentationStr() + (self.Indentation * u" ")
        for spec in self._specs:
            xml += os.linesep + ident
            xml += (tag % spec)
        return xml
        
    #---- External Api ----#

    def GetStyleSpecs(self):
        """Get the list of keyword strings
        @return: list of tuples [(style_id, "style_tag"),]

        """
        return self._specs

#----------------------------------------------------------------------------#

class PropertyList(EditraXml):
    """Container class for the syntax properties"""
    def __init__(self):
        EditraXml.__init__(self)

        # Attributes
        self.properties = list()

        # Setup
        self.SetName(EXML_PROPERTYLIST)

    def startElement(self, name, attrs):
        if name == EXML_PROPERTY:
            prop = attrs.get(EXML_VALUE, '')
            if prop:
                enable = attrs.get(EXML_ENABLE, '0')
                self.properties.append((prop, enable))
        else:
            pass

    def GetSubElements(self):
        xml = u""
        tag = u"<%s %s=" % (EXML_PROPERTY, EXML_VALUE)
        tag += ("\"%s\" " + EXML_ENABLE + "=\"%s\"/>")
        ident = self.GetIndentationStr() + (self.Indentation * u" ")
        for prop in self.properties:
            xml += os.linesep + ident
            xml += (tag % prop)
        return xml

    #---- External Api ----#

    def GetProperties(self):
        """Get the list of properties
        @return: list of tuples [("property", "1")]

        """
        return self.properties

#----------------------------------------------------------------------------#

class FeatureList(EditraXml):
    """Container for all other misc syntax features.
    Currently Available Features:
      - AutoIndent
      - StyleText

    """
    def __init__(self):
        EditraXml.__init__(self)

        # Attributes
        self._features = dict()

        # Setup
        self.SetName(EXML_FEATURELIST)

    def startElement(self, name, attrs):
        if name == EXML_FEATURE:
            meth = attrs.get(EXML_METHOD, None)
            assert meth is not None, "method not defined"
            mod = attrs.get(EXML_SOURCE, None)
            assert mod is not None, "source not defined"
            
            self._features[meth] = mod
        else:
            EditraXml.startElement(self, name, attrs)

    def GetSubElements(self):
        xml = u""
        tag = u"<%s %s=" % (EXML_FEATURE, EXML_METHOD)
        tag += ("\"%s\" " + EXML_SOURCE + "=\"%s\"/>")
        ident = self.GetIndentationStr() + (self.Indentation * u" ")
        for feature in self._features.iteritems():
            xml += (os.linesep + ident)
            xml += (tag % feature)
        return xml

    #---- External Api ----#

    def GetFeature(self, fet):
        """Get the callable feature by name
        @param fet: string (module name)

        """
        feature = None
        src = self._features.get(fet, None)
        if src is not None:
            feature = src
        return feature

#----------------------------------------------------------------------------#

def LoadHandler(path):
    """Load and initialize a SyntaxModeHandler from an on disk xml config file
    @param path: path to an EditraXml file to load a handler from
    @return: SyntaxModeHandler instance

    """
    synmode = SyntaxModeHandler(path)
    synmode.LoadFromDisk()
    return synmode
