"""sancho.code_coverage

Provides code coverage analysis.
"""
from os.path import exists
import sys, dis, types

try:
    _findlinestarts = dis.findlinestarts
except AttributeError:
    # Python 2.3 needs this function
    def _findlinestarts(code):
        byte_increments = [ord(c) for c in code.co_lnotab[0::2]]
        line_increments = [ord(c) for c in code.co_lnotab[1::2]]

        lastlineno = None
        lineno = code.co_firstlineno
        addr = 0
        for byte_incr, line_incr in zip(byte_increments, line_increments):
            if byte_incr:
                if lineno != lastlineno:
                    yield (addr, lineno)
                    lastlineno = lineno
                addr += byte_incr
            lineno += line_incr
        if lineno != lastlineno:
            yield (addr, lineno)


def _find_executable_lines(code, executable_lines=None):
    if executable_lines is None:
        executable_lines = set()
    for addr, lineno in _findlinestarts(code):
        executable_lines.add(lineno)
    for const in code.co_consts:
        if isinstance(const, types.CodeType):
            if const.co_filename == code.co_filename:
                _find_executable_lines(const, executable_lines)
    return executable_lines

class CoverageTrace:
    def __init__(self, modules):
        """modules : list of modules for which to show coverage
        """
        self.counts = {}  # {modulename: {lineno: count}}
        self.code_objects = {} # {modulename: {code_object}}
        for modulename in modules:
            self.counts[modulename] = {}
            self.code_objects[modulename] = set()

    def _trace(self, frame, why, arg):
        if why == 'line':
            modulename = frame.f_globals.get("__name__", "<unnamed module>")
            if modulename in self.counts:
                # record line number of every trace
                lineno = frame.f_lineno
                executed_lines = self.counts[modulename]
                executed_lines[lineno] = executed_lines.get(lineno, 0) + 1
                if frame.f_code not in self.code_objects[modulename]:
                    self.code_objects[modulename].add(frame.f_code)
        return self._trace

    def runfunc(self, func, *args, **kw):
        result = None
        saved_trace = sys._getframe().f_trace
        sys.settrace(self._trace)
        try:
            result = func(*args, **kw)
        finally:
            sys.settrace(saved_trace)
        return result

    def report(self, outfile=sys.stdout, show_lines=1):
        for modulename in self.counts.keys():
            executed_lines = self.counts[modulename]
            outfile.write('%s: ' % modulename)
            if not executed_lines:
                # module as not executed as all
                outfile.write("not executed\n")
                continue
            outfile.write('\n')

            filename = sys.modules[modulename].__file__
            if filename[-4:] == ".pyc" or filename[-4:] == ".pyo":
                if exists(filename[:-4] + '.qpy'):
                    filename = filename[:-4] + '.qpy'
                else:
                    filename = filename[:-4] + '.py'
            elif filename[-5:] == ".ptlc":
                filename = filename[:-1]

            try:
                lines = open(filename).readlines()
            except IOError:
                exc = sys.env_info()[1]
                sys.stderr.write("%s: Could not open %r: %s - skipping\n" %
                                 ("trace", filename, exc.strerror))
                continue

            executable_lines = set()
            for code_object in self.code_objects[modulename]:
                _find_executable_lines(code_object, executable_lines)

            for lineno, line in enumerate(lines):
                lineno += 1
                if lineno in executed_lines:
                    outfile.write('%5d: ' % executed_lines[lineno])
                else:
                    if lineno in executable_lines:
                        outfile.write('@%-5d>' % lineno)
                    else:
                        outfile.write(' '*7)
                outfile.write(line)


def report_coverage(modules, func, *args, **kwargs):
    ct = CoverageTrace(modules)
    rv = ct.runfunc(func, *args, **kwargs)
    ct.report()
    return rv
