#!/usr/bin/python
######################################################################
##
## Copyright (C) 2008,  Simon Kagstrom
##
## Filename:      shlcov
## Author:        Simon Kagstrom <simon.kagstrom@gmail.com>
## Description:   Output HTML for shell script coverage
##
## $Id:$
##
######################################################################

import os, sys, time, re, getopt, shutil

base_dir = os.path.abspath(sys.path[0] + "/../")
sys.path =  [base_dir] + sys.path

import shcov, shcov.config
from shcov.file import File
from shcov.utils import *

# Regexp to match functions
fn_regexp = re.compile("\A(function[\t, ]+)*[a-z,A-Z,_]+[a-z,A-Z,_,0-9]*[\t, ]*\([\t, ]*\)[\t, ]*")
brace_regexp = re.compile("\A(\{|\}){1}")

else_then_done_regexp = re.compile("\A(else|then|fi|done|esac|do|;;){1}\\b")
case_nr_regexp = re.compile("\A(([\',\",0-9,a-z,A-Z,_, ]+)|\*{1})\){1}\\Z")

class ShcovDataOutput:
    def __init__(self, script_base, inpath, outpath, low_limit = 15, high_limit = 50):
	self.script_base = script_base
	self.inpath = inpath
	self.outpath = outpath

        self.low_limit = low_limit
        self.high_limit = high_limit

	self.files = []

        # Setup the data path (helper files)
        try:
            self.data_path = base_dir + "/data"
            os.lstat(self.data_path)
        except:
            # No such dir, fall back to /usr/share
            self.data_path = "/usr/share/" + shcov.config.PROGRAM_NAME.lower() + "/data"

    def line_is_code(self, line):
	tmp = line.strip()
	if tmp.startswith('#'):
	    return False
        # Functions and braces
	if tmp.startswith("function") or fn_regexp.match(tmp) or brace_regexp.match(tmp):
	    return False
        # fi/else/esac etc
	if else_then_done_regexp.match(tmp):
	    return False
        if case_nr_regexp.match(tmp):
            return False
        # Empty string
	if tmp == "":
	    return False
	return True

    def calculate_statistics(self, file):
	source_data = read_file( file.get_source_path() )

	# OK, this is ugly: Just put these in the object...
	file.total_lines = 0
	file.executed_lines = 0

	nr = 1
	for line in source_data.splitlines():
	    if self.line_is_code(line):
		file.total_lines = file.total_lines + 1
                if file.lines.has_key(nr):
                    file.executed_lines = file.executed_lines + 1
            nr = nr + 1

    def get_relative_path_to_helpers(self, path):
        abs_outpath = os.path.abspath(self.outpath)

        path = path.replace(abs_outpath, '')
        path.replace('//', '/')

        return "../" * path.count('/')

    def write_header(self, of, basename, path_to_helpers, total_lines, executed_lines, is_directory = True):

	# No div-by-zero please
	if total_lines == 0:
	    total_lines = 1

	# Taken from lcov
	header="""<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">

<html lang=\"en\">

<head>
  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=ISO-8859-1\">
  <title>SHCOV - %s</title>
  <link rel=\"stylesheet\" type=\"text/css\" href=\"%sgcov.css\">
</head>

<body>
""" % (basename, path_to_helpers)
        legend_file="""        <tr>
          <td class="legendItem" width="20%">Legend:</td>
          <td class="legendValue" width="80%" colspan=4>
            <span class="legendNoCov">
              not executed
            </span>
            <span class="legendCov">
              executed
            </span>
         </td>
        </tr>
"""
        legend_directory="""        <tr>
          <td class="legendItem" width="20%%">Legend:</td>
          <td class="legendValue" width="80%%" colspan=4>
            <span class="coverLegendLo">
              <b>Low:</b> 0%% to %d%%
            </span>
            <span class="coverLegendMed">
              <b>Medium:</b> %d%% to %d%%
            </span>
            <span class="coverLegendHi">
              <b>High:</b> %d%% to 100%%
            </span>
         </td>
       </tr>
""" % (self.low_limit, self.low_limit, self.high_limit, self.high_limit)
        legend = legend_file
        if is_directory:
            legend = legend_directory
	top_table="""  <table width="100%%" border=0 cellspacing=0 cellpadding=0>
    <tr><td class="title">SHCOV by Simon K&aring;gstr&ouml;m</td></tr>
    <tr><td class="ruler"><img src="%sglass.png" width=3 height=3 alt=""></td></tr>

    <tr>
      <td width="100%%">
	<table cellpadding=1 border=0 width="100%%">
	<tr>
	  <td class="headerItem" width="20%%">Current&nbsp;view:</td>
	  <td class="headerValue" width="80%%" colspan=4>%s</td>
	</tr>
	<tr>
	  <td class="headerItem" width="20%%">Date:</td>
	  <td class="headerValue" width="20%%">%s</td>
	  <td width="20%%"></td>
	  <td class="headerItem" width="20%%">Instrumented&nbsp;lines:</td>
	  <td class="headerValue" width="20%%">%d</td>
	</tr>
	<tr>
	  <td class="headerItem" width="20%%">Code&nbsp;covered:</td>
	  <td class="headerValue" width="20%%">%.1f %%</td>
	  <td width="20%%"></td>
	  <td class="headerItem" width="20%%">Executed&nbsp;lines:</td>
	  <td class="headerValue" width="20%%">%d</td>
	</tr>
        %s
	</table>
      </td>
    </tr>
    <tr><td class="ruler"><img src="%sglass.png" width=3 height=3 alt=""></td></tr>
  </table>
""" % (path_to_helpers, basename, time.strftime("%Y-%m-%d"),
       total_lines, (float(executed_lines) / float(total_lines)) * 100,
       executed_lines, legend, path_to_helpers)

	of.write(header)

	of.write(top_table)


    def write_footer(self, of):
	of.write("""</body>
</html>""")


    def handle_one_file(self, file):
	dirname = os.path.dirname(file.path)
	basename = os.path.basename(file.path)
	path = self.outpath + "/root/" + os.path.abspath(dirname)
        path = path.replace('//', '/')

	try:
	    os.makedirs( path )
	except OSError, e:
	    # This is OK (already exists)
	    pass

	of = open(os.path.join(path, basename + ".html"), "w")
        path_to_helpers = self.get_relative_path_to_helpers(os.path.abspath(path) )

	source_data = read_file(self.script_base + file.path)

	table="""  <table cellpadding=0 cellspacing=0 border=0>
    <tr>
      <td><br></td>
    </tr>
    <tr>
      <td><pre class="source">
"""

	table_end="""</pre>
      </td>
    </tr>
  </table>
  <br>
"""
	self.write_header(of, basename, path_to_helpers,
			  file.total_lines, file.executed_lines, False)

	of.write(table)
	nr = 1
	for line in source_data.splitlines():
	    line.replace(">", "&gt;")
	    line.replace("<", "&lt;")
	    line.replace('"', '&quot;')

	    if file.lines.has_key(nr) and self.line_is_code(line):
		of.write("""<span class="lineNum">     %5d</span><span class="lineCov">         %6d :  %s</span>\n""" %
			 (nr, file.lines[nr], line))
	    else:
		if self.line_is_code(line):
		    of.write("""<span class="lineNum">     %5d</span><span class="lineNoCov">              0 :  %s</span>\n""" %
			     (nr, line))
		else:
		    of.write("""<span class="lineNum">     %5d</span>                :  %s\n""" % (nr, line))
	    nr = nr + 1
	of.write(table_end)

	self.write_footer(of)

    def write_directory_header(self, of):
	of.write("""<center>
  <table width="80%" cellpadding=2 cellspacing=1 border=0>

    <tr>
      <td width="50%"><br></td>
      <td width="15%"></td>
      <td width="15%"></td>
      <td width="20%"></td>
    </tr>

    <tr>
      <td class="tableHead">Directory&nbsp;name</td>
      <td class="tableHead" colspan=3>Coverage</td>
    </tr>
""")

    def write_directory_line(self, of, path_to_helpers, linkname, name, total_lines, executed_lines):
	fraction = (float(executed_lines) / float(total_lines)) * 100

	# Setup some data depending on the coverage
	covernum = "coverNumHi"
	covertype = "coverPerHi"
	color = "emerald"
	if fraction < self.high_limit:
	    covernum = "coverNumMed"
	    covertype = "coverPerMed"
	    color = "amber"
	if fraction < self.low_limit:
	    covernum = "coverNumLo"
	    covertype = "coverPerLo"
	    color = "ruby"

	of.write("""    <tr>
      <td class="coverFile"><a href="%s">%s</a></td>
      <td class="coverBar" align="center">
	<table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="%s%s.png" width=%d height=10 alt="%.1f%%"><img src="%ssnow.png" width=%d height=10 alt="%1f%%"></td></tr></table>
      </td>
      <td class="%s">%.1f&nbsp;%%</td>
      <td class="%s">%d&nbsp;/&nbsp;%d&nbsp;lines</td>
    </tr>
""" % (linkname, name, path_to_helpers, color,
       fraction, fraction, path_to_helpers, 100-fraction, fraction, covertype, fraction, covernum, executed_lines, total_lines) )


    def handle_files(self):
	"Output all shcov data"

	class Dir:
	    def __init__(self, name):
		self.name = name
		self.basename = os.path.basename(name)
		self.total_lines = 0
		self.executed_lines = 0
		self.files = []

	    def add_file(self, file):
		self.total_lines = self.total_lines + file.total_lines
		self.executed_lines = self.executed_lines + file.executed_lines
		self.files.append(file)

	dirs = {}

	for file in self.files:
	    self.calculate_statistics(file)

	# Create dirs
	for file in self.files:
	    dirname = os.path.dirname(file.path)

	    try:
		dir = dirs[dirname]
	    except KeyError, e:
		dir = Dir(dirname)
		dirs[dirname] = dir
	    dir.add_file(file)

	total_lines = 0
	executed_lines = 0

	for dir in dirs.values():
	    total_lines = total_lines + dir.total_lines
	    executed_lines = executed_lines + dir.executed_lines

	of = open(os.path.join(self.outpath, "index.html"), "w")
	self.write_header(of, "/", "", total_lines, executed_lines)

	self.write_directory_header(of)

	for dir in dirs.values():
            path = "root/" + dir.name[1:] + "/index.html"
            path = path.replace('//', '/')

	    self.write_directory_line(of, "", path, dir.name,
				      dir.total_lines, dir.executed_lines)

	self.write_footer(of)
	of.close()

	# Create indices in each dir
	for dir in dirs.values():
	    path = self.outpath + "/root/" + dir.name
            path = path.replace('//', '/')

            path_to_helpers = self.get_relative_path_to_helpers( os.path.abspath(path) )

	    try:
		os.makedirs( path )
	    except OSError, e:
		pass

	    of = open(os.path.join(path, "index.html"), "w")

	    self.write_header(of, dir.basename, path_to_helpers, dir.total_lines, dir.executed_lines)
	    self.write_directory_header(of)
	    for file in dir.files:
		self.write_directory_line(of, path_to_helpers, file.basename + ".html", file.basename,
					file.total_lines, file.executed_lines)
	    self.write_footer(of)
	    of.close()

	# Create the files one by one
	for file in self.files:
	    self.handle_one_file(file)


    def run(self):
	try:
	    os.makedirs( self.outpath )
	except OSError, e:
	    pass
        file_objs = {} # Map names to objs

	for root, dirs, files in os.walk(self.inpath, topdown=False):
	    for name in files:
                path = os.path.join(root, name)
                try:
                    file = shcov.file.load(path, script_base = self.script_base)
                    if file_objs.has_key(file.path):
                        other = file_objs[file.path]

                        # If the files are the same, merge them
                        if file.digest == other.digest:
                            other.merge_object(file)
                            continue
                        else:
                            # Not the same, replace and take the newest
                            if file.get_source_ctime() > other.get_source_ctime():
                                file_objs[file.path] = file
                                self.files.remove(other)

                    file_objs[file.path] = file
                    self.files.append(file)
                except Exception, e:
                    print "Could not load", path, "ignoring"

	self.handle_files()

        # And copy all the data
	for root, dirs, files in os.walk(self.data_path, topdown=False):
	    for name in files:
                path = os.path.join(root, name)
                shutil.copyfile(path, os.path.join(self.outpath, name))

def usage():
    print "Usage: shlcov [--script-base=path] [--limit=low,high] datadir outdir\n"
    print "Create HTML output of shcov data in 'datadir' in 'outdir'.\n"
    print "Options are"
    print "  --script-base=path   set the base path to lookup script source (default '')"
    print "  --limit=low,high     set the low and high coverage limits (default 15,50)"

    sys.exit(1)

if __name__ == "__main__":
    script_base = ''
    low_limit = 15
    high_limit = 50

    try:
        optlist, args = getopt.gnu_getopt(sys.argv[1:], "hs:l:", ["help", "script-base=", "limit="])
    except:
        usage()

    for opt, arg in optlist:
	if opt in ("-h", "--help"):
	    usage()
	if opt in ("-s", "--script-base"):
	    script_base = arg
	if opt in ("-l", "--limit"):
            lim = arg.split(",")
            if len(lim) != 2:
                usage()
            try:
                low_limit = int(lim[0])
                high_limit = int(lim[1])
            except:
                usage()

    if len(args) < 2:
	usage()

    sc = ShcovDataOutput(script_base, args[0], args[1], low_limit, high_limit)
    sc.run()
