""" Quote and unquote wiki names

    This will go into wikiutil and test_wikiutil. Because the tests are
    broken at this time, I working on this file.
"""

import unittest
import re
import time
import sys

class InvalidFileNameError(Exception): pass

# precompiled paterns
UNSAFE = re.compile(r'[^a-zA-Z0-9]+')
QUOTED = re.compile(r'\(([^\)]*)\)?')
QUOTED_VALID = re.compile(r'\(([a-fA-F0-9]+)\)')
QUOTED_PRE_13 = re.compile(r'(_[a-zA-Z0-9]{2})+')
QUOTED_DEV_13 = re.compile(r'(\([a-zA-Z0-9]{2}\))+')

# Some mock objects so we can test without moin moin
class config:
    charset = 'utf8'
    
def decodeUserInput(string, charsets=[config.charset]):
    return string.decode(charsets[0])

def quoteWikinameFS(wikiname, charset=config.charset):
    """ Return file system representation of a Unicode WikiName.
            
    Todo: We should use only unicode encoding for file names, to prevent
    possible configuration errror.
        
    @param wikiname: Unicode string possibly containing non-ascii characters
    @param charset: charset to encode string
    @rtype: string
    @return: quoted name, safe for any file system
    """
    filename = wikiname.encode(charset)
    
    quoted = []    
    start = 0
    needles = UNSAFE.finditer(filename)
    for needle in needles:
        # append leading safe stuff
        quoted.append(filename[start:needle.start()])
        start = needle.end()                    
        # Quote and append unsafe stuff           
        quoted.append('(')
        for character in needle.group():
            quoted.append('%02x' % ord(character))
        quoted.append(')')
    
    # append rest of string
    quoted.append(filename[start:len(filename)])    
    return ''.join(quoted)


def unquoteWikiname_valid_only_re(filename, charsets=[config.charset], safe=1):
    """ Return Unicode WikiName from quoted file name.
    
    We raise an InvalidFileNameError if we find an invalid name, so the
    wiki could alarm the admin or suggest the user to rename a page.
    Invalid file names should never happen in normal use, but are rather
    cheap to find.
        
    @param filename: string using charset and possible quoted parts
    @param charset: charset used by string
    @rtype: Unicode String
    @return: WikiName
    """
    ### Temporary fix start ###
    # from some places we get called with unicode
    if isinstance(filename, type(u'')):
        filename = filename.encode(config.charset)
    ### Temporary fix end ###
        
    parts = []    
    start = 0
    needles = QUOTED_VALID.finditer(filename)
    for needle in needles:    
        # append leading unquoted stuff
        parts.append(filename[start:needle.start()])
        start = needle.end()            
        # Append quoted stuff
        group =  needle.group(1)
        # Filter invalid filenames
        if (len(group) % 2 != 0):
            raise InvalidFileNameError(filename) 
        try:
            for i in range(0, len(group), 2):
                byte = group[i:i+2]
                character = chr(int(byte, 16))
                parts.append(character)
        except ValueError:
            # byte not in hex, e.g 'xy'
            raise InvalidFileNameError(filename)
    
    # append rest of string
    if start == 0:
        wikiname = filename
    else:
        parts.append(filename[start:len(filename)])   
        wikiname = ''.join(parts)
    # Filter invalid filenames. Any left (xx) must be invalid
    if safe and ('(' in wikiname or ')' in wikiname):
        raise InvalidFileNameError(filename)
    return decodeUserInput(wikiname, charsets)    


def unquoteWikiname_re(filename, charsets=[config.charset], safe=1):
    """ Return Unicode WikiName from quoted file name.
    
    We raise an InvalidFileNameError if we find an invalid name, so the
    wiki could alarm the admin or suggest the user to rename a page.
    Invalid file names should never happen in normal use, but are rather
    cheap to find.
        
    @param filename: string using charset and possible quoted parts
    @param charset: charset used by string
    @rtype: Unicode String
    @return: WikiName
    """
    ### Temporary fix start ###
    # from some places we get called with unicode
    if isinstance(filename, type(u'')):
        filename = filename.encode(config.charset)
    ### Temporary fix end ###
        
    parts = []    
    start = 0
    needles = QUOTED.finditer(filename)
    for needle in needles:    
        # append leading unquoted stuff
        parts.append(filename[start:needle.start()])
        start = needle.end()            
        # Append quoted stuff
        group =  needle.group(1)
        # Filter invalid filenames
        if (safe and 
            not needle.group().endswith(')') or
            len(group) == 0 or 
            len(group) % 2 != 0):
            raise InvalidFileNameError(filename) 
        try:
            for i in range(0, len(group), 2):
                byte = group[i:i+2]
                character = chr(int(byte, 16))
                parts.append(character)
        except ValueError:
            # byte not in hex, e.g 'xy'
            raise InvalidFileNameError(filename)
    
    # append rest of string
    if start == 0:
        wikiname = filename
    else:
        parts.append(filename[start:len(filename)])   
        wikiname = ''.join(parts)
    # Filter invalid file name with lonely ")"
    if safe and ')' in wikiname:
        raise InvalidFileNameError(filename)
    return decodeUserInput(wikiname, charsets)    

 
def unquoteWikiname(filename, charsets=[config.charset], safe=1):
    """ Return Unicode WikiName from quoted file name.
    
    We raise an InvalidFileNameError if we find an invalid name, so the
    wiki could alarm the admin or suggest the user to rename a page.
    Invalid file names should never happen in normal use, but are rather
    cheap to find.
        
    @param filename: string using charset and possible quoted parts
    @param charset: charset used by string
    @rtype: Unicode String
    @return: WikiName
    """
    ### Temporary fix start ###
    # from some places we get called with unicode
    if isinstance(filename, type(u'')):
        filename = filename.encode(config.charset)
    ### Temporary fix end ###
    
    parts = []    
    start = 0   # Start of the quoted sequence
    end = 0     # The character after the end of the last quoted sequence
    while 1:
        start = filename.find('(', end)
        if start != -1:
            # append leading unquoted stuff
            parts.append(filename[end:start])
            start += 1  # skip the "("                   
            end = filename.find(')', start)
            # Filter invalid filenames
            if safe and end == -1 or end == start or (end - start) % 2 != 0:
                raise InvalidFileNameError(filename)
            # Append quoted stuff
            try:
                for i in range(start, end, 2):
                    byte = filename[i:i+2]
                    character = chr(int(byte, 16))
                    parts.append(character)
            except ValueError:
                # byte not in hex, e.g 'xy'
                raise InvalidFileNameError(filename)
            end += 1  # skip the ")"
        else:
            # append rest of string and break
            if end == 0:
                wikiname = filename
            else:
                parts.append(filename[end:len(filename)])   
                wikiname = ''.join(parts)
            # Filter invalid filenames
            if safe and ")" in wikiname:
                raise InvalidFileNameError(filename)
            break
        
    return decodeUserInput(wikiname, charsets)    

   
def convertPre13FileName(filename, newEncoding='utf8'):
    """ Return new style quoted filename from pre 1.3 quoted file name.
    
    Unquote filename and invoke quoteWikinameForFileSystem()
    
    @param filename: string using charset and possible pre 1.3 quoting
    @rtype: String
    @return: New style quoted filename
    """
    parts = []    
    start = 0
    needles = QUOTED_PRE_13.finditer(filename)
    for needle in needles:    
        # append leading unquoted stuff
        parts.append(filename[start:needle.start()])
        start = needle.end()            
        
        # Append quoted stuff
        group =  needle.group()
        for i in range(0, len(group), 3):
            byte = group[i+1:i+3]
            character = chr(int(byte, 16))
            parts.append(character)

    # append rest of string
    parts.append(filename[start:len(filename)])
    
    wikiName = ''.join(parts).decode('utf8')
    return quoteWikinameFS(wikiName, charset=newEncoding)
    

def convertDev13FileName(filename, oldEncoding='utf8', newEncoding='utf8'):
    """ Return new style quoted filename from development 1.3 quoted
    file name.
    
    Unquote filename and invoke quoteWikinameForFileSystem()
    
    1.3 development used config.charset to encode file
    names. Theoretically, someone might have used non Unicode to encode
    file names.
        
    @param filename: string using charset and possible dev 1.3 quoting
    @param charset: charset used by string
    @rtype: String
    @return: New style quoted filename
    """
    parts = []    
    start = 0
    needles = QUOTED_DEV_13.finditer(filename)
    for needle in needles:    
        # append leading unquoted stuff
        parts.append(filename[start:needle.start()])
        start = needle.end()            
        
        # Append quoted stuff
        group =  needle.group()
        for i in range(0, len(group), 4):
            byte = group[i+1:i+3]
            character = chr(int(byte, 16))
            parts.append(character)

    # append rest of string
    parts.append(filename[start:len(filename)])
    
    wikiName = ''.join(parts).decode(oldEncoding)
    return quoteWikinameFS(wikiName, charset=newEncoding)


class QuoteTestCase(unittest.TestCase):
    """ wikiutil quoting tests """
    
    TESTS = (
        # WikiName, Quoted
        # Space
        (u' ', '(20)'),
        # Plain ASCII
        (u'WikiName', 'WikiName'),
        # Free links
        (u'free link', 'free(20)link'),
        # Underscore link
        (u'underscore_link', 'underscore(5f)link'),
        # Subpages
        (u'Page/SubPage/SubSubPage', 'Page(2f)SubPage(2f)SubSubPage'),
        # Hebrew
        (u'\u05e0\u05d9\u05e8', '(d7a0d799d7a8)'),
        # Hebrew sub pages
        (u'\u05e0\u05d9\u05e8\u002f\u05e0\u05d9\u05e8',
         '(d7a0d799d7a82fd7a0d799d7a8)'),
        # Combination
        (u'Page\u002f\u05e0\u05d9\u05e8\u002fSubPage',
         'Page(2fd7a0d799d7a82f)SubPage'),
        # Add more tests
    )
    
    def testQuoteWikiNameForFileSystem(self):
        """ wikiutil: quote wiki names for file system """
        for test, expected in self.TESTS:
            result = quoteWikinameFS(test, charset='utf8')
            self.failUnlessEqual(result, expected,
                'expected "%(expected)s" but got "%(result)s"' % locals())

    def testunQuoteFileName(self):
        """ wikiutil: unquote file using .find() """
        for expected, test in self.TESTS:
            result = unquoteWikiname(test, charsets=['utf8'])
            self.failUnlessEqual(result, expected,
                'expected "%(expected)s" but got "%(result)s"' % locals())

    def testunQuoteFileName_re(self):
        """ wikiutil: unquote file names using regex """
        for expected, test in self.TESTS:
            result = unquoteWikiname_re(test, charsets=['utf8'])
            self.failUnlessEqual(result, expected,
                'expected "%(expected)s" but got "%(result)s"' % locals())

    def testunQuoteFileName_valid_only_re(self):
        """ wikiutil: unquote file using valid only regex """
        for expected, test in self.TESTS:
            result = unquoteWikiname_valid_only_re(test, charsets=['utf8'])
            self.failUnlessEqual(result, expected,
                'expected "%(expected)s" but got "%(result)s"' % locals())


class InvalidFileNameTestCase(unittest.TestCase):
    """ wikiutil quoting tests """
    
    TESTS = (
        # Quoted file names
        'A()B',             # Empty braces
        'A(d7a)B',          # Odd number of quoted characters
        'A(xy)B',           # Non hex characters
        'A(2f',             # Open sequence
        'A2f)',             # Lonely close tag
       # Add more tests
    )

    def testInvalidFileName(self):
        """ wikiutil: invalid file names raise exception using find """
        for test in self.TESTS:
            self.failUnlessRaises(InvalidFileNameError, 
                                  unquoteWikiname, test, charsets=['utf8'])

    def testInvalidFileNameRE(self):
        """ wikiutil: invalid file names raise exception using re"""
        for test in self.TESTS:
            self.failUnlessRaises(InvalidFileNameError, 
                                  unquoteWikiname_re, test, charsets=['utf8'])

    def testInvalidFileNameValidOnlyRE(self):
        """ wikiutil: invalid file names raise exception using valid only re"""
        for test in self.TESTS:
            self.failUnlessRaises(InvalidFileNameError, 
                                  unquoteWikiname_valid_only_re, test, charsets=['utf8'])


class ConvertPre13FileNameTestCase(unittest.TestCase):
    """ Convert pre 1.3 file names to 1.3 file names
    
    Pre 1.3 used different quoting - each quoted character was prefixed
    by an underscode e.g._2f.
    """
    
    TESTS = (
        # New style quoting, Pre 1.3 style quoting
        # Space
        ('(20)', '_20'),
        # Plain ASCII
        ('WikiName', 'WikiName'),
        # Free link
        ('free(20)link', 'free_20link'),
        # Underscore link
        ('underscore(5f)link', 'underscore_5flink'),
        # Subpages
        ('Page(2f)SubPage', 'Page_2fSubPage'),
        # Hebrew
        ('(d7a0d799d7a8)', '_d7_a0_d7_99_d7_a8'),
        # Hebrew sub pages
        ('(d7a0d799d7a82fd7a0d799d7a8)',
         '_d7_a0_d7_99_d7_a8_2f_d7_a0_d7_99_d7_a8'),
        # Combination
        ('Page(2fd7a0d799d7a82f)SubPage',
         'Page_2f_d7_a0_d7_99_d7_a8_2fSubPage'),
        # Add more tests
    )

    def testConvertPre13FileName(self):
        """ wikiutil: Convert pre 1.3 file names to 1.3 file names """
        for expected, test in self.TESTS:
            result = convertPre13FileName(test, newEncoding='utf8')
            self.failUnlessEqual(result, expected,
                'expected %(expected)s but got %(result)s' % locals())


class ConvertDev13FileNameTestCase(unittest.TestCase):
    """ Convert development 1.3 file names to 1.3 file names
    
    Currently test only utf8 encoded names. Theoretically, someone might
    have used other encoding - the dev 1.3 code use the wiki charset
    encoding.
    
    Development 1.3 used different quoting - each quoted character was
    enclosed in bracdes e.g. (xx)(xx).
    """
    
    TESTS = (
        # New style quoting, Dev 1.3 style quoting
        # Space
        ('(20)', '(20)'),
        # Plain ASCII
        ('WikiName', 'WikiName'),
        # Subpages
        ('Page(2f)SubPage', 'Page(2f)SubPage'),
        # Hebrew
        ('(d7a0d799d7a8)', '(d7)(a0)(d7)(99)(d7)(a8)'),
        # Hebrew sub pages
        ('(d7a0d799d7a82fd7a0d799d7a8)',
         '(d7)(a0)(d7)(99)(d7)(a8)(2f)(d7)(a0)(d7)(99)(d7)(a8)'),
        # Combination
        ('Page(2fd7a0d799d7a82f)SubPage',
         'Page(2f)(d7)(a0)(d7)(99)(d7)(a8)(2f)SubPage'),
        # Add more tests
    )

    def testConvertDev13FileName(self):
        """ wikiutil: Convert development 1.3 file names to 1.3 file names """
        for expected, test in self.TESTS:
            result = convertDev13FileName(test, 
                                          oldEncoding='utf8', 
                                          newEncoding='utf8')
            self.failUnlessEqual(result, expected,
                'expected %(expected)s but got %(result)s' % locals())

class TimeUnquoteWikinameTestCase(unittest.TestCase):
    """ Time unquoting wiki names
    
    I'm not sure if we should use the re version which is little more
    clean, or the find version.
    
    This also test how expensive are the invalid file names.
    """
    
    # Tests contain a mix of names, typical for multi language wiki
    TESTS = (
        # Typical Plain ASCII names
        'RecentChanges', 'FrontPage', 'UserPreferences', 'WhyWikiWorks',
        # Extended name
        'extented(20)page(20)name',
        # Underscored name
        'underscored(5f)page(5f)name',
        # Subpages
        'ParentPage(2f)SubPage(2f)SubSubPage',
        # Hebrew (Recent Changes)
        '(d7a9d799d7a0d795d799d79d20d790d797d7a8d795d7a0d799d79d)',
        # Hebrew sub pages (RecentChagnes/RecentChanges)
        ('(d7a9d799d7a0d795d799d79d20d790d797d7a8d795d7a0d799d79d'
         '2f'
         'd7a9d799d7a0d795d799d79d20d790d797d7a8d795d7a0d799d79d)'),
        # Combination
        '(d7a9d799d7a0d795d799d79d20d790d797d7a8d795d7a0d799d79d2f)SubPage'
        # Add more tests
    )
    
    def time(self, callable, **kw):
        start = time.time()
        for i in range(100):
            for test in self.TESTS:
                callable(test, **kw)
        sys.stdout.write('%.4fs ' % (time.time() - start))
        sys.stdout.flush()
    
    def testSafeFind(self):
        """ Time unquoteWikiname using find safely: """
        self.time(unquoteWikiname, safe=1)

    def testFind(self):
        """ Time unquoteWikiname using find: """
        self.time(unquoteWikiname, safe=0)

    def testSafeRE(self):
        """ Time unquoteWikiname using regex safely: """
        self.time(unquoteWikiname_re, safe=1)

    def testRE(self):
        """ Time unquoteWikiname using regex: """
        self.time(unquoteWikiname_re, safe=0)

    def testValidOnlyRE(self):
        """ Time unquoteWikiname using valid only regex safely: """
        self.time(unquoteWikiname_valid_only_re, safe=1)

    def testSafeValidOnlyRE(self):
        """ Time unquoteWikiname using valid only regex: """
        self.time(unquoteWikiname_valid_only_re, safe=0)


if __name__ == '__main__':
    suite = unittest.TestSuite()
    suite.addTest(unittest.makeSuite(QuoteTestCase))
    suite.addTest(unittest.makeSuite(InvalidFileNameTestCase))
    suite.addTest(unittest.makeSuite(ConvertPre13FileNameTestCase))
    suite.addTest(unittest.makeSuite(ConvertDev13FileNameTestCase))
    suite.addTest(unittest.makeSuite(TimeUnquoteWikinameTestCase))
    unittest.TextTestRunner(verbosity=2).run(suite)
        
