#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include "debug.h"
#include "rtlib.h"
#include "rtgrading.h"
#include "sircd.h"
#include <stddef.h>
#include <assert.h>
#include <signal.h>

u_long curr_nodeID;
rt_config_file_t   curr_node_config_file;  /* The config_file  for this node */
rt_config_entry_t *curr_node_config_entry; /* The config_entry for this node */

/* function prototypes */
int read_line(client *curr_client, pool *p);
int open_listenfd(int port);
void init_pool(int listenfd, pool *p);
void add_client(int connfd, struct sockaddr_in clientaddr, pool *p);
void check_clients(pool *p);
void error(char *);
void init_node(char *nodeID, char *config_file);
void irc_server();
void init_channels(pool *p);

/* This function prints out the correct format for running the executable sircd */
void usage() {
    fprintf(stderr, "sircd [-h] [-D debug_lvl] <nodeID> <config file>\n");
    exit(-1);
}

/* The main function setups up the relevant data structures as well as sets up the irc server to accpet incoming connections */
int main(int argc, char *argv[])
{
	extern char *optarg;
	extern int optind;
	int ch;
	int listenfd, connfd, clientlen;
	struct sockaddr_in clientaddr;
    static pool pool;
	sigset_t mask;

	while ((ch = getopt(argc, argv, "hD:")) != -1)
		switch (ch) {
			case 'D':
	    		if (set_debug(optarg)) {
					exit(0);
	    		}
	    		break;
	        case 'h':
	        default: /* FALLTHROUGH */
	            usage();
	        }
	    argc -= optind;
	    argv += optind;

	    if (argc < 2) {
			usage();
		}

	init_node(argv[0], argv[1]);

    printf( "I am node %ld and I listen on port %d for new users.\n", curr_nodeID, curr_node_config_entry->irc_port );

	//ignore SIGPIPE errors
	if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
		perror("signal error");

	sigemptyset(&mask);
	sigaddset(&mask,SIGPIPE);
	sigprocmask(SIG_BLOCK,&mask,NULL);

    listenfd = open_listenfd(curr_node_config_entry->irc_port);

	//initialize poll of clients
	init_pool(listenfd, &pool);

	gethostname(pool.shostname, sizeof(pool.shostname));

	//initialize channels/users array
	init_channels(&pool);

    while (1)
    {
		/* Wait for listening/connected descriptors to become ready */
		pool.ready_set = pool.read_set;
		pool.nready = select(pool.maxfd+1, &pool.ready_set, NULL, NULL, NULL);
		clientlen = sizeof(clientaddr);

		/* If listening descriptor (listenfd) ready, add new client to pool */
		if (FD_ISSET(listenfd, &pool.ready_set))
		{
	    	connfd = accept(listenfd, (SA *)&clientaddr, &clientlen);

	    	/* check for errors. if any, ignore new connection */
			if (connfd < 0)
       			continue;

	    	add_client(connfd, clientaddr, &pool);
		}

		/* See if a client has any input */
		check_clients(&pool);
    }
}

/*
 * void init_node( int argc, char *argv[] )
 *
 * Takes care of initializing a node for an IRC server
 * from the given command line arguments
 */
void
init_node(char *nodeID, char *config_file)
{
    int i;

    curr_nodeID = atol(nodeID);
    rt_parse_config_file("sircd", &curr_node_config_file, config_file );

    /* Get config file for this node */
    for( i = 0; i < curr_node_config_file.size; ++i )
        if( curr_node_config_file.entries[i].nodeID == curr_nodeID )
             curr_node_config_entry = &curr_node_config_file.entries[i];

    /* Check to see if nodeID is valid */
    if( !curr_node_config_entry )
    {
        printf( "Invalid NodeID\n" );
        exit(1);
    }
}


/* Deals with the user's request. 1 byte is read at a time until no more input from the client is waiting. */
int read_line(client *curr_client, pool *p)
{
    int n, rc;
    char c, *bufp = curr_client->inbuf;

    for (n = 1; n < MAX_MSG_LEN; n++)
    {
		if ((rc = read(curr_client->sock, &c, 1)) == 1)
		{
	    	*bufp++ = c;
	    	if (c == '\n')
				break;
		}
		else if (rc == 0)
		{
	    	if (n == 1)
				return 0; /* EOF, no data read */
	    	else
				break;    /* EOF, some data was read */
		}
		else
		{
			if (errno == EPIPE)
				return 0;
	    	else if (errno == EINTR)
	    		continue;
	    	else
	    		return -1;	  /* bad error */
		}
    }

    *bufp = 0;

	/* Remove any trailing new line characters */
    curr_client->inbuf[strcspn(curr_client->inbuf, "\n\r")] = '\0';

    handle_line(curr_client->inbuf, curr_client, p);

    return n;
}

/* Opens a socket for client incoming requests */
int open_listenfd(int port)
{
    int listenfd, optval=1;
    struct sockaddr_in serveraddr;

    /* Create a socket descriptor */
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		error("socket");

    /* Eliminates "Address already in use" error from bind. */
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
		   (const void *)&optval , sizeof(int)) < 0)
		error("setsockopt");

    /* Listenfd will be an endpoint for all requests to port
       on any IP address for this host */
    memset(&serveraddr, 0, sizeof(serveraddr));
    //bzero((char *) &serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons((unsigned short)port);
    if (bind(listenfd, (SA *)&serveraddr, sizeof(serveraddr)) < 0)
		error("bind");

    /*socket now ready to listen for incoming connections*/
    if (listen(listenfd, LISTENQ) < 0)
		error("listen");

    return listenfd;
}

/* Initializes the client pool */
void init_pool(int listenfd, pool *p)
{
    /* Initially, there are no connected descriptors */
    int i;
    p->maxi = -1;
    for (i=0; i < MAX_CLIENTS; i++)
		p->clientfd[i] = -1;

    /* Initially, listenfd is only member of select read set */
    p->maxfd = listenfd;
    FD_ZERO(&p->read_set);
    FD_SET(listenfd, &p->read_set);
}

/* Initializes the channels array */
void init_channels(pool *p)
{
	int i, j;

	//setup channel array
	if (p->channels == NULL)
	{
		if ( (p->channels = malloc(sizeof(channel)*INIT_CAPACITY)) == NULL )
			perror("malloc");

		memset(p->channels,'\0',sizeof(channel)*INIT_CAPACITY);

		//capacity initially 10
		p->channels_capacity = INIT_CAPACITY;
		p->channel_count = 0;

		//allocate slots for 10 clients in each channel
		for(i = 0; i < INIT_CAPACITY; i++)
		{
			if ( (p->channels[i].users = malloc(sizeof(client*)*INIT_CAPACITY)) == NULL )
				perror("malloc");

			p->channels[i].users_capacity = INIT_CAPACITY;
			p->channels[i].user_count = 0;

			for(j = 0; j < INIT_CAPACITY; j++)
			{
				p->channels[i].users[j] = NULL;
			}
		}
	}
}

/* Resizes the channels array to 2x its size */
void resize_channels(pool *p)
{
	int i, j, tmpsize;

	tmpsize = p->channels_capacity;
	p->channels_capacity *= 2;
	if ( (p->channels = realloc(p->channels,sizeof(channel)*p->channels_capacity)) == NULL )
		perror("realloc");

	memset((p->channels)+(tmpsize),'\0',sizeof(channel)*(p->channels_capacity-tmpsize));

	for(i = tmpsize; i < p->channels_capacity; i++)
	{
		if ( (p->channels[i].users = malloc(sizeof(client*)*INIT_CAPACITY)) == NULL )
			perror("malloc");

		p->channels[i].users_capacity = INIT_CAPACITY;
		p->channels[i].user_count = 0;

		for(j = 0; j < INIT_CAPACITY; j++)
		{
			p->channels[i].users[j] = NULL;
		}
	}
}

/* Resizes the users array to 2x its size */
void resize_users(pool *p, int index)
{
	int j, tmpsize;

	tmpsize = p->channels[index].users_capacity;
	p->channels[index].users_capacity *= 2;
	if ( (p->channels[index].users = realloc(p->channels[index].users,sizeof(client*)*p->channels[index].users_capacity)) == NULL )
		perror("realloc");

	for (j = tmpsize; j < p->channels[index].users_capacity; j++)
	{
		p->channels[index].users[j] = NULL;
	}
}

/* Adds a client to the client pool */
void add_client(int connfd, struct sockaddr_in clientaddr, pool *p)
{
    int i;
    p->nready--;
    for (i = 0; i < MAX_CLIENTS; i++)  /* Find an available slot */
	{
		if (p->clientfd[i] < 0)
		{
	    	/* Add connected descriptor to the pool */
	    	p->clientfd[i] = connfd;

	    	/* Add the descriptor to descriptor set */
	    	FD_SET(connfd, &p->read_set);

	    	/* Update max descriptor and pool highwater mark */
	    	if (connfd > p->maxfd)
				p->maxfd = connfd;
	    	if (i > p->maxi)
				p->maxi = i;

			p->clients[i].sock = connfd;
			p->clients[i].cliaddr = clientaddr;
			p->clients[i].registered = 0;
			p->clients[i].descriptor_index = i;

			//get host information of the client
			if ((getnameinfo((SA *)&clientaddr, sizeof(clientaddr), p->clients[i].hostname, sizeof(p->clients[i].hostname), 0, 0 ,0)) != 0)
			{
				(inet_ntop(AF_INET, &clientaddr, p->clients[i].hostname, sizeof(p->clients[i].hostname)));
			}

	    	break;
		}
	}

	/* Couldn't find an empty slot */
    if (i == MAX_CLIENTS)
    {
		char msg [] = "Too many clients currently connected!\n";
		write(connfd,msg,sizeof(msg));
		//dprintf(connfd,"Too many clients currently connected!\n");
		close(connfd);
	}
}

/* Check the client pool to see what clients have data to be read and then deals with the input */
void check_clients(pool *p)
{
    int i, connfd;

    for (i = 0; (i <= p->maxi) && (p->nready > 0); i++)
    {
		connfd = p->clientfd[i];
		/* If the descriptor is ready, deal with its request */
		if ((connfd > 0) && (FD_ISSET(connfd, &p->ready_set)))
		{
	    	p->nready--;

	    	/* client closed connection if return value is 0*/
	    	if ((read_line(&(p->clients[i]), p)) <= 0)
	    	{
				cmd_quit(&(p->clients[i]), p, NULL, NULL, -1);
			}
		}
    }
}


/* Outputs an error message to the user and then exits */
void error(char *msg)
{
    perror(msg);
    exit(0);
}



