
import simpy
from components.base.bus.abst_bus_can import AbstractCANBus
import config.timing_registration as time
from tools.general import General as G, RefList, General
from config import project_registration as proj, can_registration
from tools.ecu_logging import ECULogger as L, ECULogger
from io_processing.surveillance_handler import MonitorInput, MonitorTags
import uuid
from uuid import UUID

class RapidCANBus(AbstractCANBus):
    '''
    This class implements a CAN Bus that actively
    pulls messages from the ECUs buffers
    '''
    
    def __init__(self, sim_env, bus_id, data_rate, avg_ecu_dist=2):
        ''' Constructor
            
            Input:    sim_env        simpy.Environment         environment in which this Bus acts
                      bus_id         string                    id of this Bus object
                      data_rate      float                     datarate of this bus
                      avg_ecu_dist   float                     average distance between two connected ECUs
                
            Output:   -                  
        '''
        AbstractCANBus.__init__(self, sim_env, bus_id, data_rate, avg_ecu_dist)
        
        # bus objects
        self.current_message = None  # current message on the bus [sender_ecu, message]
        self.set_settings()
        self.monitor_list = RefList()
        self._used_prop_times = {}
        self.gateways = []
        self.first = True
        
        # synchronization objects
        self.pot_messages = []  # gathers all potential messages that want to be sent at a certain point in time
        self.sync_1 = simpy.Store(self.sim_env, capacity=1)  # if the decision, who is allowed to sent is done this synchronizer starts the transmission
        self.sync_2 = simpy.Store(self.sim_env, capacity=1)  # if store is empty then the channel is busy
        self.subscribers = 0  # number of ECUs waiting for the channel to be freed
        self.current_message_length_bit = 0
            
        # project parameters
        self.SCB_GATHER_MSGS = time.SCB_GATHER_MSGS
        self.SCB_GRAB_PRIO_MSG = time.SCB_GRAB_PRIO_MSG
        self.SCB_PROPAGATION_DELAY = time.SCB_PROPAGATION_DELAY
        self.SCB_SENDING_TIME = time.SCB_SENDING_TIME
        self.SCB_WRITE_TO_TRANSCEIVER_BUFFER = time.SCB_WRITE_TO_TRANSCEIVER_BUFFER

        # ECUs Datalink layers willing to send
        self._willing_dll = []

    def monitor_update(self):
        ''' returns the input for the monitor 
        
            Input:    -
            Output:   monitor_list    list    List of MonitorInput objects
        '''
        
        self.monitor_list.clear_on_access()  # on the next access the list will be cleared        
        return self.monitor_list.get()
    
    def release_willing(self, dll):
        ''' remove the ecus that are not willing to send
            anymore
            
            Input:    dll    AbstractDataLinkLayer    Datalink layer of the ECU that is not willing to send anymore
            Output:   -  
        '''
        self._willing_dll.remove(dll)
        
    def add_willing(self, dll):
        ''' add a Datalinklayer of an ECU that is willing to send
        
            Input:    dll    AbstractDataLinkLayer    Datalink layer of the ECU that is willing to send
            Output:   -  
        '''
        if dll not in self._willing_dll:
            self._willing_dll.append(dll)
      
    def notify_bus(self):
        ''' When the bus is empty it is set to a sleep mode that waits
            until the ecu notifies it. Using this method the ECU does so.
            Thus the Bus will only be active when any ecu sends.
            
            Input:  -
            Output: -
        '''
        if self.current_message != None: return
        else: self.sync_1.put(True) 
        
    def process(self):
        ''' Constantly pull messages from all ECUs that are connected. Once the 
            Bus is done with one message it pulls the next message from the 
            connected ECUs.
            
            Input:   -
            Output:  -            
        '''
        stp = 0
        while True: 
            
            # print time
            t = self.sim_env.now
            if t > stp:
#                 print(self.sim_env.now)
                stp += 0.5
            
            # check which ECU sends
            current_minimum = float("inf"); index = 0            
            for dll in self._willing_dll:                
                val = dll.first_queue_identifier()
                if val < current_minimum and val != False:
                    current_minimum = index
                index += 1
            
            # no ECU wiling to send: wait for notify
            if current_minimum == float("inf"):
                yield self.sync_1.get() 
            else:
                self.current_message = self._willing_dll[current_minimum].controller.transmit_buffer.get().message                
                self.current_message_length_bit = self.current_message.msg_length_in_bit
                
            # transmit message
            if self.current_message != None:
                
                # monitor start
                monitor_note = self._monitor_transmission_start()
                
                # write to buffer
                yield self.sim_env.process(self._wait_transmission_time_and_buffer())
                self._reset_transmission()
                
                # monitor end
                self._monitor_transmission_end(monitor_note)
             
                # if ecus buffer is now empty remove from willing
                if(len(self._willing_dll[current_minimum].controller.transmit_buffer.queue) == 0):
                    self.release_willing(self._willing_dll[current_minimum])
                    
    def set_settings(self):
        ''' sets the initial setting association between the settings variables
            and the actual parameter
        
            Input:   -
            Output:  -
        '''
        self.settings = {}
        
        # parameter
        self.settings['t_gather_msg'] = 'SCB_GATHER_MSGS'
        self.settings['t_grab_prio_msg'] = 'SCB_GRAB_PRIO_MSG'
        self.settings['t_propagation_delay'] = 'SCB_PROPAGATION_DELAY'
        self.settings['t_sending_time'] = 'SCB_SENDING_TIME'
        self.settings['t_write_to_transceiver_buffer'] = 'SCB_WRITE_TO_TRANSCEIVER_BUFFER' 
              
    def wait_until_free(self):
        ''' when the channel is busy some ECUs can start this method in
            a simpy process. Once the channel is free this process ends 
            and the next ECU can start it's transmission             
            technically:
            count number of waiting processes and notifies them all once the channel is free
            
            Input:    -
            Output    -
        '''
        # add subscriber
        self.subscribers += 1                
        yield self.sync_2.get()
        
        # release all receivers
        while self.subscribers > 1:           
            
            self.sync_2.put(True)
            self.subscribers -= 1 
        self.subscribers = 0           
     
    def _extract_transmission_times(self):
        ''' calculates the time the current transmission takes
            
            Input:       -
            Output: t_propagation:    float    time it takes to propagate the message
                    t_sending         float    time it takes to send the message
        '''
        
        t_propagation = time.call(self.SCB_PROPAGATION_DELAY, self.avg_dist_between_ecus)  # either constant or calculated depending on config
        t_sending = time.call(self.SCB_SENDING_TIME, self.current_message_length_bit, proj.BUS_ECU_DATARATE)  # either constant or calculated depending on config
        
        return t_propagation, t_sending

    def _gateway_sends(self, ecu):
        ''' if gateway is the sender let it continue 
            and reset the message state
            
            Input:  ecu     AbstractECU    current ECU that sends the message            
            Output: bool    boolean        True if the message was sent by this ECU
        '''
        try:
            if ecu.ecu_id in self.current_message.gw_id:  # send message back and forth send could lead to errors: need to reset gw_id list
                return True
        except:
            return False
        return False
     
         
    def _get_highest_priority_msg(self, message_list):
        ''' returns the message with the highest priority
            
            Input:     message_list    list        list of messages
            Output:    message         object      message with highest priority (lowest message id)
        '''
        min_val = float("inf")
        message = None
        for cur_message in message_list:            
            if min_val > cur_message.message_identifier:
                min_val = cur_message.message_identifier
                message = cur_message
        return message
    
    def _grab_highest_priority(self):
        ''' note the time it takes to select the message with
            the highest priority
        
            Input:     -
            Output:    -            
        '''
        if self.SCB_GRAB_PRIO_MSG != 0:
            G().to_t(self.sim_env, self.SCB_GRAB_PRIO_MSG, 'SCB_GRAB_PRIO_MSG', self.__class__.__name__, self) 
            return True
        return False
    
    def _monitor_transmission_start(self):
        ''' notes the start time when this message was put on the bus
            
            Input:  -
            Output: -
        '''
        # extract information 
        uid = uuid.uuid4()
        tag = MonitorTags.CB_PROCESSING_MESSAGE
        c_id = self.comp_id
        sender_id = self.current_message.sender_id
        msg_id = self.current_message.message_identifier
        msg_uid = self.current_message.data.unique_id
        data = self.current_message.data.get();         
        
        # extract further information         
        msg = self.current_message
        size = self.current_message_length_bit / 8
        self.current_message.data.unique_id = msg_uid
        
        # send to monitor
        G().mon(self.monitor_list, MonitorInput(data, tag, c_id, self.sim_env.now, sender_id, msg_id, msg, size, msg_id, uid.hex))        
        return data, c_id, sender_id, msg_id, msg, size, uid
           
    def _monitor_transmission_end(self, mon_out):
        ''' notes the end time when this message was put on the bus
            
            Input:  -
            Output: -
        '''
        G().mon(self.monitor_list, MonitorInput(mon_out[0], MonitorTags.CB_DONE_PROCESSING_MESSAGE, \
                                                mon_out[1], self.sim_env.now, mon_out[2], mon_out[3], \
                                                mon_out[4], mon_out[5], -1, mon_out[6].hex))

    def _push_to_receivers(self):
        ''' writes the current message to all ecus that 
            are connected to this Bus
        
            Input:       -
            Output:      -
        '''
        # get gateways
        if self.first:
            self.gateways = [itm.ecu_id for itm in self.connected_ecus if isinstance(itm.ecu_id, UUID)]
            self.first = False
        
        # send only to receivers
        if General().send_only_to_receivers and self.current_message.message_identifier not in can_registration.AUTH_MESSAGES:     
            
            run_list = General().sender_receiver_map[self.current_message.sender_id][self.current_message.message_identifier] + self.gateways
                   
            for ecu in self.connected_ecus:

                if ecu.ecu_id not in run_list:
                    continue
                
                # Gateway:avoid sending to itself (loops)
                if self._gateway_sends(ecu): continue
    
                # ECU: avoid sending to itself
                if(ecu.ecu_id != self.current_message.sender_id):
                    self.current_message.current_bus = self.comp_id
    
                    ecu.ecuHW.transceiver.get(self.current_message)
        
        else:
            # iterate over receivers      
            for ecu in self.connected_ecus:
                
                # Gateway:avoid sending to itself (loops)
                if self._gateway_sends(ecu): continue
    
                # ECU: avoid sending to itself
                if(ecu.ecu_id != self.current_message.sender_id):
                    self.current_message.current_bus = self.comp_id
    
                    ecu.ecuHW.transceiver.get(self.current_message)

    
    def _reset_transmission(self):
        ''' after one message was sent three things have to be reset
            the current message. The synchronizer for the selection
            of the next higher prioritized message to be sent and the 
            list that gathered the selected potential messages
            
            Input:     -
            Output:    -
        '''
        self.current_message = None  # message is not on the line anymore
        self.sync_2.put(True)  # channel is free again
        self.pot_messages = []  # reset

    def _sending_ok(self, t_propagation, t_sending):
        ''' checks if this message is sendable
            
            Input:  t_propagation:    float    time it takes to propagate the message
                    t_sending         float    time it takes to send the message
            Output: bool              boolean  true if the time is valid
        '''
        try:
            G().to_t(self.sim_env, t_propagation + t_sending + self.SCB_WRITE_TO_TRANSCEIVER_BUFFER, 'SCB_PROPAGATION_DELAY+SCB_SENDING_TIME+SCB_WRITE_TO_TRANSCEIVER_BUFFER', self.__class__.__name__, self)
            if t_propagation + t_sending + self.SCB_WRITE_TO_TRANSCEIVER_BUFFER > 0:
                return True        
            # logging.error("Error (skipped with time 0):t_propagation =%s, t_sending = %s, self.SCB_WRITE_TO_TRANSCEIVER_BUFFER = %s // msg_length_bit = %s" % \
            #              (t_propagation, t_sending, self.SCB_WRITE_TO_TRANSCEIVER_BUFFER, self.current_message_length_bit))
        except: 
            return False
        return False
    
    def _try_logging_transmission(self, t_propagation, t_sending):
        ''' notes the times that it takes to send the messages. In case an erroneous message
            is sent this method logs the exception
        
            Input:  t_propagation:    float    time it takes to propagate the message
                    t_sending         float    time it takes to send the message
        '''
                        
        try:
            # Log transmission
            L().log(300, self.sim_env.now, self.current_message.sender_id, float(self.current_message_length_bit) / 8.0, \
                    self.current_message_length_bit, self.comp_id, self.current_message.data.get(), t_propagation + t_sending) 
        except:      
            # Log traceback
            ECULogger().log_traceback()
            try: 
                L().log(300, self.sim_env.now, self.current_message.sender_id, self.current_message.data, \
                        self.current_message_length_bit, self.comp_id, self.current_message.data, t_propagation + t_sending)
            except: pass
    
    def _wait_transmission_time_and_buffer(self):
        ''' this method times out for the duration of the transmission and
            then writes the sent messages to the receiving buffer of the ecu
            
            Input:       -
            Output:      -
        '''

        if not self.current_message_length_bit in self._used_prop_times:
            # sending times         
            t_propagation, t_sending = self._extract_transmission_times()    
            
            # duration of transmission    
            wait_time = t_propagation + t_sending + self.SCB_WRITE_TO_TRANSCEIVER_BUFFER 
            if wait_time <= 0: wait_time = 0.000001
            self._used_prop_times[self.current_message_length_bit] = wait_time      
            
        else:

            wait_time = self._used_prop_times[self.current_message_length_bit]
            
        yield self.sim_env.timeout(wait_time)
        
        # put to connected ecus
        self._push_to_receivers()
               
        
