import asyncio
import gzip
import json
import logging
from enum import Enum
from random import random

import websockets as ws

from .client import Client
from .exceptions import BinanceWebsocketUnableToConnect

DEFAULT_USER_TIMEOUT = 30 * 60  # 30 minutes


class WSListenerState(Enum):
    INITIALISING = 'Initialising'
    STREAMING = 'Streaming'
    RECONNECTING = 'Reconnecting'
    EXITING = 'Exiting'


class ReconnectingWebsocket:

    MAX_RECONNECTS = 5
    MAX_RECONNECT_SECONDS = 60
    MIN_RECONNECT_WAIT = 0.1
    TIMEOUT = 10
    NO_MESSAGE_RECONNECT_TIMEOUT = 60

    def __init__(self, loop, path, url, prefix='ws/', is_binary=False, exit_coro=None):
        self._loop = loop or asyncio.get_event_loop()
        self._log = logging.getLogger(__name__)
        self._path = path
        self._url = url
        self._exit_coro = exit_coro
        self._prefix = prefix
        self._reconnects = 0
        self._is_binary = is_binary
        self._conn = None
        self._socket = None
        self.ws = None
        self.ws_state = WSListenerState.INITIALISING
        self.reconnect_handle = None

    async def __aenter__(self):
        await self.connect()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        if self._exit_coro:
            await self._exit_coro(self._path)
        self.ws_state = WSListenerState.EXITING
        if self.ws:
            self.ws.fail_connection()
        if self._conn:
            await self._conn.__aexit__(exc_type, exc_val, exc_tb)
        self.ws = None

    async def connect(self):
        await self._before_connect()
        ws_url = self._url + self._prefix + self._path
        self._conn = ws.connect(ws_url)
        self.ws = await self._conn.__aenter__()
        self.ws_state = WSListenerState.STREAMING
        self._reconnects = 0
        await self._after_connect()

    async def _before_connect(self):
        pass

    async def _after_connect(self):
        pass

    def _handle_message(self, evt):
        if self._is_binary:
            try:
                evt = gzip.decompress(evt)
            except (ValueError, OSError):
                return None
        try:
            return json.loads(evt)
        except ValueError:
            self._log.debug('error parsing evt json:{}'.format(evt))
            return None

    async def recv(self):
        res = None
        while not res:
            if not self.ws or self.ws_state != WSListenerState.STREAMING:
                await self._wait_for_reconnect()
                break
            if self.ws_state == WSListenerState.EXITING:
                break
            try:
                res = await asyncio.wait_for(self.ws.recv(), timeout=self.TIMEOUT)
            except asyncio.TimeoutError:
                logging.debug(f"no message in {self.TIMEOUT} seconds")
            except asyncio.CancelledError as e:
                logging.debug(f"cancelled error {e}")
            except asyncio.IncompleteReadError as e:
                logging.debug(f"incomplete read error {e}")
            except Exception as e:
                logging.debug(f"exception {e}")
            else:
                if self.ws_state == WSListenerState.EXITING:
                    break
                res = await self._try_handle_msg(res)
                if self.ws_state == WSListenerState.EXITING:
                    break
        return res

    async def _wait_for_reconnect(self):
        while self.ws_state == WSListenerState.RECONNECTING:
            logging.debug("reconnecting waiting for connect")
            await asyncio.sleep(0.01)
        if not self.ws:
            logging.debug("ignore message no ws")
        else:
            logging.debug(f"ignore message {self.ws_state}")

    async def _try_handle_msg(self, res):
        msg_res = self._handle_message(res)
        if msg_res:
            # cancel error timeout
            if self.reconnect_handle:
                self.reconnect_handle.cancel()
            self.reconnect_handle = self._loop.call_later(
                self.NO_MESSAGE_RECONNECT_TIMEOUT, self._no_message_received_reconnect
            )
        return msg_res

    def _get_reconnect_wait(self, attempts: int) -> int:
        expo = 2 ** attempts
        return round(random() * min(self.MAX_RECONNECT_SECONDS, expo - 1) + 1)

    async def before_reconnect(self):
        await self._conn.__aexit__(None, None, None)
        self.ws = None
        self._reconnects += 1

    def _no_message_received_reconnect(self):
        logging.debug('No message received, reconnecting')
        asyncio.create_task(self._reconnect())

    async def _reconnect(self):
        if self.ws_state == WSListenerState.RECONNECTING:
            return
        self.ws_state = WSListenerState.RECONNECTING
        if self.reconnect_handle:
            self.reconnect_handle.cancel()
        await self.before_reconnect()
        if self._reconnects < self.MAX_RECONNECTS:
            reconnect_wait = self._get_reconnect_wait(self._reconnects)
            logging.debug(
                f"websocket reconnecting {self.MAX_RECONNECTS - self._reconnects} reconnects left - "
                f"waiting {reconnect_wait}"
            )
            await asyncio.sleep(reconnect_wait)
            await self.connect()
        else:
            logging.error(f'Max reconnections {self.MAX_RECONNECTS} reached:')
            raise BinanceWebsocketUnableToConnect


class KeepAliveWebsocket(ReconnectingWebsocket):

    def __init__(self, client, loop, url, keepalive_type, prefix='ws/', is_binary=False, exit_coro=None, user_timeout=None):
        super().__init__(loop, None, url, prefix, is_binary, exit_coro)
        self._keepalive_type = keepalive_type
        self._client = client
        self._user_timeout = user_timeout or DEFAULT_USER_TIMEOUT
        self._timer = None

    async def __aexit__(self, *args, **kwargs):
        if not self._path:
            return
        if self._timer:
            self._timer.cancel()
            self._timer = None
        await super().__aexit__(*args, **kwargs)

    async def _before_connect(self):
        if not self._path:
            self._path = await self._get_listen_key()

    async def _after_connect(self):
        self._start_socket_timer()

    def _start_socket_timer(self):
        self._timer = self._loop.call_later(
            self._user_timeout,
            self._keepalive_socket
        )

    async def _get_listen_key(self):
        if self._keepalive_type == 'user':
            listen_key = await self._client.stream_get_listen_key()
        elif self._keepalive_type == 'margin':  # cross-margin
            listen_key = await self._client.margin_stream_get_listen_key()
        elif self._keepalive_type == 'futures':
            listen_key = await self._client.futures_stream_get_listen_key()
        elif self._keepalive_type == 'coin_futures':
            listen_key = await self._client.futures_coin_stream_get_listen_key()
        else:  # isolated margin
            # Passing symbol for isolated margin
            listen_key = await self._client.isolated_margin_stream_get_listen_key(self._keepalive_type)
        return listen_key

    async def _keepalive_socket(self):
        listen_key = await self._get_listen_key()

        if listen_key != self._path:
            self._path = listen_key
            await self._reconnect()
        else:
            if self._keepalive_type == 'user':
                await self._client.stream_keepalive(self._path)
            elif self._keepalive_type == 'margin':  # cross-margin
                await self._client.margin_stream_keepalive(self._path)
            elif self._keepalive_type == 'futures':
                await self._client.futures_stream_keepalive(self._path)
            elif self._keepalive_type == 'coin_futures':
                await self._client.futures_coin_stream_keepalive(self._path)
            else:  # isolated margin
                # Passing symbol for isolated margin
                await self._client.isolated_margin_stream_keepalive(self._keepalive_type, self._path)
            self._start_socket_timer()


class BinanceSocketManager:

    STREAM_URL = 'wss://stream.binance.{}:9443/'
    FSTREAM_URL = 'wss://fstream.binance.{}/'
    DSTREAM_URL = 'wss://dstream.binance.{}/'
    VSTREAM_URL = 'wss://vstream.binance.{}/'
    VSTREAM_TESTNET_URL = 'wss://testnetws.binanceops.{}/'

    WEBSOCKET_DEPTH_5 = '5'
    WEBSOCKET_DEPTH_10 = '10'
    WEBSOCKET_DEPTH_20 = '20'

    def __init__(self, client, loop=None, user_timeout=DEFAULT_USER_TIMEOUT):
        """Initialise the BinanceSocketManager

        :param client: Binance API client
        :type client: binance.Client

        """
        self.STREAM_URL = self.STREAM_URL.format(client.tld)
        self.FSTREAM_URL = self.FSTREAM_URL.format(client.tld)
        self.DSTREAM_URL = self.DSTREAM_URL.format(client.tld)
        self.VSTREAM_URL = self.VSTREAM_URL.format(client.tld)
        self.VSTREAM_TESTNET_URL = self.VSTREAM_TESTNET_URL.format(client.tld)

        self._conns = {}
        self._loop = loop or asyncio.get_event_loop()
        self._client = client
        self._user_timeout = user_timeout

        self.testnet = self._client.testnet

    def _get_socket(self, path, stream_url=None, prefix='ws/', is_binary=False):
        stream_url = stream_url or self.STREAM_URL
        if path not in self._conns:
            self._conns[path] = ReconnectingWebsocket(
                loop=self._loop,
                path=path,
                url=stream_url,
                prefix=prefix,
                exit_coro=self._exit_socket,
                is_binary=is_binary
            )

        return self._conns[path]

    def _get_account_socket(self, path, stream_url=None, prefix='ws/', is_binary=False):
        stream_url = stream_url or self.STREAM_URL
        if path not in self._conns:
            self._conns[path] = KeepAliveWebsocket(
                client=self._client,
                loop=self._loop,
                url=stream_url,
                keepalive_type=path,
                prefix=prefix,
                exit_coro=self._exit_socket,
                is_binary=is_binary,
                user_timeout=self._user_timeout
            )

        return self._conns[path]

    def _get_futures_socket(self, path, prefix='stream?streams='):
        return self._get_socket(path, self.FSTREAM_URL, prefix)

    def _get_options_socket(self, path, prefix='ws/'):
        if self.testnet:
            stream_url = self.VSTREAM_TESTNET_URL
        else:
            stream_url = self.VSTREAM_URL

        return self._get_socket(path, stream_url, prefix, is_binary=True)

    async def _exit_socket(self, path):
        await self._stop_socket(path)

    def depth_socket(self, symbol, depth=None, interval=None):
        """Start a websocket for symbol market depth returning either a diff or a partial book

        https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#partial-book-depth-streams

        :param symbol: required
        :type symbol: str
        :param depth: optional Number of depth entries to return, default None. If passed returns a partial book instead of a diff
        :type depth: str
        :param interval: optional interval for updates, default None. If not set, updates happen every second. Must be 0, None (1s) or 100 (100ms)
        :type interval: int

        :returns: connection key string if successful, False otherwise

        Partial Message Format

        .. code-block:: python

            {
                "lastUpdateId": 160,  # Last update ID
                "bids": [             # Bids to be updated
                    [
                        "0.0024",     # price level to be updated
                        "10",         # quantity
                        []            # ignore
                    ]
                ],
                "asks": [             # Asks to be updated
                    [
                        "0.0026",     # price level to be updated
                        "100",        # quantity
                        []            # ignore
                    ]
                ]
            }


        Diff Message Format

        .. code-block:: python

            {
                "e": "depthUpdate", # Event type
                "E": 123456789,     # Event time
                "s": "BNBBTC",      # Symbol
                "U": 157,           # First update ID in event
                "u": 160,           # Final update ID in event
                "b": [              # Bids to be updated
                    [
                        "0.0024",   # price level to be updated
                        "10",       # quantity
                        []          # ignore
                    ]
                ],
                "a": [              # Asks to be updated
                    [
                        "0.0026",   # price level to be updated
                        "100",      # quantity
                        []          # ignore
                    ]
                ]
            }

        """
        socket_name = symbol.lower() + '@depth'
        if depth and depth != '1':
            socket_name = '{}{}'.format(socket_name, depth)
        if interval:
            if interval in [0, 100]:
                socket_name = '{}@{}ms'.format(socket_name, interval)
            else:
                raise ValueError("Websocket interval value not allowed. Allowed values are [0, 100]")
        return self._get_socket(socket_name)

    def kline_socket(self, symbol, interval=Client.KLINE_INTERVAL_1MINUTE):
        """Start a websocket for symbol kline data

        https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#klinecandlestick-streams

        :param symbol: required
        :type symbol: str
        :param interval: Kline interval, default KLINE_INTERVAL_1MINUTE
        :type interval: str

        :returns: connection key string if successful, False otherwise

        Message Format

        .. code-block:: python

            {
                "e": "kline",					# event type
                "E": 1499404907056,				# event time
                "s": "ETHBTC",					# symbol
                "k": {
                    "t": 1499404860000, 		# start time of this bar
                    "T": 1499404919999, 		# end time of this bar
                    "s": "ETHBTC",				# symbol
                    "i": "1m",					# interval
                    "f": 77462,					# first trade id
                    "L": 77465,					# last trade id
                    "o": "0.10278577",			# open
                    "c": "0.10278645",			# close
                    "h": "0.10278712",			# high
                    "l": "0.10278518",			# low
                    "v": "17.47929838",			# volume
                    "n": 4,						# number of trades
                    "x": false,					# whether this bar is final
                    "q": "1.79662878",			# quote volume
                    "V": "2.34879839",			# volume of active buy
                    "Q": "0.24142166",			# quote volume of active buy
                    "B": "13279784.01349473"	# can be ignored
                    }
            }
        """
        path = '{}@kline_{}'.format(symbol.lower(), interval)
        return self._get_socket(path)

    def miniticker_socket(self, update_time=1000):
        """Start a miniticker websocket for all trades

        This is not in the official Binance api docs, but this is what
        feeds the right column on a ticker page on Binance.

        :param update_time: time between callbacks in milliseconds, must be 1000 or greater
        :type update_time: int

        :returns: connection key string if successful, False otherwise

        Message Format

        .. code-block:: python

            [
                {
                    'e': '24hrMiniTicker',  # Event type
                    'E': 1515906156273,     # Event time
                    's': 'QTUMETH',         # Symbol
                    'c': '0.03836900',      # close
                    'o': '0.03953500',      # open
                    'h': '0.04400000',      # high
                    'l': '0.03756000',      # low
                    'v': '147435.80000000', # volume
                    'q': '5903.84338533'    # quote volume
                }
            ]
        """

        return self._get_socket('!miniTicker@arr@{}ms'.format(update_time))

    def trade_socket(self, symbol):
        """Start a websocket for symbol trade data

        https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#trade-streams

        :param symbol: required
        :type symbol: str

        :returns: connection key string if successful, False otherwise

        Message Format

        .. code-block:: python

            {
                "e": "trade",     # Event type
                "E": 123456789,   # Event time
                "s": "BNBBTC",    # Symbol
                "t": 12345,       # Trade ID
                "p": "0.001",     # Price
                "q": "100",       # Quantity
                "b": 88,          # Buyer order Id
                "a": 50,          # Seller order Id
                "T": 123456785,   # Trade time
                "m": true,        # Is the buyer the market maker?
                "M": true         # Ignore.
            }

        """

        return self._get_socket(symbol.lower() + '@trade')

    def aggtrade_socket(self, symbol):
        """Start a websocket for symbol trade data

        https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#aggregate-trade-streams

        :param symbol: required
        :type symbol: str

        :returns: connection key string if successful, False otherwise

        Message Format

        .. code-block:: python

            {
                "e": "aggTrade",		# event type
                "E": 1499405254326,		# event time
                "s": "ETHBTC",			# symbol
                "a": 70232,				# aggregated tradeid
                "p": "0.10281118",		# price
                "q": "8.15632997",		# quantity
                "f": 77489,				# first breakdown trade id
                "l": 77489,				# last breakdown trade id
                "T": 1499405254324,		# trade time
                "m": false,				# whether buyer is a maker
                "M": true				# can be ignored
            }

        """
        return self._get_socket(symbol.lower() + '@aggTrade')

    def aggtrade_futures_socket(self, symbol):
        """Start a websocket for aggregate symbol trade data for the futures stream

        :param symbol: required
        :type symbol: str

        :returns: connection key string if successful, False otherwise

        Message Format

        .. code-block:: python

            {
                "e": "aggTrade",  // Event type
                "E": 123456789,   // Event time
                "s": "BTCUSDT",    // Symbol
                "a": 5933014,     // Aggregate trade ID
                "p": "0.001",     // Price
                "q": "100",       // Quantity
                "f": 100,         // First trade ID
                "l": 105,         // Last trade ID
                "T": 123456785,   // Trade time
                "m": true,        // Is the buyer the market maker?
            }

        """
        return self._get_futures_socket(symbol.lower() + '@aggTrade')

    def symbol_ticker_socket(self, symbol):
        """Start a websocket for a symbol's ticker data

        https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#individual-symbol-ticker-streams

        :param symbol: required
        :type symbol: str

        :returns: connection key string if successful, False otherwise

        Message Format

        .. code-block:: python

            {
                "e": "24hrTicker",  # Event type
                "E": 123456789,     # Event time
                "s": "BNBBTC",      # Symbol
                "p": "0.0015",      # Price change
                "P": "250.00",      # Price change percent
                "w": "0.0018",      # Weighted average price
                "x": "0.0009",      # Previous day's close price
                "c": "0.0025",      # Current day's close price
                "Q": "10",          # Close trade's quantity
                "b": "0.0024",      # Best bid price
                "B": "10",          # Bid bid quantity
                "a": "0.0026",      # Best ask price
                "A": "100",         # Best ask quantity
                "o": "0.0010",      # Open price
                "h": "0.0025",      # High price
                "l": "0.0010",      # Low price
                "v": "10000",       # Total traded base asset volume
                "q": "18",          # Total traded quote asset volume
                "O": 0,             # Statistics open time
                "C": 86400000,      # Statistics close time
                "F": 0,             # First trade ID
                "L": 18150,         # Last trade Id
                "n": 18151          # Total number of trades
            }

        """
        return self._get_socket(symbol.lower() + '@ticker')

    def ticker_socket(self):
        """Start a websocket for all ticker data

        By default all markets are included in an array.

        https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#all-market-tickers-stream

        :param coro: callback function to handle messages
        :type coro: function

        :returns: connection key string if successful, False otherwise

        Message Format

        .. code-block:: python

            [
                {
                    'F': 278610,
                    'o': '0.07393000',
                    's': 'BCCBTC',
                    'C': 1509622420916,
                    'b': '0.07800800',
                    'l': '0.07160300',
                    'h': '0.08199900',
                    'L': 287722,
                    'P': '6.694',
                    'Q': '0.10000000',
                    'q': '1202.67106335',
                    'p': '0.00494900',
                    'O': 1509536020916,
                    'a': '0.07887800',
                    'n': 9113,
                    'B': '1.00000000',
                    'c': '0.07887900',
                    'x': '0.07399600',
                    'w': '0.07639068',
                    'A': '2.41900000',
                    'v': '15743.68900000'
                }
            ]
        """
        return self._get_socket('!ticker@arr')

    def symbol_mark_price_socket(self, symbol, fast=True):
        """Start a websocket for a symbol's futures mark price
        https://binance-docs.github.io/apidocs/futures/en/#mark-price-stream
        :param symbol: required
        :type symbol: str
        :param fast: use faster or 1s default
        :type fast: bool
        :returns: connection key string if successful, False otherwise
        Message Format
        .. code-block:: python
            {
                "e": "markPriceUpdate",  // Event type
                "E": 1562305380000,      // Event time
                "s": "BTCUSDT",          // Symbol
                "p": "11185.87786614",   // Mark price
                "r": "0.00030000",       // Funding rate
                "T": 1562306400000       // Next funding time
            }
        """
        stream_name = '@markPrice@1s' if fast else '@markPrice'
        return self._get_futures_socket(symbol.lower() + stream_name)

    def all_mark_price_socket(self, fast=True):
        """Start a websocket for all futures mark price data
        By default all symbols are included in an array.
        https://binance-docs.github.io/apidocs/futures/en/#mark-price-stream-for-all-market
        :param fast: use faster or 1s default
        :type fast: bool
        :returns: connection key string if successful, False otherwise
        Message Format
        .. code-block:: python

            [
                {
                    "e": "markPriceUpdate",  // Event type
                    "E": 1562305380000,      // Event time
                    "s": "BTCUSDT",          // Symbol
                    "p": "11185.87786614",   // Mark price
                    "r": "0.00030000",       // Funding rate
                    "T": 1562306400000       // Next funding time
                }
            ]
        """
        stream_name = '!markPrice@arr@1s' if fast else '!markPrice@arr'
        return self._get_futures_socket(stream_name)

    def symbol_ticker_futures_socket(self, symbol):
        """Start a websocket for a symbol's ticker data
        By default all markets are included in an array.
        https://binance-docs.github.io/apidocs/futures/en/#individual-symbol-book-ticker-streams
        :param symbol: required
        :type symbol: str
        :param coro: callback function to handle messages
        :type coro: function
        :returns: connection key string if successful, False otherwise
        .. code-block:: python
            [
                {
                  "u":400900217,     // order book updateId
                  "s":"BNBUSDT",     // symbol
                  "b":"25.35190000", // best bid price
                  "B":"31.21000000", // best bid qty
                  "a":"25.36520000", // best ask price
                  "A":"40.66000000"  // best ask qty
                }
            ]
        """
        return self._get_futures_socket(symbol.lower() + '@bookTicker')

    def individual_symbol_ticker_futures_socket(self, symbol):
        """Start a futures websocket for a single symbol's ticker data
        https://binance-docs.github.io/apidocs/futures/en/#individual-symbol-ticker-streams
        :param symbol: required
        :type symbol: str
        :returns: connection key string if successful, False otherwise
        .. code-block:: python
            {
                "e": "24hrTicker",  // Event type
                "E": 123456789,     // Event time
                "s": "BTCUSDT",     // Symbol
                "p": "0.0015",      // Price change
            }
        """
        return self._get_futures_socket(symbol.lower() + '@ticker')

    def all_ticker_futures_socket(self):
        """Start a websocket for all ticker data
        By default all markets are included in an array.
        https://binance-docs.github.io/apidocs/futures/en/#all-book-tickers-stream
        :returns: connection key string if successful, False otherwise
        Message Format
        .. code-block:: python
            [
                {
                  "u":400900217,     // order book updateId
                  "s":"BNBUSDT",     // symbol
                  "b":"25.35190000", // best bid price
                  "B":"31.21000000", // best bid qty
                  "a":"25.36520000", // best ask price
                  "A":"40.66000000"  // best ask qty
                }
            ]
        """

        return self._get_futures_socket('!bookTicker')

    def symbol_book_ticker_socket(self, symbol):
        """Start a websocket for the best bid or ask's price or quantity for a specified symbol.

        https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#individual-symbol-book-ticker-streams

        :param symbol: required
        :type symbol: str

        :returns: connection key string if successful, False otherwise

        Message Format

        .. code-block:: python

            {
                "u":400900217,     // order book updateId
                "s":"BNBUSDT",     // symbol
                "b":"25.35190000", // best bid price
                "B":"31.21000000", // best bid qty
                "a":"25.36520000", // best ask price
                "A":"40.66000000"  // best ask qty
            }

        """
        return self._get_socket(symbol.lower() + '@bookTicker')

    def book_ticker_socket(self):
        """Start a websocket for the best bid or ask's price or quantity for all symbols.

        https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#all-book-tickers-stream

        :returns: connection key string if successful, False otherwise

        Message Format

        .. code-block:: python

            {
                // Same as <symbol>@bookTicker payload
            }

        """
        return self._get_socket('!bookTicker')

    def multiplex_socket(self, streams):
        """Start a multiplexed socket using a list of socket names.
        User stream sockets can not be included.

        Symbols in socket name must be lowercase i.e bnbbtc@aggTrade, neobtc@ticker

        Combined stream events are wrapped as follows: {"stream":"<streamName>","data":<rawPayload>}

        https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md

        :param streams: list of stream names in lower case
        :type streams: list

        :returns: connection key string if successful, False otherwise

        Message Format - see Binance API docs for all types

        """
        path = 'streams={}'.format('/'.join(streams))
        return self._get_socket(path, prefix='stream?')

    def options_multiplex_socket(self, streams):
        """Start a multiplexed socket using a list of socket names.
        User stream sockets can not be included.

        Symbols in socket name must be lowercase i.e bnbbtc@aggTrade, neobtc@ticker

        Combined stream events are wrapped as follows: {"stream":"<streamName>","data":<rawPayload>}

        https://binance-docs.github.io/apidocs/voptions/en/#account-and-trading-interface

        :param streams: list of stream names in lower case
        :type streams: list

        :returns: connection key string if successful, False otherwise

        Message Format - see Binance API docs for all types

        """
        stream_path = 'streams={}'.format('/'.join([s.lower() for s in streams]))
        return self._get_options_socket(stream_path, prefix='stream?')

    def user_socket(self):
        """Start a websocket for user data

            https://github.com/binance-exchange/binance-official-api-docs/blob/master/user-data-stream.md
            https://binance-docs.github.io/apidocs/spot/en/#listen-key-spot

        :returns: connection key string if successful, False otherwise

        Message Format - see Binance API docs for all types
        """
        return self._get_account_socket('user')

    def margin_socket(self):
        """Start a websocket for cross-margin data

        https://binance-docs.github.io/apidocs/spot/en/#listen-key-margin

        :returns: connection key string if successful, False otherwise

        Message Format - see Binance API docs for all types
        """
        return self._get_account_socket('margin')

    def futures_socket(self):
        """Start a websocket for futures data

            https://binance-docs.github.io/apidocs/futures/en/#websocket-market-streams

        :returns: connection key string if successful, False otherwise

        Message Format - see Binance API docs for all types
        """
        return self._get_account_socket('futures', stream_url=self.FSTREAM_URL)

    def coin_futures_socket(self):
        """Start a websocket for coin futures data

            https://binance-docs.github.io/apidocs/delivery/en/#websocket-market-streams

        :returns: connection key string if successful, False otherwise

        Message Format - see Binance API docs for all types
        """
        return self._get_account_socket('coin_futures', stream_url=self.DSTREAM_URL)

    def isolated_margin_socket(self, symbol):
        """Start a websocket for isolated margin data

        https://binance-docs.github.io/apidocs/spot/en/#listen-key-isolated-margin

        :param symbol: required - symbol for the isolated margin account
        :type symbol: str

        :returns: connection key string if successful, False otherwise

        Message Format - see Binance API docs for all types
        """
        return self._get_account_socket(symbol)

    def options_ticker_socket(self, symbol):
        """Subscribe to a 24 hour ticker info stream

        https://binance-docs.github.io/apidocs/voptions/en/#market-streams-payload-24-hour-ticker

        :param symbol: required
        :type symbol: str
        """
        return self._get_options_socket(symbol.lower() + '@ticker')

    def options_recent_trades_socket(self, symbol):
        """Subscribe to a latest completed trades stream

        https://binance-docs.github.io/apidocs/voptions/en/#market-streams-payload-latest-completed-trades

        :param symbol: required
        :type symbol: str
        """
        return self._get_options_socket(symbol.lower() + '@trade')

    def options_kline_socket(self, symbol, interval=Client.KLINE_INTERVAL_1MINUTE):
        """Subscribe to a candlestick data stream

        https://binance-docs.github.io/apidocs/voptions/en/#market-streams-payload-candle

        :param symbol: required
        :type symbol: str
        :param interval: Kline interval, default KLINE_INTERVAL_1MINUTE
        :type interval: str
        """
        return self._get_options_socket(symbol.lower() + '@kline_' + interval)

    def options_depth_socket(self, symbol, depth='10'):
        """Subscribe to a depth data stream

        https://binance-docs.github.io/apidocs/voptions/en/#market-streams-payload-depth

        :param symbol: required
        :type symbol: str
        :param depth: optional Number of depth entries to return, default 10.
        :type depth: str
        """
        return self._get_options_socket(symbol.lower() + '@depth' + str(depth))

    async def _stop_socket(self, conn_key):
        """Stop a websocket given the connection key

        :param conn_key: Socket connection key
        :type conn_key: string

        :returns: None
        """
        if conn_key not in self._conns:
            return

        del(self._conns[conn_key])
