#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>

#include <stdio.h>
#include <string.h>
#include <pthread.h>

#include "UDPoverTCPtunel.h"

//Identificador de los sockets UDP y TCP
int udpSocketId, tcpSocketId;

//Información del Servidor Remoto
int dataLoaded = 0; //1: Los datos fueron recibidos
char host[64];
int port;

/***************************************************************
 * Queda en escucha por paquetes UDP,
 * para cada uno: 
 *   - Calcula su tamaño en bytes,
 *   - Envía un string con el tamaño del paquete al tunel TCP,
 *   - Envía el delimitador \0 al tunel TCP,
 *   - Envía la data del paquete UDP al tunel TCP,
 ***************************************************************/ 
static void *udp2tcp(void *argv){
  int n, dataSize;
  char data[BUFSIZE];
  char sDataSize[5]; //max: "xxxx\n"
  while(1){ //Termina con un break por algún error de I/O en los sockets
    //Espera por un paquete UDP
    bzero(data,BUFSIZE);
    n = read(udpSocketId,data,BUFSIZE);
    if(n <= 0){ printf("Can't read datagram\n"); break;}
    
    //Guarda el tamaño del paquete
    dataSize = n;
    printf("Received %d bytes by UDP...\n",dataSize);
    
    //Envía al tunel TCP el largo del paquete en un string ASCII
    sprintf(sDataSize,"%d",n);
    n = write(tcpSocketId,sDataSize,strlen(sDataSize));
    if(n <= 0){ printf("Can't write datasize\n"); break;}
    //Envía el delimitador tamaño-data
    n = write(tcpSocketId,"\0",1);
    //Envía la data del paquete UDP por el tunel TCP
    if(n <= 0){ printf("Can't write datasize end\n"); break;}
    n = write(tcpSocketId,data,dataSize);
    if(n <= 0){ printf("Can't write datagram\n"); break;}
    printf("Forwarding %d bytes to TCP tunnel...\n",dataSize);
    
  }
}

/***************************************************************
 * Queda en escucha por paquetes TCP:
 *   - Recibe de a 1 los bytes del string que indica el tamaño de la data
 *     hasta encontrar el delimitador \0,
 *   - Recibe los paquetes siguientes acumulándolos en el buffer hasta completar
 *     el tamaño de la data indicado por el string recibido anteriormente, 
 *   - Envía la data acumulada en el buffer al socket UDP.
 ***************************************************************/ 
static void *tcp2udp(void *argv){
  char data[BUFSIZE];
  char sDataSize[5]; //max: "xxxx\n"
  int n, dataSize, offset;
  
  do{ //Termina con un paquete sin datos
    //Espera por un paquete TCP con el tamaño de la data en formato ASCII
    offset = 0;
    //Obtiene 1 a 1 los bytes del string ASCII con el tamaño de la data
    do{
      if(read(tcpSocketId, &sDataSize[offset], 1) != 1){
        printf("Failed at reading!\n");
	
        return 0;
      }
      offset++;
    }while(sDataSize[offset-1]!=0); //hasta encontrar el delimitador \0
    
    //Guarda el tamaño de la data
    dataSize = atoi(sDataSize);
    printf("Received %d bytes from TCP tunnel...\n",dataSize);
    
    //Espera por los paquetes TCP con la data
    offset = 0;
    //Lee uno a uno los paquetes hasta que se alcanza el tamaño especificado de la data 
    do{
      if ((n = read(tcpSocketId, &data[offset], dataSize - offset)) <= 0){
        printf("Failed at reading!\n");
        return 0;
      }
      offset += n;
    } while (offset < dataSize);
    
    //Envía la data obtenida del tunel TCP hacia el socket UDP
    printf("Forwarding %d bytes by UDP...\n",dataSize);
    send(udpSocketId, data, dataSize, 0);
  
  } while (dataSize != 0);
  
}

/***************************************************************
 * Crea una hebra para enviar datos desde el tunel TCP al socket UDP
 ***************************************************************/ 
int tcp2udpThread(pthread_t *tid){
  pthread_attr_t attr;
  pthread_attr_init(&attr);
  //permite que al thread se le pueda hacer join()
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
  return pthread_create(tid, &attr, tcp2udp, NULL);              
}

/***************************************************************
 * Crea una hebra para enviar datos desde el socket UDP al tunel TCP
 ***************************************************************/ 
int udp2tcpThread(pthread_t *tid){
  pthread_attr_t attr;
  pthread_attr_init(&attr);
  //permite que al thread se le pueda hacer join()
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
  return pthread_create(tid, &attr, udp2tcp, NULL);              
}

/***************************************************************
 * Carga la información del servidor remoto a las variables globales
 ***************************************************************/ 
int loadServerData(char ahost[64], int aport){
  strcpy(host,ahost);
  port=aport;
  dataLoaded=1;
}

/***************************************************************
 * Inicializa el socket UDP conectándose al servidor indicado
 * por la información contenida en las variables globales host y port
 ***************************************************************/ 
int initClientUDP(){
  int n, sock, len;
  char buf[BUFSIZE];
  struct hostent *hp;
  struct sockaddr_in name;
    
  //Busca nuestra dirección de red.
  hp = gethostbyname(host);
  //Crea un socket UDP en el dominio INET*/
  sock = socket(AF_INET, SOCK_DGRAM, 0);

  //Crea la dirección del servidor
  name.sin_family = AF_INET;
  name.sin_port = htons(port);
  memcpy(&name.sin_addr, hp->h_addr_list[0], hp->h_length);
  len = sizeof(struct sockaddr_in);

  // Genera la conexión al servidor para no tener que pasar cada vez la dirección
  if(connect(sock, (struct sockaddr *) &name, len) < 0)
    return -1;

  udpSocketId = sock;
  return 0;
}

/***************************************************************
 * Envía un ACK al UDPoverTCPClient para indicarle que la información
 * de host y port del Servidor Remoto fue recibida satisfactoriamente.
 ***************************************************************/ 
int sendACK(){
  if(write(tcpSocketId,"\1",1) <= 0)
    return -1;
  printf("ACK signal sended...\n");
}

/***************************************************************
 * Espera por el ACK enviado por el UDPoverTCPServer indicando que la información
 * de host y port del Servidor Remoto fue recibida satisfactoriamente.
 ***************************************************************/ 
int getACK(){
  char rc[1] = "\0";
  if(read(tcpSocketId,rc,1) != 1){
    printf("Error getting verification signal\n");
    return -1;
  }
  if(rc[0]!=1)
    return -1;
  
  printf("ACK signal obtained\n");
  return 0;
}

/***************************************************************
 * Envía la información de host y port del Servidor Remoto al UDPoverTCPServer
 ***************************************************************/ 
int sendServerData(){
  char sPort[6];
  
  sprintf(sPort,"%d",port);
  if(write(tcpSocketId,host,strlen(host)) <= 0)
    return -1;
  if(write(tcpSocketId,"\0",1) <= 0)
    return -1;
  if(write(tcpSocketId,sPort,strlen(sPort)) <= 0)
    return -1;
  if(write(tcpSocketId,"\0",1) <= 0)
    return -1;
  
  return getACK();
}

/***************************************************************
 * Espera por la información de host y port enviada por el UDPoverTCPClient
 ***************************************************************/ 
int getServerData(){
  char sPort[6];
  char data[100];
  int offset=0;
  
  do{
    if(read(tcpSocketId,&host[offset++],1) != 1)
      return -1;
  }while(host[offset-1]!=0);
  
  offset=0;
  do{
    if(read(tcpSocketId,&sPort[offset++],1) != 1)
      return -1;
  }while(sPort[offset-1]!=0);
  
  port=atoi(sPort);
  printf("Remote server obtained: \n  host : %s\n  port : %d\n",host,port);
  
  return 0;
}

/***************************************************************
 * Inicia el tunel UDPoverTCP. 
 * Esta función es llamada tanto por el UDPoverTCPClient como por el UDPoverTCPServer
 * y tiene como argumentos el Id de los Sockets UDP y TCP
 * Nota: El UDPoverTCPServer no sabe inicialmente los datos del Servidor Remoto, por lo que
 *       se toma un udpId=0 lo que hace que éste espere por la información de host y port para
 *       ahí recién generar el socket UDP con el servidor remoto
 ***************************************************************/ 
int startTunel(int udpId, int tcpId){
  int rc;
  pthread_t udp2tcpId, tcp2udpId;
  
  udpSocketId = udpId;
  tcpSocketId = tcpId;
   
  if(udpId==0){ ////El UDPoverTCPtunelServer no conoce el Servidor Remoto
    if(getServerData() < 0){ //y se lo pide al cliente
      printf("Can't get server data.\n");
      return -1;
    }
    printf("Connecting to remote server...");
    if(initClientUDP() < 0){ //Inicializa el Cliente UDP
      printf("FAILED!\n");
      return -1;
    }
    printf("OK!\n");
    if(sendACK() < 0){
      printf("Error sending ACK\n");
    }
  }
  else{
    printf("Sending remote server information...\n");
    if(dataLoaded==1){  //verifica llamado previo a loadServerData()
      if(sendServerData() < 0){ //El UDPoverTCPClient le envía al UDPoverTCPServer los datos del Servidor Remoto
        printf("Can't send server data.\n");
        return -1;
      }
    }
    else{
      printf("First load server data...\n");
      return -1;
    }
  }
  
  //Crea la hebra que gestiona los datos desde el tunel TCP al socket UDP
  if(tcp2udpThread(&tcp2udpId) < 0){
    printf("Can't create tcp2udp thread!\n");
    return -1;
  }
  
  //Crea la hebra que gestiona los datos desde el socket UDP al tunel TCP
  if(udp2tcpThread(&udp2tcpId) < 0){
    printf("Can't create udp2tcp thread!\n");
    return -1;
  }
  
  //Espera a que la conexión se cierre por el lado del tunel
  rc = pthread_join(tcp2udpId, NULL);
  
  close(udpId);
  close(tcpId);
  return 0;
}
