##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""Manage the asyncore mainloop in a multi-threaded app.

In a multi-threaded application, only a single thread runs the
asyncore mainloop.  This thread (the "mainloop thread") may not start
the mainloop before another thread needs to perform an async action
that requires it.  As a result, other threads need to coordinate with
the mainloop thread to find out whether the mainloop is running.

This module implements a callback mechanism that allows other threads
to be notified when the mainloop starts.  A thread calls
register_loop_callback() to register interest.  When the mainloop
thread calls loop(), each registered callback will be called with the
socket map as its first argument.
"""

import asyncore
import thread

# Zope pokes a non-None value into exit_status when it wants the loop()
# function to exit.  Indeed, there appears to be no other way to tell
# Zope3 to shut down.
exit_status = None

_loop_lock = thread.allocate_lock()
_looping = None # changes to socket map when loop() starts
_loop_callbacks = []

def register_loop_callback(callback, args=(), kw=None):
    """Register callback function to be called when mainloop starts.

    The callable object callback will be invokved when the mainloop
    starts.  If the mainloop is currently running, the callback will
    be invoked immediately.

    The callback will be called with a single argument, the mainloop
    socket map, unless the optional args or kw arguments are used.
    args defines a tuple of extra arguments to pass after the socket
    map.  kw defines a dictionary of keyword arguments.
    """
    _loop_lock.acquire()
    try:
        if _looping is not None:
            callback(_looping, *args, **(kw or {}))
        else:
            _loop_callbacks.append((callback, args, kw))
    finally:
        _loop_lock.release()

def remove_loop_callback(callback):
    """Remove a callback function registered earlier.

    This is useful if loop() was never called.
    """
    for i, value in enumerate(_loop_callbacks):
        if value[0] == callback:
            del _loop_callbacks[i]
            return

# Because of the exit_status magic, we can't just invoke asyncore.loop(),
# and that's a shame.
# The signature of asyncore.loop changed between Python 2.3 and 2.4, and
# this loop() has 2.4's signature, which added the optional `count` argument.
# Since we physically replace asyncore.loop with this `loop`, and want
# compatibility with both Pythons, we need to support the most recent
# signature.  Applications running under 2.3 should (of course) avoid using
# the `count` argument, since 2.3 doesn't have it.
def loop(timeout=30.0, use_poll=False, map=None, count=None):
    global _looping
    global exit_status

    exit_status = None

    if map is None:
        map = asyncore.socket_map

    # This section is taken from Python 2.3's asyncore.loop, and is more
    # elaborate than the corresponding section of 2.4's:  in 2.4 poll2 and
    # poll3 are aliases for the same function, in 2.3 they're different
    # functions.
    if use_poll:
        if hasattr(select, 'poll'):
            poll_fun = asyncore.poll3
        else:
            poll_fun = asyncore.poll2
    else:
        poll_fun = asyncore.poll

    # The loop is about to start:  invoke any registered callbacks.
    _loop_lock.acquire()
    try:
        _looping = map
        while _loop_callbacks:
            cb, args, kw = _loop_callbacks.pop()
            cb(map, *args, **(kw or {}))
    finally:
        _loop_lock.release()

    # Run the loop.  This is 2.4's logic, with the addition that we stop
    # if/when this module's exit_status global is set to a non-None value.
    if count is None:
        while map and exit_status is None:
            poll_fun(timeout, map)
    else:
        while map and count > 0 and exit_status is None:
            poll_fun(timeout, map)
            count -= 1

    _loop_lock.acquire()
    try:
        _looping = None
    finally:
        _loop_lock.release()

# Evil:  rebind asyncore.loop to the above loop() function.
#
# Code should explicitly call ThreadedAsync.loop() instead of asyncore.loop().
# Most of ZODB has been fixed, but ripping this out may break 3rd party code.
# Maybe we should issue a warning and let it continue for a while (NOTE:  code
# to raise DeprecationWarning was written but got commented out below; don't
# know why it got commented out).  Or maybe we should get rid of this
# mechanism entirely, and have each piece that needs one run its own asyncore
# loop in its own thread.

##def deprecated_loop(*args, **kws):
##    import warnings
##    warnings.warn("""\
##ThreadedAsync.loop() called through sneaky asyncore.loop() rebinding.
##You should change your code to call ThreadedAsync.loop() explicitly.""",
##                  DeprecationWarning)
##    loop(*args, **kws)
##
##asyncore.loop = deprecated_loop

asyncore.loop = loop
