'''
    Scheduler Class is responsible to keep all scheduled objects and to run the
    simulation
'''

from tuby import scheduled, task, sink

class Scheduler(object):
    '''
        a scheduler object is responsible to accumulate the objects in the workflow
        and execute in solving the objects dependencies
    '''

    def __init__(self, steplen):
        '''
            the constructor takes the Delta Time (in seconds) and create a scheduler
        '''
        self.__steplen = steplen
        self.__steps = 0 # step done
        self.__scheduled_dict = {} #all the scheduled elements
        self.__initiated = False
        self.__sinks = []

    def add_scheduled(self, scheduled_el):
        '''
            add a scheduled element to the scheduler. If it is a task or a sink
            takes appropriate actions. It crashes if the object is not recognized.
            It also crash if an object is added after the simulation started.
        '''
        if self.__initiated:
            raise RuntimeError('Objects cannot be added anymore after initialization')

        if not isinstance(scheduled_el, scheduled.Scheduled):
            raise TypeError('the added object must be a subclass of Scheduler')

        if scheduled_el.get_name() in self.__scheduled_dict:
            raise RuntimeError("the object {} cannot be inserted in this scheduler multiple times".format(scheduled_el.get_name()))

        scheduled_el._decorate_scheduler_methods(self)
        self.__scheduled_dict[scheduled_el.get_name()] = scheduled_el
        scheduled_el.on_add()

    def step(self):
        '''
            advance the simulation of a single step. initiate_simulation must be called
            once before starting the simulation
        '''
        if not self.__initiated:
            raise RuntimeError('initiate_simulation() must be run once before step()')

        self.__steps += 1
        for (sink_obj, exe_fun) in self.__sinks:
            if sink_obj.is_active():
                exe_fun()
                sink_obj._on_activate()

    def simulate(self, time):
        '''
            advance the simulation of time. initiate_simulation must be called once
            before starting the simulation
        '''
        if not self.__initiated:
            raise RuntimeError('initiate_simulation() must be run once before simulate()')

        for _ in range(0, int(time/self.__steplen)):
            self.step()
        
        for obj in self.__scheduled_dict.values():
            obj._on_end_simulate()

    def get_time(self):
        '''
            get the simulation time
        '''
        return self.__steps * self.__steplen

    def get_step_length(self):
        '''
            get the simulation step length
        '''
        return self.__steplen

    def get_steps(self):
        '''
            get the simulated steps
        '''
        return self.__steps

    def __getitem__(self, objname):
        '''
            get a scheduled object name
        '''
        return self.__scheduled_dict[objname]

    def get_scheduled_iterator(self):
        '''
            get an iterator of scheduled objects
        '''
        return iter(self.__scheduled_dict.items())

    def initiate_simulation(self):
        '''
            initiate the simulation. It checks if all links are acceptable and
            prepare the envinroment to test. after that execution function can be used
            Note that after the initialization no more object can be added
        '''
        self.__initiated = True
        for sch in self.__scheduled_dict.values():
            sch._on_initiate()

        sinks = filter(lambda sched_obj: isinstance(sched_obj, sink.Sink), self.__scheduled_dict.values())
        sinks = sorted(sinks, key = lambda obj: -obj.get_prio())
        self.__sinks = list(map(lambda sink_obj: (sink_obj, self._get_exe_fun(sink_obj)), sinks))
    
    def _get_exe_fun(self, obj):
        '''
            return the function to be executed to obtain the dependencies of a sink
        '''
        exe_objs = self._get_exec_objdeps_nocontrol(obj.get_name())
        exe_funs = list(map(lambda obj: obj._execute, exe_objs))
       
        def exe_fun():
            '''
                call _execute() on all dependencies of obj
            '''
            for f in exe_funs:
                f()

        return exe_fun

    def _get_exec_objdeps(self, objname):
        '''
            find all the dependencies to be executed to execute an object
        '''
        if isinstance(self.__scheduled_dict[objname], sink.Sink):
            return []
        return self._get_exec_objdeps_nocontrol(objname)

    def _get_exec_objdeps_nocontrol(self, objname):
        '''
            find all the dependencies to be executed to execute an object. No control
        '''
        obj = self.__scheduled_dict[objname]
        deps = []
        for name in obj.get_bounded_objects():
            if isinstance(self.__scheduled_dict[name], task.Task):
                new_deps = self._get_exec_objdeps(name)
                new_deps.append(self.__scheduled_dict[name])
                new_deps = filter(lambda obj: obj not in deps, new_deps)
                deps += new_deps
        return deps
   
    def __contains__(self,x):
        '''
            find if a particular object is registered in the scheduler
        '''
        return x in self.__scheduled_dict.keys()

