import threading
import arq_session
import helpers
from modem_frametypes import FRAME_TYPE
from codec2 import FREEDV_MODE
from enum import Enum
import time
from p2p_connection import States as P2PStates

class IRS_State(Enum):
    """Enumeration representing the states of an IRS (Information Receiving Station) ARQ session.

    This enumeration defines the various states that an ARQ session on the
    Information Receiving Station (IRS) side can transition through during
    the data transfer process.

    Attributes:
        NEW: Initial state of a new session.
        OPEN_ACK_SENT: State after sending an acknowledgement to the session open request.
        INFO_ACK_SENT: State after sending an acknowledgement to the session information frame.
        BURST_REPLY_SENT: State after sending an acknowledgement to a burst data frame.
        ENDED: State after successfully receiving all data and confirming the checksum.
        FAILED: State after a session failure, such as a timeout or CRC mismatch.
        ABORTED: State after the session is explicitly aborted.
        RESUME: State allowing transmission resumption after a waiting period. This state is set after a period longer than TIMEOUT_DATA to ensure clean states.
    """
    NEW = 0
    OPEN_ACK_SENT = 1
    INFO_ACK_SENT = 2
    BURST_REPLY_SENT = 3
    ENDED = 4
    FAILED = 5
    ABORTED = 6
    RESUME = 7 # State, which allows resuming of a transmission - will be set after some waiting time, higher than TIMEOUT_DATA for ensuring clean states

class ARQSessionIRS(arq_session.ARQSession):
    """Manages an ARQ session on the Information Receiving Station (IRS) side.

    This class extends the base ARQSession class and implements the specific
    logic and state transitions for the IRS during an ARQ data transfer.
    """

    TIMEOUT_CONNECT = 55 #14.2
    TIMEOUT_DATA = 90

    STATE_TRANSITION = {
        IRS_State.NEW: { 
            FRAME_TYPE.ARQ_SESSION_OPEN.value : 'send_open_ack',
            FRAME_TYPE.ARQ_STOP.value: 'send_stop_ack'
        },
        IRS_State.OPEN_ACK_SENT: { 
            FRAME_TYPE.ARQ_SESSION_OPEN.value: 'send_open_ack',
            FRAME_TYPE.ARQ_SESSION_INFO.value: 'send_info_ack',
            FRAME_TYPE.ARQ_STOP.value: 'send_stop_ack'

        },
        IRS_State.INFO_ACK_SENT: {
            FRAME_TYPE.ARQ_SESSION_INFO.value: 'send_info_ack',
            FRAME_TYPE.ARQ_BURST_FRAME.value: 'receive_data',
            FRAME_TYPE.ARQ_STOP.value: 'send_stop_ack'

        },
        IRS_State.BURST_REPLY_SENT: {
            FRAME_TYPE.ARQ_BURST_FRAME.value: 'receive_data',
            FRAME_TYPE.ARQ_STOP.value: 'send_stop_ack'

        },
        IRS_State.ENDED: {
            FRAME_TYPE.ARQ_BURST_FRAME.value: 'receive_data',
            FRAME_TYPE.ARQ_STOP.value: 'send_stop_ack'

        },
        IRS_State.FAILED: {
            FRAME_TYPE.ARQ_BURST_FRAME.value: 'receive_data',
            #FRAME_TYPE.ARQ_SESSION_OPEN.value: 'send_open_ack',
        },
        IRS_State.ABORTED: {
            FRAME_TYPE.ARQ_STOP.value: 'send_stop_ack',
            #FRAME_TYPE.ARQ_SESSION_OPEN.value: 'send_open_ack',
            #FRAME_TYPE.ARQ_SESSION_INFO.value: 'send_info_ack',
            #FRAME_TYPE.ARQ_BURST_FRAME.value: 'receive_data',
        },
        IRS_State.RESUME: {
            FRAME_TYPE.ARQ_SESSION_OPEN.value: 'send_open_ack',
            FRAME_TYPE.ARQ_BURST_FRAME.value: 'receive_data',

        }



    }

    def __init__(self, ctx, dxcall: str, session_id: int):
        """Initializes a new ARQ session on the Information Receiving Station (IRS) side.

        This method initializes an ARQSessionIRS object, setting up the session
        parameters and state for the IRS. It inherits from the arq_session.ARQSession
        class and adds IRS-specific attributes.

        Args:
            config (dict): The configuration dictionary.
            modem: The modem object.
            dxcall (str): The DX call sign.
            session_id (int): The unique ID of the session.
        """
        super().__init__(ctx, dxcall)
        self.ctx = ctx
        self.id = session_id
        self.dxcall = dxcall
        self.version = self.ctx.constants.ARQ_PROTOCOL_VERSION
        self.is_IRS = True

        self.state = IRS_State.NEW
        self.state_enum = IRS_State  # needed for access State enum from outside

        # instance of p2p connection
        self.running_p2p_connection = None
        try:
            self.running_p2p_connection = self.ctx.state_manager.get_p2p_connection_session(self.id)
            # register arq session in p2p connection
            self.running_p2p_connection.running_arq_session = self
            self.running_p2p_connection.set_state(P2PStates.ARQ_SESSION)
        except Exception as e:
            self.log("Error getting p2p connection session")

        self.type_byte = None
        self.total_length = 0
        self.total_crc = ''
        self.received_data = None
        self.received_bytes = 0
        self.received_crc = None

        self.maximum_bandwidth = 0

        self.abort = False

    def all_data_received(self):
        """Checks if all data has been received.

        This method compares the total expected length of the data with the
        number of bytes received so far.

        Returns:
            bool: True if all data has been received, False otherwise.
        """
        print(f"{self.total_length} vs {self.received_bytes}")
        return self.total_length == self.received_bytes

    def final_crc_matches(self) -> bool:
        """Checks if the final CRC matches the received data.

        This method calculates the CRC32 checksum of the received data and
        compares it with the expected CRC received from the sender.

        Returns:
            bool: True if the CRC matches, False otherwise.
        """
        return self.total_crc == helpers.get_crc_32(bytes(self.received_data)).hex()

    def transmit_and_wait(self, frame, timeout, mode):
        """Transmits a frame and waits for a response.

        This method transmits the given frame using the specified mode and waits
        for a response event within the given timeout period. If no response is
        received and the session is not aborted or failed, it logs a timeout
        message and calls the transmission_failed method.

        Args:
            frame: The frame to be transmitted.
            timeout: The timeout period in seconds.
            mode: The FreeDV mode to use for transmission.
        """
        self.event_frame_received.clear()
        self.transmit_frame(frame, mode)
        self.log(f"Waiting {timeout} seconds...")
        if not self.event_frame_received.wait(timeout) and self.state not in [IRS_State.ABORTED, IRS_State.FAILED]:
            self.log("Timeout waiting for ISS. Session failed.")
            self.transmission_failed()

    def launch_transmit_and_wait(self, frame, timeout, mode):
        """Launches the transmit_and_wait method in a separate thread.

        This method creates and starts a new daemon thread to execute the
        transmit_and_wait method, allowing the main thread to continue
        without blocking.

        Args:
            frame: The frame to be transmitted.
            timeout: The timeout period in seconds.
            mode: The FreeDV mode to use for transmission.
        """
        thread_wait = threading.Thread(target = self.transmit_and_wait, 
                                       args = [frame, timeout, mode], daemon=True)
        thread_wait.start()
    
    def send_open_ack(self, open_frame):
        """Sends an acknowledgement to the ARQ session open request.

        This method handles the reception of an ARQ_SESSION_OPEN frame.
        It negotiates the maximum transmission bandwidth, checks protocol
        version compatibility, and sends an ARQ_SESSION_OPEN_ACK frame.

        Args:
            open_frame: The received ARQ_SESSION_OPEN frame.

        Returns:
            Tuple[None, None]: Returns None for both data and type_byte as this method doesn't handle data.
        """
        # check for maximum bandwidth. If ISS bandwidth is higher than own, then use own
        if open_frame['maximum_bandwidth'] > self.ctx.config_manager.config['MODEM']['maximum_bandwidth']:
            self.maximum_bandwidth = self.ctx.config_manager.config['MODEM']['maximum_bandwidth']
        else:
            self.maximum_bandwidth = open_frame['maximum_bandwidth']
        self.log(f"Negotiated transmission bandwidth {self.maximum_bandwidth}Hz")

        self.ctx.event_manager.send_arq_session_new(
            False, self.id, self.dxcall, 0, self.state.name)

        if open_frame['protocol_version'] not in [self.protocol_version]:
            self.abort = True
            self.log(f"Protocol version mismatch! Setting disconnect flag!", isWarning=True)
            self.set_state(IRS_State.ABORTED)

        ack_frame = self.frame_factory.build_arq_session_open_ack(
            self.id,
            self.dxcall, 
            self.version,
            self.snr, flag_abort=self.abort)

        self.launch_transmit_and_wait(ack_frame, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
        if not self.abort:
            self.set_state(IRS_State.OPEN_ACK_SENT)
        return None, None

    def send_info_ack(self, info_frame):
        """Sends an acknowledgement to the ARQ session information frame.

        This method handles the reception of an ARQ_SESSION_INFO frame.
        It extracts session details, calibrates speed settings, and sends
        an ARQ_SESSION_INFO_ACK frame.

        Args:
            info_frame: The received ARQ_SESSION_INFO frame.

        Returns:
            Tuple[None, None]: Returns None for both data and type_byte as this method doesn't handle data.
        """
        # Get session info from ISS
        if self.received_bytes == 0:
            self.received_data = bytearray(info_frame['total_length'])
        self.total_length = info_frame['total_length']
        self.total_crc = info_frame['total_crc']
        self.dx_snr.append(info_frame['snr'])
        self.type_byte = info_frame['type']

        self.calibrate_speed_settings()

        self.log(f"New transfer of {self.total_length} bytes, received_bytes: {self.received_bytes}")
        self.ctx.event_manager.send_arq_session_new(False, self.id, self.dxcall, self.total_length, self.state.name)

        info_ack = self.frame_factory.build_arq_session_info_ack(
            self.id, self.received_bytes, self.snr,
            self.speed_level, self.frames_per_burst, flag_abort=self.abort)
        self.launch_transmit_and_wait(info_ack, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
        if not self.abort:
            self.set_state(IRS_State.INFO_ACK_SENT)
        return None, None

    def process_incoming_data(self, frame):
        """Processes incoming data from a received frame.

        This method handles incoming data frames, checking for offset
        mismatches, extracting the relevant data portion, and updating the
        received data buffer. It also logs progress and sends ARQ session
        progress events.

        Args:
            frame: The received data frame.

        Returns:
            bool: True if the data was processed successfully, False otherwise.
        """
        if frame['offset'] != self.received_bytes:
            # TODO: IF WE HAVE AN OFFSET BECAUSE OF A SPEED LEVEL CHANGE FOR EXAMPLE,
            # TODO: WE HAVE TO DISCARD THE LAST BYTES, BUT NOT returning False!!
            # CASE: ACK is going lost.
            self.log(f"Discarding data offset: Offset = {frame['offset']} | Already received: {self.received_bytes}", isWarning=True)
            self.received_bytes = frame['offset']
            

            #return False

        # update p2p connection timeout
        if self.running_p2p_connection:
            try:
                self.running_p2p_connection.last_data_timestamp = time.time()
                self.running_p2p_connection.running_arq_session = self
                self.running_p2p_connection.set_state(P2PStates.ARQ_SESSION)
            except Exception as e:
                self.log(f"Error handling P2P states: {e}")

        remaining_data_length = self.total_length - self.received_bytes
        self.log(f"Remaining data: {remaining_data_length}", isWarning=True)
        # Is this the last data part?
        if remaining_data_length < len(frame['data']):
            # we only want the remaining length, not the entire frame data 
            data_part = frame['data'][:remaining_data_length]
        else:
            # we want the entire frame data
            data_part = frame['data']
            

            

        self.received_data[frame['offset']:] = data_part
        #self.received_bytes += len(data_part)
        self.received_bytes = len(self.received_data)
        print(self.received_bytes)
        print(self.received_data)
        self.log(f"Received {self.received_bytes}/{self.total_length} bytes")
        self.ctx.event_manager.send_arq_session_progress(
            False, self.id, self.dxcall, self.received_bytes, self.total_length, self.state.name, self.speed_level, self.calculate_session_statistics(self.received_bytes, self.total_length))

        return True

    def receive_data(self, burst_frame):
        """Receives and processes a burst data frame.

        This method processes the incoming burst data frame, sends acknowledgements,
        updates session state, and handles session completion or failure.

        Args:
            burst_frame: The received ARQ_BURST_FRAME.

        Returns:
            Tuple[bytearray, int]: The received data and its type byte if the transmission is successful,
                                    or None, None in case of failure or incomplete transmission.
        """
        self.process_incoming_data(burst_frame)
        # update statistics
        self.update_histograms(self.received_bytes, self.total_length)

        if not self.all_data_received():
            self.calibrate_speed_settings(burst_frame=burst_frame)
            ack = self.frame_factory.build_arq_burst_ack(
                self.id,
                self.speed_level,
                flag_abort=self.abort
            )

            self.set_state(IRS_State.BURST_REPLY_SENT)
            self.ctx.event_manager.send_arq_session_progress(False, self.id, self.dxcall, self.received_bytes,
                                                         self.total_length, self.state.name, self.speed_level,
                                                         statistics=self.calculate_session_statistics(
                                                             self.received_bytes, self.total_length))

            self.launch_transmit_and_wait(ack, self.TIMEOUT_DATA, mode=FREEDV_MODE.signalling_ack)
            return None, None

        if self.final_crc_matches():
            self.log("All data received successfully!")
            ack = self.frame_factory.build_arq_burst_ack(self.id,
                                                         self.speed_level,
                                                         flag_final=True,
                                                         flag_checksum=True)
            self.transmit_frame(ack, mode=FREEDV_MODE.signalling_ack)
            self.log("ACK sent")
            self.session_ended = time.time()
            self.set_state(IRS_State.ENDED)

            return self.received_data, self.type_byte
        else:
            ack = self.frame_factory.build_arq_burst_ack(self.id,
                                                         self.speed_level,
                                                         flag_final=True,
                                                         flag_checksum=False)
            self.transmit_frame(ack, mode=FREEDV_MODE.signalling_ack)
            self.log("CRC fail at the end of transmission!", isWarning=True)
            self.log(f"Failed Data [Is real:{len(self.received_data)}/Is calculated:{seld.received_bytes}/Should:{self.total_bytes}]:{self.received_data}",isWarning=True)
            return self.transmission_failed()


    def calibrate_speed_settings(self, burst_frame=None):
        """Calibrates the speed settings for the ARQ session based on SNR and bandwidth.

        This method determines the appropriate speed level for the session based on the
        current SNR and available bandwidth. It also sets the appropriate decode
        modes for the modem, ensuring smooth transitions between speed levels.

        Args:
            burst_frame (optional): The received burst frame, used to get the sender's speed level. Defaults to None.

        Returns:
            int: The calculated speed level.
        """
        if burst_frame:
            received_speed_level = burst_frame['speed_level']
        else:
            received_speed_level = 0

        latest_snr = self.snr if self.snr else -10
        appropriate_speed_level = self.get_appropriate_speed_level(latest_snr, self.maximum_bandwidth)
        modes_to_decode = {}

        # Log the latest SNR, current, appropriate speed levels, and the previous speed level
        self.log(
            f"Latest SNR: {latest_snr}, Current Speed Level: {self.speed_level}, Appropriate Speed Level: {appropriate_speed_level}, Previous Speed Level: {self.previous_speed_level}",
            isWarning=True)

        # Adjust the speed level by one step towards the appropriate level, if needed
        #if appropriate_speed_level > self.speed_level and self.speed_level < len(self.SPEED_LEVEL_DICT) - 1:
        #    # we need to ensure, the received data is equal to our speed level before changing it
        #    if received_speed_level == self.speed_level:
        #        self.speed_level += 1
        #elif appropriate_speed_level < self.speed_level and self.speed_level > 0:
        #    #if received_speed_level == self.speed_level:
        #    #    self.speed_level -= 1

        # Always decode the current mode
        current_mode = self.get_mode_by_speed_level(self.speed_level).value
        modes_to_decode[current_mode] = True

        # Update previous speed level
        if self.previous_speed_level != self.speed_level:
            self.previous_speed_level = self.speed_level  # Update the previous speed level

        # Ensure, previous mode is decoded as well
        previous_mode = self.get_mode_by_speed_level(self.previous_speed_level).value
        modes_to_decode[previous_mode] = True

        # Ensure, appropriate mode is decoded as well
        appropriate_mode = self.get_mode_by_speed_level(appropriate_speed_level).value
        modes_to_decode[appropriate_mode] = True

        self.log(f"Modes to Decode: {list(modes_to_decode.keys())}", isWarning=True)
        # Apply the new decode mode based on the updated and previous speed levels
        self.ctx.rf_modem.demodulator.set_decode_mode(modes_to_decode)

        # finally update the speed level to the appropriate one
        self.speed_level = appropriate_speed_level

        return self.speed_level

    def abort_transmission(self):
        """Aborts the ARQ transmission.

        This method sets the abort flag to True, signaling that the
        transmission should be terminated.
        """
        self.log("Aborting transmission... setting abort flag")
        self.abort = True

    def send_stop_ack(self, stop_frame):
        """Sends a stop acknowledgement and finalizes the session.

        This method handles the reception of an ARQ_STOP frame by sending an
        ARQ_STOP_ACK, setting the session state to ABORTED, and sending session
        finished events. It also pushes session statistics if enabled.

        Args:
            stop_frame: The received ARQ_STOP frame.

        Returns:
            Tuple[None, None]: Returns None for both data and type_byte as this method doesn't handle data.
        """
        stop_ack = self.frame_factory.build_arq_stop_ack(self.id)
        self.launch_transmit_and_wait(stop_ack, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling_ack)
        self.set_state(IRS_State.ABORTED)
        self.ctx.state_manager.setARQ(False)
        session_stats = self.calculate_session_statistics(self.received_bytes, self.total_length)

        self.ctx.event_manager.send_arq_session_finished(
                False, self.id, self.dxcall, False, self.state.name, statistics=session_stats)
        if self.ctx.config_manager.config['STATION']['enable_stats']:
            self.statistics.push(self.state.name, session_stats, self.dxcall)

        return None, None

    def transmission_failed(self, irs_frame=None):
        """Handles transmission failures.

        This method is called when a transmission fails. It sets the session
        state to FAILED, logs the failure, sends session finished events,
        pushes session statistics if enabled, and disables ARQ.

        Args:
            irs_frame (optional): The received IRS frame. Defaults to None.

        Returns:
            Tuple[None, None]: Returns None for both data and type_byte.
        """
        # final function for failed transmissions
        self.session_ended = time.time()
        self.set_state(IRS_State.FAILED)
        self.log("Transmission failed!")
        #self.ctx.rf_modem.demodulator.set_decode_mode()
        session_stats = self.calculate_session_statistics(self.received_bytes, self.total_length)
        self.received_data = None
        self.received_bytes = 0
        self.ctx.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, statistics=session_stats)
        if self.ctx.config_manager.config['STATION']['enable_stats']:
            self.statistics.push(self.state.name, session_stats, self.dxcall)

        self.ctx.state_manager.setARQ(False)
        return None, None

    def transmission_aborted(self):
        """Handles the abortion of the transmission.

        This method is called when the transmission is aborted. It sets the
        session state to ABORTED, logs the abortion, sends session finished
        events, and disables ARQ.

        Returns:
            Tuple[None, None]: Returns None for both data and type_byte.
        """
        if self.state not in [IRS_State.ABORTED, IRS_State.ENDED]:
            self.log("session aborted")
            self.session_ended = time.time()
            self.set_state(IRS_State.ABORTED)
            # break actual retries
            self.event_frame_received.set()


            #self.ctx.rf_modem.demodulator.set_decode_mode()
            self.ctx.event_manager.send_arq_session_finished(
                True, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics(self.received_bytes, self.total_length))
            self.ctx.state_manager.setARQ(False)
        return None, None