#!/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