import time

from google.cloud.firestore_v1 import DocumentSnapshot, DocumentReference

"""
_Watch is ported from from google.cloud.firestore_v1.watch

Copy of the license of google.cloud.firestore_v1.watch: 
# Copyright 2017 Google LLC All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""


# Copyright 2017 Google LLC All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import collections
import threading
import datetime
from enum import Enum
import functools

import pytz

from google.api_core.bidi import ResumableBidiRpc
from google.api_core.bidi import BackgroundConsumer
from google.cloud.firestore_v1.proto import firestore_pb2
from google.cloud.firestore_v1 import _helpers

from google.api_core import exceptions

import grpc

"""Python client for Google Cloud Firestore Watch."""

_LOGGER = logging.getLogger(__name__)

INITIAL_TARGET_ID = 0x0  # "Py"

GRPC_STATUS_CODE = {
    "OK": 0,
    "CANCELLED": 1,
    "UNKNOWN": 2,
    "INVALID_ARGUMENT": 3,
    "DEADLINE_EXCEEDED": 4,
    "NOT_FOUND": 5,
    "ALREADY_EXISTS": 6,
    "PERMISSION_DENIED": 7,
    "UNAUTHENTICATED": 16,
    "RESOURCE_EXHAUSTED": 8,
    "FAILED_PRECONDITION": 9,
    "ABORTED": 10,
    "OUT_OF_RANGE": 11,
    "UNIMPLEMENTED": 12,
    "INTERNAL": 13,
    "UNAVAILABLE": 14,
    "DATA_LOSS": 15,
    "DO_NOT_USE": -1,
}
_RPC_ERROR_THREAD_NAME = "Thread-OnRpcTerminated"
_RECOVERABLE_STREAM_EXCEPTIONS = (
    exceptions.Aborted,
    exceptions.Cancelled,
    exceptions.Unknown,
    exceptions.DeadlineExceeded,
    exceptions.ResourceExhausted,
    exceptions.InternalServerError,
    exceptions.ServiceUnavailable,
    exceptions.Unauthenticated,
)
_TERMINATING_STREAM_EXCEPTIONS = (exceptions.Cancelled,)

DocTreeEntry = collections.namedtuple("DocTreeEntry", ["value", "index"])


class WatchDocTree(object):
    # TODO: this class has been modified to suit the purpose of flask-boiler
    # TODO: note that the RAM usage will keep increasing since consumed
    #           document snapshot in _removed are not deleted
    #           so that we can find the original version of the document
    #           when a document is changed.

    def __init__(self):
        self._dict = {}
        self._index = 0
        self._removed = {}

    def keys(self):
        return [*self._removed.keys(), *self._dict.keys()]

    def _copy(self):
        wdt = WatchDocTree()
        wdt._dict = self._dict.copy()
        wdt._index = self._index
        wdt._removed = self._removed.copy()
        self = wdt
        return self

    def insert(self, key, value):
        self = self._copy()
        self._dict[key] = DocTreeEntry(value, self._index)
        self._index += 1
        return self

    def find(self, key):
        return self._dict[key]

    def remove(self, key):
        self = self._copy()
        self._removed[key] = self._dict[key]
        del self._dict[key]
        return self

    def __iter__(self):
        for k in self._dict:
            yield k

    def __len__(self):
        return len(self._dict)

    def __contains__(self, k):
        return k in self._dict


class ChangeType(Enum):
    ADDED = 1
    REMOVED = 2
    MODIFIED = 3


class DocumentChange(object):
    def __init__(self, type, document, old_index, new_index):
        """DocumentChange

        Args:
            type (ChangeType):
            document (document.DocumentSnapshot):
            old_index (int):
            new_index (int):
        """
        # TODO: spec indicated an isEqual param also
        self.type = type
        self.document = document
        self.old_index = old_index
        self.new_index = new_index


class WatchResult(object):
    def __init__(self, snapshot, name, change_type):
        self.snapshot = snapshot
        self.name = name
        self.change_type = change_type


def _maybe_wrap_exception(exception):
    """Wraps a gRPC exception class, if needed."""
    if isinstance(exception, grpc.RpcError):
        return exceptions.from_grpc_error(exception)
    return exception


def document_watch_comparator(doc1, doc2):
    assert doc1 == doc2, "Document watches only support one document."
    return 0


def _should_recover(exception):
    wrapped = _maybe_wrap_exception(exception)
    return isinstance(wrapped, _RECOVERABLE_STREAM_EXCEPTIONS)


def _should_terminate(exception):
    wrapped = _maybe_wrap_exception(exception)
    return isinstance(wrapped, _TERMINATING_STREAM_EXCEPTIONS)


class _Watch(object):

    BackgroundConsumer = BackgroundConsumer  # FBO unit tests
    ResumableBidiRpc = ResumableBidiRpc  # FBO unit tests

    def __init__(
        self,
        firestore,
        # target,
        comparator,
        # snapshot_callback,
        document_snapshot_cls,
        document_reference_cls,
        BackgroundConsumer=None,  # FBO unit testing
        ResumableBidiRpc=None,  # FBO unit testing
    ):
        """
        Args:
            firestore:
            target: can be None
            comparator:
            snapshot_callback: Callback method to process snapshots.
                Args:
                    docs (List(DocumentSnapshot)): A callback that returns the
                        ordered list of documents stored in this snapshot.
                    changes (List(str)): A callback that returns the list of
                        changed documents since the last snapshot delivered for
                        this watch.
                    read_time (string): The ISO 8601 time at which this
                        snapshot was obtained.

            document_snapshot_cls: instance of DocumentSnapshot
            document_reference_cls: instance of DocumentReference
        """
        # self._document_reference = document_reference
        self._firestore = firestore
        self._api = firestore._firestore_api
        self._targets = dict()
        self._target_callbacks = dict()
        # if target is not None:
        #     target_id = target['target_id']
        #     self._targets[target_id] = target
        self._comparator = comparator
        self.DocumentSnapshot = document_snapshot_cls
        self.DocumentReference = document_reference_cls
        # self._snapshot_callback = snapshot_callback
        self._closing = threading.Lock()
        self._closed = False

        self.resume_token = None

        # rpc_request = self._get_rpc_request

        if ResumableBidiRpc is None:
            ResumableBidiRpc = self.ResumableBidiRpc  # FBO unit tests

        self._rpc = ResumableBidiRpc(
            self._api.transport.listen,
            should_recover=_should_recover,
            should_terminate=_should_terminate,
            # initial_request=rpc_request,
            metadata=self._firestore._rpc_metadata,
        )

        # TODO: recover this line somewhere
        # if self.resume_token is not None:
        # # self._targets["resume_token"] = self.resume_token

        self._rpc.add_done_callback(self._on_rpc_done)

        # Initialize state for on_snapshot
        # The sorted tree of QueryDocumentSnapshots as sent in the last
        # snapshot. We only look at the keys.
        self.doc_tree = WatchDocTree()

        # A map of document names to QueryDocumentSnapshots for the last sent
        # snapshot.
        self.doc_map = {}

        # The accumulates map of document changes (keyed by document name) for
        # the current snapshot.
        self.change_map = {}

        from collections import defaultdict
        self.change_log = defaultdict(list)

        # The current state of the query results.
        self.current = False

        # We need this to track whether we've pushed an initial set of changes,
        # since we should push those even when there are no changes, if there
        # aren't docs.
        self.has_pushed = False

        # The server assigns and updates the resume token.
        if BackgroundConsumer is None:  # FBO unit tests
            BackgroundConsumer = self.BackgroundConsumer

        self._consumer = BackgroundConsumer(self._rpc, self.on_snapshot)
        self._consumer.start()

    # def _get_rpc_request(self):
    #     if self.resume_token is not None:
    #         self._targets["resume_token"] = self.resume_token
    #     if INITIAL_TARGET_ID in self._targets:
    #         return firestore_pb2.ListenRequest(
    #             database=self._firestore._database_string,
    #             add_target=self._targets
    #         )
    #     else:
    #         return firestore_pb2.ListenRequest(
    #             database=self._firestore._database_string
    #         )

    def add_target(self, target, callback):
        """ Only new targets can be added.

        :param target:
        :return:
        """
        target_id = target['target_id']
        if target_id in self._targets:
            raise ValueError
        self._targets[target_id] = target
        self._target_callbacks[target_id] = callback
        self._rpc.send(
            firestore_pb2.ListenRequest(
                database=self._firestore._database_string,
                add_target=self._targets[target_id]
            )
        )

    @property
    def is_active(self):
        """bool: True if this manager is actively streaming.

        Note that ``False`` does not indicate this is complete shut down,
        just that it stopped getting new messages.
        """
        return self._consumer is not None and self._consumer.is_active

    def close(self, reason=None):
        """Stop consuming messages and shutdown all helper threads.

        This method is idempotent. Additional calls will have no effect.

        Args:
            reason (Any): The reason to close this. If None, this is considered
                an "intentional" shutdown.
        """
        with self._closing:
            if self._closed:
                return

            # Stop consuming messages.
            if self.is_active:
                _LOGGER.debug("Stopping consumer.")
                self._consumer.stop()
            self._consumer = None

            self._rpc.close()
            self._rpc = None
            self._closed = True
            _LOGGER.debug("Finished stopping manager.")

        if reason:
            # Raise an exception if a reason is provided
            _LOGGER.debug("reason for closing: %s" % reason)
            if isinstance(reason, Exception):
                raise reason
            raise RuntimeError(reason)

    def _on_rpc_done(self, future):
        """Triggered whenever the underlying RPC terminates without recovery.

        This is typically triggered from one of two threads: the background
        consumer thread (when calling ``recv()`` produces a non-recoverable
        error) or the grpc management thread (when cancelling the RPC).

        This method is *non-blocking*. It will start another thread to deal
        with shutting everything down. This is to prevent blocking in the
        background consumer and preventing it from being ``joined()``.
        """
        _LOGGER.info("RPC termination has signaled manager shutdown.")
        future = _maybe_wrap_exception(future)
        thread = threading.Thread(
            name=_RPC_ERROR_THREAD_NAME, target=self.close, kwargs={"reason": future}
        )
        thread.daemon = True
        thread.start()

    def unsubscribe(self):
        self.close()

    @classmethod
    def for_document(
        cls,
        document_ref,
        snapshot_callback,
        snapshot_class_instance,
        reference_class_instance,
    ):
        """
        Creates a watch snapshot listener for a document. snapshot_callback
        receives a DocumentChange object, but may also start to get
        targetChange and such soon

        Args:
            document_ref: Reference to Document
            snapshot_callback: callback to be called on snapshot
            snapshot_class_instance: instance of DocumentSnapshot to make
                snapshots with to pass to snapshot_callback
            reference_class_instance: instance of DocumentReference to make
                references

        """
        return cls(
            document_ref,
            document_ref._client,
            {
                "documents": {"documents": [document_ref._document_path]},
                "target_id": INITIAL_TARGET_ID,
            },
            document_watch_comparator,
            snapshot_callback,
            snapshot_class_instance,
            reference_class_instance,
        )

    @classmethod
    def for_query(
        cls, query, snapshot_callback, snapshot_class_instance, reference_class_instance
    ):
        parent_path, _ = query._parent._parent_info()
        query_target = firestore_pb2.Target.QueryTarget(
            parent=parent_path, structured_query=query._to_protobuf()
        )

        return cls(
            query,
            query._client,
            {"query": query_target, "target_id": INITIAL_TARGET_ID},
            query._comparator,
            snapshot_callback,
            snapshot_class_instance,
            reference_class_instance,
        )

    def _on_snapshot_target_change_no_change(self, proto):
        _LOGGER.debug("on_snapshot: target change: NO_CHANGE")
        change = proto.target_change

        no_target_ids = change.target_ids is None or len(change.target_ids) == 0

        """
        If empty, the change applies to all targets.
        """

        # TODO: move resume token key in self._targets to elsewhere
        target_ids = list(self._targets.keys()) if no_target_ids \
            else change.target_ids

        if no_target_ids and change.read_time and self.current:
            # TargetChange.CURRENT followed by TargetChange.NO_CHANGE
            # signals a consistent state. Invoke the onSnapshot
            # callback as specified by the user.
            self.push(change.read_time, change.resume_token)

    def _on_snapshot_target_change_add(self, proto):
        pass
        # _LOGGER.debug("on_snapshot: target change: ADD")
        # target_id = proto.target_change.target_ids[0]
        # if target_id not in self._targets:
        #     raise RuntimeError("Unexpected target ID %s sent by server" % target_id)
        # if target_id != WATCH_TARGET_ID:
        #     raise RuntimeError("Unexpected target ID %s sent by server" % target_id)

    def _on_snapshot_target_change_remove(self, proto):
        pass
        # _LOGGER.debug("on_snapshot: target change: REMOVE")
        # change = proto.target_change
        #
        # code = 13
        # message = "internal error"
        # if change.cause:
        #     code = change.cause.code
        #     message = change.cause.message
        #
        # message = "Error %s:  %s" % (code, message)
        #
        # raise RuntimeError(message)

    def _on_snapshot_target_change_reset(self, proto):
        # Whatever changes have happened so far no longer matter.
        _LOGGER.debug("on_snapshot: target change: RESET")
        self._reset_docs()

    def _on_snapshot_target_change_current(self, proto):
        _LOGGER.debug("on_snapshot: target change: CURRENT")
        self.current = True

    def on_snapshot(self, proto):
        """
        Called everytime there is a response from listen. Collect changes
        and 'push' the changes in a batch to the customer when we receive
        'current' from the listen response.

        Args:
            listen_response(`google.cloud.firestore_v1.types.ListenResponse`):
                Callback method that receives a object to
        """
        _LOGGER.info(proto)
        TargetChange = firestore_pb2.TargetChange

        target_changetype_dispatch = {
            TargetChange.NO_CHANGE: self._on_snapshot_target_change_no_change,
            TargetChange.ADD: self._on_snapshot_target_change_add,
            TargetChange.REMOVE: self._on_snapshot_target_change_remove,
            TargetChange.RESET: self._on_snapshot_target_change_reset,
            TargetChange.CURRENT: self._on_snapshot_target_change_current,
        }

        target_change = getattr(proto, "target_change", "")
        document_change = getattr(proto, "document_change", "")
        document_delete = getattr(proto, "document_delete", "")
        document_remove = getattr(proto, "document_remove", "")
        filter_ = getattr(proto, "filter", "")

        if str(target_change):
            target_change_type = target_change.target_change_type
            _LOGGER.debug("on_snapshot: target change: " + str(target_change_type))
            meth = target_changetype_dispatch.get(target_change_type)
            if meth is None:
                _LOGGER.info(
                    "on_snapshot: Unknown target change " + str(target_change_type)
                )
                self.close(
                    reason="Unknown target change type: %s " % str(target_change_type)
                )
            else:
                try:
                    meth(proto)
                except Exception as exc2:
                    _LOGGER.debug("meth(proto) exc: " + str(exc2))
                    raise

            # NOTE:
            # in other implementations, such as node, the backoff is reset here
            # in this version bidi rpc is just used and will control this.

        elif str(document_change):
            _LOGGER.debug("on_snapshot: document change")

            target_ids = document_change.target_ids or []

            for target_id in target_ids:
                self.change_log[target_id].append(proto)

        # NB: document_delete and document_remove (as far as we, the client,
        # are concerned) are functionally equivalent

        elif str(document_delete):
            _LOGGER.debug("on_snapshot: document change: DELETE")
            name = document_delete.document
            self.change_map[name] = ChangeType.REMOVED

            target_ids = document_delete.removed_target_ids or []

            for target_id in target_ids:
                self.change_log[target_id].append(proto)

        elif str(document_remove):
            _LOGGER.debug("on_snapshot: document change: REMOVE")
            name = document_remove.document
            self.change_map[name] = ChangeType.REMOVED

            target_ids = document_change.target_ids or []

            for target_id in target_ids:
                self.change_log[target_id].append(proto)

        elif filter_:
            _LOGGER.debug("on_snapshot: filter update")
            if filter_.count != self._current_size():
                # We need to remove all the current results.
                self._reset_docs()
                # The filter didn't match, so re-issue the query.
                # TODO: reset stream method?
                # self._reset_stream();

        elif proto is None:
            self.close()
        else:
            _LOGGER.debug("UNKNOWN TYPE. UHOH")
            self.close(reason=ValueError("Unknown listen response type: %s" % proto))

    def push(self, read_time, next_resume_token):
        """
        Assembles a new snapshot from the current set of changes and invokes
        the user's callback. Clears the current changes on completion.
        """
        # deletes, adds, updates = _Watch._extract_changes(
        #     self.doc_map, self.change_map, read_time
        # )
        #
        # updated_tree, updated_map, appliedChanges = self._compute_snapshot(
        #     self.doc_tree, self.doc_map, deletes, adds, updates
        # )

        # TODO: It is possible in the future we will have the tree order
        # on insert. For now, we sort here.
        # key = functools.cmp_to_key(self._comparator)
        # keys = sorted(updated_tree.keys(), key=key)
        for target_id, changes in self.change_log.items():
            if target_id not in self._target_callbacks:
                continue
            callback = self._target_callbacks[target_id]
            callback(target_id, changes, read_time)
        self.has_pushed = True
        #
        # self.doc_tree = updated_tree
        # self.doc_map = updated_map
        self.change_log.clear()
        self.change_map.clear()
        self.resume_token = next_resume_token

    @staticmethod
    def _extract_changes(doc_map, changes, read_time):
        deletes = []
        adds = []
        updates = []

        for name, value in changes.items():
            if value == ChangeType.REMOVED:
                if name in doc_map:
                    deletes.append(name)
            elif name in doc_map:
                if read_time is not None:
                    value.read_time = read_time
                updates.append(value)
            else:
                if read_time is not None:
                    value.read_time = read_time
                adds.append(value)

        return (deletes, adds, updates)

    def _compute_snapshot(
        self, doc_tree, doc_map, delete_changes, add_changes, update_changes
    ):
        updated_tree = doc_tree
        updated_map = doc_map

        assert len(doc_tree) == len(doc_map), (
            "The document tree and document map should have the same "
            + "number of entries."
        )

        def delete_doc(name, updated_tree, updated_map):
            """
            Applies a document delete to the document tree and document map.
            Returns the corresponding DocumentChange event.
            """
            assert name in updated_map, "Document to delete does not exist"
            old_document = updated_map.get(name)
            # TODO: If a document doesn't exist this raises IndexError. Handle?
            existing = updated_tree.find(old_document)
            old_index = existing.index
            updated_tree = updated_tree.remove(old_document)
            del updated_map[name]
            return (
                DocumentChange(ChangeType.REMOVED, old_document, old_index, -1),
                updated_tree,
                updated_map,
            )

        def add_doc(new_document, updated_tree, updated_map):
            """
            Applies a document add to the document tree and the document map.
            Returns the corresponding DocumentChange event.
            """
            name = new_document.reference._document_path
            assert name not in updated_map, "Document to add already exists"
            updated_tree = updated_tree.insert(new_document, None)
            new_index = updated_tree.find(new_document).index
            updated_map[name] = new_document
            return (
                DocumentChange(ChangeType.ADDED, new_document, -1, new_index),
                updated_tree,
                updated_map,
            )

        def modify_doc(new_document, updated_tree, updated_map):
            """
            Applies a document modification to the document tree and the
            document map.
            Returns the DocumentChange event for successful modifications.
            """
            name = new_document.reference._document_path
            assert name in updated_map, "Document to modify does not exist"
            old_document = updated_map.get(name)
            if old_document.update_time != new_document.update_time:
                remove_change, updated_tree, updated_map = delete_doc(
                    name, updated_tree, updated_map
                )
                add_change, updated_tree, updated_map = add_doc(
                    new_document, updated_tree, updated_map
                )
                return (
                    DocumentChange(
                        ChangeType.MODIFIED,
                        new_document,
                        remove_change.old_index,
                        add_change.new_index,
                    ),
                    updated_tree,
                    updated_map,
                )

            return None, updated_tree, updated_map

        # Process the sorted changes in the order that is expected by our
        # clients (removals, additions, and then modifications). We also need
        # to sort the individual changes to assure that old_index/new_index
        # keep incrementing.
        appliedChanges = []

        key = functools.cmp_to_key(self._comparator)

        # Deletes are sorted based on the order of the existing document.
        delete_changes = sorted(delete_changes)
        for name in delete_changes:
            change, updated_tree, updated_map = delete_doc(
                name, updated_tree, updated_map
            )
            appliedChanges.append(change)

        add_changes = sorted(add_changes, key=key)
        _LOGGER.debug("walk over add_changes")
        for snapshot in add_changes:
            _LOGGER.debug("in add_changes")
            change, updated_tree, updated_map = add_doc(
                snapshot, updated_tree, updated_map
            )
            appliedChanges.append(change)

        update_changes = sorted(update_changes, key=key)
        for snapshot in update_changes:
            change, updated_tree, updated_map = modify_doc(
                snapshot, updated_tree, updated_map
            )
            if change is not None:
                appliedChanges.append(change)

        assert len(updated_tree) == len(updated_map), (
            "The update document "
            + "tree and document map should have the same number of entries."
        )
        return (updated_tree, updated_map, appliedChanges)

    def _affects_target(self, target_ids, current_id):
        if target_ids is None:
            return True

        return current_id in target_ids

    def _current_size(self):
        """
        Returns the current count of all documents, including the changes from
        the current changeMap.
        """
        deletes, adds, _ = _Watch._extract_changes(self.doc_map, self.change_map, None)
        return len(self.doc_map) + len(adds) - len(deletes)

    def _reset_docs(self):
        """
        Helper to clear the docs on RESET or filter mismatch.
        """
        _LOGGER.debug("resetting documents")
        self.change_map.clear()
        self.resume_token = None

        # Mark each document as deleted. If documents are not deleted
        # they will be sent again by the server.
        for snapshot in self.doc_tree.keys():
            name = snapshot.reference._document_path
            self.change_map[name] = ChangeType.REMOVED

        self.current = False


# class _Watch(Watch):
#     pass
#     # def _on_snapshot_target_change_remove(self, proto):
#     #     """
#     #     target removed: assuming once=True and on_snapshot invoked
#     #     :param proto:
#     #     :return:
#     #     """
#     #     pass

class DataListener:

    def __init__(self, document_refs=None, snapshot_callback=None, firestore=None, once=False, query=None):
        """

        :param document_refs: a list of string
        :param snapshot_callback:
        """

        def comparator(doc1: DocumentSnapshot, doc2: DocumentSnapshot):
            """ TODO: use carefully

            :param doc1:
            :param doc2:
            :return:
            """
            return 1
            # if doc1.reference._document_path > doc2.reference._document_path:
            #     return 1
            # elif doc1.reference._document_path == doc2.reference._document_path:
            #     return 0
            # else:
            #     return -1

        if document_refs is not None:

            target = {
                    "documents": {"documents": document_refs},
                    "target_id": WATCH_TARGET_ID,
                    "once": once
                }

            self.watch = _Watch(document_reference=None,
                                firestore=firestore,
                                target=target,
                                comparator=comparator,
                                snapshot_callback=snapshot_callback,
                                document_snapshot_cls=DocumentSnapshot,
                                document_reference_cls=DocumentReference,
                                )

        elif query is not None:

            parent_path, _ = query._parent._parent_info()
            query_target = firestore_pb2.Target.QueryTarget(
                parent=parent_path, structured_query=query._to_protobuf()
            )
            target = {"query": query_target, "target_id": WATCH_TARGET_ID}

            self.watch = _Watch(document_reference=query,
                                firestore=query._client,
                                target=target,
                                comparator=comparator,
                                snapshot_callback=snapshot_callback,
                                document_snapshot_cls=DocumentSnapshot,
                                document_reference_cls=DocumentReference,
                                )
        else:
            raise ValueError

    def wait_for_once_done(self):
        """
        TODO: Note that for now this method only detects >= 1 updates, instead
            of exactly one update.
        :return:
        """
        # TODO: Find a better way
        # TODO: review and test
        # time.sleep(2)
        while not self.watch.current:
            # Note: the smallest interval that time.sleep can
            #   be invoked with differs by OS
            time.sleep(.020)  # Check back in 20ms
        # self.watch._consumer._thread.join()

    def __del__(self):
        self.watch.close()
