"""
A bounded, cancellation-aware single-producer/single-consumer hand-off queue.

Unlike stdlib ``queue.Queue``, both blocking operations wake OUT OF BAND on a
stop signal, so a consumer that wants to shut down does not have to poll with a
timeout: a parked ``put`` or ``drain`` is released the instant ``stop()`` runs.
This is what lets a producer thread and a consumer be torn down immediately on
close, with no timeout-poll latency and no in-band sentinel item that a
concurrent drainer could swallow.

All state lives under a single ``threading.Condition``; every blocking wait
re-checks its predicate in a ``while`` loop, so there are no lost wakeups.

Semantics:
  - ``put(item)``       blocks while full, wakes on freed space OR ``stop()``;
                        returns ``False`` if stopped before the item could be
                        enqueued (item dropped), ``True`` if enqueued.
  - ``drain(n)``        blocks while empty, wakes on an item, ``stop()``, OR
                        ``producer_done()``; pops up to ``n`` items in FIFO
                        order and returns ``(batch, done)``, where ``done`` is
                        ``True`` once stopped, or once the producer has finished
                        AND the queue is fully drained.
  - ``stop()``          consumer-abort / close: wakes a blocked ``put`` AND a
                        blocked ``drain``.
  - ``producer_done()`` the producer has finished and will enqueue nothing more:
                        wakes a blocked ``drain`` so it can report ``done`` once
                        the remaining backlog has been handed over.
"""
import collections
import threading


class HandoffQueue(object):
    """Bounded SPSC hand-off whose blocking put/drain wake out-of-band on stop.

    See the module docstring for the full semantics.  Intended for one producer
    thread and one consumer; it is nonetheless fully lock-guarded, so concurrent
    callers are safe -- it is just not optimized for many of them.
    """

    def __init__(self, maxsize):
        # maxsize == 0 would make put() block forever (capacity never reached),
        # so require a real bound -- this is a backpressure buffer, not a
        # zero-capacity rendezvous.
        if maxsize < 1:
            raise ValueError("maxsize must be >= 1")
        self._items = collections.deque()
        self._maxsize = maxsize
        self._cond = threading.Condition()
        self._stopped = False
        self._producer_done = False

    def stop(self):
        """Abort: wake any blocked put and drain.  Idempotent.  After stop(),
        put() returns False and drain() reports done."""
        with self._cond:
            self._stopped = True
            self._cond.notify_all()

    def producer_done(self):
        """Signal that the producer has finished and will enqueue nothing more.
        Wakes a blocked drain so it can report done once the queue empties.
        Idempotent."""
        with self._cond:
            self._producer_done = True
            self._cond.notify_all()

    def put(self, item):
        """Append, blocking while the queue is full.  Returns False if the queue
        was stopped before the item could be enqueued (the item is dropped),
        True otherwise."""
        with self._cond:
            while not self._stopped and len(self._items) >= self._maxsize:
                self._cond.wait()
            if self._stopped:
                return False
            self._items.append(item)
            self._cond.notify_all()  # wake a blocked drain
            return True

    def drain(self, max_items):
        """Block until at least one item is available, or the queue is stopped,
        or the producer is done; then pop up to ``max_items`` in FIFO order.

        Returns ``(batch, done)``.  ``done`` is True once the queue is stopped,
        or once the producer is done and the queue has been fully drained -- so
        a consumer loops until ``done`` to apply every remaining item."""
        if max_items < 1:
            raise ValueError("max_items must be >= 1")
        with self._cond:
            while (not self._items and not self._stopped and not self._producer_done):
                self._cond.wait()
            batch = []
            while self._items and len(batch) < max_items:
                batch.append(self._items.popleft())
            if batch:
                self._cond.notify_all()  # wake blocked putters (freed space)
            done = self._stopped or (self._producer_done and not self._items)
            return batch, done
