#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include "sircd.h"
#include "irc_proto.h"
#include "debug.h"

#define MAX_COMMAND 16


//TO DO:
//realloc when users/channels capacity is > 2x the current count...need to find a way to do this properly though


/* You'll want to define the CMD_ARGS to match up with how you
 * keep track of clients.  Probably add a few args...
 * The command handler functions will look like
 * void cmd_nick(CMD_ARGS)
 * e.g., void cmd_nick(your_client_thingy *c, char *prefix, ...)
 * or however you set it up.
 */

/* Function prototypes */
ssize_t writen(int fd, void *usrbuf, size_t n);
void send_message(int connfd, const char *format,...);

#define CMD_ARGS client *curr_client, pool *p, char *prefix, char **params, int n_params
typedef void (*cmd_handler_t)(CMD_ARGS);
#define COMMAND(cmd_name) void cmd_name(CMD_ARGS)


struct dispatch {
    char cmd[MAX_COMMAND];
    int needreg; /* Must the user be registered to issue this cmd? */
    int minparams; /* send NEEDMOREPARAMS if < this many params */
    cmd_handler_t handler;
};


#define NELMS(array) (sizeof(array) / sizeof(array[0]))

/* Define the command handlers here.  This is just a quick macro
 * to make it easy to set things up */
COMMAND(cmd_nick);
COMMAND(cmd_user);
COMMAND(cmd_quit);
COMMAND(cmd_join);
COMMAND(cmd_part);
COMMAND(cmd_list);
COMMAND(cmd_who);
COMMAND(cmd_privmsg);


/* Dispatch table.  "reg" means "user must be registered in order
 * to call this function".  "#param" is the # of parameters that
 * the command requires.  It may take more optional parameters.
 */
struct dispatch cmds[] = {
    /* cmd,    reg  #parm  function */
    { "NICK",    0, 0, cmd_nick },
    { "USER",    0, 4, cmd_user },
    { "QUIT",    0, 0, cmd_quit },
    { "JOIN",    1, 1, cmd_join },
    { "PART",    1, 1, cmd_part },
    { "LIST",    1, 0, cmd_list },
    { "WHO",     1, 1, cmd_who },
    { "PRIVMSG", 1, 1, cmd_privmsg },
    /* Fill in the blanks... */
};


/* Handles a command line by parsing and dispatching it to the correct function. */
void handle_line(char *line, client *curr_client, pool *p)
{
    char *prefix = NULL, *command, *pstart, *params[MAX_MSG_TOKENS];
    int n_params = 0;
    char *trailing = NULL;



    DPRINTF(DEBUG_INPUT, "Handling line: %s\n", line);
    command = line;
    if (*line == ':') {
	prefix = ++line;
	command = strchr(prefix, ' ');
    }
    if (!command || *command == '\0') {
	/* Send an unknown command error! */
		if (curr_client->registered == 0)
       		send_message(curr_client->sock,error_replies[ERR_UNKNOWNCOMMAND-400],p->shostname,"*",line);
       	else
       		send_message(curr_client->sock,error_replies[ERR_UNKNOWNCOMMAND-400],p->shostname,curr_client->nick,line);
	return;
    }

    while (*command == ' ') {
	*command++ = 0;
    }
    if (*command == '\0') {
		if (curr_client->registered == 0)
        	send_message(curr_client->sock,error_replies[ERR_UNKNOWNCOMMAND-400],p->shostname,"*",line);
        else
        	send_message(curr_client->sock,error_replies[ERR_UNKNOWNCOMMAND-400],p->shostname,curr_client->nick,line);
	/* Send an unknown command error! */
	return;
    }
    pstart = strchr(command, ' ');
    if (pstart) {
	while (*pstart == ' ') {
	    *pstart++ = '\0';
	}
	if (*pstart == ':') {
	    trailing = pstart;
	} else {
	    trailing = strstr(pstart, " :");
	}
	if (trailing) {
	    while (*trailing == ' ')
		*trailing++ = 0;
	    if (*trailing == ':')
		*trailing++ = 0;
	}

	do {
	    if (*pstart != '\0') {
		params[n_params++] = pstart;
	    } else {
		break;
	    }
	    pstart = strchr(pstart, ' ');
	    if (pstart) {
		while (*pstart == ' ') {
		    *pstart++ = '\0';
		}
	    }
	} while (pstart != NULL && n_params < MAX_MSG_TOKENS);
    }

    if (trailing && n_params < MAX_MSG_TOKENS) {
	params[n_params++] = trailing;
    }

    DPRINTF(DEBUG_INPUT, "Prefix:  %s\nCommand: %s\nParams (%d):\n",
	    prefix ? prefix : "<none>", command, n_params);
    int i;
    for (i = 0; i < n_params; i++) {
	DPRINTF(DEBUG_INPUT, "   %s\n", params[i]);
    }
    DPRINTF(DEBUG_INPUT, "\n");

    for (i = 0; i < NELMS(cmds); i++)
    {

		if (strcasecmp(cmds[i].cmd, command) == 0)
		{
	    	if (cmds[i].needreg && curr_client->registered == 0) {
				/* ERROR - the client is not registered and they need
		 		* to be in order to use this command! */
                send_message(curr_client->sock,error_replies[ERR_NOTREGISTERED-400],p->shostname,"*",line);
	    	} else if (n_params < cmds[i].minparams) {
				/* ERROR - the client didn't specify enough parameters
		 		* for this command! */
		 		if (curr_client->registered == 0)
                	send_message(curr_client->sock,error_replies[ERR_NEEDMOREPARAMS-400],p->shostname,"*",line);
                else
                	send_message(curr_client->sock,error_replies[ERR_NEEDMOREPARAMS-400],p->shostname,curr_client->nick,line);
	    	} else {
				/* Here's the call to the cmd_foo handler... modify
		 		* to send it the right params per your program
		 		* structure. */
				(*cmds[i].handler)(curr_client, p, prefix, params, n_params);
	    	}

	   		break;

		}
    }

    if (i == NELMS(cmds)) {
		/* ERROR - unknown command! */
		if (curr_client->registered == 0)
			send_message(curr_client->sock,error_replies[ERR_UNKNOWNCOMMAND-400],p->shostname,"*",line);
		else
			send_message(curr_client->sock,error_replies[ERR_UNKNOWNCOMMAND-400],p->shostname,curr_client->nick,line);

    }
}

/* This function takes in a non fixed set of arguments. The arguments passed in are determined by the specific
   error or reply message that is sent to the user. The structure of each message is defined in irc_proto.h and
   the arguments are parsed accordingly */
void send_message(int connfd, const char *format,...)
{
	char buf[1024], buf2[1024];
	va_list msg;
	int len;

	va_start(msg, format);
	vsprintf(buf, format, msg);
    len = snprintf(buf2, sizeof(buf2), "%s\r\n", buf);

	//write
	if(writen(connfd,buf2,len) < 0)
		perror("write error");

}

/* This function writes n bytes to a client buffer. Simple read errors such as interrupted sig handleres are
   ignored */
ssize_t writen(int fd, void *usrbuf, size_t n)
{
    size_t nleft = n;
    ssize_t nwritten;
    char *bufp = usrbuf;

	//keep checking to ensure all bytes were successfully written to the buffer.
    while (nleft > 0)
    {
		if ((nwritten = write(fd, bufp, nleft)) <= 0)
		{
			if (errno == EPIPE)
				return 0;
	    	else if (errno == EINTR)  /* interrupted by sig handler return */
				nwritten = 0;    /* and call write() again */
	   		else
				return -1;       /* errorno set by write() */
		}

		nleft -= nwritten;
		bufp += nwritten;
    }

    return n;
}



/* Command handlers */

/* This function sets the nickname of a client as per his/her request. If the client already has set user
 information, then the client is set as registered. */
void cmd_nick(client *curr_client, pool *p, char *prefix, char **params, int n_params)
{
	int i, j;

	/*No nickname was provided*/
	if (*params == NULL)
	{
		send_message(curr_client->sock,error_replies[ERR_NONICKNAMEGIVEN-400],p->shostname,"*",*params);
		return;
	}

	//look through all the users
	for(i = 0; i <= p->maxi; i++)
	{
		if (strcasecmp(p->clients[i].nick, *params) == 0)
		{
			if (curr_client->sock != p->clients[i].sock)
			{
				if (curr_client->registered	== 0)
					send_message(curr_client->sock,error_replies[ERR_NICKNAMEINUSE-400],p->shostname,"*",*params);
				else
					send_message(curr_client->sock,error_replies[ERR_NICKNAMEINUSE-400],p->shostname,curr_client->nick,*params);
			}

			return;
		}
	}

	/*Client is changing his nick, notify all those on the same channel*/
	if ( (curr_client->set_nick == 1) && (curr_client->registered == 1) && (strlen(curr_client->channel) != 0) )
	{
		for(i = 0; i < p->channels_capacity; i++)
		{
			if (strcasecmp(p->channels[i].name, curr_client->channel) == 0)
			{
				for(j = 0; j < p->channels[i].users_capacity; j++)
				{
					if ( (p->channels[i].users[j] != NULL) && (strcasecmp(p->channels[i].users[j]->nick, curr_client->nick) != 0) )
					{
						send_message(p->channels[i].users[j]->sock,command_replies[RPL_CHANGENICK-300],curr_client->nick,curr_client->user,curr_client->hostname,*params);
					}
				}
				break;
			}
		}
	}

	strncpy(curr_client->nick,*params,MAX_USERNAME);
	curr_client->set_nick = 1;

	//client already has a username, etc set, so he/she is now registered.
	if( (curr_client->set_user == 1) && (curr_client->registered == 0))
	{
		curr_client->registered = 1;
		send_message(curr_client->sock,command_replies[RPL_MOTDSTART-300],p->shostname,curr_client->nick,p->shostname);
		send_message(curr_client->sock,command_replies[RPL_MOTD-300],p->shostname,curr_client->nick,SERVER_MSG);
		send_message(curr_client->sock,command_replies[RPL_ENDOFMOTD-300],p->shostname,curr_client->nick);
	}

}

/* This function sets the user information of a client as per his/her request. If the client has already set a nickname, then
the client is set as registered */
void cmd_user(client *curr_client, pool *p, char *prefix, char **params, int n_params)
{
		if(curr_client->registered)
		{
			send_message(curr_client->sock,error_replies[ERR_ALREADYREGISTRED-400],p->shostname,curr_client->nick);
			return;
		}
		else
		{
			curr_client->set_user = 1;
			strncpy(curr_client->user,*params,MAX_USERNAME);
			strncpy(curr_client->servername,*((params)+2),MAX_SERVERNAME);
			strncpy(curr_client->realname,*((params)+3),MAX_HOSTNAME);
		}

		//client already has a nick set, so this means he/she is now registered.
		if( (curr_client->set_nick == 1) && (curr_client->registered == 0) )
		{
			curr_client->registered = 1;
			send_message(curr_client->sock,command_replies[RPL_MOTDSTART-300],p->shostname,curr_client->nick,p->shostname);
			send_message(curr_client->sock,command_replies[RPL_MOTD-300],p->shostname,curr_client->nick,SERVER_MSG);
			send_message(curr_client->sock,command_replies[RPL_ENDOFMOTD-300],p->shostname,curr_client->nick);
		}
}

/* This function sends a QUIT message to each user (including the user who sent the message) on the senders's channel */
void cmd_quit(client *curr_client, pool *p, char *prefix, char **params, int n_params)
{
		int i, j;

		if (strlen(curr_client->channel) > 0)
		{
			for(i = 0; i < p->channels_capacity; i++)
			{
				if (strcasecmp(p->channels[i].name, curr_client->channel) == 0)
				{
					for(j = 0; j < p->channels[i].users_capacity; j++)
					{

						if(p->channels[i].users[j] != NULL)
						{
							if (n_params == -1)
								send_message(p->channels[i].users[j]->sock,command_replies[RPL_QUIT-300],curr_client->nick,curr_client->user,curr_client->hostname,"Connection closed unexpectedly");
							else if (*params == NULL)
								send_message(p->channels[i].users[j]->sock,command_replies[RPL_QUIT-300],curr_client->nick,curr_client->user,curr_client->hostname,"Connection closed");
							else
								send_message(p->channels[i].users[j]->sock,command_replies[RPL_QUIT-300],curr_client->nick,curr_client->user,curr_client->hostname,*params);

							if (strcasecmp(p->channels[i].users[j]->nick, curr_client->nick) == 0)
							{
								p->channels[i].users[j] = NULL;
							}
						}
					}

					//check if client was the only user in that channel and remove the channel if so...
					if (p->channels[i].user_count == 1)
					{
						p->channel_count = p->channel_count - 1;
						strcpy(p->channels[i].name,"\0");
					}

					p->channels[i].user_count = p->channels[i].user_count - 1;
					break;
				}
			}
		}

		/* Clean out the struct of the client who just closed connection */
		strcpy(curr_client->hostname,"");
		strcpy(curr_client->servername,"");
		strcpy(curr_client->user,"");
		strcpy(curr_client->nick,"");
		strcpy(curr_client->realname,"");
		strcpy(curr_client->channel,"");
		curr_client->registered = 0;
		curr_client->set_nick = 0;
		curr_client->set_user = 0;

		/* Close the file descriptor */
		close(curr_client->sock);
		FD_CLR(curr_client->sock, &p->read_set);
		p->clientfd[curr_client->descriptor_index] = -1;
		curr_client->descriptor_index = -1;
		curr_client->sock = -1;

}

/* This function sets a client to a specified channel. If the client is already in a channel, the client
 is removed from the current channel he/she is in and then set to the new channel. If no channel exists, the
 channel is created and the user is added to the channel. A JOIN message is sent to all users already on
 the channel */
void cmd_join(client *curr_client, pool *p, char *prefix, char **params, int n_params)
{

	int i, j, found = 0, channel_id;

	//check if client is already on that channel, ignore
	if (strcasecmp(curr_client->channel, *params) == 0)
		return;

	//Look for channel and add client to it if found
	for(i = 0; i < p->channels_capacity; i++)
	{
		//channel found, add client to it
		if (strcasecmp(p->channels[i].name, *params) == 0)
		{
			channel_id = i;
			//resize user array if necessary
			if(p->channels[i].user_count == p->channels[i].users_capacity)
			{
				resize_users(p,i);
			}

			for(j = 0; j < p->channels[i].users_capacity; j++)
			{
				if(p->channels[i].users[j] == NULL)
				{
					//check if the user is already on a channel and brocast a PART message if so
					if(strlen(curr_client->channel) > 0)
					{
						char *param[1];
						param[0] = curr_client->channel;
						cmd_part(curr_client,p,NULL,param,1);
					}

					p->channels[i].users[j] = curr_client;
					strncpy(curr_client->channel,*params,MAX_CHANNAME);
					break;
				}
			}
			p->channels[i].user_count = p->channels[i].user_count + 1;
			found = 1;
			break;
		}
	}

	//channel not found, create it and add client to it
	if (!found)
	{
		//resize channel array if necessary
		if (p->channel_count == p->channels_capacity)
		{
			resize_channels(p);
		}

		for(i = 0; i < p->channels_capacity; i++)
		{
			if (strcmp(p->channels[i].name, "\0") == 0)
			{
				//check if the user is already on a channel and brodcast a PART message
				if(strlen(curr_client->channel) > 0)
				{
					char *params[1];
					params[0] = curr_client->channel;
					cmd_part(curr_client,p,NULL,params,1);
				}

				channel_id = i;
				strncpy(p->channels[i].name,*params,MAX_CHANNAME);
				p->channels[i].users[0] = curr_client;
				strncpy(curr_client->channel,*params,MAX_CHANNAME);
				p->channels[i].user_count = 1;
				p->channel_count = p->channel_count + 1;
				break;
			}
		}
	}

	//create array of users connected to the channel to be used in sending out a JOIN message
	char* client_nicks = malloc(p->channels[channel_id].user_count * MAX_USERNAME);
	strcpy(client_nicks,"\0");


	for(j = 0; j < p->channels[channel_id].users_capacity; j++)
	{
		if(p->channels[channel_id].users[j] != NULL)
		{
			client_nicks = strcat(client_nicks,p->channels[channel_id].users[j]->nick);
			client_nicks = strcat(client_nicks," ");
			send_message(p->channels[channel_id].users[j]->sock,command_replies[RPL_JOIN-300],curr_client->nick,*params);
		}
	}

	send_message(curr_client->sock,command_replies[RPL_NAMREPLY-300],p->shostname,curr_client->nick,curr_client->channel,client_nicks);
	send_message(curr_client->sock,command_replies[RPL_ENDOFNAMES-300],p->shostname,curr_client->nick,curr_client->channel);

	free(client_nicks);

}

/* This function removes a user from a specified channel. A message to all users on the channel telling of the senders departure. */
void cmd_part(client *curr_client, pool *p, char *prefix, char **params, int n_params)
{

	int i,j, k, channel_found, on_channel;
	char *target;

	//part requests are separated by commas (ie PART #chan1,chan2,...)
	target = strtok( *params, "," );

	//sender may have requested to leave multiple channels. Check each one.
	while( target != NULL )
	{
		channel_found = 0;
		on_channel = 0;

		for(i = 0; i < p->channels_capacity && channel_found == 0; i++)
		{
			if (strcasecmp(p->channels[i].name, target) == 0)
			{
				channel_found = 1;
				for(j = 0; j < p->channels[i].users_capacity && on_channel == 0; j++)
				{
					if ( (p->channels[i].users[j] != NULL) && (strcasecmp(p->channels[i].users[j]->nick, curr_client->nick) == 0))
					{
						on_channel = 1;
						//send message to all users on specified channel
						for (k = 0; k < p->channels[i].users_capacity; k++)
						{
							if (p->channels[i].users[k] != NULL)
								send_message(p->channels[i].users[k]->sock,command_replies[RPL_QUIT-300],curr_client->nick,curr_client->user,curr_client->hostname,"Left channel");
						}
						p->channels[i].user_count = p->channels[i].user_count - 1;


						//resize if the users array is taking up too much excess space
						//..NEED TO DO
						if (p->channels[i].users_capacity/2 > p->channels[i].user_count+10)
						{

						}

						//no more users in the channel, remove it
						if (p->channels[i].user_count == 0)
						{
							p->channel_count = p->channel_count - 1;
							strcpy(p->channels[i].name,"\0");
						}

						//resize if the channels array is taking up too much excess space
						//..NEED TO DO

						p->channels[i].users[j] = NULL;
						strcpy(curr_client->channel,"");
						break;
					}

				}

				//sender is not on that channel, send a message saying such
				if (!on_channel)
				{
					send_message(curr_client->sock,error_replies[ERR_NOTONCHANNEL-400],p->shostname,curr_client->nick,target);
				}
			}
		}

		//no channel was found. Send a message saying such.
		if (!channel_found)
		{
			send_message(curr_client->sock,error_replies[ERR_NOSUCHCHANNEL-400],p->shostname,curr_client->nick,target);
		}

		target = strtok( NULL, "," );

	}

}

/* This function lists all the channels currently found on the server and the number of users in each channel */
void cmd_list(client *curr_client, pool *p, char *prefix, char **params, int n_params)
{
	int i;

	send_message(curr_client->sock,command_replies[RPL_LISTSTART-300],p->shostname,curr_client->nick);
	for(i = 0; i < p->channels_capacity; i++)
	{
		if (strcmp(p->channels[i].name, "\0") != 0)
			send_message(curr_client->sock,command_replies[RPL_LIST-300],p->shostname,curr_client->nick,p->channels[i].name,p->channels[i].user_count);

	}
	send_message(curr_client->sock,command_replies[RPL_LISTEND-300],p->shostname,curr_client->nick);
}

/* This function lists the names of the users in the requested channel(s) */
void cmd_who(client *curr_client, pool *p, char *prefix, char **params, int n_params)
{
	int i, j;
	char *target;

	if (*params == NULL)
		return;

	//channel requests are separated by commas (ie WHO #chan1,chan2,...)
	target = strtok( *params, "," );

	//user may have requested multiple channels, check each one.
	while( target != NULL )
	{
		for(i = 0; i < p->channels_capacity; i++)
		{
			if (strcmp(p->channels[i].name, target) == 0)
			{
				for(j = 0; j < p->channels[i].users_capacity; j++)
				{
					if(p->channels[i].users[j] != NULL)
					{
						send_message(curr_client->sock,command_replies[RPL_WHOREPLY-300],p->shostname,curr_client->nick,p->channels[i].name,p->channels[i].users[j]->user,p->channels[i].users[j]->hostname,p->channels[i].users[j]->servername,p->channels[i].users[j]->nick,p->channels[i].users[j]->realname);
					}
				}
				break;
			}
		}

		send_message(curr_client->sock,command_replies[RPL_ENDOFWHO-300],p->shostname,curr_client->nick,target);
		target = strtok( NULL, "," );
	}
}

/* This function sends a message to either a specific user or to all the users in the specified channel */
void cmd_privmsg(client *curr_client, pool *p, char *prefix, char **params, int n_params)
{

	int i,j, found;
	char *target, *msg;

	//grab the message
	msg = *(++params);

	//check to see if the client included a message to send
	if (msg == NULL)
	{
		send_message(curr_client->sock,error_replies[ERR_NOTEXTTOSEND-400],p->shostname,curr_client->nick);
		return;
	}

	//backtrack to the targets
	*params--;

	target = strtok( *params, "," );

	while( target != NULL )
	{
		found = 0;

		//check to see if this target is a channel we are going to broadcast to
	   	for(i = 0; i < p->channels_capacity; i++)
		{
			//it is a channel, now broadcast to everyone in that channel
			if (strcmp(p->channels[i].name, target) == 0)
		   	{
				for(j = 0; j < p->channels[i].users_capacity; j++)
				{
					if ( (p->channels[i].users[j] != NULL) && (strcasecmp(p->channels[i].users[j]->nick, curr_client->nick) != 0) )
					{
						send_message(p->channels[i].users[j]->sock,command_replies[RPL_PRIVMSG-300],curr_client->nick,p->channels[i].name,msg);
					}
				}
				//move on to next target
				found = 1;
				break;
			}

		}

		//not a channel, lets hope it is a nick
		if (!found)
		{
			found = 0;
			for(i = 0; i <= p->maxi; i++)
			{
				if (strcasecmp(p->clients[i].nick, target) == 0)
				{
					send_message(p->clients[i].sock,command_replies[RPL_PRIVMSG-300],curr_client->nick,p->clients[i].nick,msg);
					found = 1;
				}

			}

			//not a channel nor a nick, so send an error
			if (!found)
				send_message(curr_client->sock,error_replies[ERR_NOSUCHNICK-400],p->shostname,curr_client->nick,target);
		}

	    target = strtok( NULL, "," );
	}

}
