import inspect
import gc
import sys
import os.path
import difflib
from earthdragon.func_util import get_parent

import pandas as pd


def is_property(code):
    """
    Using some CPython gc magics, check if a code object is a property

    gc idea taken from trace.py from stdlib
    """
    # use of gc.get_referrers() was suggested by Michael Hudson
    # all functions which refer to this code object
    gc.collect()
    code_refs = gc.get_referrers(code)
    funcs = [f for f in code_refs
             if inspect.isfunction(f)]
    if len(funcs) != 1:
        return False

    # property object will reference the original func
    props = [p for p in gc.get_referrers(funcs[0])
             if isinstance(p, property)]
    return len(props) == 1


def is_class_dict(dct):
    if not isinstance(dct, dict):
        return False
    if '__dict__' not in dct \
       or not inspect.isgetsetdescriptor(dct['__dict__']):
        return False
    return True


class Follow(object):
    """
    Follows execution path.

    Meant as a quick way to see what a function does.

    In [2]: with Follow() as f:
    ...:     df.sum()
    ...:

    In [3]: f.pprint(depth=2)
    stat_func generic.py:3542
        _reduce frame.py:3995
            _get_axis_number generic.py:285
            _get_agg_axis frame.py:4128
            as_matrix generic.py:1938
    """
    def __init__(self, depth=1, silent=False, parent=False):
        self.depth = depth
        self.silent = silent
        self.timings = []
        self.frame_cache = {}
        self._caller_cache = {}
        self.parent = parent

        self.stack_depth_cache = {}

    def current_depth(self, frame):
        current_depth = None
        i = 0
        f = frame.f_back
        while f:
            i += 1
            parent_depth = self.stack_depth_cache.get(id(f), None)
            if parent_depth is not None:
                current_depth = i + parent_depth
                break
            # if we're already past depth, don't bother finding real depth
            if i > self.depth:
                return None
            f = f.f_back

        # should always at least get back to base parent
        return current_depth

    def trace_dispatch(self, frame, event, arg):
        if len(self.stack_depth_cache) == 0:
            # __enter__ is the intial frame
            self.stack_depth_cache[id(frame.f_back)] = 0

        # the lower parts get heavy. don't do anything or frames deeper
        # than depth
        current_depth = self.current_depth(frame)
        if current_depth is None:
            return

        if current_depth > self.depth:
            return

        if event not in ['call', 'c_call']:
            return

        # skip built in funcs
        if inspect.isbuiltin(arg):
            return

        # skip properties, we're only really interested in function calls
        # this will unfortunently skip any important logic that is wrapped
        # in property logic
        code = frame.f_code
        if is_property(code):
            return

        # note that get_parent is supa slow.
        parent_name = None
        if self.parent: