#include <stdio.h>
#include <stdlib.h>
#include <strings.h>

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

#include <pthread.h>

#include "UDPoverTCPtunelServer.h"
#include "UDPoverTCPtunel.h"

//define el número de conexiones máximas
#define MAXCONN 100

int nconn = 0;
pthread_mutex_t nconnMutex;  //mutex para la variable con el número de conexiones

//variables para la generación de sockets
int sockfd, portno, clilen, pid;
struct sockaddr_in serv_addr, cli_addr;

//Arreglo de t_socket para gestional las multiples hebras/clientes (ver UDPoverTCPtunelServer.h)
t_socket socks[MAXCONN];

int main(int argc, char *argv[]){
  
  if(argc!=2){
    printf("Usage: %s [tcp_listening_port]\n",argv[0]);
    exit(-1);
  }
  
  printf("UDP over TCP Multi-Thread/Client Server.\n");
  printf("Listening for connections...\n");
  
  //Inicializa el socket que escucha por nuevas conexiones
  initSock(atoi(argv[1]));
  //Se queda en escucha
  listenSock();
  
  return 0;
}

/***************************************************************
 * Manejo muy simple de errores
 ***************************************************************/ 
void error(char *msg){
    printf("%s.\n",msg);
    //exit(1);
}

/***************************************************************
 * Se crea una nueva conexión reservando la ranura e incrementando nconn
 ***************************************************************/ 
void lockSocket(t_socket *sock){
  pthread_mutex_lock(&(sock->mutex));
    sock->status = OCUPADO;
  pthread_mutex_unlock(&(sock->mutex));  
  pthread_mutex_lock(&nconnMutex);
    nconn++;
  pthread_mutex_unlock(&nconnMutex);  
}

/***************************************************************
 * Se termina una conexión liberando la ranura y decrementando nconn
 ***************************************************************/ 
void unlockSocket(t_socket *sock){
  pthread_mutex_lock(&(sock->mutex));
    sock->status = DISPONIBLE;
  pthread_mutex_unlock(&(sock->mutex));  
  pthread_mutex_lock(&nconnMutex);
    nconn--;
  pthread_mutex_unlock(&nconnMutex);  
}

/***************************************************************
 * Obtiene el número de conexiones de forma segura
 ***************************************************************/ 
int getConnNumber(){
  int n;
  pthread_mutex_lock(&nconnMutex);
    n=nconn;
  pthread_mutex_unlock(&nconnMutex);
  return n;
}

/***************************************************************
 * Proceso que crea el tunel para el nuevo cliente
 ***************************************************************/ 
static void *careClient(void *asock){
  int n;
  t_socket *sock;
  char buffer[10000];
  
  //reservamos la ranura del socket para que no pueda ser usada
  sock = (t_socket *)asock;
  lockSocket(sock);
  
  printf("Clients: %d\n", getConnNumber());
  printf("New Server thread...\n");
  
  //Inicia el nuevo tunel
  if(startTunel(0, sock->id) < 0)
    printf("Ended with comunication errors!\n");
  
  //Terminando sin errores libera el socket
  close(sock->id);
  unlockSocket(sock);
  printf("Client connection closed...\n");
  printf("Clients: %d\n", getConnNumber());
  
  //Cierra el thread
  pthread_exit(0);
}

/***************************************************************
 * Crea una hebra para atender al nuevo cliente
 ***************************************************************/ 
int createClientThread(t_socket *sock){
  int rc;
  pthread_t tid;
  if((rc = pthread_create(&tid, NULL, careClient, (void *)sock)) != 0) {              
    error("ERROR: can't create the thread");
    return(-1);      
  }
  else {
    return 0;
  }
}

/***************************************************************
 * Inicializa el socket donde se escucha por clientes
 ***************************************************************/ 
void initSock(int port){
  int i;
  
  //inicializa el arreglo de sockets
  for(i=0; i<MAXCONN; i++){
    pthread_mutex_init(&(socks[i].mutex),NULL);
    socks[i].status = DISPONIBLE;
  }
  
  //se crea un socket TCP en el dominio INET
  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  if (sockfd < 0) 
    error("ERROR opening socket");
  
  //carga los datos de la conexion
  bzero((char *) &serv_addr, sizeof(serv_addr));
  portno = port;
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_addr.s_addr = INADDR_ANY;
  serv_addr.sin_port = htons(portno);
  
  //Asocia el puerto a los datos de la conexión 
  if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) 
    error("ERROR on binding");
  
  //Se pone a escucha en el socket 
  listen(sockfd,5);
  
  clilen = sizeof(cli_addr);
}

/***************************************************************
 * Obtiene la siguiente ranura disponible en el arreglo de sockets
 ***************************************************************/ 
int getNextSock(){
  int i, index=-1;
  for(i=0; i<MAXCONN; i++)
    if(pthread_mutex_trylock(&(socks[i].mutex))==0){
      if(socks[i].status==DISPONIBLE){
	index=i;
	pthread_mutex_unlock(&(socks[i].mutex));
        break;
      }
      pthread_mutex_unlock(&(socks[i].mutex));
    }
  return index;
}

/***************************************************************
 * Gestiona la recepción o el rechazo de clientes dada la disponibilidad de ranuras
 ***************************************************************/ 
void listenSock(){
  int i, id, sockindex, serverStatus = DISPONIBLE;
  while (1) {
    //Inicialmente acepta al cliente
    id = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
    sockindex=getNextSock();
    
    if(sockindex<0){ //rechaza la conexión si no hay ranuras disponibles
      if(serverStatus == DISPONIBLE){
        serverStatus = OCUPADO;
        error("Server full, rejecting all connections...");
      }
      error("Client Rejected!...");
      close(id);
    }  
    else{
      //Pone al socket en estado ASIGNADO esperando a que la nueva hebra lo ponga en estado OCUPADO
      serverStatus = DISPONIBLE;
      socks[sockindex].status = ASIGNADO;
      socks[sockindex].id = id;
      if (id < 0){
	//error al obtener el socket
	socks[sockindex].status = DISPONIBLE;
        error("ERROR on accept");
      }
      else if (createClientThread(&socks[sockindex]) != 0){
        //error al crear la hebra
	socks[sockindex].status = DISPONIBLE;
        close(socks[sockindex].id);
      }
      else{
	//la conexión fue satisfactoria
        printf("New client connection detected...\n");
      }
    }
  }
}