import sys
import types
from sqlobject.include.pydispatch import dispatcher
from weakref import ref


subclassClones = {}

def listen(receiver, soClass, signal, alsoSubclasses=True, weak=True):
    """
    Listen for the given ``signal`` on the SQLObject subclass
    ``soClass``, calling ``receiver()`` when ``send(soClass, signal,
    ...)`` is called.

    If ``alsoSubclasses`` is true, receiver will also be called when
    an event is fired on any subclass.
    """
    dispatcher.connect(receiver, signal=signal, sender=soClass, weak=weak)
    weakReceiver = ref(receiver)
    subclassClones.setdefault(soClass, []).append((weakReceiver, signal))

# We export this function:
send = dispatcher.send

class Signal(object):
    """
    Base event for all SQLObject events.

    In general the sender for these methods is the class, not the
    instance.
    """

class ClassCreateSignal(Signal):
    """
    Signal raised after class creation.  The sender is the superclass
    (in case of multiple superclasses, the first superclass).  The
    arguments are ``(new_class_name, bases, new_attrs, post_funcs,
    early_funcs)``.  ``new_attrs`` is a dictionary and may be modified
    (but ``new_class_name`` and ``bases`` are immutable).
    ``post_funcs`` is an initially-empty list that can have callbacks
    appended to it.

    Note: at the time this event is called, the new class has not yet
    been created.  The functions in ``post_funcs`` will be called
    after the class is created, with the single arguments of
    ``(new_class)``.  Also, ``early_funcs`` will be called at the
    soonest possible time after class creation (``post_funcs`` is
    called after the class's ``__classinit__``).
    """

def _makeSubclassConnections(new_class_name, bases, new_attrs,
                             post_funcs, early_funcs):
    early_funcs.insert(0, _makeSubclassConnectionsPost)

def _makeSubclassConnectionsPost(new_class):
    for cls in new_class.__bases__:
        for weakReceiver, signal in subclassClones.get(cls, []):
            receiver = weakReceiver()
            if not receiver:
                continue
            listen(receiver, new_class, signal)

dispatcher.connect(_makeSubclassConnections, signal=ClassCreateSignal)

# @@: Should there be a class reload event?  This would allow modules
# to be reloaded, possibly.  Or it could even be folded into
# ClassCreateSignal, since anything that listens to that needs to pay
# attention to reloads (or else it is probably buggy).

class RowCreateSignal(Signal):
    """
    Called before an instance is created, with the class as the
    sender.  Called with the arguments ``(kwargs, post_funcs)``.
    There may be a ``connection`` argument.  ``kwargs``may be usefully
    modified.  ``post_funcs`` is a list of callbacks, intended to have
    functions appended to it, and are called with the arguments
    ``(new_instance)``.

    Note: this is not called when an instance is created from an
    existing database row.
    """
class RowCreatedSignal(Signal):
    """
    Called after an instance is created, with the class as the
    sender.  Called with the arguments ``(kwargs, post_funcs)``.
    There may be a ``connection`` argument.  ``kwargs``may be usefully
    modified.  ``post_funcs`` is a list of callbacks, intended to have
    functions appended to it, and are called with the arguments
    ``(new_instance)``.

    Note: this is not called when an instance is created from an
    existing database row.
    """
# @@: An event for getting a row?  But for each row, when doing a
# select?  For .sync, .syncUpdate, .expire?

class RowDestroySignal(Signal):
    """
    Called before an instance is deleted.  Sender is the instance's
    class.  Arguments are ``(instance, post_funcs)``.

    ``post_funcs`` is a list of callbacks, intended to have
    functions appended to it, and are called without arguments. If
    any of the post_funcs raises an exception, the deletion is only
    affected if this will prevent a commit.

    You cannot cancel the delete, but you can raise an exception (which will
    probably cancel the delete, but also cause an uncaught exception if not
    expected).

    Note: this is not called when an instance is destroyed through
    garbage collection.

    @@: Should this allow ``instance`` to be a primary key, so that a
    row can be deleted without first fetching it?
    """

class RowUpdateSignal(Signal):
    """
    Called when an instance is updated through a call to ``.set()``
    (or a column attribute assignment).  The arguments are
    ``(instance, kwargs)``.  ``kwargs`` can be modified.  This is run
    *before* the instance is updated; if you want to look at the
    current values, simply look at ``instance``.
    """

class AddColumnSignal(Signal):
    """
    Called when a column is added to a class, with arguments ``(cls,
    connection, column_name, column_definition, changeSchema,
    post_funcs)``.  This is called *after* the column has been added,
    and is called for each column after class creation.

    post_funcs are called with ``(cls, so_column_obj)``
    """

class DeleteColumnSignal(Signal):
    """
    Called when a column is removed from a class, with the arguments
    ``(cls, connection, column_name, so_column_obj, post_funcs)``.
    Like ``AddColumnSignal`` this is called after the action has been
    performed, and is called for subclassing (when a column is
    implicitly removed by setting it to ``None``).

    post_funcs are called with ``(cls, so_column_obj)``
    """

# @@: Signals for indexes and joins?  These are mostly event consumers,
# though.

class CreateTableSignal(Signal):
    """
    Called when a table is created.  If ``ifNotExists==True`` and the
    table exists, this event is not called.

    Called with ``(cls, connection, extra_sql, post_funcs)``.
    ``extra_sql`` is a list (which can be appended to) of extra SQL
    statements to be run after the table is created.  ``post_funcs``
    functions are called with ``(cls, connection)`` after the table
    has been created.  Those functions are *not* called simply when
    constructing the SQL.
    """

class DropTableSignal(Signal):
    """
    Called when a table is dropped.  If ``ifExists==True`` and the
    table doesn't exist, this event is not called.

    Called with ``(cls, connection, extra_sql, post_funcs)``.
    ``post_funcs`` functions are called with ``(cls, connection)``
    after the table has been dropped.
    """

############################################################
## Event Debugging
############################################################

def summarize_events_by_sender(sender=None, output=None, indent=0):
    """
    Prints out a summary of the senders and listeners in the system,
    for debugging purposes.
    """
    if output is None:
        output = sys.stdout
    if sender is None:
        send_list = [
            (deref(dispatcher.senders.get(sid)), listeners)
            for sid, listeners in dispatcher.connections.items()
            if deref(dispatcher.senders.get(sid))]
        for sender, listeners in sorted_items(send_list):
            real_sender = deref(sender)
            if not real_sender:
                continue
            header = 'Sender: %r' % real_sender
            print >> output, (' '*indent) + header
            print >> output, (' '*indent) + '='*len(header)
            summarize_events_by_sender(real_sender, output=output, indent=indent+2)
    else:
        for signal, receivers in sorted_items(dispatcher.connections.get(id(sender), [])):
            receivers = [deref(r) for r in receivers if deref(r)]
            header = 'Signal: %s (%i receivers)' % (sort_name(signal),
                                                    len(receivers))
            print >> output, (' '*indent) + header
            print >> output, (' '*indent) + '-'*len(header)
            for receiver in sorted(receivers, key=sort_name):
                print >> output, (' '*indent) + '  ' + nice_repr(receiver)

def deref(value):
    if isinstance(value, dispatcher.WEAKREF_TYPES):
        return value()
    else:
        return value

def sorted_items(a_dict):
    if isinstance(a_dict, dict):
        a_dict = a_dict.items()
    return sorted(a_dict, key=lambda t: sort_name(t[0]))

def sort_name(value):
    if isinstance(value, type):
        return value.__name__
    elif isinstance(value, types.FunctionType):
        return value.func_name
    else:
        return str(value)

_real_dispatcher_send = dispatcher.send
_real_dispatcher_sendExact = dispatcher.sendExact
_real_dispatcher_disconnect = dispatcher.disconnect
_real_dispatcher_connect = dispatcher.connect
_debug_enabled = False
def debug_events():
    global _debug_enabled, send
    if _debug_enabled:
        return
    _debug_enabled = True
    dispatcher.send = send = _debug_send
    dispatcher.sendExact = _debug_sendExact
    dispatcher.disconnect = _debug_disconnect
    dispatcher.connect = _debug_connect

def _debug_send(signal=dispatcher.Any, sender=dispatcher.Anonymous,
                *arguments, **named):
    print "send %s from %s: %s" % (
        nice_repr(signal), nice_repr(sender), fmt_args(*arguments, **named))
    return _real_dispatcher_send(signal, sender, *arguments, **named)

def _debug_sendExact(signal=dispatcher.Any, sender=dispatcher.Anonymous,
                     *arguments, **named):
    print "sendExact %s from %s: %s" % (
        nice_repr(signal), nice_repr(sender), fmt_args(*arguments, **name))
    return _real_dispatcher_sendExact(signal, sender, *arguments, **named)

def _debug_connect(receiver, signal=dispatcher.Any, sender=dispatcher.Any,
                   weak=True):
    print "connect %s to %s signal %s" % (
        nice_repr(receiver), nice_repr(signal), nice_repr(sender))
    return _real_dispatcher_connect(receiver, signal, sender, weak)

def _debug_disconnect(receiver, signal=dispatcher.Any, sender=dispatcher.Any,
                      weak=True):
    print "disconnecting %s from %s signal %s" % (
        nice_repr(receiver), nice_repr(signal), nice_repr(sender))
    return disconnect(receiver, signal, sender, weak)

def fmt_args(*arguments, **name):
    args = [repr(a) for a in arguments]
    args.extend([
        '%s=%r' % (n, v) for n, v in sorted(name.items())])
    return ', '.join(args)

def nice_repr(v):
    """
    Like repr(), but nicer for debugging here.
    """
    if isinstance(v, (types.ClassType, type)):
        return v.__module__ + '.' + v.__name__
    elif isinstance(v, types.FunctionType):
        if '__name__' in v.func_globals:
            if getattr(sys.modules[v.func_globals['__name__']],
                       v.func_name, None) is v:
                return '%s.%s' % (v.func_globals['__name__'], v.func_name)
        return repr(v)
    elif isinstance(v, types.MethodType):
        return '%s.%s of %s' % (
            nice_repr(v.im_class), v.im_func.func_name,
            nice_repr(v.im_self))
    else:
        return repr(v)

try:
    sorted
except NameError:
    # For Python 2.2 and 2.3:
    def sorted(lst, cmp=None, key=None, reverse=False):
        if key:
            lst = [(key(i), i) for i in lst]
        lst = lst[:]
        if cmp:
            lst.sort(cmp)
        else:
            lst.sort()
        if key:
            lst = [i for k, i in lst]
        if reverse:
            lst.reverse()
        return lst

__all__ = ['listen', 'send']
for name, value in globals().items():
    if isinstance(value, type) and issubclass(value, Signal):
        __all__.append(name)
