"""
Handles each individual test/button. Every time a command
is sent to the communication pipe, it should be one
complete, individual test. One test can implement multiple
steps and will determine if one step needs to be left running.


| 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"


Note:
    Every full command sent through the communication
    pipe is formatted as follows:

    (key, command, data)
    key: Is any number just to help identify what the
         executor is responding to.
    command: Is the command that needs to be run
    data: Is the needed data associated with the command.
          Usually this is formatted in YAML.

    Any full command that is not formatted in this manner
    will be ignored.

    Any full command that is properly formatted will
    reply with an acknoledgement in the form:

    (key, command, 'ACK')
    Where key and command are the same as what was
    received by the executor.
"""

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

# --------------Packages---------------- #
import threading
import queue
import ruamel.yaml

# --------------Local------------------- #
# pylint: disable=wrong-import-order
# I want all local imports to come last
import test


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

class CommandHandler:
    '''
    The CommunicationPipe receives and transmits all of the
    commands, but when it comes down to it, it doesn't know what
    to do with them. That's where the CommandHandler comes in.
    The CommandHandler will determine the type of command and
    how it should be handled by the program, if that is creating
    a new test, updating the UI, or simply logging data.

    Note:

    Args:
        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

    Attributes:
        tests (queue): all the tests that neeed to be run
            in order
        all_tests (dict): A dictionary of all the tests
            run during this session. Used to check the status
            and run the tests
        stop (bool): Will force the currently running test
            to stop

    Todo:
        * See if there is a good way to reduce the number of attribues
        * See if there is a good way to reduce the number of arguments
    '''
    # pylint: disable=too-many-instance-attributes
    # 9 is reaonable with how many queues we are managing
    def __init__(self, commander_send_queue, commander_receive_queue,
                 executor_send_queue, executor_receive_queue,
                 logger, executor_logger):
        # pylint: disable=too-many-arguments
        # 9 is reasonable with how many queues we are managing
        self.commander_send_queue = commander_send_queue
        self.commander_receive_queue = commander_receive_queue
        self.executor_send_queue = executor_send_queue
        self.executor_receive_queue = executor_receive_queue

        self.logger = logger
        self.executor_logger = executor_logger

        self.tests = queue.Queue()
        self.all_tests = dict()

        # stops just the test running not the determine executor
        self.stop = False

    def determine_commander(self):
        '''
        Parses each of the commands in the commander_receive_queue
        then sends them to the necessary log file.

        Attributes:
            full_command (tuple): Any data that was put in
                the receive queue
        '''
        while True:
            full_command = self.commander_receive_queue.get()
            if full_command[1] == 'log':
                self.logger.handle(full_command[2])

    def determine_executor(self):
        '''
        Parses each of the commands in the executor_receive_queue
        and determines the needed methods and functions
        that need to be run based on the command that was
        received.

        Attributes:
            full_command (tuple): Any data that was put in
                the receive queue
            key (int): The current key that is being worked
                with based on the last received command
            command (string): The exact command to be run
            data: The data used, usually yaml.
        '''
        while True:
            full_command = self.executor_receive_queue.get()

            # Check to make sure the full command was formatted properly
            if isinstance(full_command, tuple) and (len(full_command) == 3):
                key = full_command[0]
                command = full_command[1]
                data = full_command[2]
                self.executor_send_queue.put((key, command, 'ACK'))

            if command == 'terminate':
                self.clear_tests()
                self.executor_send_queue.put((key, 'terminate', 'terminating'))
                self.stop = True
                break
            if command == 'clean':
                self.clear_tests()
            elif command == 'stop':
                self.executor_send_queue.put((key, 'stop', 'stopping'))
                self.stop = True
            elif command == 'status':
                pass
            elif command == 'test':
                self.tests.put((key, command, data))

    def clear_tests(self):
        '''
        Empty out the entire tests queue, we are discarding
        everything.
        '''
        while not self.tests.empty():
            try:
                self.tests.get(False)
            except queue.Empty:
                continue

    def next_test(self):
        '''
        Gets the next test in the queue and sets it
        to the current test. It then returns the current
        key of the (now) current test.

        Attributes:
            current_test (tuple): The test that is about
                to start running

        Returns:
            int: the key of the test that is about to run
        '''
        if self.tests.empty():
            return 0
        current_test = self.tests.get()

        if current_test[0] not in self.all_tests.keys():
            self.all_tests.update({current_test[0]: (current_test[2], None)})
            return current_test[0]
        # else:
        check_test = self.all_tests.get(current_test[0], (None, None))
        if not check_test[0] == current_test[2]:
            self.all_tests.update({current_test[0]: (current_test[2],
                                                     None)})
        return current_test[0]

    def run_test(self, key):
        '''
        Create a test class and run the test. No other
        tests will be able to run during the duration of
        this test. The test is run in a seaparate thread
        so that the program can watch for a stop condition
        and notify the thread of a stop if need be.

        Attributes:
            test_yaml (string): The yaml associated with
                the test about to be run
            yaml (YAML): The object that is used to parse
                a string to a dictionary
            test_config (dict): The dictionary that is defined
                from the yaml and used to create a test
            start_test (Test): Setup the test class that
                is about to start running
            run_test_thread (thread): The actual thread
                that is executing each of the steps
                within the the test.
        '''
        if key == 0:
            return
        # parse the yaml to a dictionary ------->>
        test_yaml = self.all_tests.get(key, (None, None))[0]
        # For some reason if the key doesn't exist we don't
        # want to continue
        if test_yaml is None:
            return
        # Make sure the yaml is valid
        try:
            yaml = ruamel.yaml.YAML()
            test_config = yaml.load(test_yaml)
        except ruamel.yaml.scanner.ScannerError:
            self.executor_logger.error("Invalid YAML for requested test")
        # -------<<

        # Setup the test class
        check_test = self.all_tests.get(key, (None, None))
        if check_test[0] is None:
            return
        if check_test[1] is None:
            start_test = test.Test(test_config)
        else:
            start_test = check_test[1]

        # Modify the all_tests dictionary to include the test class
        self.all_tests.update({key: (test_yaml, start_test)})

        # Setup the thread to run the test and run it
        run_test_thread = threading.Thread(target=start_test.run_steps,
                                           name='run_test')
        run_test_thread.setDaemon(True)
        run_test_thread.start()

        # Don't continue until the test is over
        while run_test_thread.isAlive():
            # Forcibly stop the test
            if self.stop:
                start_test.stop_steps()

        # don't continue until the test is over
        run_test_thread.join()
