import sys
import time
import unittest
import unittest.mock
import queue
import threading
import random
import structlog
import base64
import numpy as np

sys.path.append('freedata_server')

from config import CONFIG
from context import AppContext
from event_manager import EventManager
from state_manager import StateManager
from data_frame_factory import DataFrameFactory
from frame_dispatcher import DISPATCHER
import codec2
import command_message_send


class TestModem:
    def __init__(self, event_q, state_q):
        self.data_queue_received = queue.Queue()
        self.demodulator = unittest.mock.Mock()
        self.event_manager = EventManager([event_q])
        self.logger = structlog.get_logger('Modem')
        self.states = StateManager(state_q)

    def getFrameTransmissionTime(self, mode):
        samples = 0
        c2instance = codec2.open_instance(mode.value)
        samples += codec2.api.freedv_get_n_tx_preamble_modem_samples(c2instance)
        samples += codec2.api.freedv_get_n_tx_modem_samples(c2instance)
        samples += codec2.api.freedv_get_n_tx_postamble_modem_samples(c2instance)
        time = samples / 8000
        return time

    def transmit(self, mode, repeats: int, repeat_delay: int, frames: bytearray) -> bool:
        tx_time = self.getFrameTransmissionTime(mode) + 0.1
        self.logger.info(f"TX {tx_time} seconds...")
        threading.Event().wait(tx_time)

        transmission = {
            'mode': mode,
            'bytes': frames,
        }
        self.data_queue_received.put(transmission)


class TestMessageProtocol(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        cls.logger = structlog.get_logger("TESTS")

        # ISS

        cls.ctx_ISS = AppContext('freedata_server/config.ini.example')
        cls.ctx_ISS.TESTMODE = True
        cls.ctx_ISS.startup()


        # IRS
        cls.ctx_IRS = AppContext('freedata_server/config.ini.example')
        cls.ctx_IRS.TESTMODE = True
        cls.ctx_IRS.startup()

        # simulate a busy condition
        cls.ctx_IRS.state_manager.channel_busy_slot = [True, False, False, False, False]
        # Frame loss probability in %
        cls.loss_probability = 0


        cls.channels_running = False

    @classmethod
    def tearDownClass(cls):
        cls.ctx_IRS.shutdown()
        cls.ctx_ISS.shutdown()


    def channelWorker(self, ctx_a, ctx_b):
        while self.channels_running:
            try:
                # Station A gets the data from its transmit queue
                transmission = ctx_a.TESTMODE_TRANSMIT_QUEUE.get(timeout=1)
                print(f"Station A sending: {transmission[1]}", len(transmission[1]), transmission[0])

                transmission[1] += bytes(2) # 2bytes crc simulation

                if random.randint(0, 100) < self.loss_probability:
                    self.logger.info(f"[{threading.current_thread().name}] Frame lost...")
                    continue

                # Forward data from Station A to Station B's receive queue
                if ctx_b:
                    ctx_b.TESTMODE_RECEIVE_QUEUE.put(transmission)
                    self.logger.info(f"Data forwarded to Station B")

                frame_bytes = transmission[1]
                if len(frame_bytes) == 5:
                    mode_name = "SIGNALLING_ACK"
                else:
                    mode_name = transmission[0]

                snr = 15
                ctx_b.service_manager.frame_dispatcher.process_data(
                    frame_bytes, None, len(frame_bytes), snr, 0, mode_name=mode_name
                )

            except queue.Empty:
                continue

        self.logger.info(f"[{threading.current_thread().name}] Channel closed.")

    def waitForSession(self, event_queue, outbound=False):
        key = 'arq-transfer-outbound' if outbound else 'arq-transfer-inbound'
        while self.channels_running:
            try:
                ev = event_queue.get(timeout=2)
                if key in ev and ('success' in ev[key] or 'ABORTED' in ev[key]):
                    self.logger.info(f"[{threading.current_thread().name}] {key} session ended.")
                    break
            except queue.Empty:
                continue

    def establishChannels(self):
        self.channels_running = True
        self.channelA = threading.Thread(target=self.channelWorker,args=[self.ctx_ISS, self.ctx_IRS],name = "channelA")
        self.channelA.start()

        self.channelB = threading.Thread(target=self.channelWorker,args=[self.ctx_IRS, self.ctx_ISS],name = "channelB")
        self.channelB.start()

    def waitAndCloseChannels(self):
        self.waitForSession(self.ctx_ISS.modem_events, True)
        self.channels_running = False
        self.waitForSession(self.ctx_IRS.modem_events, False)
        self.channels_running = False

    def testMessageViaSession(self):
        self.loss_probability = 0  # no loss
        self.establishChannels()

        params = {
            'destination': "AA1AAA-1",
            'body': 'Hello World',
        }

        command = command_message_send.SendMessageCommand(self.ctx, params)
        command.run()

        #del cmd
        print(self.ctx_ISS.TESTMODE_EVENTS.empty())

        while not self.ctx_ISS.TESTMODE_EVENTS.empty():
            event = self.ctx_ISS.TESTMODE_EVENTS.get()
            success = event.get('arq-transfer-outbound', {}).get('success', None)
            if success is not None:
                self.assertTrue(success, f"Test failed because of wrong success: {success}")

if __name__ == '__main__':
    unittest.main()
