"""
Implements the communication method between the user
and where this application is running.

The user can communicate through the communication pipe
using a GUI, a command line, or a TCL script.
The communication pipe then uses ethernet, I2C, or Uart
to communicate with the rest of the program.

A logger is used to handle all communications going from the
executor to the commander. Since the executor isn't strictly
under the control of the user, using a logger makes it widely
available to any python module so that any module can easily
communicate with the communication pipe.

A 'communication pipe' is used to handle all communications
from the commander to the executor. This could be sending
raw text through the uart, sending text through an ehternet
frame, or even text through i2c.

| author     = "Seth Miers"
| copyright  = "Copyright, Xilinx Inc. All rights reserved."
| credits    = ["Seth Miers"]
| license    = "GPL"
| version    = "3.0"
| maintainer = "Seth Miers"
| email      = "sethm@xilinx.com"
| status     = "Development"


"""

##########################################
#               Imports                  #
##########################################

# --------------Packages---------------- #
# import socket
import threading
import logging
# import pickle
# import base64
import queue
# import time

# --------------Local------------------- #
import commandhandler
# from ethernetcomm import EncodeDecode


##########################################
#              Main Code                 #
##########################################

class QueueLoggingHandler(logging.StreamHandler):
    '''
    A logging handler for the logging library that will take all logs
    and format them as ('log', record)  and then put them in a queue

    Note:
        The objective of this class is so that all logging records
        that are recorded by the executor will be sent across the
        communication pipe and interpreted by the commander. This
        is done using a queue.

    Args:
        post_queue (queue): The queue that the logging records
        will be posted to

    Attributes:
        key (int): The current key that the logger is reporting to.
    '''
    def __init__(self, post_queue):
        logging.StreamHandler.__init__(self)
        self.post_queue = post_queue
        self.key = 0

    def set_key(self, key):
        '''
        Everytime the logger is called, it needs to be sent
        somewhere. The key helps determine what the logger
        is responding to

        Args:
            key (int): The new key that the logger should
                report to.
        '''
        self.key = key

    def emit(self, record):
        '''
        Every time a logger is called, the emit method is also
        called. This is where the record is put in the queue.

        Attributes:
            data (tuple): newly formatted record to indicate
                that it is a log type.
        '''
        if isinstance(record, tuple) and (len(record) == 3):
            data = (self.key,) + record
        else:
            data = (self.key, 'log', record)
        self.post_queue.put(data)


class CommunicationPipe:
    '''
    The communication pipe serves as the mediator between
    the user and the functional code. The user will interact
    with the 'commander', the communication pipe will then
    send this information to the 'executor'. Multiple
    'commanders' can exist per board, but only one
    'executor' can exist per board. If running completely
    locally on the board, then the python application will
    act as both the 'commander' and the 'executor'. The
    communication pipe is the heart of the program which
    provides the life blood to everythign else.

    Note:
        All subclasses handle populating the receive
        queues and emptying the send queues.

    Args:
        mode (string): accepts 3 strings 'commander',
            'executor' or 'both', which identify to the program
            what type of behavior it should have
        logger_level (int): the default logger level

    Attributes:
        stop (bool): When stop is set to true all
            loops will break, threads will be closed, and
            the communication pipe will be shutdown. This should
            never be written to directly, use the
            stop_communication_pipe method to change this value.
        commander_send_queue (queue): What the commander
            should send to the executor
        commander_receive_queue (queue): What the comannder
            has received from the executor
        executor_send_queue (queue): What the executor
            should send to the commander
        executor_receive_queue (queue): What the executor
            has received from the commander
        logger (logging): The main logger for the program
        executor_logger: The executor logger

    Todo:
        * See if there is a good way to reduce the number of
          attributes
        * Populate the start_communication_pipe module
          to send and receive commands to a Test() class
    '''
    # pylint: disable=too-many-instance-attributes
    # Nine is reasonable here for each of the queues
    # that are needed
    def __init__(self, mode, logger_level):
        if mode not in ('commander', 'executor', 'both'):
            raise ValueError("mode must be 'commander', \
                              'executor', or 'both'")
        self.mode = mode
        self.stop = False

        self.commander_send_queue = queue.Queue()
        self.commander_receive_queue = queue.Queue()
        self.executor_send_queue = queue.Queue()
        self.executor_receive_queue = queue.Queue()

        self.logger_level = logger_level
        self.logger = None
        self.executor_logger = None
        self.setup_logger()

    def setup_logger(self):
        '''
        Used to set up the loggers, both for the local
        program and for the remote program. Bot the executor
        and the commander will have loggers, so this function
        is always needed.
        '''
        self.logger = logging.getLogger("BoardFramework.Commander")
        self.logger.setLevel(self.logger_level)
        self.logger.addHandler(logging.StreamHandler())

        if (self.mode == 'executor') or (self.mode == 'both'):
            self.executor_logger = logging.getLogger('BoardFramework.Executor')
            self.executor_logger.setLevel(self.logger_level)
            handler = QueueLoggingHandler(self.executor_send_queue)
            self.executor_logger.addHandler(handler)

    def start_communication_pipe(self):
        '''
        Start up all the necessary threads in order to populate
        the send and receive queues. The data from these
        queues is then used to create tests.

        Attributes:
            command_handler (CommandHandler): the command
                handler will processes each command
            command_handler_thread (thread): Will process
                each command and determine if it needs
                to be processed instantaniously or if it
                needs to be put in the tests queue.
            key (int): The key (identifier) of the test
                that is currently running
        '''
        command_handler = \
            commandhandler.CommandHandler(self.commander_send_queue,
                                          self.commander_receive_queue,
                                          self.executor_send_queue,
                                          self.executor_receive_queue,
                                          self.logger,
                                          self.executor_logger)
        # Run this in a constant loop thread so that received
        # commands on the executor are always being processed
        command_handler_executor_thread = \
            threading.Thread(target=command_handler.determine_executor,
                             name='command_executor_handler')
        command_handler_executor_thread.setDaemon(True)
        command_handler_executor_thread.start()
        # Run this in a constant loop thread so that received
        # commands on the commander are always being processed
        command_handler_commander_thread = \
            threading.Thread(target=command_handler.determine_commander,
                             name='command_commander_handler')
        command_handler_commander_thread.setDaemon(True)
        command_handler_commander_thread.start()
        while (not self.stop) and (command_handler_executor_thread.isAlive()):
            # Parse out a single received test and find the key
            key = command_handler.next_test()
            # Set the key of the logger so that all
            # logs will be sent to the commander with
            # the given key
            for handler in self.executor_logger.handlers:
                if isinstance(handler, QueueLoggingHandler):
                    handler.set_key(key)
            # Run the command, usually a test
            # All logs will be sent to the key
            command_handler.run_test(key)
            # Set the key back to 0 so that all logs that
            # come after this are not associated with
            # any command.
            for handler in self.executor_logger.handlers:
                if isinstance(handler, QueueLoggingHandler):
                    handler.set_key(0)
        if command_handler_executor_thread.isAlive():
            # Send a command to the executor to tell it to terminate
            # the command_handler, we are stopping.
            self.executor_receive_queue.put((0, 'terminate', ''))
        command_handler_executor_thread.join()

    def stop_communication_pipe(self):
        '''
        When the program is shutting down, this is called
        to safely close out of all threads and while loops.
        '''
        self.stop = True


# class Ethernet(CommunicationPipe):
#     '''
#     The ethernet communication pipe is used when talking locally
#     to a board (the board is the test host) or when talking
#     over a network to a board (the computer is the test host
#     and the board is running the executor).
#
#     Args:
#         tcp_port (int): The port (socket) that all
#             commander<->executor commands will communicate through
#         udp_port (int): The port that will be used for
#             IP discovery
#         number_of_connections (int): The maximum number of connections
#             that a single executor is allowed to serve
#
#     Attributes:
#         receive_size (int): The maximum packet size that a socket
#             can receive
#         executor_connections (list): a list of all the connections
#             that are currently active to the executor
#
#     _Todo:
#         * See if there is a good way to reduce the number of
#           attributes
#         * Should receive_size be an argument?
#     '''
#     EXECUTOR_RESPONSE = 'pfg_ip_response_serv'
#     COMMANDER_MESSAGE = 'pfg_ip_broadcast_cl'
#
#     def __init__(self, mode, logger_level,
#                  tcp_port, udp_port, number_of_connections):
#         # pylint: disable=too-many-arguments
#         # Six is a reasonable ammount considering this is the
#         # heart of the program
#         CommunicationPipe.__init__(self, mode, logger_level)
#         self.tcp_port = tcp_port
#         self.udp_port = udp_port
#         self.number_of_connections = number_of_connections
#         self.receive_size = 8192
#         self.executor_connections = []
#         self.client_thread = None
#         self.server_thread = None
#
#     def start_communication_pipe(self):
#         '''
#         Start up all the necessary threads in order to populate
#         the send and receive queues. The data from these
#         queues is then used to create tests.
#         '''
#         # Exeggutor!
#         if (self.mode == 'executor') or (self.mode == 'both'):
#             self.client_thread = threading.Thread(target=self.executor,
#                                                   name='executor')
#             self.client_thread.setDaemon(True)
#             self.client_thread.start()
#
#         # Give us your command. My life for Aiur!
#         if (self.mode == 'commander') or (self.mode == 'both'):
#             self.server_thread = threading.Thread(target=self.commander,
#                                                   name='commander')
#             self.server_thread.setDaemon(True)
#             self.server_thread.start()
#         super().start_communication_pipe()
#
#     def stop_communication_pipe(self):
#         '''
#         When the program is shutting down, this is called
#         to safely close out of all threads and while loops.
#         '''
#         super().stop_communication_pipe()
#         self.client_thread.join()
#         self.server_thread.join()
#
#     def commander(self):
#         '''
#         This method is run as a separate thread.
#         The commander is what the user has the most control over.
#         The user could even access the commander directly through a
#         command line. This method searches for executors by using a
#         UDP broadcast. Once an executor is found, the commander
#         establishes a connection using TCP. The program will
#         then loop over sending and receiving data from the
#         executor and putting that data into the apropriate location.
#
#         Attributes:
#             sock (socket): the socket we are currently connecting to.
#                 At first the sock will be the UDP connection, once a
#                 executor is found then the sock will be replaced with
#                 a TCP connection
#             server_address (tuple): The subnet range and the port
#                 to look for executors on.
#             message (string): The message to send to the executor
#                 to confirm that this is a commander
#             server (tuple): Stores the IP address of the executor
#         '''
#         # In case the commander gets disconnected we want to
#         # run this in a while loop
#         while not self.stop:
#             # Create the broadcast socket
#             sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
#             sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
#             sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
#             sock.settimeout(1)
#
#             # Assign the broadcast range and port
#             server_address = ('255.255.255.255', self.udp_port)
#
#             # Send out broadcasts until we receive a response
#             while not self.stop:
#                 try:
#                     sock.sendto(self.COMMANDER_MESSAGE.encode(),
#                                 server_address)
#                     data, server = sock.recvfrom(self.receive_size)
#                     if data.decode('UTF_8') == self.EXECUTOR_RESPONSE:
#                         break
#                 except socket.timeout:
#                     self.logger.debug("Timed out looking for server, \
#                                        trying again...")
#             # We know what IP we are connecting to so the socket
#             # is no longer needed
#             sock.close()
#
#             # Create a new socket for a TCP connection
#             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#             sock.connect((server[0], self.tcp_port))
#             sock.settimeout(0.1)
#             # Add the conneciton to a list
#             connections = [(sock, server[0])]
#
#             # Send and receive commands for the rest of the program
#             while not self.stop:
#                 self.send_all_data(self.commander_send_queue,
#                                    connections,
#                                    self.logger)
#                 self.receive_all_data(self.commander_receive_queue,
#                                       connections,
#                                       self.logger)
#                 if not connections:
#                     # Something unexpected happened, time to start over
#                     break
#
#             if connections:
#                 for (conn, _) in connections:
#                     conn.close()
#
#     def executor(self):
#         '''
#         This method is run as a separate thread.
#         The executor handles everything that the program will
#         eventually run. It 'executes' the commands. It starts off
#         by setting up a thread so that it can be discovered,
#         then enters a continious TCP accept mode so that we can handle
#         up to number_of_connections clients at once.
#
#         Attributes:
#             sock (socket): The socket that will be running for
#                 TCP connections.
#             executor_server (threading): The thread that will handle
#                 the UDP broadcasts so that any commander can discover
#                 the executor.
#             executor_client (threading): The thread that will handle
#             all the commanders (clients) connected to the executor.
#         '''
#         # Setup the thread that listens for UDP broadcasts for discovery
#         executor_server = threading.Thread(target=self.executor_udp,
#                                            name='executor_udp')
#         executor_server.setDaemon(True)
#         executor_server.start()
#
#         # Setup the socket to listen for TCP connections
#         sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#         sock.bind(('', self.tcp_port))
#         sock.listen(self.number_of_connections)
#         sock.settimeout(0.1)
#
#         # Setup the thread that handles each of the TCP connections
#         executor_client = threading.Thread(target=self.executor_tcp,
#                                            name='executor_tcp')
#         executor_client.setDaemon(True)
#         executor_client.start()
#
#         # Continously accept new TCP connections and add them
#         # to the connections list
#         while not self.stop:
#             try:
#                 conn, addr = sock.accept()
#                 self.executor_connections.append((conn, addr))
#                 conn.settimeout(0.1)
#             except socket.timeout:
#                 self.executor_logger.debug("Timed out while waiting \
#                                             for a new connection")
#             except socket.error:
#                 self.executor_logger.debug("Timed out while waiting \
#                                             for a new connection")
#
#         # Program is over, so join all the threads and close out
#         executor_client.join()
#
#         sock.close()
#
#         executor_server.join()
#
#     def executor_udp(self):
#         '''
#         This method is run as a separate thread.
#         When the commander sends a UDP broadcast, the executor
#         needs to be listening in order to respond so that the
#         commander can discover the executor IP adddress. Here
#         the executor will continiously loop receiving and replying
#         to packets that contain a certain message and response.
#
#         Attributes:
#             sock (socket): The socket that will be running for
#                 the UDP connection.
#             resposne (string): The particular response to send
#                 back to the commander so that it knows we are an
#                 executor.
#
#         '''
#         # Create a socket to listen for UDP broadcasts
#         sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
#         server_address = ('', self.udp_port)
#         sock.bind(server_address)
#         sock.settimeout(1)
#
#         # Loop for the entire length of the program
#         # Reply to broadcasts when needed
#         while not self.stop:
#             try:
#                 data, address = sock.recvfrom(4096)
#                 data = str(data.decode('UTF-8'))
#                 if data == self.COMMANDER_MESSAGE:
#                     sock.sendto(self.EXECUTOR_RESPONSE.encode(), address)
#             except socket.timeout:
#                 pass
#         # The program has finished, we no longer need the socket
#         sock.close()
#
#     def executor_tcp(self):
#         '''
#         This method is run as a separate thread.
#         All TCP connections will receive the same
#         response, therefore they are all handled in the same
#         thread. However, we will search for commands from
#         all of them. It is up to the commander to make sure
#         it does not interfere with another commander. This
#         could pose security risks. The executor_tcp prioritieses
#         the data it needs to send out over the commands it may
#         be receiving.
#         '''
#         # Loop sending and receiving
#         while not self.stop:
#             self.send_all_data(self.executor_send_queue,
#                                self.executor_connections,
#                                self.executor_logger)
#             self.receive_all_data(self.executor_receive_queue,
#                                   self.executor_connections,
#                                   self.executor_logger)
#         # Close out all of the connections
#         # The program is over so we no longer need them
#         for (conn, _) in self.executor_connections:
#             conn.close()
#
#     def send_all_data(self, send_queue, connections, logger):
#         '''
#         Takes all the data from a sender queue and sends it over
#         specified connections (1 or more). Sends all the data
#         in the queue until the queue is empty or a stop has
#         been set to True.
#
#         Attributes:
#             encode_decode (EncodeDecode): Used to easily convert
#                 betwen data and raw when communicating between
#                 commander and executor.
#         '''
#         if connections:
#             encode_decode = EncodeDecode(self.receive_size)
#             # Loop while there is still stuff to send
#             while (not send_queue.empty()) and (not self.stop):
#                 try:
#                     encode_decode.data = send_queue.get(False)
#                     for (conn, _) in connections:
#                         conn.sendall(encode_decode.raw)
#                 except queue.Empty:
#                     logger.debug("Nothing to send in send_all_data")
#         else:
#             logger.debug("No connections to send to")
#
#     def receive_all_data(self, receive_queue, connections, logger):
#         '''
#         Receives all the data from the specified connections
#         (1 or more). Stores all the data into the specified
#         queue. If a connection results in a ConnectionResetError
#         then the connection is closed and removed from the
#         list of connections.
#
#         Attributes:
#             encode_decode (EncodeDecode): Used to easily convert
#                 betwen data and raw when communicating between
#                 commander and executor.
#             raws (string): The raw data received from one or
#                 more TCP packets
#         '''
#         if connections:
#             encode_decode = EncodeDecode(self.receive_size)
#             for (conn, addr) in connections:
#                 try:
#                     # Run this while loop until a timeout occurs
#                     # or the program has stopped (usually a timeout)
#                     # A timeout indicates there is nothing more to receive
#                     while not self.stop:
#                         raws = conn.recv(self.receive_size)
#                         for raw in raws.splitlines():
#                             encode_decode.raw = raw
#                             receive_queue.put(encode_decode.data)
#                 # The connection closed out unexpectedly, handle this
#                 except ConnectionResetError:
#                     conn.close()
#                     connections.remove((conn, addr))
#                     logger.debug("Connection has ended...")
#                 except (socket.timeout, socket.error):
#                     logger.debug("Nothing to receive in receive_all_data")
#         else:
#             logger.debug("No connections to receive from")


##########################################
#             Unit Testing               #
##########################################

if __name__ == "__main__":
    pass
    # X = Ethernet('both', logging.INFO, 16339, 16340, 1)
    # X_THREAD = threading.Thread(target=X.start_communication_pipe,
    #                             name='X_THREAD')
    # X_THREAD.setDaemon(True)
    # X_THREAD.start()
    # for i in range(0, 3):
    #     print("-------------------------------------------")
    #     X.commander_send_queue.put(('1234',
    #                                 'test',
    #                                 'button:\n    steps:\n    - command: \
    #                                         "thisisatest"\n      type: \
    #                                         uart\n    - command: \
    #                                       "thisisatest"\n      type: uart'))
    #     time.sleep(6)
    # X.commander_send_queue.put(('0',
    #                             'terminate',
    #                             ''))
    # X_THREAD.join()
