"""A sync client for interacting with the rippled WebSocket API."""
from __future__ import annotations

from asyncio import AbstractEventLoop, new_event_loop, run_coroutine_threadsafe
from concurrent.futures import CancelledError, TimeoutError
from threading import Thread
from types import TracebackType
from typing import Any, Dict, Iterator, Optional, Type, Union, cast

from xrpl.asyncio.clients.exceptions import XRPLWebsocketException
from xrpl.asyncio.clients.websocket_base import WebsocketBase
from xrpl.clients.sync_client import SyncClient
from xrpl.models.requests.request import Request
from xrpl.models.response import Response


class WebsocketClient(SyncClient, WebsocketBase):
    """
    A sync client for interacting with the rippled WebSocket API.

    Instead of calling ``open`` and ``close`` yourself, you
    can use a context like so::

        with WebsocketClient(url) as client:
            # inside the context the client is open
        # after exiting the context, the client is closed

    Doing this will open and close the client for you and is
    preferred.

    NOTE: if you are not using subscriptions or other WebSocket-only
    features of rippled, you may not need to do anything other than
    open the client and make requests::

        from xrpl.clients import WebsocketClient
        from xrpl.ledger import get_fee
        from xrpl.models import Fee


        with WebsocketClient(url) as client:
            # using helper functions
            print(get_fee(client))

            # using raw requests yourself
            print(client.request(Fee())

    However, if you are using some functionality that makes use of
    subscriptions or other "websocket-y" things, you can iterate over
    the client like so to read incoming messages::

        with WebsocketClient(url) as client:
            # inside the context the client is open
            for message in client:
                # do something with a message
        # after exiting the context, the client is closed

    NOTE: doing the above will cause the client to listen for
    messages indefinitely. For this reason, ``WebsocketClient``
    takes an optional ``timeout`` parameter which will stop
    iterating on messages if none are received in that timeframe.
    Generally, if you have complex needs with python, xrpl, and
    websockets, you should consider using the ``asyncio`` support
    provided by this library and the ``xrpl.asyncio.clients.AsyncWebsocketClient``
    instead.
    """

    def __init__(
        self: WebsocketClient, url: str, timeout: Optional[Union[int, float]] = None
    ) -> None:
        """
        Constructs a WebsocketClient.

        Arguments:
            url: The URL of the rippled node to submit requests to.
            timeout: Maximum seconds to wait for a new message when
                iterating. A value of 0 or None will result in no limit.
                If this limit is met, iteration will stop.
        """
        self.timeout = timeout
        self._loop: Optional[AbstractEventLoop] = None
        self._thread: Optional[Thread] = None
        super().__init__(url)

    def is_open(self: WebsocketClient) -> bool:
        """
        Returns whether the client is currently open.

        Returns:
            True if the client is currently open, False otherwise.
        """
        return self._loop is not None and self._thread is not None and super().is_open()

    def open(self: WebsocketClient) -> None:
        """Connects the client to the Web Socket API at the given URL."""
        if self.is_open():
            return

        # make a new asyncio event loop
        self._loop = new_event_loop()

        # create and start a thread to run that event loop
        self._thread = Thread(
            target=self._loop.run_forever,
            daemon=True,
        )
        self._thread.start()

        # run WebsocketBase._do_open on the event loop of the child thread and
        # wait for it to finish
        run_coroutine_threadsafe(self._do_open(), self._loop).result()

    def close(self: WebsocketClient) -> None:
        """Closes the connection."""
        if not self.is_open():
            return

        # run WebsocketBase._do_close on the event loop of the child thread and
        # wait for it to finish
        run_coroutine_threadsafe(
            self._do_close(), cast(AbstractEventLoop, self._loop)
        ).result()

        # request the child thread to stop the loop and wait for it to
        # terminate
        cast(AbstractEventLoop, self._loop).call_soon_threadsafe(
            cast(AbstractEventLoop, self._loop).stop
        )
        cast(Thread, self._thread).join()

        # close the stopped loop
        cast(AbstractEventLoop, self._loop).close()

        # clear state
        self._loop = None
        self._thread = None

    def __enter__(self: WebsocketClient) -> WebsocketClient:
        """
        Enters a context after opening itself.

        Returns:
            The opened client.
        """
        self.open()
        return self

    def __exit__(
        self: WebsocketClient,
        _exc_type: Type[BaseException],
        _exc_val: BaseException,
        _trace: TracebackType,
    ) -> None:
        """Exits a context after closing itself."""
        self.close()

    def __iter__(self: WebsocketClient) -> Iterator[Dict[str, Any]]:
        """
        Iterate on received messages. This iterator will block until
        a message is received. If no message is received within
        `self.timeout` seconds then the iterator will exit. If
        `self.timeout` is `None` or `0` then the iterator will block
        indefinetly for the next messsage.
        """
        while self.is_open():
            future = run_coroutine_threadsafe(
                self._do_pop_message(), cast(AbstractEventLoop, self._loop)
            )
            try:
                yield future.result(self.timeout)
            except TimeoutError:
                # in this case, the future reached its timeout. we can safely
                # cancel and stop listening
                future.cancel()
                break
            except CancelledError:
                # in this case, the future was cancelled by someone else. we
                # stop listening but don't need to cancel it
                break

    def send(self: WebsocketClient, request: Request) -> None:
        """
        Submit the request represented by the request to the
        rippled node specified by this client's URL. Unlike ``request``,
        ``send`` does not wait for this request's response. In many cases
        it may be more convenient to use ``request``.

        Arguments:
            request: A Request object representing information about a rippled request.

        Raises:
            XRPLWebsocketException: If there is already an open request by the
                request's ID, or if this WebsocketClient is not open.
        """
        if not self.is_open():
            raise XRPLWebsocketException("Websocket is not open")
        run_coroutine_threadsafe(
            self._do_send(request), cast(AbstractEventLoop, self._loop)
        ).result()

    async def _request_impl(self: WebsocketClient, request: Request) -> Response:
        """
        ``_request_impl`` implementation for sync websockets that ensures the
        ``WebsocketBase._do_request_impl`` implementation is run on the other thread.

        Arguments:
            request: An object representing information about a rippled request.

        Returns:
            The response from the server, as a Response object.

        Raises:
            XRPLWebsocketException: If there is already an open request by the
                request's ID, or if this WebsocketClient is not open.

        :meta private:
        """
        if not self.is_open():
            raise XRPLWebsocketException("Websocket is not open")

        # it's unusual to write an async function that has no `await` and also
        # has no `async with` or `async for` but in this case that's
        # exactly what we want. the reason we need this is that the helper
        # functions all expect async functions, but since this
        # is a sync client we want to completely block until the request is
        # complete. also, `asyncio.run_coroutine_threadsafe` returns a
        # concurrent.futures.Future which is not awaitable.
        #
        # when this is run via `await client._request_impl`, it will
        # completely block the main thread until completed,
        # just as if it were not async.
        return run_coroutine_threadsafe(
            self._do_request_impl(request),
            cast(AbstractEventLoop, self._loop),
        ).result()
