"""
Internal module (hooks)

This is an internal module that contains implementations of all the hooks
that are used. Some of the things that are hooked are things such as
comment creation, function and segment scoping, etc. This is not intended
to be used by the average user.
"""

import six
import sys, logging
import functools, operator, itertools, types

import database, function, instruction, ui
import internal
from internal import comment, utils, interface, exceptions as E

import idaapi

def greeting():
    barrier = 93
    available = ['database', 'function', 'instruction', 'segment', 'structure', 'enumeration']

    print '=' * barrier
    print "Welcome to the ida-minsc plugin!"
    print ""
    print "You can find documentation at https://arizvisa.github.io/ida-minsc/"
    print ""
    print "The available namespaces are: {:s}".format(', '.join(available))
    print "Please use `help(namespace)` for their usage."
    print ""
    print "Your globals have also been cleaned, use `dir()` to see your work."
    print '-' * barrier

### comment hooks
class commentbase(object):
    @classmethod
    def database_init(cls, idp_modname):
        if hasattr(cls, 'event'):
            return
        cls.event = cls._event()
        next(cls.event)

    @classmethod
    def nw_database_init(cls, nw_code, is_old_database):
        idp_modname = idaapi.get_idp_name()
        return cls.database_init(idp_modname)

    @classmethod
    def is_ready(cls):
        global State
        return State == state.ready

class address(commentbase):
    @classmethod
    def _is_repeatable(cls, ea):
        f = idaapi.get_func(ea)
        return True if f is None else False

    @classmethod
    def _update_refs(cls, ea, old, new):
        f = idaapi.get_func(ea)
        for key in old.viewkeys() ^ new.viewkeys():
            if key not in new:
                logging.debug(u"{:s}.update_refs({:#x}) : Decreasing refcount for {!s} at {:s}. Updating old keys ({!s}) to new keys ({!s}).".format('.'.join((__name__, cls.__name__)), ea, utils.string.repr(key), 'address', utils.string.repr(old.viewkeys()), utils.string.repr(new.viewkeys())))
                if f: internal.comment.contents.dec(ea, key)
                else: internal.comment.globals.dec(ea, key)
            if key not in old:
                logging.debug(u"{:s}.update_refs({:#x}) : Increasing refcount for {!s} at {:s}. Updating old keys ({!s}) to new keys ({!s}).".format('.'.join((__name__, cls.__name__)), ea, utils.string.repr(key), 'address', utils.string.repr(old.viewkeys()), utils.string.repr(new.viewkeys())))
                if f: internal.comment.contents.inc(ea, key)
                else: internal.comment.globals.inc(ea, key)
            continue
        return

    @classmethod
    def _create_refs(cls, ea, res):
        f = idaapi.get_func(ea)
        for key in res.viewkeys():
            logging.debug(u"{:s}.create_refs({:#x}) : Increasing refcount for {!s} at {:s} for keys ({!s}).".format('.'.join((__name__, cls.__name__)), ea, utils.string.repr(key), 'address', utils.string.repr(res.viewkeys())))
            if f: internal.comment.contents.inc(ea, key)
            else: internal.comment.globals.inc(ea, key)
        return

    @classmethod
    def _delete_refs(cls, ea, res):
        f = idaapi.get_func(ea)
        for key in res.viewkeys():
            logging.debug(u"{:s}.delete_refs({:#x}) : Decreasing refcount for {!s} at {:s} for keys ({!s}).".format('.'.join((__name__, cls.__name__)), ea,  utils.string.repr(key), 'address', utils.string.repr(res.viewkeys())))
            if f: internal.comment.contents.dec(ea, key)
            else: internal.comment.globals.dec(ea, key)
        return

    @classmethod
    def _event(cls):
        while True:
            # cmt_changing event
            ea, rpt, new = (yield)
            old = utils.string.of(idaapi.get_cmt(ea, rpt))
            f, o, n = idaapi.get_func(ea), internal.comment.decode(old), internal.comment.decode(new)

            # update references before we update the comment
            cls._update_refs(ea, o, n)

            # wait for cmt_changed event
            newea, nrpt, none = (yield)

            # now fix the comment the user typed
            if (newea, nrpt, none) == (ea, rpt, None):
                ncmt, repeatable = utils.string.of(idaapi.get_cmt(ea, rpt)), cls._is_repeatable(ea)

                if (ncmt or '') != new:
                    logging.warn(u"{:s}.event() : Comment from event at address {:#x} is different from database. Expected comment ({!s}) is different from current comment ({!s}).".format('.'.join((__name__, cls.__name__)), ea, utils.string.repr(new), utils.string.repr(ncmt)))

                ## if the comment is of the correct format, then we can simply
                ## write the comment to the given address
                if internal.comment.check(new):
                    idaapi.set_cmt(ea, utils.string.to(new), repeatable)

                ## if there's a comment to set, then assign it to the requested
                ## address
                elif new:
                    idaapi.set_cmt(ea, utils.string.to(new), rpt)

                ## otherwise, we can just delete all the references at the address
                else:
                    cls._delete_refs(ea, n)
                continue

            # if the changed event doesn't happen in the right order
            logging.fatal(u"{:s}.event() : Comment events are out of sync at address {:#x}, updating tags from previous comment. Expected comment ({!s}) is different from current comment ({!s}).".format('.'.join((__name__, cls.__name__)), ea, utils.string.repr(o), utils.string.repr(n)))

            # delete the old comment
            cls._delete_refs(ea, o)
            idaapi.set_cmt(ea, '', rpt)
            logging.warn(u"{:s}.event() : Deleted comment at address {:#x} was {!s}.".format('.'.join((__name__, cls.__name__)), ea, utils.string.repr(o)))

            # new comment
            new = utils.string.of(idaapi.get_cmt(newea, nrpt))
            n = internal.comment.decode(new)
            cls._create_refs(newea, n)

            continue
        return

    @classmethod
    def changing(cls, ea, repeatable_cmt, newcmt):
        if not cls.is_ready():
            return logging.debug(u"{:s}.changing({:#x}, {:d}, {!s}) : Ignoring comment.changing event (database not ready) for a {:s} comment at {:#x}.".format('.'.join((__name__, cls.__name__)), ea, repeatable_cmt, utils.string.repr(newcmt), 'repeatable' if repeatable_cmt else 'non-repeatable', ea))

        # Grab our old comment, because we're going to submit this later to a coro
        logging.debug(u"{:s}.changing({:#x}, {:d}, {!s}) : Received comment.changing event for a {:s} comment at {:#x}.".format('.'.join((__name__, cls.__name__)), ea, repeatable_cmt, utils.string.repr(newcmt), 'repeatable' if repeatable_cmt else 'non-repeatable', ea))
        oldcmt = utils.string.of(idaapi.get_cmt(ea, repeatable_cmt))

        # First disable our hooks so that we can prevent re-entrancy issues
        [ ui.hook.idb.disable(event) for event in ['changing_cmt', 'cmt_changed'] ]

        # Now we can use our coroutine to begin the comment update, so that
        # later, the "changed" event can do the actual update.
        try:
            cls.event.send((ea, bool(repeatable_cmt), utils.string.of(newcmt)))

        # If a StopIteration was raised when submitting the comment to the
        # coroutine, then we somehow desynchronized. Re-initialize the coroutine
        # with the hope that things are fixed.
        except StopIteration, E:
            logging.fatal(u"{:s}.changing({:#x}, {:d}, {!s}) : Unexpected termination of event handler. Re-instantiating it.".format('.'.join((__name__, cls.__name__)), ea, repeatable_cmt, utils.string.repr(newcmt)))
            cls.event = cls._event(); next(cls.event)

        # Last thing to do is to re-enable the hooks that we disabled
        finally:
            [ ui.hook.idb.enable(event) for event in ['changing_cmt', 'cmt_changed'] ]

        # And then we can leave..
        return

    @classmethod
    def changed(cls, ea, repeatable_cmt):
        if not cls.is_ready():
            return logging.debug(u"{:s}.changed({:#x}, {:d}) : Ignoring comment.changed event (database not ready) for a {:s} comment at {:#x}.".format('.'.join((__name__, cls.__name__)), ea, repeatable_cmt, 'repeatable' if repeatable_cmt else 'non-repeatable', ea))

        # Grab our new comment, because we're going to submit this later to our coro
        logging.debug(u"{:s}.changed({:#x}, {:d}) : Received comment.changed event for a {:s} comment at {:#x}.".format('.'.join((__name__, cls.__name__)), ea, repeatable_cmt, 'repeatable' if repeatable_cmt else 'non-repeatable', ea))
        newcmt = utils.string.of(idaapi.get_cmt(ea, repeatable_cmt))

        # First disable our hooks so that we can prevent re-entrancy issues
        [ ui.hook.idb.disable(event) for event in ['changing_cmt', 'cmt_changed'] ]

        # Now we can use our coroutine to update the comment state, so that the
        # coroutine will perform the final update.
        try:
            cls.event.send((ea, bool(repeatable_cmt), None))

        # If a StopIteration was raised when submitting the comment to the
        # coroutine, then we somehow desynchronized. Re-initialize the coroutine
        # with the hope that things are fixed.
        except StopIteration, E:
            logging.fatal(u"{:s}.changed({:#x}, {:d}) : Unexpected termination of event handler. Re-instantiating it.".format('.'.join((__name__, cls.__name__)), ea, repeatable_cmt))
            cls.event = cls._event(); next(cls.event)

        # Re-enable our hooks that we had prior disabled
        finally:
            [ ui.hook.idb.enable(event) for event in ['changing_cmt', 'cmt_changed'] ]

        # Updating the comment was complete, that should've been it.
        return

    @classmethod
    def old_changed(cls, ea, repeatable_cmt):
        if not cls.is_ready():
            return logging.debug(u"{:s}.old_changed({:#x}, {:d}) : Ignoring comment.changed event (database not ready) for a {:s} comment at {:#x}.".format('.'.join((__name__, cls.__name__)), ea, repeatable, 'repeatable' if repeatable else 'non-repeatable', ea))

        # first we'll grab our comment that the user updated
        logging.debug(u"{:s}.old_changed({:#x}, {:d}) : Received comment.changed event for a {:s} comment at {:#x}.".format('.'.join((__name__, cls.__name__)), ea, repeatable, 'repeatable' if repeatable else 'non-repeatable', ea))
        cmt = utils.string.of(idaapi.get_cmt(ea, repeatable_cmt))
        fn = idaapi.get_func(ea)

        # if we're in a function, then clear our contents.
        if fn:
            internal.comment.contents.set_address(ea, 0)

        # otherwise, just clear the tags globally
        else:
            internal.comment.globals.set_address(ea, 0)

        # simply grab the comment and update its refs
        res = internal.comment.decode(cmt)
        if res:
            cls._create_refs(ea, res)

        # otherwise, there's nothing to do if its empty
        else:
            return

        # and then re-write it back to its address, but not before disabling
        # our hooks that brought is here so that we can avoid any re-entrancy issues.
        ui.hook.idb.disable('cmt_changed')
        try:
            idaapi.set_cmt(ea, utils.string.to(internal.comment.encode(res)), repeatable_cmt)

        # now we can "finally" re-enable our hook
        finally:
            ui.hook.idb.enable('cmt_changed')

        # and then leave because hopefully things were updated properly
        return

class globals(commentbase):
    @classmethod
    def _update_refs(cls, fn, old, new):
        for key in old.viewkeys() ^ new.viewkeys():
            if key not in new:
                logging.debug(u"{:s}.update_refs({:#x}) : Decreasing refcount for {!s} at {:s}. Updating old keys ({!s}) to new keys ({!s}).".format('.'.join((__name__, cls.__name__)), interface.range.start(fn) if fn else idaapi.BADADDR, utils.string.repr(key), 'function' if fn else 'global', utils.string.repr(old.viewkeys()), utils.string.repr(new.viewkeys())))
                internal.comment.globals.dec(interface.range.start(fn), key)
            if key not in old:
                logging.debug(u"{:s}.update_refs({:#x}) : Increasing refcount for {!s} at {:s}. Updating old keys ({!s}) to new keys ({!s}).".format('.'.join((__name__, cls.__name__)), interface.range.start(fn) if fn else idaapi.BADADDR, utils.string.repr(key), 'function' if fn else 'global', utils.string.repr(old.viewkeys()), utils.string.repr(new.viewkeys())))
                internal.comment.globals.inc(interface.range.start(fn), key)
            continue
        return

    @classmethod
    def _create_refs(cls, fn, res):
        for key in res.viewkeys():
            internal.comment.globals.inc(interface.range.start(fn), key)
            logging.debug(u"{:s}.create_refs({:#x}) : Increasing refcount for {!s} at {:s} for keys ({!s}).".format('.'.join((__name__, cls.__name__)), interface.range.start(fn) if fn else idaapi.BADADDR, utils.string.repr(key), 'function' if fn else 'global', utils.string.repr(res.viewkeys())))
        return

    @classmethod
    def _delete_refs(cls, fn, res):
        for key in res.viewkeys():
            internal.comment.globals.dec(interface.range.start(fn), key)
            logging.debug(u"{:s}.delete_refs({:#x}) : Decreasing refcount for {!s} at {:s} for keys ({!s}).".format('.'.join((__name__, cls.__name__)), interface.range.start(fn) if fn else idaapi.BADADDR, utils.string.repr(key), 'function' if fn else 'global', utils.string.repr(res.viewkeys())))
        return

    @classmethod
    def _event(cls):
        while True:
            # cmt_changing event
            ea, rpt, new = (yield)
            fn = idaapi.get_func(ea)
            old = utils.string.of(idaapi.get_func_cmt(fn, rpt))
            o, n = internal.comment.decode(old), internal.comment.decode(new)

            # update references before we update the comment
            cls._update_refs(fn, o, n)

            # wait for cmt_changed event
            newea, nrpt, none = (yield)

            # now we can fix the user's new coment
            if (newea, nrpt, none) == (ea, rpt, None):
                ncmt = utils.string.of(idaapi.get_func_cmt(fn, rpt))

                if (ncmt or '') != new:
                    logging.warn(u"{:s}.event() : Comment from event for function {:#x} is different from database. Expected comment ({!s}) is different from current comment ({!s}).".format('.'.join((__name__, cls.__name__)), ea, utils.string.repr(new), utils.string.repr(ncmt)))

                ## if the comment is correctly formatted as a tag, then we
                ## can simply write the comment at the given address
                if internal.comment.check(new):
                    idaapi.set_func_cmt(fn, utils.string.to(new), rpt)

                ## if there's a comment to set, then assign it to the requested
                ## function address
                elif new:
                    idaapi.set_func_cmt(fn, utils.string.to(new), rpt)

                ## otherwise, there's no comment there and we need to delete
                ## all references at the address
                else:
                    cls._delete_refs(fn, n)
                continue

            # if the changed event doesn't happen in the right order
            logging.fatal(u"{:s}.event() : Comment events are out of sync for function {:#x}, updating tags from previous comment. Expected comment ({!s}) is different from current comment ({!s}).".format('.'.join((__name__, cls.__name__)), ea, utils.string.repr(o), utils.string.repr(n)))

            # delete the old comment
            cls._delete_refs(fn, o)
            idaapi.set_func_cmt(fn, '', rpt)
            logging.warn(u"{:s}.event() : Deleted comment for function {:#x} was ({!s}).".format('.'.join((__name__, cls.__name__)), ea, utils.string.repr(o)))

            # new comment
            newfn = idaapi.get_func(newea)
            new = utils.string.of(idaapi.get_func_cmt(newfn, nrpt))
            n = internal.comment.decode(new)
            cls._create_refs(newfn, n)

            continue
        return

    @classmethod
    def changing(cls, cb, a, cmt, repeatable):
        if not cls.is_ready():
            return logging.debug(u"{:s}.changing({!s}, {:#x}, {!s}, {:d}) : Ignoring comment.changing event (database not ready) for a {:s} comment at {:#x}.".format('.'.join((__name__, cls.__name__)), utils.string.repr(cb), interface.range.start(a), utils.string.repr(cmt), repeatable, 'repeatable' if repeatable else 'non-repeatable', interface.range.start(a)))

        # First we'll check to see if this is an actual function comment by confirming
        # that we're in a function, and that our comment is not empty.
        logging.debug(u"{:s}.changing({!s}, {:#x}, {!s}, {:d}) : Received comment.changing event for a {:s} comment at {:#x}.".format('.'.join((__name__, cls.__name__)), utils.string.repr(cb), interface.range.start(a), utils.string.repr(cmt), repeatable, 'repeatable' if repeatable else 'non-repeatable', interface.range.start(a)))
        fn = idaapi.get_func(interface.range.start(a))
        if fn is None and not cmt:
            return

        # Grab our old comment, because we're going to submit this later to a coro
        oldcmt = utils.string.of(idaapi.get_func_cmt(fn, repeatable))

        # We need to disable our hooks so that we can prevent re-entrancy issues
        hooks = ['changing_area_cmt', 'area_cmt_changed'] if idaapi.__version__ < 7.0 else ['changing_range_cmt', 'range_cmt_changed']
        [ ui.hook.idb.disable(event) for event in hooks ]

        # Now we can use our coroutine to begin the comment update, so that
        # later, the "changed" event can do the actual update.
        try:
            cls.event.send((interface.range.start(fn), bool(repeatable), utils.string.of(cmt)))

        # If a StopIteration was raised when submitting the comment to the
        # coroutine, then we somehow desynchronized. Re-initialize the coroutine
        # with the hope that things are fixed.
        except StopIteration, E:
            logging.fatal(u"{:s}.changing({!s}, {:#x}, {!s}, {:d}) : Unexpected termination of event handler. Re-instantiating it.".format('.'.join((__name__, cls.__name__)), utils.string.repr(cb), interface.range.start(a), utils.string.repr(cmt), repeatable))
            cls.event = cls._event(); next(cls.event)

        # Last thing to do is to re-enable the hooks that we disabled
        finally:
            [ ui.hook.idb.enable(event) for event in hooks ]

        # And then we're ready for the "changed" event
        return

    @classmethod
    def changed(cls, cb, a, cmt, repeatable):
        if not cls.is_ready():
            return logging.debug(u"{:s}.changed({!s}, {:#x}, {!s}, {:d}) : Ignoring comment.changed event (database not ready) for a {:s} comment at {:#x}.".format('.'.join((__name__, cls.__name__)), utils.string.repr(cb), interface.range.start(a), utils.string.repr(cmt), repeatable, 'repeatable' if repeatable else 'non-repeatable', interface.range.start(a)))

        # First we'll check to see if this is an actual function comment by confirming
        # that we're in a function, and that our comment is not empty.
        logging.debug(u"{:s}.changed({!s}, {:#x}, {!s}, {:d}) : Received comment.changed event for a {:s} comment at {:#x}.".format('.'.join((__name__, cls.__name__)), utils.string.repr(cb), interface.range.start(a), utils.string.repr(cmt), repeatable, 'repeatable' if repeatable else 'non-repeatable', interface.range.start(a)))
        fn = idaapi.get_func(interface.range.start(a))
        if fn is None and not cmt:
            return

        # Grab our new comment, because we're going to submit this later to a coro
        newcmt = utils.string.of(idaapi.get_func_cmt(fn, repeatable))

        # We need to disable our hooks so that we can prevent re-entrancy issues
        hooks = ['changing_area_cmt', 'area_cmt_changed'] if idaapi.__version__ < 7.0 else ['changing_range_cmt', 'range_cmt_changed']
        [ ui.hook.idb.disable(event) for event in hooks ]

        # Now we can use our coroutine to update the comment state, so that the
        # coroutine will perform the final update.
        try:
            cls.event.send((interface.range.start(fn), bool(repeatable), None))

        # If a StopIteration was raised when submitting the comment to the
        # coroutine, then we somehow desynchronized. Re-initialize the coroutine
        # with the hope that things are fixed.
        except StopIteration, E:
            logging.fatal(u"{:s}.changed({!s}, {:#x}, {!s}, {:d}) : Unexpected termination of event handler. Re-instantiating it.".format('.'.join((__name__, cls.__name__)), utils.string.repr(cb), interface.range.start(a), utils.string.repr(cmt), repeatable))
            cls.event = cls._event(); next(cls.event)

        # Last thing to do is to re-enable the hooks that we disabled
        finally:
            [ ui.hook.idb.enable(event) for event in hooks ]

        # We're done updating the comment, that should be it.
        return

    @classmethod
    def old_changed(cls, cb, a, cmt, repeatable):
        if not cls.is_ready():
            return logging.debug(u"{:s}.old_changed({!s}, {:#x}, {!s}, {:d}) : Ignoring comment.changed event (database not ready) for a {:s} comment at {:#x}.".format('.'.join((__name__, cls.__name__)), utils.string.repr(cb), interface.range.start(a), utils.string.repr(cmt), repeatable, 'repeatable' if repeatable else 'non-repeatable', interface.range.start(a)))

        # first thing to do is to identify whether we're in a function or not,
        # so we first grab the address from the area_t...
        logging.debug(u"{:s}.old_changed({!s}, {:#x}, {!s}, {:d}) : Received comment.changed event for a {:s} comment at {:#x}.".format('.'.join((__name__, cls.__name__)), utils.string.repr(cb), interface.range.start(a), utils.string.repr(cmt), repeatable, 'repeatable' if repeatable else 'non-repeatable', interface.range.start(a)))
        ea = interface.range.start(a)

        # then we can use it to verify that we're in a function. if not, then
        # this is a false alarm and we can leave.
        fn = idaapi.get_func(ea)
        if fn is None:
            return

        # we're using an old version of ida here, so start out empty
        internal.comment.globals.set_address(ea, 0)

        # grab our comment here and re-create its refs
        res = internal.comment.decode(utils.string.of(cmt))
        if res:
            cls._create_refs(fn, res)

        # if it's empty, then there's nothing to do and we can leave
        else:
            return

        # now we can simply re-write it it, but not before disabling our hooks
        # that got us here, so that we can avoid any re-entrancy issues.
        ui.hook.idb.disable('area_cmt_changed')
        try:
            idaapi.set_func_cmt(fn, utils.string.to(internal.comment.encode(res)), repeatable)

        # now we can "finally" re-enable our hook
        finally:
            ui.hook.idb.enable('area_cmt_changed')

        # that should've been it, so we can now just leave
        return

### database scope
class state(object):
    # database notification state
    init = type('init', (object,), {})()
    loaded = type('loaded', (object,), {})()
    ready = type('ready', (object,), {})()

State = None

def on_init(idp_modname):
    '''IDP_Hooks.init'''

    # Database has just been opened, setup the initial state.
    global State
    if State == None:
        State = state.init
    else:
        logging.debug(u"{:s}.on_init({!s}) : Received unexpected state transition from state ({!s}).".format(__name__, utils.string.repr(idp_modname), utils.string.repr(State)))

def nw_on_init(nw_code, is_old_database):
    idp_modname = idaapi.get_idp_name()
    return on_init(idp_modname)

def on_newfile(fname):
    '''IDP_Hooks.newfile'''

    # Database has been created, switch the state to loaded.
    global State
    if State == state.init:
        State = state.loaded
    else:
        logging.debug(u"{:s}.on_newfile({!s}) : Received unexpected state transition from state ({!s}).".format(__name__, utils.string.repr(fname), utils.string.repr(State)))
    # FIXME: save current state like base addresses and such

def nw_on_newfile(nw_code, is_old_database):
    if is_old_database:
        return
    fname = idaapi.cvar.database_idb
    return on_newfile(fname)

def on_oldfile(fname):
    '''IDP_Hooks.oldfile'''

    # Database has been loaded, switch the state to ready.
    global State
    if State == state.init:
        State = state.ready

        __check_functions()
    else:
        logging.debug(u"{:s}.on_oldfile({!s}) : Received unexpected state transition from state ({!s}).".format(__name__, utils.string.repr(fname), utils.string.repr(State)))
    # FIXME: save current state like base addresses and such

def nw_on_oldfile(nw_code, is_old_database):
    if not is_old_database:
        return
    fname = idaapi.cvar.database_idb
    return on_oldfile(fname)

def __check_functions():
    # FIXME: check if tagcache needs to be created
    return

def on_ready():
    '''IDP_Hooks.auto_empty'''
    global State

    # Queues have just been emptied, so now we can transition
    if State == state.loaded:
        State = state.ready

        # update tagcache using function state
        __process_functions()

    elif State == state.ready:
        logging.debug(u"{:s}.on_ready() : Database is already ready ({!s}).".format(__name__, utils.string.repr(State)))

    else:
        logging.debug(u"{:s}.on_ready() : Received unexpected transition from state ({!s}).".format(__name__, utils.string.repr(State)))

def auto_queue_empty(type):
    """This waits for the analysis queue to be empty.

    If the database is ready to be tampered with, then we proceed by executing
    the `on_ready` function which will perform any tasks required to be done
    on the database at startup.
    """
    if type == idaapi.AU_FINAL:
        on_ready()

def __process_functions(percentage=0.10):
    """This prebuilds the tag-cache for the entire database.

    It's intended to be called once the database is ready to be tampered with.
    """
    p = ui.Progress()
    globals = set(internal.comment.globals.address())

    total = 0

    funcs = list(database.functions())
    p.update(current=0, max=len(funcs), title=u"Pre-building tagcache...")
    p.open()
    six.print_(u"Pre-building tagcache for {:d} functions.".format(len(funcs)))
    for i, fn in enumerate(funcs):
        chunks = list(function.chunks(fn))

        text = functools.partial(u"Processing function {:#x} ({chunks:d} chunk{plural:s}) -> {:d} of {:d}".format, fn, i + 1, len(funcs))
        p.update(current=i)
        ui.navigation.procedure(fn)
        if i % (int(len(funcs) * percentage) or 1) == 0:
            six.print_(u"Processing function {:#x} -> {:d} of {:d} ({:.02f}%)".format(fn, i+1, len(funcs), i / float(len(funcs)) * 100.0))

        contents = set(internal.comment.contents.address(fn))
        for ci, (l, r) in enumerate(chunks):
            p.update(text=text(chunks=len(chunks), plural='' if len(chunks) == 1 else 's'), tooltip="Chunk #{:d} : {:#x} - {:#x}".format(ci, l, r))
            ui.navigation.analyze(l)
            for ea in database.address.iterate(l, r):
                # FIXME: no need to iterate really since we should have
                #        all of the addresses
                for k, v in six.iteritems(database.tag(ea)):
                    if ea in globals: internal.comment.globals.dec(ea, k)
                    if ea not in contents: internal.comment.contents.inc(ea, k, target=fn)
                    total += 1
                continue
            continue
        continue
    six.print_(u"Successfully built tag-cache composed of {:d} tag{:s}.".format(total, '' if total == 1 else 's'))
    p.close()

def rebase(info):
    """This is for when the user re-bases the entire database.

    We update the entire database in two parts. First we iterate through all
    the functions, and transform its cache to its new address. Next we iterate
    through all of the known global tags and then transform those.
    """
    functions, globals = map(utils.fcompose(sorted, list), (database.functions(), internal.netnode.alt.fiter(internal.comment.tagging.node())))

    p = ui.Progress()
    p.update(current=0, title=u"Rebasing tagcache...", min=0, max=len(functions)+len(globals))
    fcount = gcount = 0

    scount = info.size() + 1
    six.print_(u"{:s}.rebase({!s}) : Rebasing tagcache for {:d} segments.".format(__name__, utils.string.repr(info), scount))

    # for each segment
    p.open()
    for si in six.moves.range(scount):
        msg = u"Rebasing tagcache for segment {:d} of {:d} : {:#x} ({:+#x}) -> {:#x}".format(si, scount, info[si]._from, info[si].size, info[si].to)
        p.update(title=msg), six.print_(msg)

        # for each function (using target address because ida moved the netnodes for us)
        res = [n for n in functions if info[si].to <= n < info[si].to + info[si].size]
        for i, fn in __rebase_function(info[si]._from, info[si].to, info[si].size, iter(res)):
            text = u"Function {:d} of {:d} : {:#x}".format(i + fcount, len(functions), fn)
            p.update(value=sum((fcount, gcount, i)), text=text)
            ui.navigation.procedure(fn)
        fcount += len(res)

        # for each global
        res = [(ea, count) for ea, count in globals if info[si]._from <= ea < info[si]._from + info[si].size]
        for i, ea in __rebase_globals(info[si]._from, info[si].to, info[si].size, iter(res)):
            text = u"Global {:d} of {:d} : {:#x}".format(i + gcount, len(globals), ea)
            p.update(value=sum((fcount, gcount, i)), text=text)
            ui.navigation.analyze(ea)
        gcount += len(res)
    p.close()

def __rebase_function(old, new, size, iterable):
    key = internal.comment.tagging.__address__
    failure, total = [], list(iterable)

    for i, fn in enumerate(total):
        # grab the contents dictionary
        try:
            state = internal.comment.contents._read(None, fn)
        except E.FunctionNotFoundError:
            logging.fatal(u"{:s}.rebase({:#x}, {:#x}, {:-#x}, {!r}) : Address {:#x} -> {:#x} is not a function.".format(__name__, old, new, size, iterable, fn - new + old, fn))
            state = None
        if state is None: continue

        # now we can erase the old one
        res = fn - new + old
        internal.comment.contents._write(res, None, None)

        # update the addresses
        res, state[key] = state[key], {ea - old + new : ref for ea, ref in six.iteritems(state[key])}

        # and put the new addresses back
        ok = internal.comment.contents._write(None, fn, state)
        if not ok:
            logging.fatal(u"{:s}.rebase({:#x}, {:#x}, {:-#x}, {!r}) : Failure trying to write refcount for function {:#x} while trying to update old reference count ({!s}) to new one ({!s}).".format(__name__, old, new, size, iterable, fn, utils.string.repr(res), utils.string.repr(state[key])))
            failure.append((fn, res, state[key]))

        yield i, fn
    return

def __rebase_globals(old, new, size, iterable):
    node = internal.comment.tagging.node()
    failure, total = [], list(iterable)
    for i, (ea, count) in enumerate(total):
        # remove the old address
        ok = internal.netnode.alt.remove(node, ea)
        if not ok:
            logging.fatal(u"{:s}.rebase({:#x}, {:#x}, {:-#x}, {!r}) : Failure trying to remove refcount ({!r}) for global {:#x}.".format(__name__, old, new, size, iterable, count, ea))

        # now add the new address
        res = ea - old + new
        ok = internal.netnode.alt.set(node, res, count)
        if not ok:
            logging.fatal(u"{:s}.rebase({:#x}, {:#x}, {:-#x}, {!r}) : Failure trying to store refcount ({!r}) from {:#x} to {:#x}.".format(__name__, old, new, size, iterable, count, ea, res))

            failure.append((ea, res, count))
        yield i, ea
    return

def segm_start_changed(s):
    # XXX: not yet implemented
    return

def segm_end_changed(s):
    # XXX: not yet implemented
    return

def segm_moved(source, destination, size):
    # XXX: not yet implemented
    return

# address naming
def rename(ea, newname):
    """This hook is when a user adds a name or removes it from the database.

    We simply increase the refcount for the "__name__" key, or decrease it
    if the name is being removed.
    """
    fl = database.type.flags(ea)
    labelQ, customQ = (fl & n == n for n in {idaapi.FF_LABL, idaapi.FF_NAME})
    #r, fn = database.xref.up(ea), idaapi.get_func(ea)
    fn = idaapi.get_func(ea)

    # figure out whether a global or function name is being changed, otherwise it's the function's contents
    ctx = internal.comment.globals if not fn or (interface.range.start(fn) == ea) else internal.comment.contents

    # if a name is being removed
    if not newname:
        # if it's a custom name
        if (not labelQ and customQ):
            ctx.dec(ea, '__name__')
            logging.debug(u"{:s}.rename({:#x}, {!s}) : Decreasing refcount for tag {!r} at address due to an empty name.".format(__name__, ea, utils.string.repr(newname), '__name__'))
        return

    # if it's currently a label or is unnamed
    if (labelQ and not customQ) or all(not q for q in {labelQ, customQ}):
        ctx.inc(ea, '__name__')
        logging.debug(u"{:s}.rename({:#x}, {!s}) : Increasing refcount for tag {!r} at address due to a new name.".format(__name__, ea, utils.string.repr(newname), '__name__'))
    return

def extra_cmt_changed(ea, line_idx, cmt):
    # FIXME: persist state for extra_cmts in order to determine what the original
    #        value was before modification. IDA doesn't seem to have an
    #        extra_cmt_changing event and instead calls this hook twice for every
    #        insertion.
    # XXX: this function is now busted in later versions of IDA because for some
    #      reason, Ilfak, is now updating the extra comment prior to dispatching
    #      this event. unfortunately, our tag cache doesn't allow us to identify
    #      the actual number of tags that are at an address, so there's no way
    #      to identify the actual change to the extra comment that the user made,
    #      which totally fucks up the refcount. in the current implementation, if
    #      we can't distinguish between the old and new extra comments, then its
    #      simply a no-op. this is okay for now...

    oldcmt = internal.netnode.sup.get(ea, line_idx)
    if oldcmt is not None: oldcmt = oldcmt.rstrip('\x00')
    ctx = internal.comment.contents if idaapi.get_func(ea) else internal.comment.globals

    MAX_ITEM_LINES = (idaapi.E_NEXT-idaapi.E_PREV) if idaapi.E_NEXT > idaapi.E_PREV else idaapi.E_PREV-idaapi.E_NEXT
    prefix = (idaapi.E_PREV, idaapi.E_PREV+MAX_ITEM_LINES, '__extra_prefix__')
    suffix = (idaapi.E_NEXT, idaapi.E_NEXT+MAX_ITEM_LINES, '__extra_suffix__')

    for l, r, key in (prefix, suffix):
        if l <= line_idx < r:
            if oldcmt is None and cmt is not None: ctx.inc(ea, key)
            elif oldcmt is not None and cmt is None: ctx.dec(ea, key)
            logging.debug(u"{:s}.extra_cmt_changed({:#x}, {:d}, {!s}, oldcmt={!s}) : {:s} refcount at address for tag {!s}.".format(__name__, ea, line_idx, utils.string.repr(cmt), utils.string.repr(oldcmt), 'Increasing' if oldcmt is None and cmt is not None else 'Decreasing' if oldcmt is not None and cmt is None else 'Doing nothing to', utils.string.repr(key)))
        continue
    return

### function scope
def thunk_func_created(pfn):
    pass

def func_tail_appended(pfn, tail):
    """This hook is for when a chunk is appended to a function.

    We simply iterate through the new chunk, decrease all of its tags in the
    global context, and increase their reference within the function context.
    """
    global State
    if State != state.ready: return
    # tail = func_t
    for ea in database.address.iterate(*interface.range.unpack(tail)):
        for k in database.tag(ea):
            internal.comment.globals.dec(ea, k)
            internal.comment.contents.inc(ea, k, target=interface.range.start(pfn))
            logging.debug(u"{:s}.func_tail_appended({:#x}, {:#x}) : Exchanging (decreasing) refcount for global tag {!s} and (increasing) refcount for contents tag {!s}.".format(__name__, interface.range.start(pfn), interface.range.start(tail), utils.string.repr(k), utils.string.repr(k)))
        continue
    return

def removing_func_tail(pfn, tail):
    """This hook is for when a chunk is removed from a function.

    We simply iterate through the old chunk, decrease all of its tags in the
    function context, and increase their reference within the global context.
    """
    global State
    if State != state.ready: return
    # tail = range_t
    for ea in database.address.iterate(*interface.range.unpack(tail)):
        for k in database.tag(ea):
            internal.comment.contents.dec(ea, k, target=interface.range.start(pfn))
            internal.comment.globals.inc(ea, k)
            logging.debug(u"{:s}.removing_func_tail({:#x}, {:#x}) : Exchanging (increasing) refcount for global tag {!s} and (decreasing) refcount for contents tag {!s}.".format(__name__, interface.range.start(pfn), interface.range.start(tail), utils.string.repr(k), utils.string.repr(k)))
        continue
    return

def func_tail_removed(pfn, ea):
    """This hook is for when a chunk is removed from a function in older versions of IDA.

    We simply iterate through the old chunk, decrease all of its tags in the
    function context, and increase their reference within the global context.
    """
    global State
    if State != state.ready: return

    # first we'll grab the addresses from our refs
    res = internal.comment.contents.address(ea, target=interface.range.start(pfn))

    # these are sorted, so first we'll filter out what doesn't belong
    missing = [ item for item in res if idaapi.get_func(item) != pfn ]

    # now iterate through the min/max of the list as hopefully this is
    # our event.
    for ea in database.address.iterate(min(missing), max(missing)):
        for k in database.tag(ea):
            internal.comment.contents.dec(ea, k, target=interface.range.start(pfn))
            internal.comment.globals.inc(ea, k)
            logging.debug(u"{:s}.func_tail_removed({:#x}, {:#x}) : Exchanging (increasing) refcount for global tag {!s} and (decreasing) refcount for contents tag {!s}.".format(__name__, interface.range.start(pfn), ea, utils.string.repr(k), utils.string.repr(k)))
        continue
    return

def tail_owner_changed(tail, owner_func):
    """This hook is for when a chunk is moved to another function and is for older versions of IDA.

    We simply iterate through the new chunk, decrease all of its tags in its
    previous function's context, and increase their reference within the new
    function's context.
    """
    # XXX: this is for older versions of IDA
    global State
    if State != state.ready: return

    # this is easy as we just need to walk through tail and add it
    # to owner_func
    for ea in database.address.iterate(*interface.range.unpack(tail)):
        for k in database.tag(ea):
            internal.comment.contents.dec(ea, k)
            internal.comment.contents.inc(ea, k, target=owner_func)
            logging.debug(u"{:s}.tail_owner_changed({:#x}, {:#x}) : Exchanging (increasing) refcount for contents tag {!s} and (decreasing) refcount for contents tag {!s}.".format(__name__, interface.range.start(tail), owner_func, utils.string.repr(k), utils.string.repr(k)))
        continue
    return

def add_func(pfn):
    """This is called when a new function is created.

    When a new function is created, its entire area needs its tags transformed
    from global tags to function tags. This iterates through each chunk belonging
    to the function and does exactly that.
    """
    global State
    if State != state.ready: return

    # convert all globals into contents
    for l, r in function.chunks(pfn):
        for ea in database.address.iterate(l, r):
            for k in database.tag(ea):
                internal.comment.globals.dec(ea, k)
                internal.comment.contents.inc(ea, k, target=interface.range.start(pfn))
                logging.debug(u"{:s}.add_func({:#x}) : Exchanging (decreasing) refcount for global tag {!s} and (increasing) refcount for contents tag {!s}.".format(__name__, interface.range.start(pfn), utils.string.repr(k), utils.string.repr(k)))
            continue
        continue
    return

def del_func(pfn):
    """This is called when a function is removed/deleted.

    When a function is removed, all of its tags get moved from the function back
    into the database as global tags. We iterate through the entire function and
    transform its tags by decreasing its refcount within the function, and then
    increasing it for the database. Afterwards we simply remove the refcount
    cache for the function.
    """
    global State
    if State != state.ready: return

    # convert all contents into globals
    for l, r in function.chunks(pfn):
        for ea in database.address.iterate(l, r):
            for k in database.tag(ea):
                internal.comment.contents.dec(ea, k, target=interface.range.start(pfn))
                internal.comment.globals.inc(ea, k)
                logging.debug(u"{:s}.del_func({:#x}) : Exchanging (increasing) refcount for global tag {!s} and (decreasing) refcount for contents tag {!s}.".format(__name__, interface.range.start(pfn), utils.string.repr(k), utils.string.repr(k)))
            continue
        continue

    # remove all function tags
    for k in function.tag(interface.range.start(pfn)):
        internal.comment.globals.dec(interface.range.start(pfn), k)
        logging.debug(u"{:s}.del_func({:#x}) : Removing (global) tag {!s} from function.".format(__name__, interface.range.start(pfn), utils.string.repr(k)))
    return

def set_func_start(pfn, new_start):
    """This is called when the user changes the beginning of the function to another address.

    If this happens, we simply walk from the new address to the old address of
    the function that was changed. Then we can update the refcount for any
    globals that were tagged by moving them into the function's tagcache.
    """
    global State
    if State != state.ready: return

    # new_start has removed addresses from function
    # replace contents with globals
    if interface.range.start(pfn) > new_start:
        for ea in database.address.iterate(new_start, interface.range.start(pfn)):
            for k in database.tag(ea):
                internal.comment.contents.dec(ea, k, target=interface.range.start(pfn))
                internal.comment.globals.inc(ea, k)
                logging.debug(u"{:s}.set_func_start({:#x}, {:#x}) : Exchanging (increasing) refcount for global tag {!s} and (decreasing) refcount for contents tag {!s}.".format(__name__, interface.range.start(pfn), new_start, utils.string.repr(k), utils.string.repr(k)))
            continue
        return

    # new_start has added addresses to function
    # replace globals with contents
    elif interface.range.start(pfn) < new_start:
        for ea in database.address.iterate(interface.range.start(pfn), new_start):
            for k in database.tag(ea):
                internal.comment.globals.dec(ea, k)
                internal.comment.contents.inc(ea, k, target=interface.range.start(pfn))
                logging.debug(u"{:s}.set_func_start({:#x}, {:#x}) : Exchanging (decreasing) refcount for global tag {!s} and (increasing) refcount for contents tag {!s}.".format(__name__, interface.range.start(pfn), new_start, utils.string.repr(k), utils.string.repr(k)))
            continue
        return
    return

def set_func_end(pfn, new_end):
    """This is called when the user changes the ending of the function to another address.

    If this happens, we simply walk from the old end of the function to the new
    end of the function that was changed. Then we can update the refcount for any
    globals that were tagged by moving them into the function's tagcache.
    """
    global State
    if State != state.ready: return
    # new_end has added addresses to function
    # replace globals with contents
    if new_end > interface.range.end(pfn):
        for ea in database.address.iterate(interface.range.end(pfn), new_end):
            for k in database.tag(ea):
                internal.comment.globals.dec(ea, k)
                internal.comment.contents.inc(ea, k, target=interface.range.start(pfn))
                logging.debug(u"{:s}.set_func_end({:#x}, {:#x}) : Exchanging (decreasing) refcount for global tag {!s} and (increasing) refcount for contents tag {!s}.".format(__name__, interface.range.start(pfn), new_end, utils.string.repr(k), utils.string.repr(k)))
            continue
        return

    # new_end has removed addresses from function
    # replace contents with globals
    elif new_end < interface.range.end(pfn):
        for ea in database.address.iterate(new_end, interface.range.end(pfn)):
            for k in database.tag(ea):
                internal.comment.contents.dec(ea, k, target=interface.range.start(pfn))
                internal.comment.globals.inc(ea, k)
                logging.debug(u"{:s}.set_func_end({:#x}, {:#x}) : Exchanging (increasing) refcount for global tag {!s} and (decreasing) refcount for contents tag {!s}.".format(__name__, interface.range.start(pfn), new_end, utils.string.repr(k), utils.string.repr(k)))
            continue
        return
    return

def make_ida_not_suck_cocks(nw_code):
    '''Start hooking all of IDA's API.'''

    ## initialize the priorityhook api for all three of IDA's interfaces
    ui.hook.__start_ida__()

    ## setup default integer types for the typemapper once the loader figures everything out
    if idaapi.__version__ >= 7.0:
        ui.hook.idp.add('ev_newprc', interface.typemap.__ev_newprc__, 0)
    elif idaapi.__version__ >= 6.9:
        ui.hook.idp.add('newprc', interface.typemap.__newprc__, 0)
    else:
        idaapi.__notification__.add(idaapi.NW_OPENIDB, interface.typemap.__nw_newprc__, -40)

    ## monitor when ida enters its various states so we can pre-build the tag cache
    if idaapi.__version__ >= 7.0:
        ui.hook.idp.add('ev_init', on_init, -100)
        ui.hook.idp.add('ev_newfile', on_newfile, -100)
        ui.hook.idp.add('ev_oldfile', on_oldfile, -100)
        ui.hook.idp.add('ev_auto_queue_empty', auto_queue_empty, -100)

    elif idaapi.__version__ >= 6.9:
        ui.hook.idp.add('init', on_init, -100)
        ui.hook.idp.add('newfile', on_newfile, -100)
        ui.hook.idp.add('oldfile', on_oldfile, -100)
        ui.hook.idp.add('auto_empty', on_ready, -100)

    else:
        idaapi.__notification__.add(idaapi.NW_OPENIDB, nw_on_init, -50)
        idaapi.__notification__.add(idaapi.NW_OPENIDB, nw_on_newfile, -20)
        idaapi.__notification__.add(idaapi.NW_OPENIDB, nw_on_oldfile, -20)
        ui.hook.idp.add('auto_empty', on_ready, 0)

    ## create the tagcache netnode when a database is created
    if idaapi.__version__ >= 7.0:
        ui.hook.idp.add('ev_init', comment.tagging.__init_tagcache__, -1)
    elif idaapi.__version__ >= 6.9:
        ui.hook.idp.add('init', comment.tagging.__init_tagcache__, -1)
    else:
        idaapi.__notification__.add(idaapi.NW_OPENIDB, comment.tagging.__nw_init_tagcache__, -40)

    ## hook any user-entered comments so that they will also update the tagcache
    if idaapi.__version__ >= 7.0:
        ui.hook.idp.add('ev_init', address.database_init, 0)
        ui.hook.idp.add('ev_init', globals.database_init, 0)
        ui.hook.idb.add('changing_range_cmt', globals.changing, 0)
        ui.hook.idb.add('range_cmt_changed', globals.changed, 0)
    elif idaapi.__version__ >= 6.9:
        ui.hook.idp.add('init', address.database_init, 0)
        ui.hook.idp.add('init', globals.database_init, 0)
        ui.hook.idb.add('changing_area_cmt', globals.changing, 0)
        ui.hook.idb.add('area_cmt_changed', globals.changed, 0)
    else:
        idaapi.__notification__.add(idaapi.NW_OPENIDB, address.nw_database_init, -30)
        idaapi.__notification__.add(idaapi.NW_OPENIDB, globals.nw_database_init, -30)
        ui.hook.idb.add('area_cmt_changed', globals.old_changed, 0)

    if idaapi.__version__ >= 6.9:
        ui.hook.idb.add('changing_cmt', address.changing, 0)
        ui.hook.idb.add('cmt_changed', address.changed, 0)
    else:
        ui.hook.idb.add('cmt_changed', address.old_changed, 0)

    ## hook naming and "extra" comments to support updating the implicit tags
    if idaapi.__version__ >= 7.0:
        ui.hook.idp.add('ev_rename', rename, 0)
    else:
        ui.hook.idp.add('rename', rename, 0)

    if idaapi.__version__ >= 6.9:
        ui.hook.idb.add('extra_cmt_changed', extra_cmt_changed, 0)
    else:
        # earlier versions of IDAPython don't expose anything about "extra" comments
        # so we can't do anything here.
        pass

    ## hook function transformations so we can shuffle their tags between types
    if idaapi.__version__ >= 7.0:
        ui.hook.idb.add('deleting_func_tail', removing_func_tail, 0)
        ui.hook.idb.add('func_added', add_func, 0)
        ui.hook.idb.add('deleting_func', del_func, 0)
        ui.hook.idb.add('set_func_start', set_func_start, 0)
        ui.hook.idb.add('set_func_end', set_func_end, 0)
    elif idaapi.__version__ >= 6.9:
        ui.hook.idb.add('removing_func_tail', removing_func_tail, 0)
        [ ui.hook.idp.add(item.__name__, item, 0) for item in [add_func, del_func, set_func_start, set_func_end] ]
    else:
        ui.hook.idb.add('func_tail_removed', func_tail_removed, 0)
        ui.hook.idp.add('add_func', add_func, 0)
        ui.hook.idp.add('del_func', del_func, 0)
        ui.hook.idb.add('tail_owner_changed', tail_owner_changed, 0)

    [ ui.hook.idb.add(item.__name__, item, 0) for item in [thunk_func_created, func_tail_appended] ]

    ## rebase the entire tagcache when the entire database is rebased.
    if idaapi.__version__ >= 6.9:
        ui.hook.idb.add('allsegs_moved', rebase, 0)
    else:
        ui.hook.idb.add('segm_start_changed', segm_start_changed, 0)
        ui.hook.idb.add('segm_end_changed', segm_end_changed, 0)
        ui.hook.idb.add('segm_moved', segm_moved, 0)

    ## switch the instruction set when the processor is switched
    if idaapi.__version__ >= 7.0:
        ui.hook.idp.add('ev_newprc', instruction.__ev_newprc__, 0)
    elif idaapi.__version__ >= 6.9:
        ui.hook.idp.add('newprc', instruction.__newprc__, 0)
    else:
        idaapi.__notification__.add(idaapi.NW_OPENIDB, instruction.__nw_newprc__, -10)

    ## just some debugging notification hooks
    #[ ui.hook.ui.add(n, notify(n), -100) for n in ('range','idcstop','idcstart','suspend','resume','term','ready_to_run') ]
    #[ ui.hook.idp.add(n, notify(n), -100) for n in ('ev_newfile','ev_oldfile','ev_init','ev_term','ev_newprc','ev_newasm','ev_auto_queue_empty') ]
    #[ ui.hook.idb.add(n, notify(n), -100) for n in ('closebase','savebase','loader_finished', 'auto_empty', 'thunk_func_created','func_tail_appended') ]
    #[ ui.hook.idp.add(n, notify(n), -100) for n in ('add_func','del_func','set_func_start','set_func_end') ]
    #ui.hook.idb.add('allsegs_moved', notify('allsegs_moved'), -100)
    #[ ui.hook.idb.add(n, notify(n), -100) for n in ('cmt_changed', 'changing_cmt', 'range_cmt_changed', 'changing_range_cmt') ]

    ### ...and that's it for all the hooks, so give out our greeting
    return greeting()

def make_ida_suck_cocks(nw_code):
    '''Unhook all of IDA's API.'''
    idaapi.__notification__.unhook()
    ui.hook.__stop_ida__()

def ida_is_busy_sucking_cocks(*args, **kwargs):
    make_ida_not_suck_cocks(idaapi.NW_INITIDA)
    idaapi.__notification__.add(idaapi.NW_TERMIDA, make_ida_suck_cocks, +1000)
    return -1