import base36

from collections import deque
from threading import Thread, get_ident
from typing import Callable

from .subscriber import Subscriber


class Subscription:
    """
    Subscriptions are generated by calling hub.subscribe(). They manage a
    worker thread, which listens on an exclusive channel to which data which
    is published to the subscribed channel's name (self.channel.name).
    
    Subscription keeps a ring buffer of the previous N data items received,
    in self.history.
    
    When published to, a subscription's registered callback is invoked.
    """

    def __init__(
        self,
        channel: 'Channel',
        callback: Callable,
        history: int = 1,
    ):
        self.channel = channel
        self.hub = channel.hub
        self.log = channel.hub.log
        self.callback = callback
        self.history = deque()
        self._history_capacity = history
        self._subscriber = None
        self._is_active = False
        self._thread = None

    def __repr__(self):
        return f'Subscription(channel={self.channel.name})'

    @property
    def subscriber(self) -> 'Subscriber':
        return self._subscriber

    @property
    def is_active(self) -> bool:
        """
        Is the subscription alive and listening for data to pass into its
        callback?
        """
        return self._is_active

    def start(self):
        """
        In a background thread, we wait for incoming channel data. Upon
        reciept, we pass it along to the subscribing callback and then push it
        into a ring history buffer for easy access of previous data in
        subsequent calls.
        """

        def run(subscriber):
            """
            Thread target function
            """
            self._is_active = True

            while self._is_active:
                # import ipdb; ipdb.set_trace()
                # enforce finite history capacity
                if len(self.history) > self._history_capacity:
                    self.history.popleft()

                channel = self.hub[subscriber.name]
                data = channel.receive()

                if data is not None:
                    # trigger callback with received data
                    self.callback(self, data)
                    # append the new data item to subscription history
                    self.history.append(data)
                else:
                    self._is_active = False
                    channel.remove()

            self.log.info(
                f'{self.channel.name} subscription ended'
            )

        # already active?
        if self._thread is not None:
            self.log.debug(
                f'{b36_thread_id} already subscribed to {self.channel.name}'
            )
            return

        # create the Subscriber
        b36_thread_id = base36.dumps(get_ident())
        subscriber_name = f'{self.channel.name}:{b36_thread_id}'
        subscriber = Subscriber(self.hub, subscriber_name)

        self._subscriber = subscriber

        # tell the publisher we want to subscribe
        channel = self.hub[f'{self.channel.name}:subscribe']
        channel.send(subscriber.to_dict())

        # start the background thread
        self._thread = Thread(target=run, args=(subscriber, ), daemon=True)
        self._thread.start()

    def cancel(self, timeout=None):
        """
        Deactivate the subscription, stopping the worker thread, if alive.
        """
        if self._is_active:
            self.log.debug(f'canceling {self.channel.name} subscription')

            self.hub[self._subscriber.name].send(None)

            if timeout and self._thread and self._thread.is_alive():
                self._thread.join(timeout=timeout)

            self._subscriber = None
