# emitator Reliable UDP
from helper import *
from argparse import ArgumentParser
import socket
import logging
import sys
import random
import traceback
from datetime import datetime
from threading import Thread, Lock, get_ident
import time

# logging.basicConfig(format = u'[LINE:%(lineno)d]# %(levelname)-8s [%(asctime)s]  %(message)s', level = logging.NOTSET)
logging.basicConfig(filename='emitator.log', filemode='w', format = u'[LINE:%(lineno)d]# %(levelname)-8s [%(asctime)s]  %(message)s', level = logging.NOTSET)

TIMEOUT_LOWERBOUND = 1
TIMEOUT_UPPERBOUND = 20

TIMEOUT = 3 # Stores our Smoothed Round Trip Time (SRTT)
ALPHA = 0.8 # Factor for Smoothing our RTT
INCREASE_CONSTANT = 1.1 # Factor for increasing the timeout, in case of socket.timeout

FINALIZE_TIMEOUT = 60 * 5 # 5 minutes

def smoothTimeout(currentTimeout, currentRTT):
    return max(ALPHA * currentTimeout + (1 - ALPHA) * currentRTT, TIMEOUT_LOWERBOUND)

def increaseTimeout(currentTimeout):
    return min(INCREASE_CONSTANT * currentTimeout, TIMEOUT_UPPERBOUND)

def connect(sock, adresa_receptor):
    global TIMEOUT
    '''
    Functie care initializeaza conexiunea cu receptorul.
    Returneaza ack_nr de la receptor si window
    '''
    seq_nr = random.randint(0, MAX_UINT32 // 2) # TODO: setati initial sequence number
    
    packet = SenderPacket(data = b'start connection', msg_type = 'S', seq_nr = seq_nr)
    mesaj = packet.raw

    while True:

        sock.sendto(mesaj, adresa_receptor)
        
        try:
            data, server = sock.recvfrom(MAX_SEGMENT)

            if verifica_checksum(data) is False:
                #daca checksum nu e ok, mesajul de la receptor trebuie ignorat
                logging.info("Mesajul de confirmare a fost corupt, retrying...")
                continue

            ack_nr, checksum, window = parse_header_receptor(data)

            if ack_nr != seq_nr + 1:
                logging.info("Mesajul are un Acknowledgement Number gresit, retrying....")
                continue

            logging.info('Ack Nr: "%d"', ack_nr)
            logging.info('Checksum: "%d"', checksum)
            logging.info('Window: "%d"', window)

            return ack_nr, window
        
        except socket.timeout as e:
            logging.info("Timeout la connect, retrying...")
            TIMEOUT = increaseTimeout(TIMEOUT)
            sock.settimeout(TIMEOUT)
            # TODO: cat timp nu primește confirmare de connect, incearca din nou



def finalize(sock, adresa_receptor, seq_nr):
    global TIMEOUT
    '''
    Functie care trimite mesajul de finalizare
    cu seq_nr dat ca parametru.
    '''
    # TODO:
    # folositi pasii de la connect() pentru a construi headerul
    # valorile de checksum si pentru a verifica primirea mesajului a avut loc
    
    packet = SenderPacket(data = b'close connection', msg_type = 'F', seq_nr = seq_nr)
    mesaj = packet.raw
    
    began_finalize = datetime.now()

    while True:

        if ((datetime.now() - began_finalize).total_seconds() > FINALIZE_TIMEOUT):
            logging.info("Waited for finalise acknowledgement too much, exiting...")
            return

        sock.sendto(mesaj, adresa_receptor)
        
        try:
            data, server = sock.recvfrom(MAX_SEGMENT)

            if verifica_checksum(data) is False:
                logging.info("Mesajul de confirmare a fost corupt, retrying...")
                continue

            ack_nr, checksum, window = parse_header_receptor(data)

            if ack_nr != seq_nr + 1:
                logging.info("Mesajul are un Acknowledgement Number gresit, retrying....")
                continue

            logging.info('Ack Nr: "%d"', ack_nr)
            logging.info('Checksum: "%d"', checksum)
            logging.info('Window: "%d"', window)

            return
        
        except socket.timeout as e:
            logging.info("Timeout la disconnect, retrying...")
            TIMEOUT = increaseTimeout(TIMEOUT)
            sock.settimeout(TIMEOUT)

    return


def main():
    global TIMEOUT
    parser = ArgumentParser(usage=__file__ + ' '
                                             '-a/--adresa IP '
                                             '-p/--port PORT'
                                             '-f/--fisier FILE_PATH',
                            description='Reliable UDP Emitter')

    parser.add_argument('-a', '--adresa',
                        dest='adresa',
                        default='receptor',
                        help='Adresa IP a receptorului (IP-ul containerului, localhost sau altceva)')

    parser.add_argument('-p', '--port',
                        dest='port',
                        default='10000',
                        help='Portul pe care asculta receptorul pentru mesaje')

    parser.add_argument('-f', '--fisier',
                        dest='fisier',
                        help='Calea catre fisierul care urmeaza a fi trimis')

    # Parse arguments
    args = vars(parser.parse_args())

    ip_receptor = args['adresa']
    port_receptor = int(args['port'])
    fisier = args['fisier']

    adresa_receptor = (ip_receptor, port_receptor)
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, proto=socket.IPPROTO_UDP)
    # setam timeout pe socket in cazul in care recvfrom nu primeste nimic in 3 secunde
    sock.settimeout(TIMEOUT)

    try:
        '''
        TODO:
        1. initializeaza conexiune cu receptor
        2. deschide fisier, citeste segmente de octeti
        3. trimite `window` segmente catre receptor,
         send trebuie sa trimită o fereastră de window segmente
         până primșete confirmarea primirii tuturor segmentelor
        4. asteapta confirmarea segmentelor, 
        in cazul pierderilor, retransmite fereastra sau doar segmentele lipsa
        5. in functie de diferenta de timp dintre trimitere si receptia confirmarii,
        ajusteaza timeout
        6. la finalul trimiterilor, notifica receptorul ca fisierul s-a incheiat
        '''
        seq_nr, window = connect(sock, adresa_receptor)
        file_descriptor = open(fisier, 'rb')

        segments = []               # payload-ul pentru fiecare segment din window
        segmentSentTime = []        # timpul la care s-a trimis fiecare segment din window
        segmentConfirmed = []       # True / False daca receptorul a confirmat primirea segmentului
        fileFinished = False

        start_window = seq_nr + 1
        windowBuffer = dict()

        mutex = Lock()

        def ListenForAck():
            nonlocal start_window
            nonlocal window
            nonlocal windowBuffer
            nonlocal fileFinished

            global TIMEOUT

            while fileFinished is False or window > 0:
                try:

                    data, server = sock.recvfrom(MAX_SEGMENT)
                    logging.info('Content primit: "%s"', data)

                    if verifica_checksum(data) is False:
                        logging.info("Emitatorul a primit un pachet corupt de la receptor!")
                        continue

                    r_ack_nr, r_checksum, r_window = parse_header_receptor(data)

                    mutex.acquire()

                    segmentPos = r_ack_nr - start_window
                    if (segmentPos >= 0 and segmentPos < window and segmentConfirmed[segmentPos] == False):
                        segmentConfirmed[segmentPos] = True
                        elapsedTime = datetime.now() - segmentSentTime[segmentPos]
                        TIMEOUT = smoothTimeout(TIMEOUT, elapsedTime.total_seconds())
                        sock.settimeout(TIMEOUT)

                    while len(segmentConfirmed) > 0 and segmentConfirmed[0] is True:
                        segmentConfirmed.pop(0)
                        segments.pop(0)
                        segmentSentTime.pop(0)
                        start_window += 1
                        window -= 1

                    if fileFinished is False:
                        window = max(window, r_window)

                    logging.info('Ack Nr: "%d"', r_ack_nr)
                    logging.info('Checksum: "%d"', r_checksum)
                    logging.info('Window: "%d"', r_window)
                    
                    mutex.release()
                    
                except socket.timeout as e:
                    logging.info("Timeout la asteptare pentru confirmare, retrying...")
                    mutex.acquire()
                    TIMEOUT = increaseTimeout(TIMEOUT)
                    sock.settimeout(TIMEOUT)
                    mutex.release()

        listener_thread = Thread(target=ListenForAck)
        listener_thread.start()

        while fileFinished is False or window > 0:
            # Race conditions
            mutex.acquire()

            # Mai citim din fisier in cazul cand s-a shiftat sau s-a marit window-ul
            for i in range(len(segments), window):
                segments.append(None)
                segmentConfirmed.append(False)
                segmentSentTime.append(None)
                if fileFinished is True:
                    window = i
                    break
                info = citeste_segment(file_descriptor)
                if len(info) == 0:
                    fileFinished = True
                    window = i
                    break
                segments[i] = info
            
            
            # Iteram si trimitem sau retrimitem in caz de timeout
            for i in range(window):
                if (segmentSentTime[i] == None) or (not segmentConfirmed[i] and (datetime.now() - segmentSentTime[i]).total_seconds() > TIMEOUT):
                    packet = SenderPacket(data = segments[i], msg_type='P', seq_nr=start_window + i)
                    sock.sendto(packet.raw, adresa_receptor)
                    segmentSentTime[i] = datetime.now()
                    TIMEOUT = increaseTimeout(TIMEOUT)

            mutex.release()

        listener_thread.join()

        finalize(sock, adresa_receptor, seq_nr + 1)
        sock.close()
        file_descriptor.close()
    except Exception as e:
        logging.exception(traceback.format_exc())
        sock.close()
        file_descriptor.close()



if __name__ == '__main__':
    main()
# emitator Reliable UDP
from helper import *
from argparse import ArgumentParser
import socket
import logging
import sys
import random
import traceback
from datetime import datetime
from threading import Thread, Lock, get_ident
import time

# logging.basicConfig(format = u'[LINE:%(lineno)d]# %(levelname)-8s [%(asctime)s]  %(message)s', level = logging.NOTSET)
logging.basicConfig(filename='emitator.log', filemode='w', format = u'[LINE:%(lineno)d]# %(levelname)-8s [%(asctime)s]  %(message)s', level = logging.NOTSET)

TIMEOUT_LOWERBOUND = 1
TIMEOUT_UPPERBOUND = 20

TIMEOUT = 3 # Stores our Smoothed Round Trip Time (SRTT)
ALPHA = 0.8 # Factor for Smoothing our RTT
INCREASE_CONSTANT = 1.1 # Factor for increasing the timeout, in case of socket.timeout

FINALIZE_TIMEOUT = 60 * 5 # 5 minutes

def smoothTimeout(currentTimeout, currentRTT):
    return max(ALPHA * currentTimeout + (1 - ALPHA) * currentRTT, TIMEOUT_LOWERBOUND)

def increaseTimeout(currentTimeout):
    return min(INCREASE_CONSTANT * currentTimeout, TIMEOUT_UPPERBOUND)

def connect(sock, adresa_receptor):
    global TIMEOUT
    '''
    Functie care initializeaza conexiunea cu receptorul.
    Returneaza ack_nr de la receptor si window
    '''
    seq_nr = random.randint(0, MAX_UINT32 // 2) # TODO: setati initial sequence number
    
    packet = SenderPacket(data = b'start connection', msg_type = 'S', seq_nr = seq_nr)
    mesaj = packet.raw

    while True:

        sock.sendto(mesaj, adresa_receptor)
        
        try:
            data, server = sock.recvfrom(MAX_SEGMENT)

            if verifica_checksum(data) is False:
                #daca checksum nu e ok, mesajul de la receptor trebuie ignorat
                logging.info("Mesajul de confirmare a fost corupt, retrying...")
                continue

            ack_nr, checksum, window = parse_header_receptor(data)

            if ack_nr != seq_nr + 1:
                logging.info("Mesajul are un Acknowledgement Number gresit, retrying....")
                continue

            logging.info('Ack Nr: "%d"', ack_nr)
            logging.info('Checksum: "%d"', checksum)
            logging.info('Window: "%d"', window)

            return ack_nr, window
        
        except socket.timeout as e:
            logging.info("Timeout la connect, retrying...")
            TIMEOUT = increaseTimeout(TIMEOUT)
            sock.settimeout(TIMEOUT)
            # TODO: cat timp nu primește confirmare de connect, incearca din nou



def finalize(sock, adresa_receptor, seq_nr):
    global TIMEOUT
    '''
    Functie care trimite mesajul de finalizare
    cu seq_nr dat ca parametru.
    '''
    # TODO:
    # folositi pasii de la connect() pentru a construi headerul
    # valorile de checksum si pentru a verifica primirea mesajului a avut loc
    
    packet = SenderPacket(data = b'close connection', msg_type = 'F', seq_nr = seq_nr)
    mesaj = packet.raw
    
    began_finalize = datetime.now()

    while True:

        if ((datetime.now() - began_finalize).total_seconds() > FINALIZE_TIMEOUT):
            logging.info("Waited for finalise acknowledgement too much, exiting...")
            return

        sock.sendto(mesaj, adresa_receptor)
        
        try:
            data, server = sock.recvfrom(MAX_SEGMENT)

            if verifica_checksum(data) is False:
                logging.info("Mesajul de confirmare a fost corupt, retrying...")
                continue

            ack_nr, checksum, window = parse_header_receptor(data)

            if ack_nr != seq_nr + 1:
                logging.info("Mesajul are un Acknowledgement Number gresit, retrying....")
                continue

            logging.info('Ack Nr: "%d"', ack_nr)
            logging.info('Checksum: "%d"', checksum)
            logging.info('Window: "%d"', window)

            return
        
        except socket.timeout as e:
            logging.info("Timeout la disconnect, retrying...")
            TIMEOUT = increaseTimeout(TIMEOUT)
            sock.settimeout(TIMEOUT)

    return


def main():
    global TIMEOUT
    parser = ArgumentParser(usage=__file__ + ' '
                                             '-a/--adresa IP '
                                             '-p/--port PORT'
                                             '-f/--fisier FILE_PATH',
                            description='Reliable UDP Emitter')

    parser.add_argument('-a', '--adresa',
                        dest='adresa',
                        default='receptor',
                        help='Adresa IP a receptorului (IP-ul containerului, localhost sau altceva)')

    parser.add_argument('-p', '--port',
                        dest='port',
                        default='10000',
                        help='Portul pe care asculta receptorul pentru mesaje')

    parser.add_argument('-f', '--fisier',
                        dest='fisier',
                        help='Calea catre fisierul care urmeaza a fi trimis')

    # Parse arguments
    args = vars(parser.parse_args())

    ip_receptor = args['adresa']
    port_receptor = int(args['port'])
    fisier = args['fisier']

    adresa_receptor = (ip_receptor, port_receptor)
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, proto=socket.IPPROTO_UDP)
    # setam timeout pe socket in cazul in care recvfrom nu primeste nimic in 3 secunde
    sock.settimeout(TIMEOUT)

    try:
        '''
        TODO:
        1. initializeaza conexiune cu receptor
        2. deschide fisier, citeste segmente de octeti
        3. trimite `window` segmente catre receptor,
         send trebuie sa trimită o fereastră de window segmente
         până primșete confirmarea primirii tuturor segmentelor
        4. asteapta confirmarea segmentelor, 
        in cazul pierderilor, retransmite fereastra sau doar segmentele lipsa
        5. in functie de diferenta de timp dintre trimitere si receptia confirmarii,
        ajusteaza timeout
        6. la finalul trimiterilor, notifica receptorul ca fisierul s-a incheiat
        '''
        seq_nr, window = connect(sock, adresa_receptor)
        file_descriptor = open(fisier, 'rb')

        segments = []               # payload-ul pentru fiecare segment din window
        segmentSentTime = []        # timpul la care s-a trimis fiecare segment din window
        segmentConfirmed = []       # True / False daca receptorul a confirmat primirea segmentului
        fileFinished = False

        start_window = seq_nr + 1
        windowBuffer = dict()

        mutex = Lock()

        def ListenForAck():
            nonlocal start_window
            nonlocal window
            nonlocal windowBuffer
            nonlocal fileFinished

            global TIMEOUT

            while fileFinished is False or window > 0:
                try:

                    data, server = sock.recvfrom(MAX_SEGMENT)
                    logging.info('Content primit: "%s"', data)

                    if verifica_checksum(data) is False:
                        logging.info("Emitatorul a primit un pachet corupt de la receptor!")
                        continue

                    r_ack_nr, r_checksum, r_window = parse_header_receptor(data)

                    mutex.acquire()

                    segmentPos = r_ack_nr - start_window
                    if (segmentPos >= 0 and segmentPos < window and segmentConfirmed[segmentPos] == False):
                        segmentConfirmed[segmentPos] = True
                        elapsedTime = datetime.now() - segmentSentTime[segmentPos]
                        TIMEOUT = smoothTimeout(TIMEOUT, elapsedTime.total_seconds())
                        sock.settimeout(TIMEOUT)

                    while len(segmentConfirmed) > 0 and segmentConfirmed[0] is True:
                        segmentConfirmed.pop(0)
                        segments.pop(0)
                        segmentSentTime.pop(0)
                        start_window += 1
                        window -= 1

                    if fileFinished is False:
                        window = max(window, r_window)

                    logging.info('Ack Nr: "%d"', r_ack_nr)
                    logging.info('Checksum: "%d"', r_checksum)
                    logging.info('Window: "%d"', r_window)
                    
                    mutex.release()
                    
                except socket.timeout as e:
                    logging.info("Timeout la asteptare pentru confirmare, retrying...")
                    mutex.acquire()
                    TIMEOUT = increaseTimeout(TIMEOUT)
                    sock.settimeout(TIMEOUT)
                    mutex.release()

        listener_thread = Thread(target=ListenForAck)
        listener_thread.start()

        while fileFinished is False or window > 0:
            # Race conditions
            mutex.acquire()

            # Mai citim din fisier in cazul cand s-a shiftat sau s-a marit window-ul
            for i in range(len(segments), window):
                segments.append(None)
                segmentConfirmed.append(False)
                segmentSentTime.append(None)
                if fileFinished is True:
                    window = i
                    break
                info = citeste_segment(file_descriptor)
                if len(info) == 0:
                    fileFinished = True
                    window = i
                    break
                segments[i] = info
            
            
            # Iteram si trimitem sau retrimitem in caz de timeout
            for i in range(window):
                if (segmentSentTime[i] == None) or (not segmentConfirmed[i] and (datetime.now() - segmentSentTime[i]).total_seconds() > TIMEOUT):
                    packet = SenderPacket(data = segments[i], msg_type='P', seq_nr=start_window + i)
                    sock.sendto(packet.raw, adresa_receptor)
                    segmentSentTime[i] = datetime.now()
                    TIMEOUT = increaseTimeout(TIMEOUT)

            mutex.release()

        listener_thread.join()

        finalize(sock, adresa_receptor, seq_nr + 1)
        sock.close()
        file_descriptor.close()
    except Exception as e:
        logging.exception(traceback.format_exc())
        sock.close()
        file_descriptor.close()



if __name__ == '__main__':
    main()
# emitator Reliable UDP # logging.basicConfig(format = u'[LINE:%(lineno)d]# %(levelname)-8s [%(asctime)s]  %(message)s', level = logging.NOTSET) # %(levelname)-8s [%(asctime)s]  %(message)s', level = logging.NOTSET) # Stores our Smoothed Round Trip Time (SRTT) # Factor for Smoothing our RTT # Factor for increasing the timeout, in case of socket.timeout # 5 minutes Functie care initializeaza conexiunea cu receptorul.     Returneaza ack_nr de la receptor si window # TODO: setati initial sequence number #daca checksum nu e ok, mesajul de la receptor trebuie ignorat # TODO: cat timp nu primește confirmare de connect, incearca din nou Functie care trimite mesajul de finalizare     cu seq_nr dat ca parametru. # TODO: # folositi pasii de la connect() pentru a construi headerul # valorile de checksum si pentru a verifica primirea mesajului a avut loc # Parse arguments # setam timeout pe socket in cazul in care recvfrom nu primeste nimic in 3 secunde TODO:         1. initializeaza conexiune cu receptor         2. deschide fisier, citeste segmente de octeti         3. trimite `window` segmente catre receptor,          send trebuie sa trimită o fereastră de window segmente          până primșete confirmarea primirii tuturor segmentelor         4. asteapta confirmarea segmentelor,          in cazul pierderilor, retransmite fereastra sau doar segmentele lipsa         5. in functie de diferenta de timp dintre trimitere si receptia confirmarii,         ajusteaza timeout         6. la finalul trimiterilor, notifica receptorul ca fisierul s-a incheiat # payload-ul pentru fiecare segment din window # timpul la care s-a trimis fiecare segment din window # True / False daca receptorul a confirmat primirea segmentului # Race conditions # Mai citim din fisier in cazul cand s-a shiftat sau s-a marit window-ul # Iteram si trimitem sau retrimitem in caz de timeout