# auto generated by update_py.py
import asyncio
import json
import logging
import threading
import time
import traceback

import websockets

DEFAULT_HOSTNAME = 'localhost'
DEFAULT_PORT = 20202
DEFAULT_SERVER_PASSWORD = '69a955cc4b5c11eab35aacde48001122'


class LogBase(object):

    def __init__(self, name, logger):
        self._name = name
        self._logger: logging.Logger = logger

    def _log(self, level, *msg):
        msg = ' '.join(msg)
        if self._logger:
            msg = '[{}] {}'.format(self._name, msg)
            self._logger.log(level, msg)
        else:
            msg = '[{}] [{}] {}'.format(logging.getLevelName(level), self._name, msg)
            print(msg)

    def log_debug(self, *msg):
        self._log(logging.DEBUG, *msg)

    def log_info(self, *msg):
        self._log(logging.INFO, *msg)

    def log_warn(self, *msg):
        self._log(logging.WARNING, *msg)

    def log_error(self, *msg):
        self._log(logging.ERROR, *msg)

    def join(self):
        while True:
            try:
                time.sleep(1)
            except KeyboardInterrupt:
                break


class WebsocketServer(LogBase):

    def __init__(self, name, logger=None, hostname=DEFAULT_HOSTNAME, port=DEFAULT_PORT, password=DEFAULT_SERVER_PASSWORD):
        LogBase.__init__(self, name, logger)
        self._hostname = hostname
        self._port = port
        self._password = password

        self._event_loop = None
        self._protocals = {}

    def _callback(self, client_name, data):
        raise NotImplementedError()

    async def _authenticate(self, protocal):
        while True:
            auth_data = await protocal.recv()
            user, password = auth_data.split(':')
            self.log_info('recv auth data: {}@{}'.format(user, '*' * len(password)))
            if password == self._password:
                await protocal.send("ok!")
                return user
            else:
                return None

    async def _process_incoming_data(self, client_name, protocal):
        while True:
            try:
                data = await protocal.recv()
            except:
                self.log_warn('disconnected (client){}'.format(client_name))
                break
            if data == 'disconnect':
                await protocal.send('disconnected')
                break
            self._callback(client_name, data)

    async def _ws_handler(self, protocal: websockets.WebSocketServerProtocol, path: str):
        client_name = await self._authenticate(protocal)
        if not client_name:
            raise RuntimeError('connection not authenticated!')
        self._protocals[client_name] = protocal
        self.log_info('start processing data from "{}"'.format(client_name))
        await self._process_incoming_data(client_name, protocal)
        await protocal.close()

    def _server_thread(self):
        self._event_loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self._event_loop)

        self.log_info('server started, listening to {}:{}'.format(self._hostname, self._port))
        server = websockets.serve(self._ws_handler, self._hostname, self._port)
        self._event_loop.run_until_complete(server)
        self._event_loop.run_forever()

    def run(self):
        t = threading.Thread(target=self._server_thread)
        t.setDaemon(True)
        t.start()

    async def _send_data(self, data, protocal):
        await protocal.send(data)

    def send_data(self, data, client_name='*'):
        data = json.dumps(data)
        if client_name == '*':
            ps = self._protocals.values()
        else:
            ps = [self._protocals[client_name]]
        for p in ps:
            asyncio.run_coroutine_threadsafe(self._send_data(data, p), self._event_loop)

    def has_client(self, client_name):
        return client_name in self._protocals


class WebsocketClient(LogBase):

    def __init__(self, name, logger=None, hostname=DEFAULT_HOSTNAME, port=DEFAULT_PORT, password=DEFAULT_SERVER_PASSWORD):
        LogBase.__init__(self, name, logger)
        self._conn_str = 'ws://{}:{}'.format(hostname, port)
        self._client_name = name
        self._password = password

        self._conn_thread = None
        self._event_loop = None
        self._protocal: websockets.WebSocketClientProtocol = None
        self._connection_ready = False

    def _callback(self, data):
        raise NotImplementedError()

    async def _authenticate(self, protocal):
        auth_data = '{}:{}'.format(self._client_name, self._password)
        await protocal.send(auth_data)
        result = await protocal.recv()
        return result == 'ok!', result

    async def _process_incoming_data(self, protocal):
        while True:
            data = await protocal.recv()
            if data == 'disconnected':
                protocal.close()
                break
            self._callback(data)

    async def _ws_conn(self):
        while True:
            self.log_info('connecting to', self._conn_str)
            try:
                self._protocal = await websockets.connect(self._conn_str)
                is_ok, msg = await self._authenticate(self._protocal)
                if is_ok:
                    self._connection_ready = True
                    self.log_info('authenticated! (msg){}'.format(msg))
                else:
                    raise RuntimeError(msg)
                await self._process_incoming_data(self._protocal)

            except:
                self.log_error('failed to connect to the server...')
                self.log_error(traceback.format_exc())
                self.log_error('waiting 10 secs to reconnect...')
                await asyncio.sleep(10)
                self.log_error('to reconnect...')

    def _start(self):
        self._event_loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self._event_loop)
        self._event_loop.run_until_complete(self._ws_conn())
        self._event_loop.run_forever()

    def run(self):
        self._conn_thread = threading.Thread(target=self._start)
        self._conn_thread.setDaemon(True)
        self._conn_thread.start()

    async def _send_data(self, data):
        await self._protocal.send(data)

    def send_data(self, data):
        if not isinstance(data, str):
            data = json.dumps(data)
        while not self._connection_ready:
            time.sleep(0.25)
        asyncio.run_coroutine_threadsafe(self._send_data(data), self._event_loop)

    def disconnect(self):
        self.send_data('disconnect')
