#!/usr/bin/env python
import string,re,sys,stat,os,getopt,time

## <VERSION
SELF_VER    = 1796
## VERSION>

SELF_INFOR  = "ktv-docstrip (version Python.%d)" % SELF_VER
GEN_DATE    = "%d/%02d/%02d" % time.localtime()[:3]

## :: Regular Expression

## file DTX
re_begin    = re.compile("%<\*([!a-zA-Z(][()a-zA-Z0-9_|&!]*)>")    # begin block
re_end      = re.compile("%</([!a-zA-Z(][()a-zA-Z0-9_|&!]*)>")     # end block
re_sblock   = re.compile("%<([!a-zA-Z(][()a-zA-Z0-9_|&!]*)>(.+\n)")# inline block
re_code     = re.compile("^[^%]")                                  # code

## file KINS
re_from     = re.compile("\$from\(([^)]+)\)")
re_gen      = re.compile("\$gen\(([^)]+)\)[ ]*\(([^)]+)\)[ ]*(.*)")
re_asg      = re.compile("\$([a-z][a-z0-9_]*)[ ]*=[ ]*(.*)")
## re_asg      = re.compile("\$([a-z][a-z0-9_]*)[ ]*=[ ]*(.*)")
re_kasg     = re.compile("([a-z][a-zA-Z0-9_]*)[ ]*=[ ]*(.*)")
re_com      = re.compile("#.*") ## comment line
re_isnum    = re.compile("[+|-]*[0-9]+")
re_local    = re.compile("\$local\(([^)]+)\)")
re_eof      = re.compile("\$eof=([01])")
## re_eoj      = re.compile("\$eoj=([01])")
re_import   = re.compile("\$import\(([^)]+)\)") ## add, Ky Anh, 2003/12/25
re_if       = re.compile("\$if\(([^)]+)\)")
re_elif     = re.compile("\$elif\(([^)]+)\)")
re_exec     = re.compile("\$<<<(.*)")
## re_expand   = re.compile("\$\([a-z][a-z0-9_]*\)")

def _xsplit(st):
    """Tao list cac terminal/guard, bo qua &,|,!,(,)
    Chi tra ve cac list chua co trong danh sach __guards ma thoi
    """
    ## last modify: Ky Anh, 2003/12/27
    ##            : use global __guards
    global __guards
    for ops in ['&', '(', ')', '!', '|', ',', ' ']:
        st = string.replace(st, ops, ':')
    tmp = []
    for ops in string.split(st, ':'):
        if (ops != '') and (not ops in __guards):
            tmp.append(ops)
    __guards.extend(tmp)
    __use(tmp,0)

def _translate(st): #
    """Dich mot bieu thuc logic thanh loi chuan cua PYTHON"""
    st = "(%s)" % st
    idx = 0
    tmpst = ""
    find_next_op = 0
    group_level  = 0
    while idx < len(st):
        if st[idx] == '!':
            tmpst = tmpst + "(not "
            find_next_op = 1
        elif st[idx] == '(':
            tmpst = tmpst + '('
            group_level = group_level + 1
        elif st[idx] == ')':
            tmpst = tmpst + ')'
            group_level = group_level - 1
        else: # !
            if find_next_op:
                while (find_next_op > 0) and (idx < len(st)):
                    if (st[idx] in __block_ops) or (group_level == 0):
                        tmpst = tmpst + ')' + st[idx]
                        find_next_op = 0
                    else:
                        tmpst = tmpst + st[idx]
                        idx = idx + 1
            else:
                tmpst = tmpst + st[idx]
        idx = idx +1

    idx = string.count(tmpst,'(') - string.count(tmpst,')')
    while idx > 0 :
        tmpst = tmpst + ')'
        idx = idx - 1
    return tmpst[1:-1]

def __setval(guard, value=1):
    ## add, Ky Anh, 2003/12/27
    exec("global %s\n%s=%d" % (guard,guard,value))

def __use(options, value=1):
    ## add, Ky Anh, 2003/12/27
    ## mod, Ky Anh, 2003/12/30, v1792, remove __alpha/__beta option check
    for weapon in options:
        tmpst = string.strip(weapon)
        if tmpst: __setval(tmpst,value)
##             if tmpst in __alpha_options:
##                 __setval(tmpst,1)
##             elif tmpst in __beta_options:
##                 __setval(tmpst,0)
##             else:            

def _use(opts_string, value=1):
    __use(string.split(opts_string, ","), value)

def _do_global():
	## add, Ky Anh, 2003/12/27
##  _force_
##  _gonly_
##  _kins_nopre
##  _kins_nopost
##  _kins_debug
##  _kins_destdir
##  jobnames             ## list of job (<kins> files)
##  __guards             ## list of guard in DTX files
##                          add, Ky Anh, 2003/12/27
##  NO                   ## list of NO-equivalent (0, No, NO, etc.)
##  __block_ops          ## list of operators in a DTX guard (block)
##  __kins_vars          ## list of global kins variables
##  __global_kins_vars   ## kins variables that are also the global
##                          variables in this program
##  __no_skip_vars       ## variables appear in the third part of |$gen|
##                          command; these variables arenot the <test>
##  __alpha_options      ## this DTX options will always be set to 1
##  __beta_options       ## - 0
##  _tex_                ## output as the INS file
    global\
        _force_,\
        _gonly_,\
        _kins_nopre,\
        _kins_nopost,\
        _kins_debug,\
        _kins_destdir,\
        jobnames,\
        __guards,\
        NO,\
        __block_ops,\
        __kins_vars,\
        __global_kins_vars,\
        __no_skip_vars,\
        _tex_,\
        _kins_pre_def,\
        _kins_pos_def
##         __alpha_options,\
##         __beta_options

    NO = ['0','no']

    __block_ops = ['&','|']	## donot accept the !. See the _translate()
    __global_kins_vars = ["pre", "post"]
    __no_skip_vars     = ["append", "nopost", "nopre",\
                          "destdir", "source", "debug"]
##     __alpha_options    = ["alpha", "com"]
##     __beta_options     = []

    jobnames = []
    __guards = []

    _force_ = 0
    _gonly_ = 0
    _tex_   = 0

    ## initialize the global variables
    ##      these variable can be exported, i.e.,
    ##      we can use `--exec cmd' at command line.
    _kins_nopre   = '0'
    _kins_nopost  = '0'
    _kins_debug   = '0'
    _kins_destdir = '.'
    __kins_vars   = ["nopre", "nopost", "destdir", "debug"]
    _kins_pre_def = "%% This file was generate\n\
%% \tfrom the file `$source'\n\
%% \twith option `$option'\n\
%% GenDate: $date\n\
%% Generator: $generator\n"
    _kins_pos_def = "%% End of file `$dest'"

    ## init the <special> guards
    ##      we neednot do this, because if a guard appears in a DTX file
    ##      it is recognize by _xsplit and then set the value by __use
    ##      see the defintionof _xplist for more details
    ## __use(__alpha_options + __beta_options)

def _xstrip(options, debug=0):
    """Tach ma tu file DTX voi options"""
    global __ilines, __output
    _use(options, 1)
    __output = []
    __level = 0
    __prints = [1]
    if debug: print "%s" % ("=" * 72),
    for weapon in __ilines:
        print_code = 0
        if re_begin.match(weapon):
            __level = __level + 1
            if __prints[__level - 1]: # previous block
                current_bblock = re_begin.match(weapon).group(1)
                exec("print_code=%s" % _translate(current_bblock))
            __prints.append(int(print_code))
            if debug: print "\nlevel%2d  %s *%s -> "\
                    % (__level,__prints,re_begin.match(weapon).group(1)),
        elif re_end.match(weapon):
            __level = __level - 1
            if __level < 0: break
            __prints.pop()
            if debug: print "\nlevel%2d  %s    /%s -> "\
                    % (__level,__prints,re_end.match(weapon).group(1)),
        elif re_sblock.match(weapon): #
            if __prints[__level]:
                current_bblock = re_sblock.match(weapon).group(1)
                exec("print_code=%s" % _translate(current_bblock))
            if debug: print "\nlevel%2d  %s %s -> "\
                    % (__level,__prints,re_sblock.match(weapon).group(1)),
            if print_code:
                if debug: sys.stdout.write('s')
                else: __output.append(re_sblock.match(weapon).group(2))
            if debug:
                print "\nlevel%2d  %s -> " % (__level,__prints),
        elif re_code.match(weapon):
            if __prints[__level]:
                if debug: sys.stdout.write('n')
                else: __output.append(weapon)
    if __level:
        print \
"\nThe blocks of your DTX file donot balance.\n\
To debug, use <docstrip> with `--debug' option."
    if debug: print "\n%s" % ("=" * 72)

    _use(options, 0) # reset options' value to 0

def _readkins(filename):
    ## add, Ky Anh, 2003/12/25
    """Read a KINS file, remove the comment line, and strip the lines.
    This function returns a LIST of KINS lines."""
    mykinslines = []
    f = open(filename, 'r')
    iline = f.readline()
    while iline:
        iline = re_com.split(iline)[0]
        iline = string.strip(iline)
        if re_eof.match(iline):
            if re_eof.match(iline).group(1) == '1':
                break ## reach END OF FILE
        elif iline:
            mykinslines.append(iline)
        iline = f.readline()
    f.close()
    return mykinslines

def _dokins(filename):
    """Excute the KINS code. The original name is |_readkins|; this version
    wasnot support |$import|. To do |$import|, a new function |_readkins| was
    added, and this function is now named |_dokins|."""
    global _kins_pre, _kins_post, kins_lines, _kins_pre_def, _kins_pos_def
    kins_lines = []
    __imported = [] # list of imported files

    _kins_pre   = _kins_pre_def  # default preamble/
    _kins_post  = _kins_pos_def  # default /postamble
    _kins_source= ''

    in_pre = in_post = 0
    in_if     = [1]
    __iflevel = 0
    __ifcont  = []

    kins_lines = _readkins(filename)

    while kins_lines:
        iline = kins_lines.pop(0)
## :: = $if
        if re_if.match(iline):
            if_value = 0
            __iflevel = __iflevel + 1
            if in_if[__iflevel - 1]:
                var_name = re_if.match(iline).group(1)
                if re_kasg.match(var_name):
                    var_value = re_kasg.match(var_name).group(2)
                    var_name  = re_kasg.match(var_name).group(1)
                    try:
                        if var_name in __kins_vars:
                            exec("tmp_value=_kins_%s" % var_name)
                        else:
                            exec("tmp_value=_gen_%s" % var_name)
                        if var_value == tmp_value:
                            if_value = 1 
                    except NameError: pass
            in_if.append(if_value)
## :: = $elif
        elif re_elif.match(iline):
            __iflevel = __iflevel - 1
            iline = "$if%s" % iline[5:]
            kins_lines.insert(0, iline)
## :: = $else
        elif iline == "$else":
            in_if[__iflevel] = 1 - in_if[__iflevel]
## :: = $enif
        elif iline == "$enif":
            __iflevel = __iflevel - 1
            if __iflevel < 0:
                sys.stderr.write(\
                    "\nerror: to much $enif. The program is going to stop.\n")
                sys.exit(1)
            else:
                in_if.pop()
            if __iflevel == 0:
                kins_lines[0:0], __ifcont = __ifcont, []
        elif __iflevel:
            if in_if[__iflevel]:
                __ifcont.append(iline)
## :: = pre, post
        elif iline == "$pre":  _kins_pre,  in_pre  = "", 1
        elif iline == "$post": _kins_post, in_post = "", 1
        elif iline == "$enpre":  in_pre  = 0
        elif iline == "$enpost": in_post = 0
## ## :: = $(.+)
##         elif re_expand.match(iline):
##             var_name = re_expand.match(iline).group(1)
## :: = $<<<<
        elif re_exec.match(iline):
            try:
                exec(re_exec.match(iline).group(1))
            except:
                sys.stderr.write("E")
## :: = if in_pre, in_post
        ## hai do`ng sau pha?i dde^? sau bo^'n do`ng tre^n
        elif in_pre:  _kins_pre  = _kins_pre  + iline + '\n'
        elif in_post: _kins_post = _kins_post + iline + '\n'
## :: = $from
        elif re_from.match(iline): # from macro
            _kins_source = re_from.search(iline).group(1)
            _readsource(_kins_source)
            sys.stderr.write('_')
## :: = $import
        elif re_import.match(iline):
            _i_file = re_import.match(iline).group(1)
            if not _i_file in __imported:
                __imported.append(_i_file)
                if not string.lower(_i_file[-5:]) == ".kins":
                    _i_file = _i_file + ".kins"
                kins_lines[0:0] = _readkins(_i_file)
                sys.stderr.write('=')
            else:
                sys.stderr.write('*')
## :: = assign
        elif re_asg.match(iline):
            var_name = re_asg.match(iline).group(1)
            var_value= re_asg.match(iline).group(2)
## :: = = $docstrip
            if var_name == 'docstrip': #
                if SELF_VER < int(var_value):
                    sys.stderr.write(\
"need docstrip %d or higher. Current docstrip's version: %d.\n"\
                        % (int(var_value), SELF_VER))
                    return
## :: = = global kins variables
            elif var_name in __global_kins_vars:
                exec('global _kins_%s\n_kins_%s="""%s"""'\
                    % (var_name, var_name, var_value))
## :: = = other kins variables
            else:
                if not var_name in __kins_vars:
                    __kins_vars.append(var_name)
                exec('_kins_%s="""%s"""' % (var_name,var_value))
                if var_name == 'source': _readsource(_kins_source)
## :: = $local
        elif re_local.match(iline):
            zzz = re_local.match(iline).group(1)
            if re_kasg.match(zzz):
                var_name  = re_kasg.match(zzz).group(1)
                var_value = re_kasg.match(zzz).group(2)
                exec('_gen_%s="""%s"""' % (var_name,var_value))
## :: = $gen
        elif re_gen.match(iline): # gen macro
            gen_des = re_gen.match(iline).group(1)
            if (not _gonly_) or (only_files.search(gen_des)):
                gen_opt = re_gen.match(iline).group(2)
                ## neu $gen co them tham so thu ba
                ## tham so nay phai de trong ngoac: (.+)
                kins_skip    = 0
                _gen_append  = '0'          # <initial value must be '0'>
                _gen_nopre   = _kins_nopre  # get the global value
                _gen_nopost  = _kins_nopost # as the initialization
                _gen_destdir = _kins_destdir# add 2003/12/10
                _gen_debug   = _kins_debug  #
                if re_gen.search(iline).group(3) != "":
                    gen_optlist = string.split(\
                        re_gen.search(iline).group(3)[1:-1],',')
                    for item in gen_optlist:
                        item = string.strip(item)
                        if item   == "append": _gen_append = '1'
                        elif item == "nopost": _gen_nopost = '1'
                        elif item == "nopre" : _gen_nopre  = '1'
                        else:
                            zzz = re_kasg.match(item)
                            if zzz:
                                var_name = zzz.group(1)
                                var_value= str(zzz.group(2))
                                if var_name in __no_skip_vars:
                                    exec('_gen_%s="""%s"""' % (var_name,var_value))
                                else:
                                    try:
                                        if var_name in __kins_vars:
                                            exec("tmp_value=_kins_%s" % var_name)
                                        else:
                                            exec("tmp_value=_gen_%s" % var_name)
                                        if not var_value == tmp_value:
                                            kins_skip = 1
                                            break # break the FOR loop
                                            ## donot mention other check
                                    except NameError:
                                        exec('_gen_%s="""%s"""' % (var_name,var_value))
                if kins_skip:
                    sys.stderr.write('o')
                else:
                    gen_des = "%s/%s" % (_gen_destdir, gen_des)
                    _gen(gen_des,gen_opt,\
                        _gen_append,\
                        _gen_nopre,_gen_nopost,\
                        _kins_source, _gen_debug)
            else:
                sys.stderr.write('o')
## :: = anything else
        else: pass
    if __iflevel != 0:
        sys.stderr.write("\nerror: missing $if or $enif\n")

def _readsource(source):
    ## last modify: Ky Anh, 2003/12/27
    ##            : according to changes of _xsplit
    global __ilines,\
            src_time
    __ilines = []
    src_time = os.stat(source)[stat.ST_MTIME]
    f = open(source, 'r')
    iline = f.readline() #
    while iline:
        if re_code.match(iline):  # code
            __ilines.append(iline)
        elif re_begin.match(iline): # begin block
            __ilines.append(iline)
            _xsplit(re_begin.match(iline).group(1))
        elif re_sblock.match(iline):# single block
            __ilines.append(iline)
            _xsplit(re_sblock.match(iline).group(1))
        elif re_end.match(iline): # end block
            __ilines.append(iline)
        iline = f.readline()
    f.close()

def _gen(filename, opt,\
        _gen_append='0',\
        _gen_nopre='0', _gen_nopost='0',\
        source_name='', _gen_debug='0'):
    
    if not _gen_debug in NO: #yes, debug is ON
        sys.stderr.write('-')
        print "f(%s)s(%s)o(%s)a(%s)npre(%s)npos(%s)bug(%s)"\
            % (filename, source_name, opt,\
                _gen_append, _gen_nopre, _gen_nopost, _gen_debug)
        _xstrip(opt, 1)
    else:
        des = string.split(filename, '/')[-1:][0]
        try:
            dest_time = os.stat(filename)[stat.ST_MTIME]
        except os.error:
            dest_time = 0

        if _force_ or (src_time > dest_time):
            try:
                if _gen_append in NO:
                    g = open(filename, 'w')
                    sys.stderr.write('.')
                else:
                    g = open(filename, 'a')
                    g.write('\n')
                    sys.stderr.write('+')
            except IOError:
                sys.stderr.write('X')
                return

            if _gen_nopre in NO:
                global _kins_pre
                ## expand |preample|
                tmp__pre = string.replace(_kins_pre,"$date",      GEN_DATE)
                tmp__pre = string.replace(tmp__pre, "$c",         '#')
                tmp__pre = string.replace(tmp__pre, "$generator", SELF_INFOR)
                tmp__pre = string.replace(tmp__pre, "$option",    opt)
                tmp__pre = string.replace(tmp__pre, "$dest",      des)
                tmp__pre = string.replace(tmp__pre, "$source",    source_name)		
                g.write(tmp__pre)
##                 if _tex_:
##                     if tmp__pre[-1:] == '\n':
##                         tmp__pre = tmp__pre[:-1]
##                     print "\def\ktvpre{^^A"
##                     print string.replace(tmp__pre, "\n", "^^J^^A\n"),
##                     print "}\usepreamble\ktvpre"
            _xstrip(opt, 0)
            g.writelines(__output)
            if _tex_:
                print "\\generate{\\file{%s}{\\from{%s}{%s}}}"\
                    % (filename, source_name, opt)

            if _gen_nopost in NO:
                global _kins_post
                tmp__pre = _kins_post
                ## expand |posamble|. Only macro "$dest" is allowed.
                tmp__pre = string.replace(_kins_post,"$dest", des)
                tmp__pre = string.replace(tmp__pre, "$c",     "#")
                g.write(tmp__pre)# the tail

            g.close()
            os.utime(filename, (src_time, src_time))
        else:
            sys.stderr.write('|')

def usage():
    sys.stdout.write(\
"""Usage: docstrip [options] file1 file2...
    -e  --exec  code  excute <code> before any job
                      (only the assignments are accepted)
    -g  --gen   expr  generate only files whose names
                      match the regular expression <expr>
    -j  --job   job   specify the kins file named <job>
                      (the extension `.kins' can be omitted)
    -t  --tex         print the (TeX) INS code
    -d  --debug       print debug information (donot generate any file)
    -f  --force       force generatation (skip time check)
    -h  --help        show this message
    -v  --version     print version information and exit
Report:
    .   generate sucessfully
    +   generate in `append' mode
    o   skip file (specified by <kins> file)
    ?   some things wrong occur
    _   opening new source file
    =   importing a kins file
    *   file is already imported. Inogre it!
    X   cannot open file for writting
    E   failed on excuting embeded python code
    |   skip file (the source time <= the destination time)
Within the `--debug' option:
    -   debugging
    n         normal code
    s   single block code
""")

if __name__ == '__main__':
    try:
        opts, args\
            = getopt.getopt(sys.argv[1:],\
            "vdhftg:e:j:",\
            ["debug","tex","job=","version", "gen=","force","help","exec="])
    except:
        sys.stderr.write("Please try |docstrip.py --help|\n")
        sys.exit(1)

    _do_global()

    for o, a in opts:
        if o in ("-h", "--help"):
            usage()
            sys.exit(0)
        elif o in ("-v", "--version"):
            sys.stdout.write("%s\n" % SELF_INFOR)
            sys.exit(0)
        elif o in ("-f", "--force"):
            _force_ = 1
        elif o in ("-t", "--tex"):
            _tex_ = 1
        elif o in ("-d", "--debug"):
            _kins_debug = '1'
        elif o in ("-g", "--gen"):
            _gonly_ = 1
            only_files = re.compile(a)
            sys.stderr.write("  genonly = %s\n" % a)
        elif o in ("-j", "--job"):
            if not string.lower(a[-5:]) == ".kins":
                a = a + ".kins"
            jobnames.append(a)
        elif o in ("-e", "--exec"):
            a = string.split(a, ';')
            for code in a:
                zzz = re_kasg.match(string.strip(code))
                if zzz:
                    var_name = zzz.group(1)
                    var_value= zzz.group(2)
                    exec('_kins_%s="""%s"""' % (var_name,var_value))
                    if not var_name in __kins_vars:
                        __kins_vars.append(var_name)
                    sys.stderr.write("  %s = %s\n" % (var_name,var_value))
                else:
                    sys.stderr.write("  ?code: %s\n" % code)

    for item in args:
        if not string.lower(item[-5:]) == ".kins":
            item = item + ".kins"
        jobnames.append(item)

    if jobnames == []:
        sys.stderr.write("Please try |docstrip.py --help|\n")
        sys.exit(1)
    
    if _tex_:
        print\
"%%%% This file was generated by `%s'\n\
%%%% from the source(s) %s\n\
\\input docstrip.tex" % (SELF_INFOR, jobnames)

    for jobname in jobnames:
        if os.path.isfile(jobname):
            sys.stderr.write("\n<<%s>>\n" % jobname)
            _dokins(jobname)
        else:
            sys.stderr.write("\n<<%s>>: cannot open this file\n" % jobname)
    if _tex_:
        print "\\endbatchfile\n\\endinput\n%% End of file."
    
    sys.stderr.write("(ok)\n")

####################
## END OF PROGRAM ##
####################

## TO DO:

## 2003/12/27:
##  $if, $fi, $else, $elif