"""
summary: draw custom graphs

description:
  Showing custom graphs, using `ida_graph.GraphViewer`. In addition,
  show how to write actions that can be performed on those.

keywords: graph, actions

level: advanced
"""

# -----------------------------------------------------------------------
# This is an example illustrating how to use the user graphing functionality
# in Python
# (c) Hex-Rays
#

import ida_kernwin
import ida_graph
import ida_ua
import ida_idp
import ida_funcs
import ida_xref

import idautils

class _base_graph_action_handler_t(ida_kernwin.action_handler_t):
    def __init__(self, graph):
        ida_kernwin.action_handler_t.__init__(self)
        self.graph = graph

    def update(self, ctx):
        return ida_kernwin.AST_ENABLE_ALWAYS


class GraphCloser(_base_graph_action_handler_t):
    def activate(self, ctx):
        self.graph.Close()


class ColorChanger(_base_graph_action_handler_t):
    def activate(self, ctx):
        self.graph.color = self.graph.color ^ 0xffffff
        self.graph.Refresh()
        return 1


class SelectionPrinter(_base_graph_action_handler_t):
    def activate(self, ctx):
        try:
            sel = ctx.graph_selection
        except:
            # IDA < 7.4 doesn't provide graph selection as part of
            # the action_activation_ctx_t; it needs to be queried.
            sel = ida_graph.screen_graph_selection_t()
            gv = ida_graph.get_graph_viewer(self.graph.GetWidget())
            ida_graph.viewer_get_selection(gv, sel)
        if sel:
            for s in sel:
                if s.is_node:
                    print("Selected node %d" % s.node)
                else:
                    print("Selected edge %d -> %d" % (s.elp.e.src, s.elp.e.dst))
        return 1


class MyGraph(ida_graph.GraphViewer):
    def __init__(self, funcname, result):
        self.title = "call graph of " + funcname
        ida_graph.GraphViewer.__init__(self, self.title)
        self.funcname = funcname
        self.result = result
        self.color = 0xff00ff

        #
        # for the sake of this example, here's how one can use
        # 'ida_kernwin.View_Hooks' (which can be used with all
        # "listing-like" and "graph" widgets) to be notified
        # of cursor movement, current node changes, etc...
        # in this graph.
        #
        class my_view_hooks_t(ida_kernwin.View_Hooks):
            def __init__(self, v):
                ida_kernwin.View_Hooks.__init__(self)
                self.hook()
                # let's use weakrefs, so as soon as the last ref to
                # the 'MyGraph' instance is dropped, the 'my_view_hooks_t'
                # instance hooks can be automatically un-hooked, and deleted.
                # (in other words: avoid circular reference.)
                import weakref
                self.v = weakref.ref(v)

            def view_loc_changed(self, w, now, was):
                now_node = now.renderer_info().pos.node
                was_node = was.renderer_info().pos.node
                if now_node != was_node:
                    if self.v().GetWidget() == w:
                        print("Current node now: #%d (was #%d)" % (now_node, was_node))

        self.my_view_hooks = my_view_hooks_t(self)


    def OnRefresh(self):
        self.Clear()
        id = self.AddNode((self.funcname, self.color))
        for x in self.result:
            callee = self.AddNode((x, self.color))
            self.AddEdge(id, callee)

        return True

    def OnGetText(self, node_id):
        return self[node_id]

    def OnPopup(self, widget, popup_handle):
        # graph closer
        actname = "graph_closer:%s" % self.title
        desc = ida_kernwin.action_desc_t(actname, "Close: %s" % self.title, GraphCloser(self))
        ida_kernwin.attach_dynamic_action_to_popup(None, popup_handle, desc)

        # color changer
        actname = "color_changer:%s" % self.title
        desc = ida_kernwin.action_desc_t(actname, "Change colors: %s" % self.title, ColorChanger(self))
        ida_kernwin.attach_dynamic_action_to_popup(None, popup_handle, desc)

        # selection printer
        actname = "selection_printer:%s" % self.title
        desc = ida_kernwin.action_desc_t(actname, "Print selection: %s" % self.title, SelectionPrinter(self))
        ida_kernwin.attach_dynamic_action_to_popup(None, popup_handle, desc)


def show_graph():
    f = ida_funcs.get_func(ida_kernwin.get_screen_ea())
    if not f:
        print("Must be in a function")
        return
    # Iterate through all function instructions and take only call instructions
    result = []
    tmp = ida_ua.insn_t()
    for x in [x for x in f if (ida_ua.decode_insn(tmp, x) and ida_idp.is_call_insn(tmp))]:
        xb = ida_xref.xrefblk_t()
        for xref in xb.refs_from(x, ida_xref.XREF_FAR):
            if not xref.iscode: continue
            t = ida_funcs.get_func_name(xref.to)
            if not t:
                t = hex(xref.to)
            result.append(t)
    g = MyGraph(ida_funcs.get_func_name(f.start_ea), result)
    if g.Show():
        return g
    else:
        return None

g = show_graph()
if g:
    print("Graph created and displayed!")
