import asyncio
import socketio

# hack into python's low level event loop to inherit its asynchronous black magic abilities.
loop = asyncio.get_event_loop()


class SocketCommunicationManager:
    # this manager allows communication for a 3rd part IDE with either the unity editor or the CoderZ website.

    def __init__(self, configuration):
        # save a copy of the configuration for later use.
        self.__configuration = configuration

        # asyncio black magic variables to be able to work asynchronously while
        # the end-user gets a synchronous experience.
        self.__event = asyncio.Event()
        self.__event.set()
        self.__socket_response = None

        # create a socket connection to the robot.
        self.__sio = socketio.AsyncClient()
        loop.run_until_complete(
            self.__sio.connect(configuration["server_url"]))

        # if a token authentication is required, authenticate.
        if configuration["use_authentication_token"]:
            loop.run_until_complete(self.__authenticate_with_token())

        # a function that is triggered whenever there is a response from the robot.
        @self.__sio.on(self.__configuration["socket_on_route"])
        def on_message(data):
            # when receiving a message, get the response
            # data and set the event to release the waiting on the request sending function.
            self.__socket_response = data
            self.__event.set()

    # general request function to communicate with the robot.
    def send_request(self, request_object, should_wait_for_answer):
        # if the method returns value:
        if should_wait_for_answer is not None:
            res = loop.run_until_complete(
                self.__send_request_and_wait_for_response(request_object))
            return res["result"]
        # if it doesn't return a value:
        else:
            loop.run_until_complete(self.__send_command(request_object))
            return None

    async def __authenticate_with_token(self):
        # validate the socket io connection to the server using a token.

        # ask for a token string from the user.
        token = input(
            "Please insert the token generated by the CoderZ website: ")

        # an event object for async usage
        event = asyncio.Event()

        # prepare a catch function for a response from the server regarding the status of the authentication.
        @self.__sio.on('token validation')
        def on_message(data):
            if data["status"]:
                event.set()
            else:
                raise Exception('token authentication failed')

        # send an authentication request.
        await self.__sio.emit('authenticate IDE', data={"token": token})
        # wait for a response.
        await event.wait()

    async def wait_for_game_start_message(self):
        # wait until the game start before running any code

        # an event object for async usage
        event = asyncio.Event()

        # prepare a catch function for a game ready message from the server.
        @self.__sio.on('run code IDE')
        def on_message():
            event.set()

        # wait for a response.
        await event.wait()

    # send a request for data from the robot.
    async def __send_request_and_wait_for_response(self, request_object):
        # clear the event variable to open up for later responses.
        self.__event.clear()

        # emit the request and wait for a response(A.K.A a self.__event.set() somewhere in the code)
        await self.__sio.emit(self.__configuration["socket_emit_route"], data=request_object)
        await self.__event.wait()

        # return the response.
        return self.__socket_response

    # send a command to the robot which isn't supposed to return data
    async def __send_command(self, request_object):
        # emit the command.
        await self.__sio.emit(self.__configuration["socket_emit_route"], data=request_object)
        # for some weird reason, we need to wait an arbitrarily
        # small amount of time in order for the emit to work.
        # TODO: figure out why...
        await asyncio.sleep(0.000001)

    def get_configurations(self):
        return self.__configuration
