import importlib
import multiprocessing
from threading import Thread
import logging
import datetime
from .util import CreateDirFileHandler
import traceback
from . import agent

class Controller():
    """Controlls all Agents and the Communication with them over IPC
    
    Keyword Arguments:
        logging_path {str} -- the path for the logging files, if None no logging will occure (default: {None})
        DEBUG {bool} -- if True will log on DEBUG level, else on INFO level (default: {False})
    """

    def __init__(self, logging_path: str=None, DEBUG: bool=False):
        self.agents = {}
        self.agents_running = {}
        self.agents_paused = []
        self.controller_queue = multiprocessing.Queue()
        self.agent_queues = {}
        self.thread_running = False

        self.logging_path = logging_path
        self.DEBUG = DEBUG

        self.result_data = {}
        self.property_data = {}
        self.model_runs = {}

        self.create_logger(logging_path, DEBUG)

#region logging

    def create_logger(self, logging_path: str, DEBUG: bool):
        """creates the logger for the controller
        
        Arguments:
            logging_path {str} -- the path for the logging files, if None no logging will occure
            DEBUG {bool} -- if True will log on DEBUG level, else on INFO level
        """

        if logging_path:
            self.logger = logging.getLogger(__name__)
            if self.DEBUG:
                self.logger.setLevel(logging.DEBUG)
            else:
                self.logger.setLevel(logging.INFO)
            try:
                con = CreateDirFileHandler(f"{logging_path}/pyjama_log_Controller_{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.txt")
                con.setLevel(logging.DEBUG)
                formatter = logging.Formatter('[%(asctime)s][%(levelname)s][%(name)s][%(processName)s] : %(message)s')
                con.setFormatter(formatter)
                self.logger.addHandler(con)
                self.log_debug("initialized logger")
            except Exception as e:
                print("Failed to create file logger")
                print(e)
        else:
            self.logger = None

    def log_debug(self, msg: str):
        if self.logger:
            self.logger.debug(f"[CONTROLLER][{__name__}] : {msg}")

    def log_warning(self, msg: str):
        print(f"[WARNING][CONTROLLER][{__name__}] : {msg}")
        print(traceback.format_exc())
        if self.logger:
            self.logger.warning(f"[CONTROLLER][{__name__}] : {msg}")
            self.logger.warning(f"[CONTROLLER][{__name__}] : {traceback.format_exc()}")

#endregion logging

#region agent

    def add_agent(self, agent_id: 'Any', agent_name: str) -> bool:
        """create and add a new agent
        
        Arguments:
            agent_id {'Any'} -- the id that the agent will have
            agent_name {str} -- the name of the agent
        
        Returns:
            bool -- True if the agent could get created and added, False otherwise
        """

        self.log_debug('starting add_agent')
        try:
            if agent_id in self.agents:
                self.log_warning(f'agent with id {agent_id} is already existing')
                return False
            agent_queue = multiprocessing.Queue()
            self.agent_queues[agent_id] = agent_queue

            # new version TODO: check with Simon: why would u use importlib.import_module() here?
            a = agent.Agent(agent_id, agent_name, self.controller_queue, agent_queue, self.logging_path, self.DEBUG)
            # old version
            # a = importlib.import_module("pyjamas_core.agent").Agent(agent_id, agent_name, self.controller_queue, agent_queue, self.logging_path, self.DEBUG)
            # vary old version
            # a = importlib.import_module("core.agent").Agent(agent_id, agent_name, self.controller_queue, agent_queue, self.logging_path, self.DEBUG)

            self.log_debug(f'created agent (agent_id = {a.id} agent_name = {a.name})')
            self.agents[agent_id] = a
            self.log_debug(f'added agent (agent_id = {a.id} agent_name = {a.name})')
            if not self.thread_running:
                self.start_queue_reading()
                self.log_debug(f'started queue reading thread')
            return True
        except Exception:
            self.log_debug(f'agent {agent_id} could not be added')
        return False

    def force_add_agent(self, agent_id: 'Any', agent_name: str) -> bool:
        """creates and adds a new agent
        if there is already a agent with this id it will be removed
        
        Arguments:
            agent_id {'Any'} -- the id that the agent will have 
            agent_name {str} -- the name of the agent
        
        Returns:
            bool -- True if the agent could get created and added, False otherwise
        """

        self.log_debug('starting force_add_agent')
        if self.is_existing_agent(agent_id):
            if not self.remove_agent(agent_id):
                return False
        return self.add_agent(agent_id, agent_name)

    def remove_agent(self, agent_id: 'Any') -> bool:
        """removes an agent
        if the agent is running it will be terminated first
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
        
        Returns:
            bool -- True if the agent could be removed, False otherwise
        """

        self.log_debug('starting remove_agent')
        try:
            if self.is_agent_running(agent_id):
                self.log_debug(f'agent {agent_id} is running')
                self.kill_agent(agent_id)
            try:
                del self.agent_queues[agent_id]
            except KeyError:
                pass
            try:
                del self.agents[agent_id]
            except KeyError:
                pass
            try:
                del self.result_data[agent_id]
            except KeyError:
                pass

            if self.is_agent_paused(agent_id):
                self.agents_paused.remove(agent_id)
            self.log_debug(f'agent {agent_id} removed')
            if len(self.agents) <= 0:
                self.stop_queue_reading()
                self.log_debug(f'stopped queue reading thread')
            return True
        except Exception:
            self.log_warning(f'agent {agent_id} could not be removed')
        return False

    def get_agent_info(self, agent_id: 'Any'):
        if self.is_existing_agent(agent_id):
            return self.agents[agent_id].get_info()
        return None
    
#endregion agent

#region model

    def add_model(self, agent_id: 'Any', model_path: str, model_id: 'Any', model_name: str) -> bool:
        """Create a model and add it to an agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
            model_path {str} -- the path to the python file of the model
            model_id {'Any'} -- the id the model will have
            model_name {str} -- the name of the model
        
        Returns:
            bool -- True if the model could be created and added, False otherwise
        """

        self.log_debug('starting add_model')
        if self.is_existing_agent(agent_id):
            if not self.is_agent_running(agent_id):
                try:
                    mod = importlib.import_module(f"Models.{model_path}").Model(model_id,model_name)
                except Exception:
                    self.log_warning(f'Model could not be created with provided model_path ({model_path})')
                    return False
                self.log_debug(f'created model (model_id = {model_id} model_name = {model_name})')
                
                if self.agents[agent_id].add_model(mod):
                    self.log_debug(f'added model (model_id = {model_id} model_name = {model_name}) to agent {agent_id}')
                    return True
                else:
                    self.log_warning(f'model (model_id = {model_id} model_name = {model_name}) could not be added')
            else:
                self.log_warning(f'agent {agent_id} is running -> model was not added')
        return False
 
    def remove_model(self, agent_id: 'Any', model_id: 'Any') -> bool:
        """remove a model from an agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
            model_id {'Any'} -- the id of the model
        
        Returns:
            bool -- True if the model could be removed, False otherwise
        """

        self.log_debug('starting remove_model')
        if self.is_existing_agent(agent_id):
            if not self.is_agent_running(agent_id):
                if self.agents[agent_id].remove_model(model_id):
                    del self.result_data[agent_id][model_id]
                    self.log_debug(f'removed model {model_id} from agent {agent_id}')
                    return True
                else:
                    self.log_warning(f'model {model_id} could not be removed')
            else:
                self.log_warning(f'agent {agent_id} is running -> model was not removed')
        return False
    
    def link_models(self, agent_id: 'Any', output_model_id: 'Any', output_name: str, input_model_id: 'Any', input_name: str) -> bool:
        """link the output of a model to the input of another in an agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
            output_model_id {'Any'} -- the id of the model providing the output
            output_name {str} -- the name of the output
            input_model_id {'Any'} -- the id of the model providing the input
            input_name {str} -- the name of the input
        
        Returns:
            bool -- True if the models could be linked, False otherwise
        """

        self.log_debug('starting link_models')
        if self.is_existing_agent(agent_id):
            if not self.is_agent_running(agent_id):
                if self.agents[agent_id].link_models(output_model_id,output_name,input_model_id,input_name):
                    self.log_debug(f'added link from (model {output_model_id} / output {output_name}) to (model {input_model_id} / input {input_name}) in agent {agent_id}')
                    return True
                else:
                    self.log_warning(f'link from (model {output_model_id} / output {output_name}) to (model {input_model_id} / input {input_name}) in agent {agent_id} could not be created')
            else:
                self.log_debug(f'agent {agent_id} is running -> models were not linked')
        return False

    def unlink_models(self, agent_id: 'Any', output_model_id: 'Any', output_name: str, input_model_id: 'Any', input_name: str) -> bool:
        """remove the link between two models
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
            output_model_id {'Any'} -- the id of the model providing the output
            output_name {str} -- the name of the output
            input_model_id {'Any'} -- the id of the model providing the input
            input_name {str} -- the name of the input
        
        Returns:
            bool -- True if the models could be unlinked, False otherwise
        """

        self.log_debug('starting unlink_models')
        if self.is_existing_agent(agent_id):
            if not self.is_agent_running(agent_id):
                if self.agents[agent_id].unlink_models(output_model_id,output_name,input_model_id,input_name):
                    self.log_debug(f'removed link from (model {output_model_id}:{output_name}) to (model {input_model_id}:{input_name}) in agent {agent_id}')
                    return True
                else:
                    self.log_warning(f'link from (model {output_model_id}:{output_name}) to (model {input_model_id}:{input_name}) in agent {agent_id} could not be removed')
            else:
                self.log_debug(f'agent {agent_id} is running -> models were not unlinked')
        return False

    def set_property(self, agent_id: 'Any', model_id: 'Any', property_name: str, property_value: 'Any') -> bool:
        """set a new value for a property of a model in an agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
            model_id {'Any'} -- the id of the model
            property_name {str} -- the name of the property
            property_value {'Any'} -- the value of the property
        
        Returns:
            bool -- True if the property could be changed or the order could be sent (if running), False otherwise
        """

        self.log_debug('starting set_property')
        if self.is_existing_agent(agent_id):
            if self.is_agent_running(agent_id):
                if self.send_set_property_order(agent_id,model_id,property_name,property_value):
                    self.log_debug(f'set property {property_name} of model {model_id} in agent {agent_id}')
                    return True
            else:
                if self.agents[agent_id].set_property(model_id,property_name,property_value):
                    self.log_debug(f'set property {property_name} of model {model_id} in agent {agent_id}')
                    return True
                else:
                    self.log_warning(f'property {property_name} of model {model_id} in agent {agent_id} could not be set')
        return False

#endregion model

#region simulation

    def start_agent(self, agent_id: 'Any') -> bool:
        """start or unpause an existing agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
        
        Returns:
            bool -- True if the agent could be started or unpaused, False otherwise
        """

        self.log_debug('starting start_agent')
        if self.is_existing_agent(agent_id):
            if self.is_agent_paused(agent_id):
                self.log_debug(f'agent {agent_id} already running but paused')
                if self.unpause_agent(agent_id):
                    self.log_debug(f'agent {agent_id} unpaused')
                    return True
            elif not self.is_agent_running(agent_id):
                # starting a new process
                p = multiprocessing.Process(target=self.agents[agent_id].run)
                self.agents_running[agent_id] = p
                p.start()
                self.log_debug(f'agent {agent_id} started')
                return True
            else:
                self.log_debug(f'agent {agent_id} is already running')
        return False

    def pause_agent(self, agent_id: 'Any') -> bool:
        """pause a running agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
        
        Returns:
            bool -- True if the agent could be paused, False otherwise
        """

        self.log_debug(f'starting pause_agent')
        if self.is_existing_agent(agent_id):
            if self.is_agent_paused(agent_id):
                self.log_debug(f'agent {agent_id} is already paused')
                return True
            if self.is_agent_running(agent_id):
                if self.send_halt_order(agent_id):
                    self.agents_paused.append(agent_id)
                    return True
        return False

    def unpause_agent(self, agent_id: 'Any') -> bool:
        """unpause a running agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
        
        Returns:
            bool -- True if the agent could be unpaused, False otherwise
        """

        self.log_debug('starting unpause_agent')
        if self.is_existing_agent(agent_id):
            if self.is_agent_running(agent_id):
                if self.is_agent_paused(agent_id):
                    if self.send_cont_order(agent_id):
                        self.agents_paused.remove(agent_id)
                        return True
                else:
                    self.log_debug(f'agent {agent_id} is not paused')
                    return True
            else:
                self.log_debug(f'agent {agent_id} is not running')
                return True
        return False

    def stop_agent(self, agent_id: 'Any') -> bool:
        """stop a running agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
        
        Returns:
            bool -- True if the stop order could be sent, False otherwise
        """

        self.log_debug(f'starting stop_agent')
        if self.is_existing_agent(agent_id):
            if self.is_agent_running(agent_id):
                if self.send_stop_order(agent_id):
                    return True
            else:
                self.log_debug(f'agent {agent_id} is not running')
        return False

    def kill_agent(self, agent_id: 'Any') -> bool:
        """kill the process of a running agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
        
        Returns:
            bool -- True if the process could be killed
        """

        self.log_debug(f'starting kill_agent')
        if self.is_existing_agent(agent_id):
            if self.is_agent_running(agent_id):
                self.agents_running[agent_id].terminate()
                try:
                    del self.agents_running[agent_id]
                except KeyError:
                    pass
                if self.is_agent_paused(agent_id):
                    self.agents_paused.remove(agent_id)
                return True
            else:
                self.log_debug(f'agent {agent_id} is not running')
                return True
        return False

    def get_model_result(self, agent_id: 'Any', model_id: 'Any') -> dict:
        """get the result data of a model from an agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
            model_id {'Any'} -- the id of the model
        
        Returns:
            dict -- a dictionary containing the last results of all outputs of the model
        """

        self.log_debug(f'starting get_model_result')
        try:
            return self.result_data[agent_id][model_id]
        except KeyError:
            pass
        return None
    
    def get_model_results_newer_than(self, agent_id: 'Any', model_id: 'Any', run: int) -> dict:
        """get the result data of a model from an agent if it is newer than the provided run number
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
            model_id {'Any'} -- the id of the model
            run {int} -- the run number
        
        Returns:
            dict -- a dictionary containing the last results of all outputs of the model
        """

        try:
            cur_run = self.model_runs[agent_id][model_id]
            if run < cur_run:
                result = self.get_model_result(agent_id, model_id)
                return {'run': cur_run, 'result': result}
        except KeyError:
            pass
        return None

    def get_model_properties(self, agent_id: 'Any', model_id: 'Any') -> dict:
        """get the current properties of a model from an agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
            model_id {'Any'} -- the id of the model

        Returns:
            dict -- a dictionary containing the current properties of the model
        """

        self.log_debug(f'starting get_model_properties')
        try:
            return self.property_data[agent_id][model_id]
        except KeyError:
            self.log_warning(f'no property data found for model {model_id} in agent {agent_id}')
            return None

#endregion simulation

#region util

    def is_existing_agent(self, agent_id: 'Any') -> bool:
        if agent_id in self.agents:
            return True
        self.log_warning(f'agent {agent_id} is not existing')
        return False

    def is_agent_running(self, agent_id: 'Any') -> bool:
        if agent_id in self.agents_running:
            return True
        return False

    def is_agent_paused(self, agent_id: 'Any') -> bool:
        if agent_id in self.agents_paused:
            return True
        return False

    def get_agent_status(self, agent_id: 'Any') -> str:
        """get the current status of an agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
        
        Returns:
            str -- 'paused', 'running' or 'stopped'
        """

        if self.is_agent_paused(agent_id):
            return "paused"
        if self.is_agent_running(agent_id):
            return "running"
        return "stopped"

    def get_agents_running(self) -> list:
        return list(self.agents_running.keys())

    def get_agents(self) -> list:
        return list(self.agents.keys())

#endregion util

#region messaging

    # queue reader

    def start_queue_reading(self):
        self.thread_running = True
        thread = Thread(target=self.read_queue)
        thread.daemon = True
        thread.start()

    def stop_queue_reading(self):
        self.thread_running = False

    def read_queue(self):
        self.log_debug(f'starting controller queue thread')
        while self.thread_running or self.agents_running:
            try:
                msg = self.controller_queue.get(True)
                self.handle_order(msg)
            except Exception as e:
                print(e)
        self.log_debug(f'finished controller queue thread')

    def handle_order(self, msg: dict):
        try:
            self.log_debug(f'recieved order {msg["order"]} from agent {msg["agent"]}')

            if msg["order"] == "dead":
                # agent finished running
                self.handle_dead_order(msg)

            elif msg['order'] == 'data':
                # new model output data
                self.handle_data_order(msg)

            elif msg['order'] == 'cpro':
                # new model property data
                self.handle_cpro_order(msg)

            elif msg['order'] == 'kill':
                # agent requesting to get killed
                self.handle_kill_order(msg)
        except KeyError:
            self.log_warning(f'message {msg} could not be handled correctly')

    # order handling

    def handle_dead_order(self, msg: dict):
        agent_id = msg['agent']
        if self.is_agent_running(agent_id):
            self.agents_running[agent_id].terminate()
            self.log_debug(f'agent {agent_id} terminated')
            try:
                del self.agents_running[agent_id]
            except KeyError:
                pass
        if self.is_agent_paused(agent_id):
            self.agents_paused.remove(agent_id)
        self.log_debug(f'agent {agent_id} removed from running list')

    def handle_kill_order(self, msg: dict):
        agent_id = msg['agent']
        self.kill_agent(agent_id)

    def handle_data_order(self, msg: dict):
        agent_id = msg['agent']
        model_id = msg['model']
        data = msg['text']
        if not agent_id in self.result_data:
            self.result_data[agent_id] = {}
        if not model_id in self.result_data[agent_id]:
            self.result_data[agent_id][model_id] = {}

        if not agent_id in self.model_runs:
            self.model_runs[agent_id] = {}
        model_run = data[0]
        self.model_runs[agent_id][model_id] = model_run
        
        for name, result in data[1:]:
            self.result_data[agent_id][model_id][name] = result

    def handle_cpro_order(self, msg: dict):
        agent_id = msg['agent']
        model_id = msg['model']
        props = msg['text']

        if not agent_id in self.property_data:
            self.property_data[agent_id] = {}
        if not model_id in self.property_data[agent_id]:
            self.property_data[agent_id][model_id] = {}

        for key, prop in props.items():
            self.property_data[agent_id][model_id][key] = prop


    # sending orders

    def send_set_property_order(self, agent_id: 'Any', model_id: 'Any', property_name: str, property_value: 'Any') -> bool:
        text = {}
        text["property_name"] = property_name
        text["property_value"] = property_value

        order = {}
        order["order"] = "prop"
        order["agent"] = agent_id
        order["model"] = model_id
        order["text"] = text

        return self.send_order(agent_id, order)

    def send_stop_order(self, agent_id: 'Any') -> bool:
        order = {}
        order["order"] = "stop"
        order["agent"] = agent_id

        return self.send_order(agent_id, order)

    def send_halt_order(self, agent_id: 'Any') -> bool:
        order = {}
        order["order"] = "halt"
        order["agent"] = agent_id
        
        return self.send_order(agent_id, order)

    def send_cont_order(self, agent_id: 'Any') -> bool:
        order = {}
        order["order"] = "cont"
        order["agent"] = agent_id
        
        return self.send_order(agent_id, order)

    def send_order(self, agent_id: 'Any', order: dict) -> bool:
        try:
            self.agent_queues[agent_id].put(order)
            self.log_debug(f'sent order {order["order"]} to agent {order["agent"]}')
            return True
        except KeyError:
            self.log_warning(f'no queue found for agent_id {agent_id}')
        return False

#endregion messaging
import importlib
import multiprocessing
from threading import Thread
import logging
import datetime
from .util import CreateDirFileHandler
import traceback
from . import agent

class Controller():
    """Controlls all Agents and the Communication with them over IPC
    
    Keyword Arguments:
        logging_path {str} -- the path for the logging files, if None no logging will occure (default: {None})
        DEBUG {bool} -- if True will log on DEBUG level, else on INFO level (default: {False})
    """

    def __init__(self, logging_path: str=None, DEBUG: bool=False):
        self.agents = {}
        self.agents_running = {}
        self.agents_paused = []
        self.controller_queue = multiprocessing.Queue()
        self.agent_queues = {}
        self.thread_running = False

        self.logging_path = logging_path
        self.DEBUG = DEBUG

        self.result_data = {}
        self.property_data = {}
        self.model_runs = {}

        self.create_logger(logging_path, DEBUG)

#region logging

    def create_logger(self, logging_path: str, DEBUG: bool):
        """creates the logger for the controller
        
        Arguments:
            logging_path {str} -- the path for the logging files, if None no logging will occure
            DEBUG {bool} -- if True will log on DEBUG level, else on INFO level
        """

        if logging_path:
            self.logger = logging.getLogger(__name__)
            if self.DEBUG:
                self.logger.setLevel(logging.DEBUG)
            else:
                self.logger.setLevel(logging.INFO)
            try:
                con = CreateDirFileHandler(f"{logging_path}/pyjama_log_Controller_{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.txt")
                con.setLevel(logging.DEBUG)
                formatter = logging.Formatter('[%(asctime)s][%(levelname)s][%(name)s][%(processName)s] : %(message)s')
                con.setFormatter(formatter)
                self.logger.addHandler(con)
                self.log_debug("initialized logger")
            except Exception as e:
                print("Failed to create file logger")
                print(e)
        else:
            self.logger = None

    def log_debug(self, msg: str):
        if self.logger:
            self.logger.debug(f"[CONTROLLER][{__name__}] : {msg}")

    def log_warning(self, msg: str):
        print(f"[WARNING][CONTROLLER][{__name__}] : {msg}")
        print(traceback.format_exc())
        if self.logger:
            self.logger.warning(f"[CONTROLLER][{__name__}] : {msg}")
            self.logger.warning(f"[CONTROLLER][{__name__}] : {traceback.format_exc()}")

#endregion logging

#region agent

    def add_agent(self, agent_id: 'Any', agent_name: str) -> bool:
        """create and add a new agent
        
        Arguments:
            agent_id {'Any'} -- the id that the agent will have
            agent_name {str} -- the name of the agent
        
        Returns:
            bool -- True if the agent could get created and added, False otherwise
        """

        self.log_debug('starting add_agent')
        try:
            if agent_id in self.agents:
                self.log_warning(f'agent with id {agent_id} is already existing')
                return False
            agent_queue = multiprocessing.Queue()
            self.agent_queues[agent_id] = agent_queue

            # new version TODO: check with Simon: why would u use importlib.import_module() here?
            a = agent.Agent(agent_id, agent_name, self.controller_queue, agent_queue, self.logging_path, self.DEBUG)
            # old version
            # a = importlib.import_module("pyjamas_core.agent").Agent(agent_id, agent_name, self.controller_queue, agent_queue, self.logging_path, self.DEBUG)
            # vary old version
            # a = importlib.import_module("core.agent").Agent(agent_id, agent_name, self.controller_queue, agent_queue, self.logging_path, self.DEBUG)

            self.log_debug(f'created agent (agent_id = {a.id} agent_name = {a.name})')
            self.agents[agent_id] = a
            self.log_debug(f'added agent (agent_id = {a.id} agent_name = {a.name})')
            if not self.thread_running:
                self.start_queue_reading()
                self.log_debug(f'started queue reading thread')
            return True
        except Exception:
            self.log_debug(f'agent {agent_id} could not be added')
        return False

    def force_add_agent(self, agent_id: 'Any', agent_name: str) -> bool:
        """creates and adds a new agent
        if there is already a agent with this id it will be removed
        
        Arguments:
            agent_id {'Any'} -- the id that the agent will have 
            agent_name {str} -- the name of the agent
        
        Returns:
            bool -- True if the agent could get created and added, False otherwise
        """

        self.log_debug('starting force_add_agent')
        if self.is_existing_agent(agent_id):
            if not self.remove_agent(agent_id):
                return False
        return self.add_agent(agent_id, agent_name)

    def remove_agent(self, agent_id: 'Any') -> bool:
        """removes an agent
        if the agent is running it will be terminated first
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
        
        Returns:
            bool -- True if the agent could be removed, False otherwise
        """

        self.log_debug('starting remove_agent')
        try:
            if self.is_agent_running(agent_id):
                self.log_debug(f'agent {agent_id} is running')
                self.kill_agent(agent_id)
            try:
                del self.agent_queues[agent_id]
            except KeyError:
                pass
            try:
                del self.agents[agent_id]
            except KeyError:
                pass
            try:
                del self.result_data[agent_id]
            except KeyError:
                pass

            if self.is_agent_paused(agent_id):
                self.agents_paused.remove(agent_id)
            self.log_debug(f'agent {agent_id} removed')
            if len(self.agents) <= 0:
                self.stop_queue_reading()
                self.log_debug(f'stopped queue reading thread')
            return True
        except Exception:
            self.log_warning(f'agent {agent_id} could not be removed')
        return False

    def get_agent_info(self, agent_id: 'Any'):
        if self.is_existing_agent(agent_id):
            return self.agents[agent_id].get_info()
        return None
    
#endregion agent

#region model

    def add_model(self, agent_id: 'Any', model_path: str, model_id: 'Any', model_name: str) -> bool:
        """Create a model and add it to an agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
            model_path {str} -- the path to the python file of the model
            model_id {'Any'} -- the id the model will have
            model_name {str} -- the name of the model
        
        Returns:
            bool -- True if the model could be created and added, False otherwise
        """

        self.log_debug('starting add_model')
        if self.is_existing_agent(agent_id):
            if not self.is_agent_running(agent_id):
                try:
                    mod = importlib.import_module(f"Models.{model_path}").Model(model_id,model_name)
                except Exception:
                    self.log_warning(f'Model could not be created with provided model_path ({model_path})')
                    return False
                self.log_debug(f'created model (model_id = {model_id} model_name = {model_name})')
                
                if self.agents[agent_id].add_model(mod):
                    self.log_debug(f'added model (model_id = {model_id} model_name = {model_name}) to agent {agent_id}')
                    return True
                else:
                    self.log_warning(f'model (model_id = {model_id} model_name = {model_name}) could not be added')
            else:
                self.log_warning(f'agent {agent_id} is running -> model was not added')
        return False
 
    def remove_model(self, agent_id: 'Any', model_id: 'Any') -> bool:
        """remove a model from an agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
            model_id {'Any'} -- the id of the model
        
        Returns:
            bool -- True if the model could be removed, False otherwise
        """

        self.log_debug('starting remove_model')
        if self.is_existing_agent(agent_id):
            if not self.is_agent_running(agent_id):
                if self.agents[agent_id].remove_model(model_id):
                    del self.result_data[agent_id][model_id]
                    self.log_debug(f'removed model {model_id} from agent {agent_id}')
                    return True
                else:
                    self.log_warning(f'model {model_id} could not be removed')
            else:
                self.log_warning(f'agent {agent_id} is running -> model was not removed')
        return False
    
    def link_models(self, agent_id: 'Any', output_model_id: 'Any', output_name: str, input_model_id: 'Any', input_name: str) -> bool:
        """link the output of a model to the input of another in an agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
            output_model_id {'Any'} -- the id of the model providing the output
            output_name {str} -- the name of the output
            input_model_id {'Any'} -- the id of the model providing the input
            input_name {str} -- the name of the input
        
        Returns:
            bool -- True if the models could be linked, False otherwise
        """

        self.log_debug('starting link_models')
        if self.is_existing_agent(agent_id):
            if not self.is_agent_running(agent_id):
                if self.agents[agent_id].link_models(output_model_id,output_name,input_model_id,input_name):
                    self.log_debug(f'added link from (model {output_model_id} / output {output_name}) to (model {input_model_id} / input {input_name}) in agent {agent_id}')
                    return True
                else:
                    self.log_warning(f'link from (model {output_model_id} / output {output_name}) to (model {input_model_id} / input {input_name}) in agent {agent_id} could not be created')
            else:
                self.log_debug(f'agent {agent_id} is running -> models were not linked')
        return False

    def unlink_models(self, agent_id: 'Any', output_model_id: 'Any', output_name: str, input_model_id: 'Any', input_name: str) -> bool:
        """remove the link between two models
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
            output_model_id {'Any'} -- the id of the model providing the output
            output_name {str} -- the name of the output
            input_model_id {'Any'} -- the id of the model providing the input
            input_name {str} -- the name of the input
        
        Returns:
            bool -- True if the models could be unlinked, False otherwise
        """

        self.log_debug('starting unlink_models')
        if self.is_existing_agent(agent_id):
            if not self.is_agent_running(agent_id):
                if self.agents[agent_id].unlink_models(output_model_id,output_name,input_model_id,input_name):
                    self.log_debug(f'removed link from (model {output_model_id}:{output_name}) to (model {input_model_id}:{input_name}) in agent {agent_id}')
                    return True
                else:
                    self.log_warning(f'link from (model {output_model_id}:{output_name}) to (model {input_model_id}:{input_name}) in agent {agent_id} could not be removed')
            else:
                self.log_debug(f'agent {agent_id} is running -> models were not unlinked')
        return False

    def set_property(self, agent_id: 'Any', model_id: 'Any', property_name: str, property_value: 'Any') -> bool:
        """set a new value for a property of a model in an agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
            model_id {'Any'} -- the id of the model
            property_name {str} -- the name of the property
            property_value {'Any'} -- the value of the property
        
        Returns:
            bool -- True if the property could be changed or the order could be sent (if running), False otherwise
        """

        self.log_debug('starting set_property')
        if self.is_existing_agent(agent_id):
            if self.is_agent_running(agent_id):
                if self.send_set_property_order(agent_id,model_id,property_name,property_value):
                    self.log_debug(f'set property {property_name} of model {model_id} in agent {agent_id}')
                    return True
            else:
                if self.agents[agent_id].set_property(model_id,property_name,property_value):
                    self.log_debug(f'set property {property_name} of model {model_id} in agent {agent_id}')
                    return True
                else:
                    self.log_warning(f'property {property_name} of model {model_id} in agent {agent_id} could not be set')
        return False

#endregion model

#region simulation

    def start_agent(self, agent_id: 'Any') -> bool:
        """start or unpause an existing agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
        
        Returns:
            bool -- True if the agent could be started or unpaused, False otherwise
        """

        self.log_debug('starting start_agent')
        if self.is_existing_agent(agent_id):
            if self.is_agent_paused(agent_id):
                self.log_debug(f'agent {agent_id} already running but paused')
                if self.unpause_agent(agent_id):
                    self.log_debug(f'agent {agent_id} unpaused')
                    return True
            elif not self.is_agent_running(agent_id):
                # starting a new process
                p = multiprocessing.Process(target=self.agents[agent_id].run)
                self.agents_running[agent_id] = p
                p.start()
                self.log_debug(f'agent {agent_id} started')
                return True
            else:
                self.log_debug(f'agent {agent_id} is already running')
        return False

    def pause_agent(self, agent_id: 'Any') -> bool:
        """pause a running agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
        
        Returns:
            bool -- True if the agent could be paused, False otherwise
        """

        self.log_debug(f'starting pause_agent')
        if self.is_existing_agent(agent_id):
            if self.is_agent_paused(agent_id):
                self.log_debug(f'agent {agent_id} is already paused')
                return True
            if self.is_agent_running(agent_id):
                if self.send_halt_order(agent_id):
                    self.agents_paused.append(agent_id)
                    return True
        return False

    def unpause_agent(self, agent_id: 'Any') -> bool:
        """unpause a running agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
        
        Returns:
            bool -- True if the agent could be unpaused, False otherwise
        """

        self.log_debug('starting unpause_agent')
        if self.is_existing_agent(agent_id):
            if self.is_agent_running(agent_id):
                if self.is_agent_paused(agent_id):
                    if self.send_cont_order(agent_id):
                        self.agents_paused.remove(agent_id)
                        return True
                else:
                    self.log_debug(f'agent {agent_id} is not paused')
                    return True
            else:
                self.log_debug(f'agent {agent_id} is not running')
                return True
        return False

    def stop_agent(self, agent_id: 'Any') -> bool:
        """stop a running agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
        
        Returns:
            bool -- True if the stop order could be sent, False otherwise
        """

        self.log_debug(f'starting stop_agent')
        if self.is_existing_agent(agent_id):
            if self.is_agent_running(agent_id):
                if self.send_stop_order(agent_id):
                    return True
            else:
                self.log_debug(f'agent {agent_id} is not running')
        return False

    def kill_agent(self, agent_id: 'Any') -> bool:
        """kill the process of a running agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
        
        Returns:
            bool -- True if the process could be killed
        """

        self.log_debug(f'starting kill_agent')
        if self.is_existing_agent(agent_id):
            if self.is_agent_running(agent_id):
                self.agents_running[agent_id].terminate()
                try:
                    del self.agents_running[agent_id]
                except KeyError:
                    pass
                if self.is_agent_paused(agent_id):
                    self.agents_paused.remove(agent_id)
                return True
            else:
                self.log_debug(f'agent {agent_id} is not running')
                return True
        return False

    def get_model_result(self, agent_id: 'Any', model_id: 'Any') -> dict:
        """get the result data of a model from an agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
            model_id {'Any'} -- the id of the model
        
        Returns:
            dict -- a dictionary containing the last results of all outputs of the model
        """

        self.log_debug(f'starting get_model_result')
        try:
            return self.result_data[agent_id][model_id]
        except KeyError:
            pass
        return None
    
    def get_model_results_newer_than(self, agent_id: 'Any', model_id: 'Any', run: int) -> dict:
        """get the result data of a model from an agent if it is newer than the provided run number
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
            model_id {'Any'} -- the id of the model
            run {int} -- the run number
        
        Returns:
            dict -- a dictionary containing the last results of all outputs of the model
        """

        try:
            cur_run = self.model_runs[agent_id][model_id]
            if run < cur_run:
                result = self.get_model_result(agent_id, model_id)
                return {'run': cur_run, 'result': result}
        except KeyError:
            pass
        return None

    def get_model_properties(self, agent_id: 'Any', model_id: 'Any') -> dict:
        """get the current properties of a model from an agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
            model_id {'Any'} -- the id of the model

        Returns:
            dict -- a dictionary containing the current properties of the model
        """

        self.log_debug(f'starting get_model_properties')
        try:
            return self.property_data[agent_id][model_id]
        except KeyError:
            self.log_warning(f'no property data found for model {model_id} in agent {agent_id}')
            return None

#endregion simulation

#region util

    def is_existing_agent(self, agent_id: 'Any') -> bool:
        if agent_id in self.agents:
            return True
        self.log_warning(f'agent {agent_id} is not existing')
        return False

    def is_agent_running(self, agent_id: 'Any') -> bool:
        if agent_id in self.agents_running:
            return True
        return False

    def is_agent_paused(self, agent_id: 'Any') -> bool:
        if agent_id in self.agents_paused:
            return True
        return False

    def get_agent_status(self, agent_id: 'Any') -> str:
        """get the current status of an agent
        
        Arguments:
            agent_id {'Any'} -- the id of the agent
        
        Returns:
            str -- 'paused', 'running' or 'stopped'
        """

        if self.is_agent_paused(agent_id):
            return "paused"
        if self.is_agent_running(agent_id):
            return "running"
        return "stopped"

    def get_agents_running(self) -> list:
        return list(self.agents_running.keys())

    def get_agents(self) -> list:
        return list(self.agents.keys())

#endregion util

#region messaging

    # queue reader

    def start_queue_reading(self):
        self.thread_running = True
        thread = Thread(target=self.read_queue)
        thread.daemon = True
        thread.start()

    def stop_queue_reading(self):
        self.thread_running = False

    def read_queue(self):
        self.log_debug(f'starting controller queue thread')
        while self.thread_running or self.agents_running:
            try:
                msg = self.controller_queue.get(True)
                self.handle_order(msg)
            except Exception as e:
                print(e)
        self.log_debug(f'finished controller queue thread')

    def handle_order(self, msg: dict):
        try:
            self.log_debug(f'recieved order {msg["order"]} from agent {msg["agent"]}')

            if msg["order"] == "dead":
                # agent finished running
                self.handle_dead_order(msg)

            elif msg['order'] == 'data':
                # new model output data
                self.handle_data_order(msg)

            elif msg['order'] == 'cpro':
                # new model property data
                self.handle_cpro_order(msg)

            elif msg['order'] == 'kill':
                # agent requesting to get killed
                self.handle_kill_order(msg)
        except KeyError:
            self.log_warning(f'message {msg} could not be handled correctly')

    # order handling

    def handle_dead_order(self, msg: dict):
        agent_id = msg['agent']
        if self.is_agent_running(agent_id):
            self.agents_running[agent_id].terminate()
            self.log_debug(f'agent {agent_id} terminated')
            try:
                del self.agents_running[agent_id]
            except KeyError:
                pass
        if self.is_agent_paused(agent_id):
            self.agents_paused.remove(agent_id)
        self.log_debug(f'agent {agent_id} removed from running list')

    def handle_kill_order(self, msg: dict):
        agent_id = msg['agent']
        self.kill_agent(agent_id)

    def handle_data_order(self, msg: dict):
        agent_id = msg['agent']
        model_id = msg['model']
        data = msg['text']
        if not agent_id in self.result_data:
            self.result_data[agent_id] = {}
        if not model_id in self.result_data[agent_id]:
            self.result_data[agent_id][model_id] = {}

        if not agent_id in self.model_runs:
            self.model_runs[agent_id] = {}
        model_run = data[0]
        self.model_runs[agent_id][model_id] = model_run
        
        for name, result in data[1:]:
            self.result_data[agent_id][model_id][name] = result

    def handle_cpro_order(self, msg: dict):
        agent_id = msg['agent']
        model_id = msg['model']
        props = msg['text']

        if not agent_id in self.property_data:
            self.property_data[agent_id] = {}
        if not model_id in self.property_data[agent_id]:
            self.property_data[agent_id][model_id] = {}

        for key, prop in props.items():
            self.property_data[agent_id][model_id][key] = prop


    # sending orders

    def send_set_property_order(self, agent_id: 'Any', model_id: 'Any', property_name: str, property_value: 'Any') -> bool:
        text = {}
        text["property_name"] = property_name
        text["property_value"] = property_value

        order = {}
        order["order"] = "prop"
        order["agent"] = agent_id
        order["model"] = model_id
        order["text"] = text

        return self.send_order(agent_id, order)

    def send_stop_order(self, agent_id: 'Any') -> bool:
        order = {}
        order["order"] = "stop"
        order["agent"] = agent_id

        return self.send_order(agent_id, order)

    def send_halt_order(self, agent_id: 'Any') -> bool:
        order = {}
        order["order"] = "halt"
        order["agent"] = agent_id
        
        return self.send_order(agent_id, order)

    def send_cont_order(self, agent_id: 'Any') -> bool:
        order = {}
        order["order"] = "cont"
        order["agent"] = agent_id
        
        return self.send_order(agent_id, order)

    def send_order(self, agent_id: 'Any', order: dict) -> bool:
        try:
            self.agent_queues[agent_id].put(order)
            self.log_debug(f'sent order {order["order"]} to agent {order["agent"]}')
            return True
        except KeyError:
            self.log_warning(f'no queue found for agent_id {agent_id}')
        return False

#endregion messaging
Controlls all Agents and the Communication with them over IPC          Keyword Arguments:         logging_path {str} -- the path for the logging files, if None no logging will occure (default: {None})         DEBUG {bool} -- if True will log on DEBUG level, else on INFO level (default: {False}) #region logging creates the logger for the controller                  Arguments:             logging_path {str} -- the path for the logging files, if None no logging will occure             DEBUG {bool} -- if True will log on DEBUG level, else on INFO level #endregion logging #region agent create and add a new agent                  Arguments:             agent_id {'Any'} -- the id that the agent will have             agent_name {str} -- the name of the agent                  Returns:             bool -- True if the agent could get created and added, False otherwise # new version TODO: check with Simon: why would u use importlib.import_module() here? # old version # a = importlib.import_module("pyjamas_core.agent").Agent(agent_id, agent_name, self.controller_queue, agent_queue, self.logging_path, self.DEBUG) # vary old version # a = importlib.import_module("core.agent").Agent(agent_id, agent_name, self.controller_queue, agent_queue, self.logging_path, self.DEBUG) creates and adds a new agent         if there is already a agent with this id it will be removed                  Arguments:             agent_id {'Any'} -- the id that the agent will have              agent_name {str} -- the name of the agent                  Returns:             bool -- True if the agent could get created and added, False otherwise removes an agent         if the agent is running it will be terminated first                  Arguments:             agent_id {'Any'} -- the id of the agent                  Returns:             bool -- True if the agent could be removed, False otherwise #endregion agent #region model Create a model and add it to an agent                  Arguments:             agent_id {'Any'} -- the id of the agent             model_path {str} -- the path to the python file of the model             model_id {'Any'} -- the id the model will have             model_name {str} -- the name of the model                  Returns:             bool -- True if the model could be created and added, False otherwise remove a model from an agent                  Arguments:             agent_id {'Any'} -- the id of the agent             model_id {'Any'} -- the id of the model                  Returns:             bool -- True if the model could be removed, False otherwise link the output of a model to the input of another in an agent                  Arguments:             agent_id {'Any'} -- the id of the agent             output_model_id {'Any'} -- the id of the model providing the output             output_name {str} -- the name of the output             input_model_id {'Any'} -- the id of the model providing the input             input_name {str} -- the name of the input                  Returns:             bool -- True if the models could be linked, False otherwise remove the link between two models                  Arguments:             agent_id {'Any'} -- the id of the agent             output_model_id {'Any'} -- the id of the model providing the output             output_name {str} -- the name of the output             input_model_id {'Any'} -- the id of the model providing the input             input_name {str} -- the name of the input                  Returns:             bool -- True if the models could be unlinked, False otherwise set a new value for a property of a model in an agent                  Arguments:             agent_id {'Any'} -- the id of the agent             model_id {'Any'} -- the id of the model             property_name {str} -- the name of the property             property_value {'Any'} -- the value of the property                  Returns:             bool -- True if the property could be changed or the order could be sent (if running), False otherwise #endregion model #region simulation start or unpause an existing agent                  Arguments:             agent_id {'Any'} -- the id of the agent                  Returns:             bool -- True if the agent could be started or unpaused, False otherwise # starting a new process pause a running agent                  Arguments:             agent_id {'Any'} -- the id of the agent                  Returns:             bool -- True if the agent could be paused, False otherwise unpause a running agent                  Arguments:             agent_id {'Any'} -- the id of the agent                  Returns:             bool -- True if the agent could be unpaused, False otherwise stop a running agent                  Arguments:             agent_id {'Any'} -- the id of the agent                  Returns:             bool -- True if the stop order could be sent, False otherwise kill the process of a running agent                  Arguments:             agent_id {'Any'} -- the id of the agent                  Returns:             bool -- True if the process could be killed get the result data of a model from an agent                  Arguments:             agent_id {'Any'} -- the id of the agent             model_id {'Any'} -- the id of the model                  Returns:             dict -- a dictionary containing the last results of all outputs of the model get the result data of a model from an agent if it is newer than the provided run number                  Arguments:             agent_id {'Any'} -- the id of the agent             model_id {'Any'} -- the id of the model             run {int} -- the run number                  Returns:             dict -- a dictionary containing the last results of all outputs of the model get the current properties of a model from an agent                  Arguments:             agent_id {'Any'} -- the id of the agent             model_id {'Any'} -- the id of the model          Returns:             dict -- a dictionary containing the current properties of the model #endregion simulation #region util get the current status of an agent                  Arguments:             agent_id {'Any'} -- the id of the agent                  Returns:             str -- 'paused', 'running' or 'stopped' #endregion util #region messaging # queue reader # agent finished running # new model output data # new model property data # agent requesting to get killed # order handling # sending orders #endregion messaging