from asyncio import CancelledError, IncompleteReadError, LimitOverrunError
from xml.etree.cElementTree import Element, SubElement, tostring

import defusedxml.cElementTree as Et

from houdini.constants import ClientType
from houdini.handlers import AuthorityError, AbortHandlerChain, XMLPacket, XTPacket


class Spheniscidae:

    __slots__ = ['__reader', '__writer', 'server', 'logger',
                 'peer_name', 'received_packets', 'joined_world',
                 'client_type']

    Delimiter = b'\x00'

    def __init__(self, server, reader, writer):
        self.__reader = reader
        self.__writer = writer

        self.server = server
        self.logger = server.logger

        self.peer_name = writer.get_extra_info('peername')
        self.server.peers_by_ip[self.peer_name] = self

        self.joined_world = False
        self.client_type = None

        self.received_packets = set()

        super().__init__()

    @property
    def is_vanilla_client(self):
        return self.client_type == ClientType.Vanilla

    @property
    def is_legacy_client(self):
        return self.client_type == ClientType.Legacy

    async def send_error_and_disconnect(self, error, *args):
        await self.send_xt('e', error, *args)
        await self.close()

    async def send_error(self, error, *args):
        await self.send_xt('e', error, *args)

    async def send_policy_file(self):
        await self.send_line(f'<cross-domain-policy><allow-access-from domain="*" to-ports="'
                             f'{self.server.config.port}" /></cross-domain-policy>')
        await self.close()

    async def send_xt(self, handler_id, *data):
        internal_id = -1

        xt_data = '%'.join(str(d) for d in data)
        line = f'%xt%{handler_id}%{internal_id}%{xt_data}%'
        await self.send_line(line)

    async def send_xml(self, xml_dict):
        data_root = Element('msg')
        data_root.set('t', 'sys')

        sub_element_parent = data_root
        for sub_element, sub_element_attribute in xml_dict.items():
            sub_element_object = SubElement(sub_element_parent, sub_element)

            if type(xml_dict[sub_element]) is dict:
                for sub_element_attribute_key, sub_element_attribute_value in xml_dict[sub_element].items():
                    sub_element_object.set(sub_element_attribute_key, sub_element_attribute_value)
            else:
                sub_element_object.text = xml_dict[sub_element]

            sub_element_parent = sub_element_object

        xml_data = tostring(data_root)
        await self.send_line(xml_data.decode('utf-8'))

    async def send_line(self, data):
        if not self.__writer.is_closing():
            self.logger.debug(f'Outgoing data: {data}')
            self.__writer.write(data.encode('utf-8') + Spheniscidae.Delimiter)

    async def close(self):
        self.__writer.close()

        await self._client_disconnected()

    async def __handle_xt_data(self, data):
        self.logger.debug(f'Received XT data: {data}')
        parsed_data = data.split('%')[1:-1]

        packet_id = parsed_data[2]
        packet = XTPacket(packet_id, ext=parsed_data[1])

        if packet in self.server.xt_listeners:
            xt_listeners = self.server.xt_listeners[packet]
            packet_data = parsed_data[4:]

            for listener in xt_listeners:
                if not self.__writer.is_closing() and listener.client_type is None \
                        or listener.client_type == self.client_type:
                    await listener(self, packet_data)
            self.received_packets.add(packet)
        else:
            self.logger.warn('Handler for %s doesn\'t exist!', packet_id)

    async def __handle_xml_data(self, data):
        self.logger.debug(f'Received XML data: {data}')

        element_tree = Et.fromstring(data)

        if element_tree.tag == 'policy-file-request':
            await self.send_policy_file()

        elif element_tree.tag == 'msg':
            self.logger.debug('Received valid XML data')

            try:
                body_tag = element_tree[0]
                action = body_tag.get('action')
                packet = XMLPacket(action)

                if packet in self.server.xml_listeners:
                    xml_listeners = self.server.xml_listeners[packet]

                    for listener in xml_listeners:
                        if not self.__writer.is_closing() and listener.client_type is None \
                                or listener.client_type == self.client_type:
                            await listener(self, body_tag)

                    self.received_packets.add(packet)
                else:
                    self.logger.warn('Packet did not contain a valid action attribute!')

            except IndexError:
                self.logger.warn('Received invalid XML data (didn\'t contain a body tag)')
        else:
            self.logger.warn('Received invalid XML data!')

    async def _client_connected(self):
        self.logger.info(f'Client {self.peer_name} connected')

        await self.server.dummy_event_listeners.fire('connected', self)

    async def _client_disconnected(self):
        if self.peer_name in self.server.peers_by_ip:
            del self.server.peers_by_ip[self.peer_name]
            self.logger.info(f'Client {self.peer_name} disconnected')

            await self.server.dummy_event_listeners.fire('disconnected', self)

    async def __data_received(self, data):
        data = data.decode()[:-1]
        try:
            if data.startswith('<'):
                await self.__handle_xml_data(data)
            else:
                await self.__handle_xt_data(data)
        except AuthorityError:
            self.logger.debug(f'{self} tried to send game packet before authentication')
        except AbortHandlerChain as e:
            self.logger.info(f'Handler chain aborted: {str(e)}')

    async def run(self):
        await self._client_connected()
        while not self.__writer.is_closing():
            try:
                data = await self.__reader.readuntil(
                    separator=Spheniscidae.Delimiter)
                if data:
                    await self.__data_received(data)
                else:
                    self.__writer.close()
                await self.__writer.drain()
            except IncompleteReadError:
                self.__writer.close()
            except CancelledError:
                self.__writer.close()
            except ConnectionResetError:
                self.__writer.close()
            except LimitOverrunError:
                self.__writer.close()
            except BaseException as e:
                self.logger.exception(e.__traceback__)

        await self._client_disconnected()

    def __repr__(self):
        return f'<Spheniscidae {self.peer_name}>'