# Copyright (c) 2005 Nokia Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from StringIO import StringIO
import re
import os.path
import traceback
import sys

TEMPLATEFILE_SUFFIX='.in'

def is_templatefile(filename):
    return filename.endswith(TEMPLATEFILE_SUFFIX)

def outfilename_from_infilename(infilename):
    if not is_templatefile(infilename):
        raise ValueError, "expected file name with suffix "+TEMPLATEFILE_SUFFIX+", got "+infilename
    return infilename[:-len(TEMPLATEFILE_SUFFIX)]

class MacroEvaluationError(Exception): pass

macro_delimiter_re=re.compile(r'(\${{|}})')
macro_eval_re=re.compile('\${{(.*?)}}$',re.S)
macro_exec_re=re.compile('\${{!(\s*\n)?(.*?)\s*}}$',re.S)
macro_if_re  =re.compile('\${{if (.*)}}$',re.S)

def process_macros(instr, namespace=None):
    """Return result of doing macro processing on instr, in namespace namespace. 
    
    Basic substitution and evaluation:
    >>> process_macros('${{foo}}',{'foo':1})
    '1'
    >>> process_macros('${{2+2}}')
    '4'
    >>> process_macros('${{foo+bar}}',{'foo': 1, 'bar': 2})
    '3'
    
    Error cases:
    >>> process_macros('}}')
    Traceback (most recent call last):
      ...
    MacroEvaluationError: Mismatched parens in macro }}
    >>> process_macros('${{')
    Traceback (most recent call last):
      ...
    MacroEvaluationError: Mismatched parens in macro ${{

    Corner cases:
    >>> process_macros('')
    ''
    >>> process_macros(u'')
    u''

    Code execution:
    >>> process_macros("${{!print 'foo'}}")
    foo
    ''
    >>> process_macros("${{!  \\nfor k in range(4):\\n    write(str(k))}}")
    '0123'
    
    >>> process_macros('${{if 1\\nfoo\\n$else\\nbar}}')
    'foo'
    >>> process_macros('xxx${{if 1\\n${{foo}}\\n$else\\nbar}}yyy',{'foo':42})
    'xxx42yyy'
    >>> process_macros('${{!#}}')
    ''
    >>> process_macros('${{"${{foo}}"}}',{'foo':42})
    '42'
    >>> process_macros('${{"${{bar}}"}}',{'foo':42})
    Traceback (most recent call last):
      ...
    MacroEvaluationError: Error evaluating expression "${{bar}}": NameError: name 'bar' is not defined
    <BLANKLINE>
    """
    if namespace is None:
        namespace={}
    
    def process_text(text):
        #print "Processing text: "+repr(text)
        pos=0 # position of the first character of text that is not yet processed
        outbuf=[]
        macrobuf=[]
        while 1:
            m=macro_delimiter_re.search(text, pos)
            if m:
                ##print "found delimiter: "+text[m.start():]
                outbuf.append(text[pos:m.start()])
                pos=m.start()
                paren_level=0
                for m in macro_delimiter_re.finditer(text,pos): # find a single whole macro expression
                    delim=m.group(1)
                    if delim=='${{':
                        paren_level+=1
                    elif delim=='}}':
                        paren_level-=1
                    else:
                        assert 0
                    if paren_level<0:
                        raise MacroEvaluationError("Mismatched parens in macro %s"%text[pos:m.end()])
                    if paren_level == 0: # macro expression finishes here, evaluate it.
                        outbuf.append(process_text(process_macro(text[pos:m.end()])))
                        pos=m.end()
                        break
                if paren_level!=0:
                    raise MacroEvaluationError("Mismatched parens in macro %s"%text[pos:m.end()])                
            else: # no more macros, just output the rest of the plain text
                outbuf.append(text[pos:])
                break
        result=''.join(outbuf)
        #print "Text after processing: "+repr(result)
        return result
    def process_macro(macro_expression):
        #print 'Processing macro: '+repr(macro_expression)
        try: 
            outstr=StringIO()
            namespace['write']=outstr.write
            m=macro_exec_re.match(macro_expression) # ${{!code}} -- exec the code
            if m:
                exec m.group(2) in namespace
            else:
                m=macro_if_re.match(macro_expression) # ${{if -- if expression
                if m:
                    outstr.write(handle_if(process_text(m.group(1)),namespace))
                else:
                    m=macro_eval_re.match(macro_expression)
                    if m: # ${{code}}  -- eval the code
                        outstr.write(str(eval(m.group(1),namespace)))
                    else:                        
                        raise MacroEvaluationError, 'Invalid macro'
            #print 'Macro result: '+repr(outstr.getvalue())
            return outstr.getvalue()
        except:
            raise MacroEvaluationError, 'Error evaluating expression "%s": %s'%(
                macro_expression,
                '\n'.join(traceback.format_exception_only(sys.exc_info()[0],sys.exc_info()[1])))
    def if_tokenized(code):
        #print "code: "+repr(code)
        lines=code.split('\n')
        yield ('if',lines[0])
        #print 'lines: '+repr(lines)
        for line in lines[1:]:
            m=re.match('\$(elif|else)(.*)',line)
            if m:
                yield (m.group(1),m.group(2))
            else:
                #print "data line "+repr(line)
                yield ('data',line)
        raise StopIteration
    def handle_if(code,namespace):
        outbuf=[]
        true_condition_found=0
        for token,content in if_tokenized(code):
            if token=='data' and true_condition_found:
                outbuf.append(content)
            else:
                if true_condition_found: # end of true block reached
                    break
                if token=='if' or token=='elif':
                    true_condition_found=eval(content,namespace)
                elif token=='else':
                    true_condition_found=1
        return '\n'.join(outbuf)
    return process_text(instr)
    
def process_file(infilename,namespace):
    outfilename=outfilename_from_infilename(infilename)
    infile=open(infilename,'rt')
    outfile=open(outfilename,'wt')
    outfile.write(process_macros(infile.read(),namespace))
    outfile.close()
    infile.close()

def templatefiles_in_tree(rootdir):
    files=[]
    for dirpath, dirnames, filenames in os.walk(rootdir):
        files+=[os.path.join(dirpath,x) for x in filenames if is_templatefile(x)]
    return files

def misctest():
    files=templatefiles_in_tree('.')
    print "Templates in tree: "+str(files)

    for k in files:
        print "Processing file: "+k
        process_file(k,namespace)
    # import code
    # code.interact(None,None,locals())

def _test():
    import doctest
    doctest.testmod()

if __name__ == "__main__":
    _test()
    
    
