/*
 * Autor: Giovani Facchini giovani@exatas.unisinos.br

 * Transmissor que aplica algoritmos do TCP em nível de aplicação.
 * Utiliza o UDP para se comunicar mas implementa as funcionalidades no programa
 * Desenvolvido para sistemas UNIX Like (Linux, FreeBSD, NetBSD...)
 */

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/time.h>

#define CWNDMAX 100 
#define QTD_DADOS 1492
#define TIMEOUTMAX 6000000
#define TIMEOUTMIN 150000
//#define DEBUG   /*Descomente essa linha para ver o que o transmissor está fazendo*/

/* rotina que envia os pacotes desde a base até base+cwnd
   esta rotina é chamada toda a vez que ocorre um timeout ou uma inveresão de pacotes. Ela reenviará
   todos os pacotes da janela
*/
void send_packets(unsigned long pacotes[CWNDMAX], unsigned long base, unsigned long cwnd, int s, struct sockaddr_in peer, int fd) {
	int i, peerlen, lidos;
	
	struct buf {
		char buffer[QTD_DADOS];
		unsigned long long seq;
                int tam;
	} buffer;
	struct timeval tv;
	
	peerlen = sizeof(peer);
	//posiciona o file poiter no local onde dever começar a serem lidos os dados
	lseek(fd,(base*QTD_DADOS) ,SEEK_SET);
	//este for envia os pacostes e armazena o tempo de envio no vetor
	for (i = 0 ; i<cwnd && (lidos = read(fd, &buffer.buffer, QTD_DADOS)) > 0; i++){
		buffer.seq = base+i+1;
                buffer.tam = lidos;
		gettimeofday(&tv,NULL);
		pacotes[(base+1+i)%CWNDMAX] = tv.tv_sec*1000000 + tv.tv_usec;
		sendto(s,&buffer,sizeof(buffer),0,(struct sockaddr *) &peer,peerlen);	
	}
};

int main(int argc, char **argv){
	struct sockaddr_in peer;
	int s;//socket
	int porta, peerlen, rc, i, is_timeout;
	char ip[15], path[255], hand[10];//ip, caminho do arquivo a ser enviado, hand-shake
	struct buf {
		char buffer[QTD_DADOS];
		unsigned long long seq;
                int tam;
	}buffer;//estrutura do pacote
	int fd;//descritor do arquivo a ser transmitido
	struct timeval tv;
	unsigned long base=0, cwnd =2, topo=2, size, final, pkt, timeout, RTT=500000, tempo;
	float cwnd_real = 2;
	int lidos;//bytes lidos
	long pacotes[CWNDMAX];//vetor de tempo de envio dos pacotes usado de maneira cirvular
	//base informa até onde já chegou e topo qual o último elemento que foi mandado
	
	//confere se todos os parâmetros foram passados
	if(argc < 7) {
		printf("Falta parametros\n");
		printf("Parametros:\n");
		printf("-h <numero_ip>\n");
		printf("-p <porta>\n");
		printf("-n <nome_do_arquivo>\n");
		exit(1);
	}

	// Pega parametros
	for(i=1;i < argc; i++){
		if(argv[i][0]=='-') {
			switch(argv[i][1]) {
				case 'h': // Perametro IP
					i++;
					strcpy(ip,argv[i]);
					break;

					// Pega o parametro porta
				case 'p':
					i++;
					porta = atoi(argv[i]);
					if(porta < 1024) {
						printf("Valor da porta invalido\n");
						exit(1);
					}
					break;
				case 'n':
					i++;
					strcpy(path, argv[i]);
					break;

				default:
					printf("Parametro recebido invalido %d:%s\n",i,argv[i]);
					exit(1);
			}		  	 

		} else {
			printf("Parametro %d %s invalido\n",i,&argv[i]); 
			exit(1);
		}
	}
	
	// define os parâmetros de peer
	peer.sin_family = AF_INET;
	peer.sin_port = htons(porta);
	peer.sin_addr.s_addr = inet_addr(ip); 

	//cria o socket
	s = socket(AF_INET, SOCK_DGRAM,0);
	if( s < 0) {
		printf("Falha na criacao do socket\n");
		exit(1);
	}

	peerlen = sizeof(peer);

	// Envia um pacote com SIN
	strcpy(hand,"SIN");
	sendto(s,hand,sizeof(hand),0,(struct sockaddr *) &peer,peerlen);
#ifdef DEBUG
	printf("Enviado SIN\n");
#endif

	// Recebe um pacote com SIN+ACK
	rc = recvfrom(s,hand,sizeof(hand),0,(struct sockaddr *) &peer,(socklen_t *) &peerlen);
#ifdef DEBUG
	printf("Recebido %s\n",&hand);
#endif

	// Envia um pacote com ACK
	strcpy(hand,"ACK");
	sendto(s,hand,sizeof(hand),0,(struct sockaddr *) &peer,peerlen);
#ifdef DEBUG
	printf("Enviado ACK\n");
#endif

	//abre o arquivo que será enviado
	if (!(fd = open(path, O_RDONLY))){
		printf("Falha ao abrir arquivo.\n");
		exit(1);
	}
	//pega tamanho do arquivo e diz qual será o número de pacotes à enviar
	size = lseek(fd, 0, SEEK_END);	
	lseek(fd, 0,SEEK_SET);	
	final = ((int)(size / QTD_DADOS)) + 1;
#ifdef DEBUG
	printf("Último pacote %ld\n", final);
#endif
	timeout = 4 * RTT;
	//repetição que cuida do envio e garantia de recebimento de todos os pacotes
	while(base < final){//or pkt != 0
		send_packets(pacotes, base, cwnd, s, peer, fd);//envia todos os pacotes da janela
		is_timeout = 0;
		topo = base + cwnd;
		//está repetição controla se ocorreu timeout, troca de pacotes e envia o próximo da janela
		while ( is_timeout == 0){
			//tenta receber um pacote
			rc = recvfrom(s,&pkt,sizeof(unsigned long), MSG_DONTWAIT, (struct sockaddr *) &peer, (socklen_t *) &peerlen);
			//testa as 4 possibilidades de recepção
			//novo cwnd
			//novo RTT
			if (rc>0){
				if ((base+1)==pkt){
					//se o ack recebido é o esperado
					base++;
					gettimeofday(&tv, NULL);
					tempo = ((tv.tv_sec*1000000) + tv.tv_usec) - pacotes[((base +1)% CWNDMAX)];
					RTT = (RTT*0.8)+(tempo*0.2);
#ifdef DEBUG
					printf("Recebido ACK %d || Timeout: %ld || CWND: %ld || Base: %ld\n", pkt, timeout, cwnd, base );
#endif
					//testa os limites dos timeout e o reajusta
					if ((timeout >= TIMEOUTMAX) || (4*RTT > TIMEOUTMAX))
						timeout = TIMEOUTMAX;
					else if (4*RTT < TIMEOUTMIN)
						timeout = TIMEOUTMIN;
					else timeout = 4*RTT;
					//teste os limites do cnwd e o reajusta
					if (cwnd_real < CWNDMAX){
						cwnd_real = cwnd_real + (1/cwnd_real);
						cwnd = (unsigned long) cwnd_real;
					}
					
					//envia os próximos pacotes		
					for (i = topo ; i<(base + cwnd) && (lidos = read(fd, &buffer.buffer, QTD_DADOS)) > 0; i++){
						buffer.seq = i+1;
						buffer.tam = lidos;
						gettimeofday(&tv,NULL);
						//printf("%s\n", lidos);
						pacotes[((1 + i) %CWNDMAX)] = tv.tv_sec*1000000 + tv.tv_usec;
						sendto(s,&buffer,sizeof(buffer),0,(struct sockaddr *) &peer,peerlen);
					}				
					topo = base + cwnd;
					if (base == final) 
						is_timeout=1;

				} else if (pkt>base+1){//se esse if der false ignora o pacote pois recebeu um ack de pacotes já confirmados
					//pacote errado
					//usa a variável is_timeout apenas pra sair do laço, mas assume que os pacotes inverteram
#ifdef DEBUG
					printf("Recebido ACK errado %d\n", pkt);
#endif
					is_timeout = 1;
					//reajusta cwnd (observando mínimo de 1)
					if (cwnd_real>=2){
						cwnd_real = (float)cwnd_real/2;
						cwnd = (unsigned long) cwnd_real;
					} else {
						cwnd_real = 1.0;
						cwnd = 1;
					}
				} 
			} else {
				//testa timeout
				gettimeofday(&tv, NULL);
				tempo = ((tv.tv_sec*1000000) + tv.tv_usec) - pacotes[((base + 1)% CWNDMAX)];			
				if (tempo > timeout){
					//timeout ocorreu
					//observa limites do timeout e o redefine
					if ((timeout >= TIMEOUTMAX) || (4*timeout > TIMEOUTMAX)){
						timeout = TIMEOUTMAX;
						RTT = 2*RTT;
					}
					else if (4*RTT < TIMEOUTMIN)
						timeout = TIMEOUTMIN;
					else{
						timeout = 4*timeout;
						RTT = 2*RTT;
					}
					is_timeout = 1;
					//observa limites da janela e a redefine
					if (cwnd_real>=2){
						cwnd_real = (float)cwnd_real/2;
						cwnd = (unsigned long) cwnd_real;
					} else {
						cwnd_real = 1.0;
						cwnd = 1;
					}
#ifdef DEBUG
					printf("Timeout do pacote %d || Timeout em: %ldus || CWND: %ld\n", base+1, timeout, cwnd);
#endif

				}
			}		
		 
		}
	
	}
	//finaliza conexão
	i = 0;
	while ((pkt!=0) && (i<10)) {
		//quando pkt = 0, recebeu ack de confirmação de fim de conexão
		//se o ack se perdeu, ele envia 10X a terminação e assume que o receptor recebeu a finalização
		peerlen = sizeof(peer);
		buffer.seq =0;
                buffer.tam = 0;
		sendto(s,&buffer,sizeof(buffer),0,(struct sockaddr *) &peer,peerlen);
#ifdef DEBUG
		printf("Enviando fim\n"); 
#endif
		sleep(1);
		rc = recvfrom(s,&pkt,sizeof(unsigned long), MSG_DONTWAIT, (struct sockaddr *) &peer, (socklen_t *) &peerlen);
#ifdef DEBUG
		printf("PKT %ld\n",pkt);
#endif
		if (rc == -1) {
#ifdef DEBUG
			printf("Pacote vazio\n");
#endif
			pkt = 1;
			i++;
		}
	}

	//fecha os arquivos
	close(s);
	close(fd);


}

