#include <fenix/fxdll.h>
#ifdef WIN32
#include <winsock.h>
#else

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#ifndef SOCKADDR
#define SOCKADDR struct sockaddr
#endif

#endif
#pragma pack()

/* PluginVersion is used to identify the plugin structures against which
 * we're linking to prevent potential mismatches and segmentation faults
 */
unsigned int PluginVersion = FXDLL_VERSION;

fd_set socketset[32];

// Inicializamos librería WinSock (version 2) - Devuelve 0 si no hubo error
static int fsock_init (INSTANCE * my, int * params)
{
#ifdef WIN32
	WSADATA wsaData;
	return (WSAStartup(MAKEWORD(2, 2),&wsaData));
#else
    return 0;
#endif
}

// Apaga la librería WinSock (devuelve 0 si fue correcto)
static int fsock_quit (INSTANCE * my, int * params)
{
#ifdef WIN32
	return (WSACleanup());
#else
    return 0;
#endif
}

// Devuelve la cantidad maxima de fds posibles de crear
static int fsock_getfdsetsize (INSTANCE * my, int * params)
{
	return FD_SETSIZE;
}

// Crea un socket TCP y lo devuelve (-1 es error)
static int tcpsock_open (INSTANCE * my, int * params)
{
	return (socket(AF_INET,SOCK_STREAM,0));
}

// Crea un socket UDP y lo devuelve (-1 es error)
static int udpsock_open (INSTANCE * my, int * params)
{
	return (socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP));
}

// Cierra un socket TCP/UDP (si fue correcto devuelve 0)
static int fsock_close (INSTANCE * my, int * params)
{
	shutdown(params[0],0);
#ifdef WIN32
	return (closesocket(params[0]));
#else
    return (close(params[0]));
#endif
}

// Asociamos el socket (TCP o UDP) dado con el puerto especificado (si fue correcto devuelve 0)
static int fsock_bind (INSTANCE * my, int * params)
{
	struct sockaddr_in info_conexion;
	info_conexion.sin_family=AF_INET;
	info_conexion.sin_port=htons(params[1]);
	info_conexion.sin_addr.s_addr=INADDR_ANY;

	return (bind(params[0],(SOCKADDR*)&info_conexion,sizeof(info_conexion)));
}

// Fijamos el nº de peticiones que puede tener el socket anteriormente asociado (binded)
static int tcpsock_listen (INSTANCE * my, int * params)
{
	return (listen(params[0],params[1]));
}

// Acepta una petición de conexión del socket dado y devuelve su socket a usar
static int tcpsock_accept (INSTANCE * my, int * params)
{
	int socket;
	struct timeval timeout;
	fd_set readfds;
    struct sockaddr_in addr;
    int addrlen;

	FD_ZERO(&readfds);
	FD_SET(params[0], &readfds);
	timeout.tv_sec=0;
	timeout.tv_usec=0;

	if (select(FD_SETSIZE, &readfds, NULL, NULL, &timeout)>0) {
	    addrlen=sizeof(addr);
	    socket=accept(params[0], (struct sockaddr *)&addr, &addrlen);
	    if(socket!=-1) {
	        *(int *)params[1]=addr.sin_addr.s_addr; //ip
			*(int *)params[2]=ntohs(addr.sin_port); //puerto
	    }
	} else {
	    socket=-1;
	}

	return socket;
}

// Realiza una petición de conexión a la dirección IP/Host y puerto dado
// con el socket dado determinado y devuelve 0 si fue correcto
static int tcpsock_connect (INSTANCE * my, int * params)
{
	char* ip=(char*)string_get(params[1]);
	struct sockaddr_in info_conexion;
	info_conexion.sin_family=AF_INET;
	info_conexion.sin_port=htons(params[2]);
	info_conexion.sin_addr.s_addr=inet_addr(ip);

	return (connect(params[0],(SOCKADDR*)&info_conexion,sizeof(info_conexion)));
}

// Realiza un select sobre el socket set indicado devolviendo
// el número de sockets que tienen actividad
static int fsock_select (INSTANCE * my, int * params)
{
	struct timeval timeout;

	timeout.tv_sec=params[1]/1000;
	timeout.tv_usec=params[1]%1000;

	return (select(FD_SETSIZE, &socketset[params[0]], NULL, NULL, &timeout));
}

// Envía un puntero de un tamaño determinado a través del socket TCP dado
static int tcpsock_send (INSTANCE * my, int * params)
{
	char *envio=(char*)params[1];
	return (send(params[0],(void*)envio,params[2],0));
}

// Envía un puntero de un tamaño determinado a través del socket UDP dado
static int udpsock_send (INSTANCE * my, int * params)
{
	char *envio=(char*)params[1];
	char* ip=(char*)string_get(params[3]);
	struct sockaddr_in info_conexion;
	info_conexion.sin_family=AF_INET;
	info_conexion.sin_port=htons(params[4]);
	info_conexion.sin_addr.s_addr=inet_addr(ip);

	return (sendto(params[0], (void*)envio, params[2], 0, (SOCKADDR*)&info_conexion, sizeof(info_conexion)));
}

// Recibe un puntero de un tamaño determinado a través del socket TCP dado
static int tcpsock_recv (INSTANCE * my, int * params)
{
	return (recv(params[0],(void*)params[1],params[2],0));
}

// Recibe un puntero de un tamaño determinado a través del socket UDP dado
static int udpsock_recv (INSTANCE * my, int * params)
{

	struct sockaddr_in info_conexion;
	int bytesRecibidos;
	int tamanoInfoConexion;
	
	bytesRecibidos = recvfrom(params[0], (void*)params[1], params[2], 0, (SOCKADDR*)&info_conexion, &tamanoInfoConexion);

	if (params[0]!=-1){
		*(int *) params[3] = info_conexion.sin_addr.s_addr; //ip
		*(int *) params[4] = ntohs(info_conexion.sin_port); //puerto
	}

	return bytesRecibidos;
}

// Inicia o vacia el socket set indicado
static void fsock_fdzero (INSTANCE * my, int * params)
{
	FD_ZERO(&socketset[params[0]]);
}

// Añade un socket al socket set indicado
static int fsock_fdset (INSTANCE * my, int * params)
{
	FD_SET(params[1], &socketset[params[0]]);
#ifdef WIN32
	return socketset[params[0]].fd_count;
#else
	return FD_ISSET(params[1], &socketset[params[0]]);
#endif
}

// Elimina un socket del socket set indicado
static int fsock_fdclr (INSTANCE * my, int * params)
{
	FD_CLR(params[1], &socketset[params[0]]);
#ifdef WIN32
	return socketset[params[0]].fd_count;
#else
	return FD_ISSET(params[1], &socketset[params[0]]);
#endif
}

// Chequea si un socket tiene evento disponible
static int fsock_fdisset (INSTANCE * my, int * params)
{
	return (FD_ISSET(params[1], &socketset[params[0]]));
}

// Comprueba si hay actividad en el socket set indicado devolviendo
// el número de sockets que tienen actividad
static int fsock_socketset_check (INSTANCE * my, int * params)
{
	int check;
	fd_set temporal=socketset[params[0]];
	struct timeval timeout;

	timeout.tv_sec=0;
	timeout.tv_usec=0;

	check=select(FD_SETSIZE, &socketset[params[0]], NULL, NULL, &timeout);
	socketset[params[0]]=temporal;
	return check;
}

// Devuelve el nombre del host del PC o la dirección IP de la máquina
// que lo ejecuta según se especifique (0 o 1 respectivamente)
static int fsock_get_iphost (INSTANCE * my, int * params)
{
    int texto;
	char host[80];
	struct hostent *phe;
	struct in_addr addr;

	gethostname(host, sizeof(host));
	phe=gethostbyname(host);
    memcpy(&addr, phe->h_addr_list[0], sizeof(struct in_addr));

	if (!params[0])
	texto=string_new(host);
	else
	texto=string_new(inet_ntoa(addr)) ;

	string_use(texto) ;
	return texto;
}


// Devuelve la cadena con la dirección IP dada por el parámetro
// dword * ip que algunas funciones usan
static int fsock_get_ipstr (INSTANCE * my, int * params)
{
	int texto;
	struct in_addr addr;

	addr.s_addr = *(int *) params[0];

	texto=string_new(inet_ntoa(addr));

	string_use(texto);
	return texto;

}


FENIX_MainDLL RegisterFunctions (COMMON_PARAMS)
{
    FENIX_DLLImport

	FENIX_export ("FSOCK_INIT", "", TYPE_DWORD, fsock_init ) ;
	FENIX_export ("FSOCK_GETFDSETSIZE", "", TYPE_DWORD, fsock_getfdsetsize ) ;
	FENIX_export ("TCPSOCK_OPEN", "", TYPE_DWORD, tcpsock_open ) ;
	FENIX_export ("UDPSOCK_OPEN", "", TYPE_DWORD, udpsock_open ) ;
	FENIX_export ("FSOCK_CLOSE", "I", TYPE_DWORD, fsock_close ) ;// Socket
	FENIX_export ("FSOCK_BIND", "II", TYPE_DWORD, fsock_bind ) ;// Socket, Puerto
	FENIX_export ("TCPSOCK_LISTEN", "II", TYPE_DWORD, tcpsock_listen ) ;// Socket, Nº de peticiones
	FENIX_export ("TCPSOCK_ACCEPT", "IPP", TYPE_DWORD, tcpsock_accept ) ;// Socket, dword * ip, dword * Puerto
	FENIX_export ("TCPSOCK_CONNECT", "ISI", TYPE_DWORD, tcpsock_connect ) ;// Socket, IP/Host, Puerto
	FENIX_export ("FSOCK_SELECT", "II", TYPE_DWORD, fsock_select ) ; // Nº de SocketSet, Timeout (ms)
	FENIX_export ("TCPSOCK_SEND", "IPI", TYPE_DWORD, tcpsock_send ) ;// Socket, Puntero Dato, Tamaño
	FENIX_export ("UDPSOCK_SEND", "IPISI", TYPE_DWORD, udpsock_send ) ;// Socket, Puntero Dato, Tamaño, IP/Host, Puerto
	FENIX_export ("TCPSOCK_RECV", "IPI", TYPE_DWORD, tcpsock_recv ) ;// Socket, Puntero Dato, Tamaño
	FENIX_export ("UDPSOCK_RECV", "IPIPP", TYPE_DWORD, udpsock_recv ) ;// Socket, Puntero Dato, Tamaño, dword * ip, dword * Puerto
	FENIX_export ("FSOCK_SOCKETSET_CHECK", "I", TYPE_DWORD, fsock_socketset_check ) ; // Nº de SocketSet
	FENIX_export ("FSOCK_SOCKETSET_FREE", "I", TYPE_DWORD, fsock_fdzero ) ;	// Nº de SocketSet
	FENIX_export ("FSOCK_SOCKETSET_ADD", "II", TYPE_DWORD, fsock_fdset ) ; // Nº de SocketSet, Socket
	FENIX_export ("FSOCK_SOCKETSET_DEL", "II", TYPE_DWORD, fsock_fdclr ) ; // Nº de SocketSet, Socket
	FENIX_export ("FSOCK_GET_IPHOST", "I", TYPE_STRING, fsock_get_iphost ) ; // Opción (0 Host, 1 IP)
	FENIX_export ("FSOCK_GET_IPSTR", "P", TYPE_STRING, fsock_get_ipstr ); // IP en notación punto (dword * ip)
	FENIX_export ("FSOCK_QUIT", "", TYPE_DWORD, fsock_quit ) ;
	FENIX_export ("FSOCK_FDZERO", "I", TYPE_DWORD, fsock_fdzero ) ;	// Nº de SocketSet
	FENIX_export ("FSOCK_FDSET", "II", TYPE_DWORD, fsock_fdset ) ; // Nº de SocketSet, Socket
	FENIX_export ("FSOCK_FDCLR", "II", TYPE_DWORD, fsock_fdclr ) ; // Nº de SocketSet, Socket
	FENIX_export ("FSOCK_FDISSET", "II", TYPE_DWORD, fsock_fdisset ) ; // Nº de SocketSet
}