############################################################################
#                                                                          #
#             copyright (c) 2003 ITB, Humboldt-University Berlin           #
#             written by: Raphael Ritz, r.ritz@biologie.hu-berlin.de       #
#                                                                          #
############################################################################

"""BibtexParser class"""

# Python stuff
import re

# Zope stuff
from Globals import InitializeClass
from App.Dialogs import MessageDialog

# Bibliography stuff
from Products.CMFBibliographyAT.tool.parsers.base \
     import IBibliographyParser, BibliographyParser

from Products.CMFBibliographyAT.utils import _encode, _decode
from Products.CMFBibliographyAT.encodings import _latex2utf8enc_mapping

# our custom entity converter
from entities import Convert
convert = Convert()

_encoding = 'utf-8'   # XXX: should be taken from the site configuration

class BibtexParser(BibliographyParser):
    """
    A specific parser to process input in BiBTeX-format.
    """

    __implements__ = (IBibliographyParser ,)

    meta_type = "Bibtex Parser"

    format = {'name':'BibTeX',
              'extension':'bib'}

    def __init__(self,
                 id = 'bibtex',
                 title = "BibTeX parser",
                 delimiter = '}\s*@',
                 pattern = '(,\s*\w{2,}\s*=)'):
        """
        initializes including the regular expression patterns
        """
        self.id = id
        self.title = title
        self.setDelimiter(delimiter)
        self.setPattern(pattern)


    # Here we need to provide 'checkFormat' and 'parseEntry'
    # FIXME TODO - refactor with pyblbibtex
    def checkFormat(self, source):
        """
        is this my format?
        """
        # vanilla test for 'author' or 'editor'
        # in the sub-string 'source[10, 100]'
        ## rr: can definitively be improved

        teststring = source[10:100].lower()
        ai = teststring.find('author')
        ei = teststring.find('editor')
        if ai + ei > -2:
            return 1
        else:
            return 0

    def preprocess(self, source):
        """
        expands LaTeX macros
        removes LaTeX commands and special formating
        converts special characters to their HTML equivalents
        """
        source = self.expandMacros(source)
        source = self.stripComments(source)
	source = self.convertChars(source)
	# it is important to convertChars before stripping off commands!!!
	# thus, whatever command will be replaced by a unicode value... the 
	# remaining LaTeX commands will vanish here...
        source = self.stripCommands(source)
        return source

    def expandMacros(self, source):
        source = self.expandStringMacros(source)
        # add more macro conventions here if there are any
        return source

    def expandStringMacros(self, source):
        lines = source.split('\n')
        macros = []
        sourcelns = []
        for line in lines:
            if line.find('@String') > -1:
                macros.append(line)
            else:
                sourcelns.append(line)
        source = '\n'.join(sourcelns)
        for macro in macros:
            split_on = re.compile('[{=}]+')
            raw_matches = split_on.split(macro)
            matches = [m for m in raw_matches if m not in ['', ' ', '\r']]
            # raise str(matches)
            short = matches[1].strip()
            long = matches[-1].strip()
            pattern = "\\b" + short + "\\b"
            old = re.compile(pattern)
            source = old.sub(long, source)
        return source
    
    def stripCommands(self, source):
        oldstyle_cmd = re.compile(r'{\\[a-zA-Z]{2,}')
        newstyle_cmd = re.compile(r'\\[a-zA-Z]+{')
        source = oldstyle_cmd.sub('{', source)
        source = newstyle_cmd.sub('{', source)
        return source

    def stripComments(self, source):

        inside_entry = False
        waiting_for_first_brace = False
        newsource = ''

        for idx in range(len(source)):
    
            char = source[idx]
            last_char = (idx > 0) and source[idx-1] or '\n'
            next_char = (idx < len(source)-1) and source[idx+1] or '\n'

            if char == '@' and not inside_entry:
                inside_entry = True
                waiting_for_first_brace = True
                braces_nesting_level = 0
        
            if inside_entry:
        
                newsource = newsource + char
                if char == '{' and last_char != "\\":
                    braces_nesting_level += 1

                if char == '}' and last_char != "\\":
                    braces_nesting_level -= 1

                if waiting_for_first_brace and (braces_nesting_level == 1):
                    waiting_for_first_brace = False

                if (braces_nesting_level == 0) and not waiting_for_first_brace and (char == '}'):
                    inside_entry = False
                    newsource = newsource + "\n"

        completely = re.compile('.*')
        ## this line caused issue #20, leaving it here for now... mg-20061023
	# source = completely.sub(newsource, 'dummy')
	## this line fixes issue #20
	source = newsource
        return source

    def convertChars(self, source):
        source = self.convertLaTeX2Unicode(source)
        source = self.fixWhiteSpace(source)
        return self.explicitReplacements(source)

    def convertLaTeX2Unicode(self, source):

	for latex_entity in _latex2utf8enc_mapping.keys():
	    source = _encode(_decode(source).replace(latex_entity, _latex2utf8enc_mapping[latex_entity]))
    
        return source

    def uml2h(self, hit):
        return convert(r'&' + hit.group(1) + 'uml;').encode(_encoding)
    def ac2h(self, hit):
        return convert(r'&' + hit.group(1) + 'acute;').encode(_encoding)
    def gr2h(self, hit):
        return convert(r'&' + hit.group(1) + 'grave;').encode(_encoding)
    def cf2h(self, hit):
        return convert(r'&' + hit.group(1) + 'circ;').encode(_encoding)
    def tilde2h(self, hit):
        return convert(r'&' + hit.group(1) + 'tilde;').encode(_encoding)

    def fixWhiteSpace(self, source):
        ttable = [(r'\ ', ' '),
                  (r'\!', ' '),
                  ]
        source = self.mreplace(source, ttable)
        wsp_tilde = re.compile(r'[^/\\]~')
        return wsp_tilde.sub(self.tilde2wsp, source).replace('\~', '~')

    def tilde2wsp(self, hit): return hit.group(0)[0] + ' '

    def explicitReplacements(self, source):
        # list of 2 tuples; second element replaces first
        ttable = [(r'\/', ''),
                  (r'\&', '&'),
                  (r'\~', '~'),
                  (r'---', '&mdash;'),
                  (r'--', '&ndash;'),
                  ]
        return self.mreplace(source, ttable)
        
    def mreplace(self, s, ttable):
        for a, b in ttable:
            s = s.replace(a, b)
        return s
    
    # done with preprocessing

    def parseEntry(self, entry):
        """
        parses a single entry
        
        returns a dictionary to be passed to
        BibliographyEntry's edit method
        """
        result = {}
        authorlist = []
        authorURLlist = []
        
        # remove newlines and <CR>s, and remove the last '}'
        entry = entry.replace('\n', ' ').replace('\r', '').replace('\t', ' ').rstrip().rstrip('}')
        tokens = self.pattern.split(entry)
    
        try: 
            type, pid = tokens[0].strip().split('{')
            type = type.replace('@', '').strip().lower()
            result['reference_type'] = type.capitalize() + 'Reference'
            result['pid'] = pid.replace(',', '').strip()
        except:
            return "Bibtex Parser Error: malformed first line."

        for k,v in self.group(tokens[1:],2):
            key = k[1:-1].strip().lower()
            # INBOOKs mapping: title -> booktitle, chapter -> chapter and title
            if type == 'inbook':
                if key == 'title':
                    key = 'booktitle'

                if key == 'chapter':
                    result['title'] = self.clean(v.rstrip().rstrip(',').rstrip())
            
            # BibTex field "type" maps to CMFBAT field "publication_type"
            if key == 'type':
                key = 'publication_type'
                result[key] = self.clean(v.rstrip().rstrip(',').rstrip())
            
            # special procedure for authors and editors         
            elif key == 'author':
                if result.has_key('author'):
                    result[key].append( self.clean(v.rstrip().rstrip(',').rstrip()) )
                else:
                    result[key] = [ self.clean(v.rstrip().rstrip(',').rstrip()), ]
            elif (key == 'editor') and (type in ['book','proceedings']):
                if result.has_key('editor'):
                    result[key].append( self.clean(v.rstrip().rstrip(',').rstrip()) )
                else:
                    result[key] = [ self.clean(v.rstrip().rstrip(',').rstrip()), ]
            else:
                value = self.clean(v.rstrip().rstrip(',').rstrip())
                result[key] = value
                # Aliasing the value to an upper-cased key so that when this dictionary
                # is passed into <a_reference_object>.edit(**dictionary), the values
                # will match and auto-update schema fields that are specified in either
                # upper or lower case.  This is motivated by the 'DOI' field being upper-cased.
                # Of course, this won't help mixed-case fields, but we'd probably need to change
                # Archetype internals to fix that - and that would be a questionable endeavour.
                result[key.upper()] = value
        
            #print key, result[key]

        # compile authors list of dictionaries
        # we can have authors   
        if result.has_key('author'):
            for each in result['author']:
                each = each.replace(' AND', ' and')
                authorlist.extend( each.split(' and') )
        # but for some bibref types we can have editors alternatively
        elif result.has_key('editor') and (type in ['book','proceedings']):
            result['editor_flag'] = True
            for each in result['editor']:
                each = each.replace(' AND', ' and')
                authorlist.extend( each.split(' and') )
        if result.has_key('authorURLs'):
            authorURLlist = result['authorURLs'].split('and ')

        if authorlist:
            alist = []
            authorlist = [x for x in authorlist if x]
            for author in authorlist:
                fname = mname = lname = ''
                parts = self.splitAuthor(author)
                if len(parts) == 1:
                    lname = parts[0].strip()
                else:
                    lname = parts[-1].strip()
                    fname = parts[0].strip()
                    if parts[1:-1]:
                        for part in parts[1:-1]:
                            mname = mname + part.strip()
                adict = {'firstname': fname,
                         'middlename': mname,
                         'lastname': lname}
                alist.append(adict)

        if authorURLlist and alist:
            index = 0
            for url in authorURLlist:
                alist[index]['homepage'] = url.strip()
                index += 1

        if authorlist:
            result['authors'] = alist

        # do some renaming and reformatting
        tmp = result.get('note')
        while tmp and tmp[-1] in ['}', ',', '\n', '\r']:
            tmp = tmp[:-1]
        if tmp:
            result['note'] = tmp
        # make keywords a list
        try:
            tmp = eval(result.get('keywords', '[]'))
        except:
            tmp = result.get('keywords').split(',')
        result['keywords'] = tmp
        result['publication_year'] = result.get('year', '')
        result['publication_month'] = result.get('month', '')
        result['publication_url'] = result.get('url', '')
        ## result['publication_title'] = result.get('title', '')
        tmp = result.get('title','')
        for car in ('\n', '\r', '\t'):
            tmp = tmp.replace(car, ' ')
        while '  ' in tmp:
            tmp = tmp.replace('  ', ' ')
        result['title'] = tmp

        return result

    # the helper method's

    def splitAuthor(self, author=None):
        if not author: return []
        #parts = author.replace('.', ' ').split(',',1)
        parts = author.split(',',1)
        if len(parts) == 1: return parts[0].split()
        else:
            tmp = parts[1].split()
            tmp.append(parts[0])
            return tmp

    def clean(self, value):
        value = value.replace('{', '').replace('}', '').strip()
        if value and value[0] == '"' and len(value) > 1:
            value = value[1:-1]
        return value

    def group(self, p,n):
        """ Group a sequence p into a list of n tuples."""
        mlen, lft = divmod(len(p), n)
        if lft != 0: mlen += 1

        # initialize a list of suitable length
        lst = [[None]*n for i in range(mlen)]
        
        # Loop over all items in the input sequence
        for i in range(len(p)):
            j,k = divmod(i,n)
            lst[j][k] = p[i]

        return map(tuple, lst)    
    
InitializeClass(BibtexParser)

   
def manage_addBibtexParser(self, REQUEST=None):
    """ """
    try:
        self._setObject('bibtex', BibtexParser())
    except:
        return MessageDialog(
            title='Bibliography tool warning message',
            message='The parser you attempted to add already exists.',
            action='manage_main')    
    return self.manage_main(self, REQUEST)
