from abc import ABCMeta, abstractmethod
from asyncio.queues import Queue as AioQueue
from queue import Queue

from bonobo.constants import BEGIN, END
from bonobo.errors import AbstractError, InactiveReadableError, InactiveWritableError
from bonobo.nodes import noop

BUFFER_SIZE = 8192


class Readable(metaclass=ABCMeta):
    """Interface for things you can read from."""

    @abstractmethod
    def get(self, block=True, timeout=None):
        """Read. Block/timeout are there for Queue compat."""
        raise AbstractError(self.get)


class Writable(metaclass=ABCMeta):
    """Interface for things you can write to."""

    @abstractmethod
    def put(self, data, block=True, timeout=None):
        """Write. Block/timeout are there for Queue compat."""
        raise AbstractError(self.put)


class Input(Queue, Readable, Writable):
    def __init__(self, maxsize=BUFFER_SIZE):
        Queue.__init__(self, maxsize)

        self._runlevel = 0
        self._writable_runlevel = 0
        self.on_initialize = noop
        self.on_begin = noop
        self.on_end = noop
        self.on_finalize = noop

    def put(self, data, block=True, timeout=None):
        # Begin token is a metadata to raise the input runlevel.
        if data == BEGIN:
            if not self._runlevel:
                self.on_initialize()

            self._runlevel += 1
            self._writable_runlevel += 1

            # callback
            self.on_begin()

            return

        # Check we are actually able to receive data.
        if self._writable_runlevel < 1:
            raise InactiveWritableError("Cannot put() on an inactive {}.".format(Writable.__name__))

        if data == END:
            self._writable_runlevel -= 1

        return Queue.put(self, data, block, timeout)

    def _decrement_runlevel(self):
        if self._runlevel == 1:
            self.on_finalize()
        self._runlevel -= 1
        self.on_end()

    def get(self, block=True, timeout=None):
        if not self.alive:
            raise InactiveReadableError("Cannot get() on an inactive {}.".format(Readable.__name__))

        data = Queue.get(self, block, timeout)

        if data == END:
            self._decrement_runlevel()

            if not self.alive:
                raise InactiveReadableError(
                    "Cannot get() on an inactive {} (runlevel just reached 0).".format(Readable.__name__)
                )
            return self.get(block, timeout)

        return data

    def shutdown(self):
        while self._runlevel >= 1:
            self._decrement_runlevel()

    def empty(self):
        self.mutex.acquire()
        while self._qsize() and self.queue[0] == END:
            self._runlevel -= 1
            Queue._get(self)
        self.mutex.release()

        return Queue.empty(self)

    @property
    def alive(self):
        return self._runlevel > 0


class AioInput(AioQueue):
    pass
