#######################################################
# 
# FederationClientService.py
# Python implementation of the Class FederationClientService
# Generated by Enterprise Architect
# Created on:      29-Dec-2020 8:10:38 AM
# Original author: natha
# 
#######################################################
from FreeTAKServer.controllers.configuration.MainConfig import MainConfig
from FreeTAKServer.controllers.configuration.types import Types
from FreeTAKServer.controllers.services.federation.handlers import StopHandler, DisconnectHandler, ConnectHandler, SendDataHandler, SendConnectionDataHandler, SendDisconnectionDataHandler
from FreeTAKServer.model.protobufModel.fig_pb2 import FederatedEvent
from FreeTAKServer.controllers.services.service_abstracts import ServerServiceInterface, ServiceBase
from multiprocessing import Pipe as multiprocessingPipe
from FreeTAKServer.model.federate import Federate
import selectors
import socket
from typing import Tuple
import ssl
import codecs
from FreeTAKServer.controllers.serializers.protobuf_serializer import ProtobufSerializer
from FreeTAKServer.controllers.serializers.xml_serializer import XmlSerializer
from FreeTAKServer.controllers.XMLCoTController import XMLCoTController
from FreeTAKServer.model.SpecificCoT.SendOther import SendOther
from FreeTAKServer.model.FTSModel.Event import Event
from FreeTAKServer.model.ClientInformation import ClientInformation
from FreeTAKServer.model.SpecificCoT.SendDisconnect import SendDisconnect
from FreeTAKServer.controllers.DatabaseControllers.DatabaseController import DatabaseController
from FreeTAKServer.controllers.CreateLoggerController import CreateLoggerController
logger = CreateLoggerController("FederationClient").getLogger()

class FederationClientServiceController(ServerServiceInterface, ServiceBase):
#class FederationClientServiceController:
    """A service which controllers the connection too and transfer of data with
    federated servers.
    """

    def __init__(self):
        self._define_responsibility_chain()
        self.pipe = None
        self.federates: {str: Federate} = {}
        self.sel = selectors.DefaultSelector()

    def _send_connected_clients(self, connection):
        clients = self.db.query_user()
        for client in clients:
            try:
                proto_obj = FederatedEvent()
                proto_obj.contact.uid = str(client.uid)
                proto_obj.contact.callsign = str(client.CoT.detail.contact.callsign)
                proto_obj.contact.operation = 1
                proto_str = proto_obj.SerializeToString()
                header = self._generate_header(len(proto_str))
                connection.send(header + proto_str)
            except Exception as e:
                logger.warning("error thrown sending federate data to newly connected federate " + str(e))
                continue

    def _create_context(self):
        self.context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
        self.context.load_cert_chain(MainConfig.federationCert, MainConfig.federationKey,
                                     password=MainConfig.federationKeyPassword)
        self.context.set_ciphers('DEFAULT@SECLEVEL=1')

    def _define_responsibility_chain(self):
        self.m_StopHandler = StopHandler()

        self.m_ConnectHandler = ConnectHandler()
        self.m_ConnectHandler.setNextHandler(self.m_StopHandler)

        self.m_DisconnectHandler = DisconnectHandler()
        self.m_DisconnectHandler.setNextHandler(self.m_ConnectHandler)

        self.m_SendDataHandler = SendDataHandler()
        self.m_SendDataHandler.setNextHandler(self.m_DisconnectHandler)

        self.m_SendDisconnectionHandler = SendDisconnectionDataHandler()
        self.m_SendDisconnectionHandler.setNextHandler(self.m_SendDataHandler)

        # first handler in chain of responsibility and should be called first
        self.m_SendConnectionHandler = SendConnectionDataHandler()
        self.m_SendConnectionHandler.setNextHandler(self.m_SendDisconnectionHandler)

    def main(self):
        from lxml import etree
        import time
        while True:
            time.sleep(0.1)
            command = self.receive_command_data(self.pipe)
            if command:
                try:
                    self.m_SendConnectionHandler.Handle(self, command)
                except Exception as e:
                    logger.debug("exception in command chain "+str(e))
            else:
                pass

            data = self.receive_data_from_federate(1)
            if data:
                for protobuf_object in data:
                    # TODO: clean all of this up as it's just a PoC

                    try:
                        detail = etree.fromstring(protobuf_object.event.other)
                        protobuf_object.event.other = ''
                        fts_obj = ProtobufSerializer().from_format_to_fts_object(protobuf_object, Event.FederatedCoT())
                        specific_obj = SendOther()
                        event = XmlSerializer().from_fts_object_to_format(fts_obj)
                        xmlstring = event
                        xmlstring.find('detail').remove(xmlstring.find('detail').find('remarks'))
                        xmlstring.find('detail').extend([child for child in detail])
                        # specific_obj.xmlString = etree.tostring(xmlstring)
                        print(etree.tostring(xmlstring))
                        specific_obj.xmlString = etree.tostring(xmlstring)
                        self.pipe.send(specific_obj)
                    except Exception as e:
                        pass
                    """if isinstance(SpecificCoTObj, SendOtherController):
                        detail = protobuf_object.event.other
                        protobuf_object.event.other = ''
                        fts_obj = ProtobufSerializer().from_format_to_fts_object(protobuf_object, Event.Other())
                        protobuf_object.event.other = detail
                        SpecificCoTObj.object = fts_obj
                        SpecificCoTObj.Object =
                    else:
                        fts_obj = ProtobufSerializer().from_format_to_fts_object(protobuf_object, SpecificCoTObj().object)
                        self.pipe.send(data)"""
            else:
                pass

    def connect_to_server(self, server_vars: Tuple[str, str]) -> None:
        try:
            federate_db_obj = self.db.query_Federation(f'id == "{server_vars[0]}"')[0]
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
            ssock = self.context.wrap_socket(sock, server_hostname=federate_db_obj.address)
            ssock.settimeout(10)
            ssock.connect((str(federate_db_obj.address), int(federate_db_obj.port)))
            ssock.setblocking(False)
            federate = Federate()
            federate.uid = server_vars[0]
            federate.addr = federate_db_obj.address
            federate.conn = ssock
            federate.name = federate_db_obj.name
            events = selectors.EVENT_READ | selectors.EVENT_WRITE
            self.sel.register(ssock, events, federate)
            self.federates[server_vars[0]] = federate
            self._send_connected_clients(ssock)
            self.db.create_ActiveFederation(id = federate_db_obj.id, address = federate_db_obj.address,
                                            port = federate_db_obj.port, initiator = "Self")
            self.db.update_Federation({"lastError": None}, query=f'id == "{federate_db_obj.id}"')
            return None
        except Exception as e:
            try:
                self.db.remove_ActiveFederation(f'id == "{server_vars[0]}"')
            except Exception as e:
                logger.warning("exception thrown removing outgoing federation from DB "+str(e))
            logger.warning("exception thrown creating new federation "+str(e))
            try:
                self.db.update_Federation({"status": "Disabled", "lastError": str(e)}, query=f'id == "{server_vars[0]}"')
            except Exception as e:
                logger.warning("exception thrown updating federate in db "+str(e))
    def disconnect_client(self, id: str) -> None:
        try:
            try:
                federate = self.federates[id]
            except Exception as e:
                logger.warning("federate array has no item with uid "+str(id)+" federates array is len " + str(len(self.federates)))
                return None
            try:
                federate.conn.close()
                self.sel.unregister(federate.conn)
                del(self.federates[federate.uid])
            except Exception as e:
                logger.warning("exception thrown disconnecting client "+str(e))
            try:
                self.db.remove_ActiveFederation(f'id == "{federate.uid}"')
            except Exception as e:
                logger.warning("exception thrown removing outgoing federation from DB "+str(e))
            return None
        except Exception as e:
            logger.warning("exception thrown accessing client for disconnecting client " + str(e))

    def receive_data_from_federate(self, timeout):
        """called whenever data is available from any federate and immediately proceeds to
        send data through process pipe
        """
        dataarray = []
        if self.federates:
            events = self.sel.select(timeout)
            for key, mask in events:
                conn = key.fileobj
                try:
                    header = conn.recv(4)
                except Exception as e:
                    continue
                if header:
                    try:
                        buffer = self._get_header_length(header)
                        raw_protobuf_message = conn.recv(buffer)
                        print(raw_protobuf_message)
                        protobuf_object = FederatedEvent()
                        protobuf_object.ParseFromString(raw_protobuf_message)
                        dataarray.append(protobuf_object)
                    except Exception as e:
                        conn.recv(10000)
                        continue
                else:
                    self.disconnect_client(key.data.uid)
            return dataarray
        else:
            return None

    def _get_header_length(self, header):
        return int.from_bytes(header, 'big')

    def _generate_header(self, contentlength):
        return contentlength.to_bytes(4, byteorder="big")

    def send_data_to_clients(self, data):
        from lxml import etree
        try:
            if self.federates:
                xmlstring = data.xmlString
                detail = etree.fromstring(xmlstring).find('detail')
                if detail:
                    protobuf = ProtobufSerializer().from_fts_object_to_format(data.modelObject)
                    try:
                        protobuf.event.other = etree.tostring(detail)
                        protobufstring = protobuf.SerializeToString()
                        header = self._generate_header(len(protobufstring))
                        protobufstring = header + protobufstring
                        print(protobufstring)
                    except Exception as e:
                        logger.warning("creating protobuf message failed "+str(e))
                        return None
                    for client in self.federates.values():
                        client.conn.send(protobufstring)
                else:
                    return None
            else:
                return None
        except Exception as e:
            import traceback
            trace = traceback.format_exc()
            logger.warning("sending data to federates failed "+str(e))

    def send_connection_data(self, CoT: ClientInformation) -> None:
        try:
            if self.federates:
                logger.debug("connection data received in send_connection_data")
                proto_obj = FederatedEvent()
                proto_obj.contact.uid = str(CoT.modelObject.uid)
                proto_obj.contact.callsign = str(CoT.modelObject.detail.contact.callsign)
                proto_obj.contact.operation = 1
                proto_str = proto_obj.SerializeToString()
                header = self._generate_header(len(proto_str))
                for fed in self.federates.values():
                    fed.conn.send(header + proto_str)
                return None

            else:
                return None

        except Exception as e:
            logger.warning("exception throw sending new connection data to federates " + str(e))
            return None

    def send_disconnection_data(self, CoT: SendDisconnect):
        if self.federates:
            proto_obj = FederatedEvent()
            proto_obj.contact.uid = str(CoT.modelObject.detail.link.uid)
            proto_obj.contact.callsign = str(CoT.modelObject.detail.link.type)
            proto_obj.contact.operation = 4
            proto_str = proto_obj.SerializeToString()
            header = self._generate_header(len(proto_str))
            for fed in self.federates.values():
                fed.conn.send(header + proto_str)
        else:
            return None

    def start(self, pipe):
        self.db = DatabaseController()
        self.pipe = pipe
        self._create_context()
        print('started federation federate service')
        self.main()

    def stop(self):
        pass

if __name__ == "__main__":
    #FederationClientServiceController()._get_header_length(b'\x00\x00\x03>')
    """from FreeTAKServer.controllers.SpecificCoTControllers.SendOtherController import SendOtherController
    from FreeTAKServer.model.RawCoT import RawCoT
    a = FederationClientService()._get_header_length(b'\x00\x00\x02\x05')
    pipe1, pipe2 = multiprocessingPipe(True)
    pipe2.send(('3.96.240.142', 9000, 'testconn'))
    cot = RawCoT()
    cot.xmlString = '<event version="2.0" uid="ANDROID-R5CN70EYKQH" type="a-f-G-U-C" how="h-e" start="2020-12-24T18:16:22.325Z" time="2020-12-24T18:16:22.325Z" stale="2020-12-24T18:22:37.325Z"><detail><__group name="Teal" role="Team Member"/><status battery="76"/><takv version="4.2.0.4 (47e136dd).1607456856-CIV" platform="ATAK-CIV" device="SAMSUNG SM-N986U" os="29"/><track course="159.1462509079387" speed="0.0"/><contact callsign="SPAC3SLOTH" endpoint="*:-1:stcp" /><uid Droid="SPAC3SLOTH"/><precisionlocation altsrc="GPS" geopointsrc="GPS"/></detail><point le="9999999.0" ce="11.0" hae="178.84407323983876" lon="-76.675505" lat="39.664392"/></event>'
    obj = SendOtherController(RawCoT=cot)
    pipe2.send(obj.getObject())
    FederationClientService().start(pipe1)
    while True:
        data = pipe2.recv()
        print(data.xmlString)"""
