#!/usr/bin/python

# tz_web_server_macro.py
#       --copyright--                   Copyright 2011 (C) Tranzoa, Co. All rights reserved.    Warranty: You're free and on your own here. This code is not necessarily up-to-date or of public quality.
#       --url--                         http://www.tranzoa.net/tzpython/
#       --email--                       pycode is the name to send to. tranzoa.com is the place to send to.
#       --bodstamps--
#       November 15, 2011       bar
#       November 17, 2011       bar     if eval error, don't do any substitution
#       November 19, 2011       bar     exec .py files
#       November 20, 2011       bar     ./web/lib in web_paths
#       November 21, 2011       bar     .wav .mp3
#       November 29, 2011       bar     pyflake cleanup
#       March 19, 2012          bar     add gif and svg and jpeg and html
#                                       allow kwargs to replace vars
#       March 20, 2012          bar     vars.strings
#       March 21, 2012          bar     sorta recursive macros (macros can't have a single { in them)
#       March 27, 2012          bar     comment
#       April 25, 2012          bar     ogg ogv mp4 webm avi
#       May 4, 2012             bar     xul
#       May 7, 2012             bar     move default LANG_DIR to here from multi-client web server
#       May 9, 2012             bar     make the language dir work
#       May 27, 2012            bar     doxygen namespace
#       October 13, 2012        bar     non-string help and force string-osity of exec's that set the 's' variable
#       August 20, 2013         bar     comment
#       --eodstamps--
##      \file
#       \namespace              tzpython.tz_web_server_macro
#
#
#       This is the macro expansion logic.
#
#       It translates {{macro}} to other things.
#       'macro' can be ':file_name' to insert a file, which is macro-expanded if it has a macro-expanding extension. e.g. {{ : foo.htm }}
#       'macro' can be '| python code to exec, possibly multiline', which is not macro expanded. The variable, 's', is what is substituted for the macro.   e.g. {{ | s = 4 + 5 }}       {{ | def x():\n return(3)\ns = x() }}
#       Otherwise eval(macro) is substituted for the macro.
#
#

import  os
import  re
import  sys

import  tzlib
import  tz_http_server


MAX_MACRO_DEPTH = 30

LANG_DIR        = "en"

macro_re        = re.compile(r"\{\{([^\{]*?)\}\}", re.DOTALL)                                       # macro translation regular expression {{ value }}

#
#               Define some paths that we'll search for when finding un-pathed files
#               Note that the paths can contain macros.
#
web_paths       = [ "./web/{{ me.lang_dir }}", "./web/" + LANG_DIR, "./web/lib", "./web",       ]   # remember, ./web could be a sym-link
lib_paths       = [ "./tzlib", "./lib",                                                         ]
data_paths      = [ "./{{ me.data_dir }}",                                                      ]
local_paths     = [ ".",                                                                        ]



class   a_search_path(object) :
    """ List of paths to search when given a file name with the given extension. """
    def __init__(me, ext, is_text_ext = False, paths = None) :
        me.ext          = ext
        me.is_text_ext  = is_text_ext   or  False
        me.paths        = paths         or  []
    #   a_search_path



class   a_strings(object) :
    pass

class   a_vars(object) :
    def __init__(me) :
        me.strings  = a_strings
    pass


def dummy_log(s) :
    pass

class   a_processor(object) :
    """
        a_processor expands macros in strings, files, etc.
    """

    def __init__(me, search_paths = None, lang_dir = None, data_dir = None, log_rtn = None, **kwargs) :
        me.lang_dir = lang_dir  or ''
        me.data_dir = data_dir  or ''
        me.log      = log_rtn   or dummy_log
        if  search_paths is None :
            #
            #               What paths to search when give a file with a particular extension.
            #
            search_paths    = {}
            for sp in [
                        a_search_path('.png',  False, data_paths + web_paths             + local_paths),
                        a_search_path('.gif',  False, data_paths + web_paths             + local_paths),
                        a_search_path('.svg',  False, data_paths + web_paths             + local_paths),
                        a_search_path('.avi',  False,              web_paths             + local_paths),
                        a_search_path('.ico',  False,              web_paths             + local_paths),
                        a_search_path('.jpeg', False,              web_paths             + local_paths),
                        a_search_path('.jpg',  False,              web_paths             + local_paths),
                        a_search_path('.mp3',  False,              web_paths             + local_paths),
                        a_search_path('.mp4',  False,              web_paths             + local_paths),
                        a_search_path('.ogg',  False,              web_paths             + local_paths),
                        a_search_path('.ogv',  False,              web_paths             + local_paths),
                        a_search_path('.wav',  False,              web_paths             + local_paths),
                        a_search_path('.webm', False,              web_paths             + local_paths),
                        a_search_path('.css',  True,               web_paths             + local_paths),
                        a_search_path('.htm',  True,               web_paths             + local_paths),
                        a_search_path('.html', True,               web_paths             + local_paths),
                        a_search_path('.xul',  True,               web_paths             + local_paths),
                        a_search_path('.js',   True,               web_paths + lib_paths + local_paths),
                        a_search_path('.py',   True,               web_paths + lib_paths + local_paths),
                      ] :
                search_paths[sp.ext]    = sp
        me.search_paths                 = search_paths

        me.vars     = a_vars()                                  # work area for macros to put hit-persistent data in
        for k, v in kwargs.items() :
            setattr(me, k, v)                                   # note: tz_multi_client_web_server.py adds its 'srv' and 'hit' to the list of kwargs.
        me.depth    = 0
        me.err      = None


    def txlate_macro(me, g) :
        m           = g.group(1).strip()
        if  not m   :
            return(m)

        if  me.depth + 1 >= MAX_MACRO_DEPTH :
            err     = "Maximum macro depth exceeded: " + m
            me.err  = me.err or err
            me.log(err)
            return("")
        me.depth   += 1

        if  m.startswith(':') :
            m           = m[1:].lstrip()
            fn          = me.find_file(m)
            if  not fn  :
                err     = "Cannot find macro-referenced file [%s]!" % m
                me.err  = me.err or err
                me.log(err)
                s       = ""
                m       = ""
            else        :
                m       = ""
                s       = me.read_and_expand_file(fn)
                if  os.path.splitext(fn)[1].lower() == ".py" :
                    m   = '|' + s.strip()
                    s   = ""
                pass
            pass
        if  m   :
            try :
                #
                #   this is where we translate macros in the files and paths - note that code in the macro has access to the variables accesible inside this routine. In particular, 'me'.
                #
                if  m.startswith('|') :
                    s   = ""
                    exec m[1:].lstrip() + '\n'
                else    :
                    s   = eval(m.strip())
                s       = str(s)                            # in case the value is an int, for instance
            except StandardError :
                err     = "Bad macro [%s] %s!" % ( g.group(0), sys.exc_info()[1], )
                me.err  = me.err or err
                me.log(err)
                s       = g.group(0)
            pass

        me.depth       -= 1

        return(s)


    def expand_macros(me, s) :
        while True :
            try :
                ss  = macro_re.sub(me.txlate_macro, s)      # decode all inner macros in the given string
                if  ss == s :
                    break
                pass
            except TypeError :
                err     = "s is %s, not string [%s]" % ( type(s), str(s)[:100], )
                me.err  = me.err or err
                me.log(err)
                ss      = str(s)
            s   = ss

        return(s)


    def expand_file(me, fn) :
        """ Given a file name, read it, expand macros in it if appropriate, and return the results. """

        fd      = tzlib.safe_read_whole_binary_file(fn)
        ext     = os.path.splitext(fn)[1]
        if  fd and me.search_paths[ext].is_text_ext :
            fd  = me.expand_macros(fd)
        return(fd)


    def find_file(me, file_name) :
        """ From a cleaned-up file name the browser sent us, return None or a local file name we can deliver to the browser. """

        ext     = os.path.splitext(file_name)[1]
        sp      = me.search_paths.get(ext, None)
        if  sp  :
            for p in sp.paths :
                p   = os.path.join(me.expand_macros(p), file_name)
                if  os.path.isfile(p) :
                    return(p)

                pass
            pass

        return(None)


    def read_and_expand_file(me, fn) :
        """ Read the given file and expand macros in it, if appropriate. """

        fn      = me.find_file(fn)
        if  fn  :
            return(me.expand_file(fn))

        return(None)


    def safe_load_file(me, path) :
        """ Given a path the browser sent us, return None, "", or the macro-evaluated contents of a file. """

        path    = tz_http_server.safe_path(path)        # make the file name safe, without fragments or query arguments and without various things we simply don't like
        return(me.read_and_expand_file(path))


    #   a_processor



if  __name__ == '__main__' :
    me  = a_processor()
    for fn in sys.argv[1:] :
        if  os.path.isfile(fn) :
            s   = me.safe_load_file(fn)
        else :
            s   = me.expand_macros(fn)
        print   s
        print
    pass


#
#
# eof
