#! /usr/bin/env python

# TODO: add <dir> to SmallEiffel loadpath. How?

"""
htmlshort is an extension for the SmallEiffel short utility, which let's
generate HTML documentation for some classes up to a whole library. Class
names in the generated documentation are clickable. Usage:

htmlshort -a|-all             - generate documentation for all classes found
				in the SmallEiffel load path
htmlshort (-d|--dir <dir>)+   - generate documentation for all classes found
				in the specified directories
htmlshort (<class>)+          - generate documentation for all given classes

The following options are valid for all three calls:

-d|--dir <dir>		- add dir to the search path
-f|--force		- Normally, the HTML documentation is only built, if
			  the Eiffel source is newer than the HTML file
-t|--target-dir <dir>   - Write the documentation file in the given directory.
			  Normally it's written in the same directory as the
			  Eiffel source resides.
--sort --case-insensitive
--short --no-warning    - SmallEiffel options passed to short"""

import getopt, glob, os, regex, regsub, string, sys

def usage():
    print sys.modules[__name__].__dict__['__doc__']

loadpath = []

def main():
    global loadpath, force
    try:
	optlist, args = getopt.getopt(sys.argv[1:], 'afd:t:', [
	    'force', 'target-dir=', 'dir=', 'all',
	    'short', 'sort', 'no-warning', 'case-insensitive',
	    'no_warning', 'case_insensitive'])
    except getopt.error, msg:
	print msg
	usage()
	return 1
    force = 0
    do_all = 0
    target = '' # default is same directory as Eiffel class
    dirlist = []
    short_opts = []
    for (opt, optarg) in optlist:
	if opt == '-d' or opt == '--dir':
	    dirlist.append(abspath(optarg))
	elif opt == '-f' or opt == '--force':
	    force = 1
	elif opt == '-t' or opt == '--target-dir':
	    target = optarg
	elif opt == '-a' or opt == '--all':
	    do_all = 1
	elif opt == '--short':
	    short_opts.append('-short')
	elif opt == '--sort':
	    short_opts.append('-sort')
	elif opt == '--no-warning' or opt == '--no_warning':
	    short_opts.append('-no_warning')
	elif opt == '--case-insensitive' or opt == '--case_insensitive':
	    short_opts.append('-case_insensitive')
    loadpath = get_loadpath(dirlist[:])

    if do_all:
	if args != []:
	    print "--all _and_ class names on command line"
	    usage()
	    return 1
	print "generate documentation for all classes in", loadpath
	for dir in loadpath:
	    short_in_dir(dir, target, toc=1, opts=short_opts)
	return 0

    if args == []:
	if dirlist == []:
	    print "nothing to do ..."
	    return 1
	print "generate documentation for all classes in", dirlist
	for dir in dirlist:
	    short_in_dir(dir, target, toc=1, opts=short_opts)
	return 0
    else:
	for cls in args:
	    try:
		dir = locate_class(cls)
	    except IOError, msg:
		print msg
		continue
	    short(cls, dir, target, opts=short_opts)
    # end main

def short_in_dir(dir, dstdir, toc=0, opts=[]):
    if dstdir == '':
	dstdir = dir
    if toc:
	idxfile = dstdir + '/index.html'
	try:
	    fd = open(idxfile, 'w')
	except IOError, msg:
	    print msg
	    return
	fd.write('<!-- Generated by SmallEiffel shorthtml command -->\n'
		 '<html>\n'
		 '<head>\n'
		 '<title>SmallEiffel class library</title>\n'
		 '</head>\n'
		 '<body>\n'
		 '<h3>%s</h3>\n'
		 '<ul>\n'
		 % dstdir)
    for file in map(os.path.basename, glob.glob(dir + '/*.e')):
	cls, ext = os.path.splitext(file)
	short(cls, dir, dstdir, opts)
	if toc:
	    fd.write('<li> <a href="%s.html">%s</a>\n'
		     % (cls, string.upper(cls)))
    if toc:
	fd.write('</ul>\n'
		 '</body>\n'
		 '</html>\n')

def short(cls, srcpath, dstpath, opts=[]):
    if dstpath == '':
	dstpath = srcpath
    srcname = '%s/%s.e' % (srcpath, cls)
    dstname = '%s/%s.html' % (srcpath, cls)
    if not (force or newer(srcname, dstname)):
	print "do not remake: '%s'" % cls
	return
    cmd = 'short -html %s %s' % (string.join(opts), cls)
    print 'calling:', cmd
    try:
	fd = os.popen(cmd)
    except:
	print "error calling '%s' ", cmd
	return
    try:
	out = open(dstname, 'w')
    except IOError, msg:
	print msg
	return
    cls_name = string.upper(cls)
    rx1 = regex.compile('THISCLASS')
    rs1 = '\\1%s\\2' % cls_name
    rx2 = regex.compile('<a href="CLASSREF">\([A-Z_]+\)</a>')
    while 1:
	line = fd.readline()
	if not line: break
	line = regsub.sub(rx1, cls_name, line)
	while rx2.search(line) >= 0:
	    name = rx2.group(1)
	    n1, n2 = rx2.regs[0]
	    out.write(line[:n1])
	    try:
		dir = locate_class(name)
	    except IOError:
		out.write(name)
	    else:
		out.write('<a href="file://localhost/%s/%s.html">%s</a>'
			  % (dir, string.lower(name), name))
	    line = line[n2:]
	out.write(line)
    fd.close()

def get_loadpath(lpath=[]):
    try:
	fd = open('./loadpath.se', 'r')
    except IOError:
	pass
    else:
	for line in fd.readlines():
	    if line[-1] == '\012':
		lpath.append(line[:-1])
	    else:
		lpath.append(line)
	fd.close()
    try:
	ebase = os.environ['SmallEiffel']
    except:
	print "environment variable 'SmallEiffel' unset"
	return 1
    fd = tryopen(ebase + '/sys/system.se')
    system = fd.readline()[:-1]
    fd.close()
    fd = tryopen(ebase + '/sys/loadpath.' + system)
    for line in fd.readlines():
	if line[-1] == '\012':
	    lpath.append(abspath(line[:-1]))
	else:
	    lpath.append(abspath(line))
    fd.close()
    return lpath

class_cache = {}
def locate_class(cls):
    # returns directory name
    global class_cache
    cls = string.lower(cls)
    if class_cache.has_key(cls):
	return class_cache[cls]
    for dir in loadpath:
	try:
	    fp = open('%s/%s.e' % (dir, cls))
	except IOError:
	    pass
	else:
	    class_cache[cls] = dir
	    return dir
    raise IOError, "Cannot locate class '%s' in loadpath" % cls

def abspath(path):
    path = os.path.expanduser(path)
    path = os.path.expandvars(path)
    if not os.path.isdir(path):
	print "WARNING: not a directory: '%s'" % path
	return path
    if os.path.isabs(path):
	return path
    abspath = ['']
    for dir in string.split(os.getcwd() + os.sep + path, os.sep):
	if dir == '' or dir == os.curdir:
	    continue
	if dir == os.pardir and len(abspath) > 1:
	    del abspath[-1]
	else:
	    abspath.append(dir)
    return string.join(abspath, os.sep)


def newer(fn1, fn2):
    # is fn1 newer than fn2?
    stat1 = os.stat(fn1)
    if not os.path.exists(fn2):
	return 1
    stat2 = os.stat(fn2)
    return stat1[9] > stat2[9] # ctime

def tryopen(fn, mode='r'):
    try:
        fp = open(fn, mode)
    except IOError, msg:
        print 'Can\'t open "%s":' % fn, msg
        sys.exit(1)
    return fp

if __name__ == '__main__' or sys.argv[0] == __name__:
    main()
