"""Tree-like exploration of object referrers.

This module provides a base implementation for tree-like referrers browsing.
The two non-interactive classes ConsoleBrowser and FileBrowser output a tree
to the console or a file. One graphical user interface for referrers browsing
is provided as well. Further types can be subclassed.

All types share a similar initialisation. That is, you provide a root object
and may specify further settings such as the initial depth of the tree or an
output function.
Afterwards you can print the tree which will be arranged based on your previous
settings.

The interactive browser is based on a TreeWidget implemented in IDLE. It is
available only if you have Tcl/Tk installed. If you try to instantiate the
interactive browser without having Tkinter installed, an ImportError will be
raised.

"""
import gc
import inspect
import sys

from pympler import muppy
from pympler import summary

from pympler.util.compat import tkinter


class _Node(object):
    """A node as it is used in the tree structure.

    Each node contains the object it represents and a list of children.
    Children can be other nodes or arbitrary other objects. Any object
    in a tree which is not of the type _Node is considered a leaf.

    """
    def __init__(self, o, str_func=None):
        """You have to define the object this node represents. Also you can
        define an output function which will be used to represent this node.
        If no function is defined, the default str representation is used.

        keyword arguments
        str_func -- output function

        """
        self.o = o
        self.children = []
        self.str_func = str_func

    def __str__(self):
        """Override str(self.o) if str_func is defined."""
        if self.str_func is not None:
            return self.str_func(self.o)
        else:
            return str(self.o)


class RefBrowser(object):
    """Base class to other RefBrowser implementations.

    This base class provides means to extract a tree from a given root object
    and holds information on already known objects (to avoid repetition
    if requested).

    """

    def __init__(self, rootobject, maxdepth=3, str_func=summary._repr,
                 repeat=True, stream=None):
        """You have to provide the root object used in the refbrowser.

        keyword arguments
        maxdepth -- maximum depth of the initial tree
        str_func -- function used when calling str(node)
        repeat -- should nodes appear repeatedly in the tree, or should be
                  referred to existing nodes
        stream -- output stream (used in derived classes)

        """
        self.root = rootobject
        self.maxdepth = maxdepth
        self.str_func = str_func
        self.repeat = repeat
        self.stream = stream
        # objects which should be ignored while building the tree
        # e.g. the current frame
        self.ignore = []
        # set of object ids which are already included
        self.already_included = set()
        self.ignore.append(self.already_included)

    def get_tree(self):
        """Get a tree of referrers of the root object."""
        self.ignore.append(inspect.currentframe())
        return self._get_tree(self.root, self.maxdepth)

    def _get_tree(self, root, maxdepth):
        """Workhorse of the get_tree implementation.

        This is a recursive method which is why we have a wrapper method.
        root is the current root object of the tree which should be returned.
        Note that root is not of the type _Node.
        maxdepth defines how much further down the from the root the tree
        should be build.

        """
        objects = gc.get_referrers(root)
        res = _Node(root, self.str_func)
        self.already_included.add(id(root))
        if maxdepth == 0:
            return res
        self.ignore.append(inspect.currentframe())
        self.ignore.append(objects)
        for o in objects:
            # Ignore dict of _Node and RefBrowser objects
            if isinstance(o, dict) and sys.version_info < (3, 11):
                if any(isinstance(ref, (_Node, RefBrowser))
                       for ref in gc.get_referrers(o)):
                    continue
            _id = id(o)
            if not self.repeat and (_id in self.already_included):
                s = self.str_func(o)
                res.children.append("%s (already included, id %s)" %
                                    (s, _id))
                continue
            if (not isinstance(o, _Node)) and (o not in self.ignore):
                res.children.append(self._get_tree(o, maxdepth - 1))
        return res


class StreamBrowser(RefBrowser):
    """RefBrowser implementation which prints the tree to the console.

    If you don't like the looks, you can change it a little bit.
    The class attributes 'hline', 'vline', 'cross', and 'space' can be
    modified to your needs.

    """
    hline = '-'
    vline = '|'
    cross = '+'
    space = ' '

    def print_tree(self, tree=None):
        """ Print referrers tree to console.

        keyword arguments
        tree -- if not None, the passed tree will be printed. Otherwise it is
        based on the rootobject.

        """
        if tree is None:
            tree = self.get_tree()
        self._print(tree, '', '')

    def _print(self, tree, prefix, carryon):
        """Compute and print a new line of the tree.

        This is a recursive function.

        arguments
        tree -- tree to print
        prefix -- prefix to the current line to print
        carryon -- prefix which is used to carry on the vertical lines

        """
        level = prefix.count(self.cross) + prefix.count(self.vline)
        len_children = 0
        if isinstance(tree, _Node):
            len_children = len(tree.children)

        # add vertex
        prefix += str(tree)
        # and as many spaces as the vertex is long
        carryon += self.space * len(str(tree))
        if (level == self.maxdepth) or (not isinstance(tree, _Node)) or\
           (len_children == 0):
            self.stream.write(prefix + '\n')
            return
        else:
            # add in between connections
            prefix += self.hline
            carryon += self.space
            # if there is more than one branch, add a cross
            if len(tree.children) > 1:
                prefix += self.cross
                carryon += self.vline
            prefix += self.hline
            carryon += self.space

            if len_children > 0:
                # print the first branch (on the same line)
                self._print(tree.children[0], prefix, carryon)
                for b in range(1, len_children):
                    # the carryon becomes the prefix for all following children
                    prefix = carryon[:-2] + self.cross + self.hline
                    # remove the vlines for any children of last branch
                    if b == (len_children - 1):
                        carryon = carryon[:-2] + 2 * self.space
                    self._print(tree.children[b], prefix, carryon)
                    # leave a free line before the next branch
                    if b == (len_children - 1):
                        if len(carryon.strip(' ')) == 0:
                            return
                        self.stream.write(carryon[:-2].rstrip() + '\n')


class ConsoleBrowser(StreamBrowser):
    """RefBrowser that prints to the console (stdout)."""

    def __init__(self, *args, **kwargs):
        super(ConsoleBrowser, self).__init__(*args, **kwargs)
        if not self.stream:
            self.stream = sys.stdout


class FileBrowser(StreamBrowser):
    """RefBrowser implementation which prints the tree to a file."""

    def print_tree(self, filename, tree=None):
        """ Print referrers tree to file (in text format).

        keyword arguments
        tree -- if not None, the passed tree will be printed.

        """
        old_stream = self.stream
        self.stream = open(filename, 'w')
        try:
            super(FileBrowser, self).print_tree(tree=tree)
        finally:
            self.stream.close()
            self.stream = old_stream


# Code for interactive browser (GUI)
# ==================================

# The interactive browser requires Tkinter which is not always available. To
# avoid an import error when loading the module, we encapsulate most of the
# code in the following try-except-block. The InteractiveBrowser itself
# remains outside this block. If you try to instantiate it without having
# Tkinter installed, the import error will be raised.
try:
    if sys.version_info < (3, 5, 2):
        from idlelib import TreeWidget as _TreeWidget
    else:
        from idlelib import tree as _TreeWidget

    class _TreeNode(_TreeWidget.TreeNode):
        """TreeNode used by the InteractiveBrowser.

        Not to be confused with _Node. This one is used in the GUI
        context.

        """
        def reload_referrers(self):
            """Reload all referrers for this _TreeNode."""
            self.item.node = self.item.reftree._get_tree(self.item.node.o, 1)
            self.item._clear_children()
            self.expand()
            self.update()

        def print_object(self):
            """Print object which this _TreeNode represents to console."""
            print(self.item.node.o)

        def drawtext(self):
            """Override drawtext from _TreeWidget.TreeNode.

            This seems to be a good place to add the popup menu.

            """
            _TreeWidget.TreeNode.drawtext(self)
            # create a menu
            menu = tkinter.Menu(self.canvas, tearoff=0)
            menu.add_command(label="reload referrers",
                             command=self.reload_referrers)
            menu.add_command(label="print", command=self.print_object)
            menu.add_separator()
            menu.add_command(label="expand", command=self.expand)
            menu.add_separator()
            # the popup only disappears when to click on it
            menu.add_command(label="Close Popup Menu")

            def do_popup(event):
                menu.post(event.x_root, event.y_root)

            self.label.bind("<Button-3>", do_popup)
            # override, i.e. disable the editing of items

            # disable editing of TreeNodes
            def edit(self, event=None):
                pass  # see comment above

            def edit_finish(self, event=None):
                pass  # see comment above

            def edit_cancel(self, event=None):
                pass  # see comment above

    class _ReferrerTreeItem(_TreeWidget.TreeItem, tkinter.Label):
        """Tree item wrapper around _Node object."""

        def __init__(self, parentwindow, node, reftree):  # constr calls
            """You need to provide the parent window, the node this TreeItem
            represents, as well as the tree (_Node) which the node
            belongs to.

            """
            _TreeWidget.TreeItem.__init__(self)
            tkinter.Label.__init__(self, parentwindow)
            self.node = node
            self.parentwindow = parentwindow
            self.reftree = reftree

        def _clear_children(self):
            """Clear children list from any TreeNode instances.

            Normally these objects are not required for memory profiling, as
            they are part of the profiler.

            """
            new_children = []
            for child in self.node.children:
                if not isinstance(child, _TreeNode):
                    new_children.append(child)
            self.node.children = new_children

        def GetText(self):
            return str(self.node)

        def GetIconName(self):
            """Different icon when object cannot be expanded, i.e. has no
            referrers.

            """
            if not self.IsExpandable():
                return "python"

        def IsExpandable(self):
            """An object is expandable when it is a node which has children and
            is a container object.

            """
            if not isinstance(self.node, _Node):
                return False
            else:
                if len(self.node.children) > 0:
                    return True
                else:
                    return muppy._is_containerobject(self.node.o)

        def GetSubList(self):
            """This method is the point where further referrers are computed.

            Thus, the computation is done on-demand and only when needed.

            """
            sublist = []

            children = self.node.children
            if (len(children) == 0) and\
                    (muppy._is_containerobject(self.node.o)):
                self.node = self.reftree._get_tree(self.node.o, 1)
                self._clear_children()
                children = self.node.children

            for child in children:
                item = _ReferrerTreeItem(self.parentwindow, child,
                                         self.reftree)
                sublist.append(item)
            return sublist

except ImportError:
    _TreeWidget = None


def gui_default_str_function(o):
    """Default str function for InteractiveBrowser."""
    return summary._repr(o) + '(id=%s)' % id(o)


class InteractiveBrowser(RefBrowser):
    """Interactive referrers browser.

    The interactive browser is based on a TreeWidget implemented in IDLE. It is
    available only if you have Tcl/Tk installed. If you try to instantiate the
    interactive browser without having Tkinter installed, an ImportError will
    be raised.

    """
    def __init__(self, rootobject, maxdepth=3,
                 str_func=gui_default_str_function, repeat=True):
        """You have to provide the root object used in the refbrowser.

        keyword arguments
        maxdepth -- maximum depth of the initial tree
        str_func -- function used when calling str(node)
        repeat -- should nodes appear repeatedly in the tree, or should be
                  referred to existing nodes

        """
        if tkinter is None:
            raise ImportError(
                "InteractiveBrowser requires Tkinter to be installed.")
        RefBrowser.__init__(self, rootobject, maxdepth, str_func, repeat)

    def main(self, standalone=False):
        """Create interactive browser window.

        keyword arguments
        standalone -- Set to true, if the browser is not attached to other
        windows

        """
        window = tkinter.Tk()
        sc = _TreeWidget.ScrolledCanvas(window, bg="white",
                                        highlightthickness=0, takefocus=1)
        sc.frame.pack(expand=1, fill="both")
        item = _ReferrerTreeItem(window, self.get_tree(), self)
        node = _TreeNode(sc.canvas, None, item)
        node.expand()
        if standalone:
            window.mainloop()


# list to hold to referrers
superlist = []
root = "root"
for i in range(3):
    tmp = [root]
    superlist.append(tmp)


def foo(o):
    return str(type(o))


def print_sample():
    cb = ConsoleBrowser(root, str_func=foo)
    cb.print_tree()


def write_sample():
    fb = FileBrowser(root, str_func=foo)
    fb.print_tree('sample.txt')


if __name__ == "__main__":
    write_sample()
