import sys
import time
import marshal
__all__ = ['run', 'runctx', 'Profile']

class _Utils:

    def __init__(self, profiler):
        self.profiler = profiler

    def run(self, statement, filename, sort):
        prof = self.profiler()
        try:
            prof.run(statement)
        except SystemExit:
            pass
        finally:
            self._show(prof, filename, sort)

    def runctx(self, statement, globals, locals, filename, sort):
        prof = self.profiler()
        try:
            prof.runctx(statement, globals, locals)
        except SystemExit:
            pass
        finally:
            self._show(prof, filename, sort)

    def _show(self, prof, filename, sort):
        if filename is not None:
            prof.dump_stats(filename)
        else:
            prof.print_stats(sort)


def run(statement, filename=None, sort=-1):
    return _Utils(Profile).run(statement, filename, sort)


def runctx(statement, globals, locals, filename=None, sort=-1):
    return _Utils(Profile).runctx(statement, globals, locals, filename, sort)


class Profile:
    bias = 0

    def __init__(self, timer=None, bias=None):
        self.timings = {}
        self.cur = None
        self.cmd = ''
        self.c_func_name = ''
        if bias is None:
            bias = self.bias
        self.bias = bias
        if not timer:
            self.timer = self.get_time = time.process_time
            self.dispatcher = self.trace_dispatch_i
        else:
            self.timer = timer
            t = self.timer()
            try:
                length = len(t)
            except TypeError:
                self.get_time = timer
                self.dispatcher = self.trace_dispatch_i
            if length == 2:
                self.dispatcher = self.trace_dispatch
            else:
                self.dispatcher = self.trace_dispatch_l

            def get_time_timer(timer=timer, sum=sum):
                return sum(timer())

            self.get_time = get_time_timer
        self.t = self.get_time()
        self.simulate_call('profiler')

    def trace_dispatch(self, frame, event, arg):
        timer = self.timer
        t = timer()
        t = t[0] + t[1] - self.t - self.bias
        if event == 'c_call':
            self.c_func_name = arg.__name__
        if self.dispatch[event](self, frame, t):
            t = timer()
            self.t = t[0] + t[1]
        else:
            r = timer()
            self.t = r[0] + r[1] - t

    def trace_dispatch_i(self, frame, event, arg):
        timer = self.timer
        t = timer() - self.t - self.bias
        if event == 'c_call':
            self.c_func_name = arg.__name__
        if self.dispatch[event](self, frame, t):
            self.t = timer()
        else:
            self.t = timer() - t

    def trace_dispatch_mac(self, frame, event, arg):
        timer = self.timer
        t = timer()/60.0 - self.t - self.bias
        if event == 'c_call':
            self.c_func_name = arg.__name__
        if self.dispatch[event](self, frame, t):
            self.t = timer()/60.0
        else:
            self.t = timer()/60.0 - t

    def trace_dispatch_l(self, frame, event, arg):
        get_time = self.get_time
        t = get_time() - self.t - self.bias
        if event == 'c_call':
            self.c_func_name = arg.__name__
        if self.dispatch[event](self, frame, t):
            self.t = get_time()
        else:
            self.t = get_time() - t

    def trace_dispatch_exception(self, frame, t):
        (rpt, rit, ret, rfn, rframe, rcur) = self.cur
        if rframe is not frame and rcur:
            return self.trace_dispatch_return(rframe, t)
        self.cur = (rpt, rit + t, ret, rfn, rframe, rcur)
        return 1

    def trace_dispatch_call(self, frame, t):
        if self.cur and frame.f_back is not self.cur[-2]:
            (rpt, rit, ret, rfn, rframe, rcur) = self.cur
            if not isinstance(rframe, Profile.fake_frame):
                self.trace_dispatch_return(rframe, 0)
        fcode = frame.f_code
        fn = (fcode.co_filename, fcode.co_firstlineno, fcode.co_name)
        self.cur = (t, 0, 0, fn, frame, self.cur)
        timings = self.timings
        if fn in timings:
            (cc, ns, tt, ct, callers) = timings[fn]
            timings[fn] = (cc, ns + 1, tt, ct, callers)
        else:
            timings[fn] = (0, 0, 0, 0, {})
        return 1

    def trace_dispatch_c_call(self, frame, t):
        fn = ('', 0, self.c_func_name)
        self.cur = (t, 0, 0, fn, frame, self.cur)
        timings = self.timings
        if fn in timings:
            (cc, ns, tt, ct, callers) = timings[fn]
            timings[fn] = (cc, ns + 1, tt, ct, callers)
        else:
            timings[fn] = (0, 0, 0, 0, {})
        return 1

    def trace_dispatch_return(self, frame, t):
        if frame is not self.cur[-2]:
            self.trace_dispatch_return(self.cur[-2], 0)
        (rpt, rit, ret, rfn, frame, rcur) = self.cur
        rit = rit + t
        frame_total = rit + ret
        (ppt, pit, pet, pfn, pframe, pcur) = rcur
        self.cur = (ppt, pit + rpt, pet + frame_total, pfn, pframe, pcur)
        timings = self.timings
        (cc, ns, tt, ct, callers) = timings[rfn]
        if not ns:
            ct = ct + frame_total
            cc = cc + 1
        if pfn in callers:
            callers[pfn] = callers[pfn] + 1
        else:
            callers[pfn] = 1
        timings[rfn] = (cc, ns - 1, tt + rit, ct, callers)
        return 1

    dispatch = {'call': trace_dispatch_call, 'exception': trace_dispatch_exception, 'return': trace_dispatch_return, 'c_call': trace_dispatch_c_call, 'c_exception': trace_dispatch_return, 'c_return': trace_dispatch_return}

    def set_cmd(self, cmd):
        if self.cur[-1]:
            return
        self.cmd = cmd
        self.simulate_call(cmd)

    class fake_code:

        def __init__(self, filename, line, name):
            self.co_filename = filename
            self.co_line = line
            self.co_name = name
            self.co_firstlineno = 0

        def __repr__(self):
            return repr((self.co_filename, self.co_line, self.co_name))

    class fake_frame:

        def __init__(self, code, prior):
            self.f_code = code
            self.f_back = prior

    def simulate_call(self, name):
        code = self.fake_code('profile', 0, name)
        if self.cur:
            pframe = self.cur[-2]
        else:
            pframe = None
        frame = self.fake_frame(code, pframe)
        self.dispatch['call'](self, frame, 0)

    def simulate_cmd_complete(self):
        get_time = self.get_time
        t = get_time() - self.t
        while self.cur[-1]:
            self.dispatch['return'](self, self.cur[-2], t)
            t = 0
        self.t = get_time() - t

    def print_stats(self, sort=-1):
        import pstats
        pstats.Stats(self).strip_dirs().sort_stats(sort).print_stats()

    def dump_stats(self, file):
        with open(file, 'wb') as f:
            self.create_stats()
            marshal.dump(self.stats, f)

    def create_stats(self):
        self.simulate_cmd_complete()
        self.snapshot_stats()

    def snapshot_stats(self):
        self.stats = {}
        for (func, (cc, ns, tt, ct, callers)) in self.timings.items():
            callers = callers.copy()
            nc = 0
            for callcnt in callers.values():
                nc += callcnt
            self.stats[func] = (cc, nc, tt, ct, callers)

    def run(self, cmd):
        import __main__
        dict = __main__.__dict__
        return self.runctx(cmd, dict, dict)

    def runctx(self, cmd, globals, locals):
        self.set_cmd(cmd)
        sys.setprofile(self.dispatcher)
        try:
            exec(cmd, globals, locals)
        finally:
            sys.setprofile(None)
        return self

    def runcall(self, func, *args, **kw):
        self.set_cmd(repr(func))
        sys.setprofile(self.dispatcher)
        try:
            return func(*args, **kw)
        finally:
            sys.setprofile(None)

    def calibrate(self, m, verbose=0):
        if self.__class__ is not Profile:
            raise TypeError('Subclasses must override .calibrate().')
        saved_bias = self.bias
        self.bias = 0
        try:
            return self._calibrate_inner(m, verbose)
        finally:
            self.bias = saved_bias

    def _calibrate_inner(self, m, verbose):
        get_time = self.get_time

        def f1(n):
            for i in range(n):
                x = 1

        def f(m, f1=f1):
            for i in range(m):
                f1(100)

        f(m)
        t0 = get_time()
        f(m)
        t1 = get_time()
        elapsed_noprofile = t1 - t0
        if verbose:
            print('elapsed time without profiling =', elapsed_noprofile)
        p = Profile()
        t0 = get_time()
        p.runctx('f(m)', globals(), locals())
        t1 = get_time()
        elapsed_profile = t1 - t0
        if verbose:
            print('elapsed time with profiling =', elapsed_profile)
        total_calls = 0.0
        reported_time = 0.0
        for ((filename, line, funcname), (cc, ns, tt, ct, callers)) in p.timings.items():
            if funcname in ('f', 'f1'):
                total_calls += cc
                reported_time += tt
        if verbose:
            print("'CPU seconds' profiler reported =", reported_time)
            print('total # calls =', total_calls)
        if total_calls != m + 1:
            raise ValueError('internal error: total calls = %d' % total_calls)
        mean = (reported_time - elapsed_noprofile)/2.0/total_calls
        if verbose:
            print('mean stopwatch overhead per profile event =', mean)
        return mean


def main():
    import os
    from optparse import OptionParser
    usage = 'profile.py [-o output_file_path] [-s sort] scriptfile [arg] ...'
    parser = OptionParser(usage=usage)
    parser.allow_interspersed_args = False
    parser.add_option('-o', '--outfile', dest='outfile', help='Save stats to <outfile>', default=None)
    parser.add_option('-s', '--sort', dest='sort', help='Sort order when printing to stdout, based on pstats.Stats class', default=-1)
    if not sys.argv[1:]:
        parser.print_usage()
        sys.exit(2)
    (options, args) = parser.parse_args()
    sys.argv[:] = args
    if len(args) > 0:
        progname = args[0]
        sys.path.insert(0, os.path.dirname(progname))
        with open(progname, 'rb') as fp:
            code = compile(fp.read(), progname, 'exec')
        globs = {'__file__': progname, '__name__': '__main__', '__package__': None, '__cached__': None}
        runctx(code, globs, None, options.outfile, options.sort)
    else:
        parser.print_usage()
    return parser

if __name__ == '__main__':
    main()
