text: # Written by github.com/KirinFuji
  
  """
  MIT License
  
  Copyright (c) 2021 KirinFuji@users.noreply.github.com
  
  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:
  
  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
  
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  SOFTWARE.
  """
  from os.path import isfile
  import ssl
  import time
  from asyncio import StreamReader, StreamWriter
  from typing import Tuple, Dict
  import asyncio
  import asyncio.trsock
  import _asyncio
  
  
  class Server:
      """Placeholder"""
      def __init__(self, bind_ip='localhost', bind_port=15555):
          self.Clients = {}  # type: Dict[Tuple[str, int], Tuple[StreamReader, StreamWriter]]
          self.bind_ip = bind_ip
          self.bind_port = bind_port
          self.Server = None
          self.Loop = asyncio.get_event_loop()
          self.Server_Task = None
  
      def startup(self):
          """Placeholder"""
          self.Server_Task = self.Loop.create_task(self._run_server(), name='Server')
          self.Loop.set_task_factory(self.task_factory)
          self.Loop.run_forever()
  
      async def _run_server(self):
          """Placeholder"""
          # ssl_context = ssl.create_default_context()
          # ssl_context.load_cert_chain("selfsigned.crt", "private.key")
          self.Server = await asyncio.start_server(self.handle_client, host=self.bind_ip, port=self.bind_port, ssl=None)
          async with self.Server:
              print(f'Listening on {self.bind_ip}:{self.bind_port}')
              await self.Server.serve_forever()
  
      @staticmethod
      async def close_peer(writer: StreamWriter):
          """Placeholder"""
          if writer:
              writer.write_eof()
              await writer.drain()
  
      @staticmethod
      async def send_data(data: bytes, writer):
          """Send data to remote server coroutine"""
          if writer:
              raw_peer = writer.get_extra_info('peername')  # type: Tuple[str, int]
              peer = f'{raw_peer[0]}:{raw_peer[1]}'
              try:
                  content_length = len(data)
                  writer.write(content_length.to_bytes(4, byteorder='big'))
                  writer.write(data)
                  print(f'Sending {content_length} bytes to {peer}:\n Content: {data}')
                  await writer.drain()
                  return True
              except (ConnectionResetError, ConnectionError, ConnectionRefusedError, ConnectionAbortedError) as err:
                  raise ConnectionError(f'Connection with {peer} ERROR: {err}.')
  
      # Logic for handling incoming data
      async def responses(self, content: bytes, writer: StreamWriter, peer):
          if b'Hello!' in content:
              await self.send_data(f'Welcome, {peer}!'.encode('utf8'), writer)
          if b'Test!' in content:
              await self.send_data(f'Test Received, {peer}!'.encode('utf8'), writer)
          if b'Ping!' in content:
              await self.send_data(f'Pong! {time.time()}'.encode('utf8'), writer)
          if b'Disconnect!' in content:
              await self.close_peer(writer)
          if b'File!' in content:
              content_array = content.split(b'::', 4)
              print(f'Filename: {content_array[1]}\n'
                    f'FileSize: {content_array[2]}\n'
                    f'FileHash: {content_array[3]}')
              if not isfile(content_array[1]):
                  await self.send_data('File-ACK!::'.encode('utf8') + content_array[1], writer)
              # if not isfile(content_array[1]):
                  # with open(content_array[1], 'wb') as file:
                  #     file.write(content_array[2])
  
      async def handle_client(self, reader: StreamReader, writer: StreamWriter):
          """Creates 'Receive loop tasks' when a client connects, data is parsed, and responded to accordingly"""
          _task = asyncio.current_task(self.Loop)      # type: _asyncio.Task
          peer = writer.get_extra_info('peername')    # type: Tuple[str, int]
          _sock = writer.get_extra_info('socket')      # type: asyncio.trsock.TransportSocket
          self.Clients[peer] = (reader, writer)       # type: Dict[Tuple[str, int],Tuple[StreamReader, StreamWriter]]
          print(f'Client: {peer} : Connected')
          # writer.write(f'Hello: {peer} \n'.encode('utf8'))
          conversation_count = 0
          try:
              # Start an 'await loop' and await on read
              while True:
  
                  # Get the content_length from remote end
                  try:
                      byte_count = await reader.readexactly(4)
                      if not byte_count:
                          print(f'Invalid byte_count: {peer} closing connection.')
                          break
                  except asyncio.IncompleteReadError:
                      print(f'IncompleteRead: {peer} closing connection.')
                      break
  
                  # Try to convert it to an integer
                  content_length = int.from_bytes(byte_count, byteorder='big')
  
                  # Read the content of the data received
                  try:
                      content = await reader.readexactly(content_length)
                      if not byte_count:
                          print(f'Invalid byte_count: {peer} closing connection.')
                          break
                  except asyncio.IncompleteReadError:
                      print(f'IncompleteRead: {peer} closing connection.')
                      break
  
                  # Handle the content
                  print(f'Received {content_length} bytes from {peer}\n Content: {content}')
                  await self.responses(content, writer, peer)
  
                  conversation_count += 1
                  # await asyncio.sleep(0.1)
  
              # Connection broken, end await loop
  
          except ConnectionResetError as _err:
              print(f'ConnReset: {peer} closing connection.')
              pass
          except asyncio.CancelledError:
              print(f'Coro Canceled: Remote {peer} closing connection.')
  
          finally:
              # Close the socket and remove the peer from the Clients dict
              writer.close()
              await writer.wait_closed()
              print(f'Closed: {peer}')
              self.Clients.pop(peer)
  
      @staticmethod
      def handle_client_end_callback(task: asyncio.Task):
          """Placeholder"""
          print(f'Task: {task.get_coro().__name__} Finished')
  
      def task_factory(self, loop, coro):
          """Placeholder"""
          task = asyncio.Task(coro, loop=loop)
          if task.get_coro().__name__ == 'handle_client':
              task.add_done_callback(self.handle_client_end_callback)
          return task  # type: asyncio.Task
  
  
  if __name__ == '__main__':
      TCPServer = Server()
      TCPServer.startup()
      print('End')
  