/*

oer - IRC bot

See ftp://nic.funet.fi/pub/unix/irc/docs/FAQ.gz section 11 for the
definition of the word bot.

Copyright (C) 2000 EQU <equ@equnet.org>

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/

#include "oer-common.h"
#include "oer.h"
#include "parse.h"
#include "ds.h"
#include "misc.h"
#include "list.h"

/* prototype definitions */
int serverconnection(void);
int registerconnection(void);
int handleserverdata(void);
int processmmodes(void);
int processtimeds(void);
void establishconnection(void);
void reconnect(void);

void reconnect()
{
	char stringbuffer[WRITE_BUFFER_LENGTH + 1];

	/* check if connected */
	if(mystate->current_server == NULL) {
		return;
	}
	if(mystate->reconnect == OER_RECONNECT_NONE) {
		return;
	}
	switch(mystate->reconnect) {
	case OER_RECONNECT_ADMIN:
		snprintf(stringbuffer, WRITE_BUFFER_LENGTH, "QUIT :%s\n", mystate->signoff);
		oer_debug(OER_DEBUG_INFO, "reconnect->%s", stringbuffer);
		if(write(mystate->sockfd, stringbuffer, strlen(stringbuffer)) < 0) {
			oer_debug(OER_DEBUG_ERROR, "reconnect->write failed: %s\n", strerror(errno));
		}
		mystate->current_server->lastping = mystate->now;
		close(mystate->sockfd);
		mystate->current_server->registered = OER_REGISTERCONNECTION_STATUS_BEGIN;
		mystate->current_server->connected = 0;
		mystate->current_server = NULL;
		initall();
		oer_debug(OER_DEBUG_INFO, "reconnect->sleeping 30 seconds to avoid throttling\n");
		sleep(OER_RECONNECT_DELAY);
		establishconnection();
		if(mystate->bailout) {
			exit(EXIT_FAILURE);
		}
		mystate->reconnect = OER_RECONNECT_NONE;
		break;
	case OER_RECONNECT_STONED:
		oer_debug(OER_DEBUG_INFO, "main->current server is stoned, jumping to next one if available\n");
		close(mystate->sockfd);
		mystate->current_server->registered = OER_REGISTERCONNECTION_STATUS_BEGIN;
		mystate->current_server->connected = 0;
		mystate->current_server = NULL;
		initall();
		oer_debug(OER_DEBUG_INFO, "reconnect->sleeping 30 seconds to avoid throttling\n");
		sleep(OER_RECONNECT_DELAY);
		establishconnection();
		if(mystate->bailout) {
			exit(EXIT_FAILURE);
		}
		mystate->reconnect = OER_RECONNECT_NONE;
		break;
	case OER_RECONNECT_ERROR:
		if(mystate->quitting) {
			oer_debug(OER_DEBUG_INFO, "reconnect->%s exiting\n", mystate->progname);
			close(mystate->sockfd);
			exit(EXIT_SUCCESS);
		}
		oer_debug(OER_DEBUG_INFO, "reconnect->failed to read/write from/to current server\n");
		oer_debug(OER_DEBUG_INFO, "reconnect->jumping to next server if available\n");
		close(mystate->sockfd);
		mystate->current_server->registered = OER_REGISTERCONNECTION_STATUS_BEGIN;
		mystate->current_server->connected = 0;
		mystate->current_server = NULL;
		initall();
		oer_debug(OER_DEBUG_INFO, "reconnect->sleeping 30 seconds to avoid throttling\n");
		sleep(OER_RECONNECT_DELAY);
		establishconnection();
		if(mystate->bailout) {
			exit(EXIT_FAILURE);
		}
		mystate->reconnect = OER_RECONNECT_NONE;
		break;
	}
}

void establishconnection()
{
	/* loop until a fatal error */
	while(!mystate->bailout) {
		mystate->now = time(NULL);
		switch(serverconnection()) {
		case OER_SERVERCONNECTION_ERR_SOCKET:
			oer_debug(OER_DEBUG_FATAL, "establishconnection->socket() failed, exiting.\n");
			mystate->bailout = 1;
			return;
		case OER_SERVERCONNECTION_ERR_GETHOSTNAME:
			oer_debug(OER_DEBUG_FATAL, "establishconnection->gethostname() failed, exiting.\n");
			mystate->bailout = 1;
			return;
		case OER_SERVERCONNECTION_ERR_GETHOSTBYNAME_RETRY:
			oer_debug(OER_DEBUG_ERROR, "establishconnection->gethostbyname() temporary error, retrying.\n");
			sleep(OER_WAIT_BETWEEN_CONNECTION_ATTEMPTS);
			break;
		case OER_SERVERCONNECTION_ERR_GETSERVER_RETRY:
			oer_debug(OER_DEBUG_ERROR, "establishconnection->getserver() temporary error, retrying.\n");
			sleep(OER_WAIT_BETWEEN_CONNECTION_ATTEMPTS);
			break;
		case OER_SERVERCONNECTION_ERR_GETHOSTBYNAME:
			oer_debug(OER_DEBUG_FATAL, "establishconnection->gethostbyname() fatal error, exiting.\n");
			mystate->bailout = 1;
			return;
		case OER_SERVERCONNECTION_ERR_BIND:
			oer_debug(OER_DEBUG_FATAL, "establishconnection->bind() fatal error, exiting.\n");
			mystate->bailout = 1;
			return;
		case OER_SERVERCONNECTION_ERR_CONNECT_RETRY:
			oer_debug(OER_DEBUG_ERROR, "establishconnection->connect() temporary error, retrying.\n");
			sleep(OER_WAIT_BETWEEN_CONNECTION_ATTEMPTS);
			break;
		case OER_SERVERCONNECTION_ERR_CONNECT:
			oer_debug(OER_DEBUG_FATAL, "establishconnection->connect() fatal error, exiting.\n");
			mystate->bailout = 1;
			return;
		case OER_SERVERCONNECTION_ALREADY_CONNECTED:
		case OER_SERVERCONNECTION_EVERYTHING_OK:
			return;
		}
	}
}

int serverconnection()
{
	struct hostent *hptr;
	struct sockaddr_in servaddr;

	/* check if we are already connected */
	if(mystate->current_server != NULL) {
		if(mystate->current_server->connected) {
			return OER_SERVERCONNECTION_ALREADY_CONNECTED;
		}
	}
	if((mystate->sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		oer_debug(OER_DEBUG_ERROR, "serverconnection->socket() failed: %s\n", strerror(errno));
		return OER_SERVERCONNECTION_ERR_SOCKET;
	}
	memset(&servaddr, 0, sizeof(struct sockaddr_in));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = INADDR_ANY;
	/* if we don't have a vhost, just use the default system hostname */
	if(!mystate->vhost) {
		if(gethostname(mystate->host, HOSTLEN) < 0) {
			oer_debug(OER_DEBUG_ERROR, "serverconnection->gethostname() failed: %s\n", strerror(errno));
			close(mystate->sockfd);
			return OER_SERVERCONNECTION_ERR_GETHOSTNAME;
		}
	}
	if((hptr = (struct hostent *) gethostbyname(mystate->host)) == NULL) {
#ifndef _NO_H_ERRNO
		oer_debug(OER_DEBUG_ERROR, "serverconnection->gethostbyname() for %s failed (h_errono = %d)\n", mystate->host, h_errno);
		if(h_errno == TRY_AGAIN) {
			oer_debug(OER_DEBUG_ERROR, "serverconnection->gethostbyname() non-fatal error\n");
			close(mystate->sockfd);
			return OER_SERVERCONNECTION_ERR_GETHOSTBYNAME_RETRY;
		} else {
			close(mystate->sockfd);
			return OER_SERVERCONNECTION_ERR_GETHOSTBYNAME;
		}
#else
		/* i'm feeling lucky, and lazy too :) */
		oer_debug(OER_DEBUG_ERROR, "serverconnection->gethostbyname() unknown error\n");
		close(mystate->sockfd);
		return OER_SERVERCONNECTION_ERR_GETHOSTBYNAME_RETRY;
#endif
	}
	memcpy((char *) &servaddr.sin_addr, hptr->h_addr, hptr->h_length);
	if(mystate->vhost) {
		/* bind is only necessary for vhost */
		if(bind(mystate->sockfd, (struct sockaddr *) &servaddr, sizeof(struct sockaddr)) < 0) {
			oer_debug(OER_DEBUG_ERROR, "serverconnection->bind() failed: %s (check your vhost setting)\n", strerror(errno));
			close(mystate->sockfd);
			return OER_SERVERCONNECTION_ERR_BIND;
		}
	}
	if((mystate->current_server = getserver()) == NULL) {
		/* we didn't get a server this time but we will eventually
		   once the used & stoned flags are cleared. Unless
		   ofcourse the servers are permanently unreachable. */
		close(mystate->sockfd);
		return OER_SERVERCONNECTION_ERR_GETSERVER_RETRY;
	}
	mystate->current_server->used = 1;
	oer_debug(OER_DEBUG_INFO, "serverconnection->using server %s port %d\n", mystate->current_server->serverhost, mystate->current_server->serverport);
	if((hptr = (struct hostent *) gethostbyname(mystate->current_server->serverhost)) == NULL) {
#ifndef _NO_H_ERRNO
		oer_debug(OER_DEBUG_ERROR, "serverconnection->gethostbyname() for %s failed (h_errno = %d)\n", mystate->current_server->serverhost, h_errno);
		if(h_errno == NO_ADDRESS || h_errno == HOST_NOT_FOUND || h_errno == TRY_AGAIN) {
			oer_debug(OER_DEBUG_INFO, "serverconnection->switching to next server if available\n");
			close(mystate->sockfd);
			return OER_SERVERCONNECTION_ERR_GETHOSTBYNAME_RETRY;
		}
#else
		/* i'm feeling lucky, and lazy too :) */
		oer_debug(OER_DEBUG_ERROR, "serverconnection->gethostbyname() unknown error\n");
		close(mystate->sockfd);
		return OER_SERVERCONNECTION_ERR_GETHOSTBYNAME_RETRY;
#endif
        }
	memcpy(&servaddr.sin_addr, hptr->h_addr_list[0], sizeof(struct in_addr));
	servaddr.sin_port = htons(mystate->current_server->serverport);
	if(connect(mystate->sockfd, (struct sockaddr *) &servaddr, sizeof(struct sockaddr_in)) < 0) {
		oer_debug(OER_DEBUG_ERROR, "serverconnection->connect() failed: %s\n", strerror(errno));
		if(errno == ECONNREFUSED || errno == ETIMEDOUT || errno == ENETUNREACH || errno == EHOSTUNREACH) {
			oer_debug(OER_DEBUG_INFO, "serverconnection->connect() non-fatal error\n");
			oer_debug(OER_DEBUG_INFO, "serverconnection->switching to next server if available\n");
			close(mystate->sockfd);
			return OER_SERVERCONNECTION_ERR_CONNECT_RETRY;
		}
		close(mystate->sockfd);
		return OER_SERVERCONNECTION_ERR_CONNECT;
	}
	mystate->current_server->connected = 1;
	return OER_SERVERCONNECTION_EVERYTHING_OK;
}

int registerconnection()
{
	char stringbuffer[WRITE_BUFFER_LENGTH + 1];
	struct passwd *pwd;

	/* check if connected */
	if(mystate->current_server == NULL) {
		return OER_REGISTERCONNECTION_EVERYTHING_OK;
	}
	/* check if registered */
	if(mystate->current_server->registered == OER_REGISTERCONNECTION_STATUS_DONE) {
		return OER_REGISTERCONNECTION_EVERYTHING_OK;
	}
	if(mystate->current_server->registered == OER_REGISTERCONNECTION_STATUS_BEGIN) {
		oer_debug(OER_DEBUG_INFO, "registerconnection->registering on the server\n");
		snprintf(stringbuffer, WRITE_BUFFER_LENGTH, "NICK %s\n", (mystate->use_alt_nick) ? mystate->altnick : mystate->nick);
		oer_debug(OER_DEBUG_INFO, "registerconnection->%s", stringbuffer);
		if(write(mystate->sockfd, stringbuffer, strlen(stringbuffer)) < 0) {
			oer_debug(OER_DEBUG_ERROR, "registerconnection->write failed: %s\n", strerror(errno));
			return OER_REGISTERCONNECTION_ERR_WRITE;
		}
		mystate->current_server->lastping = mystate->now;
		mystate->current_server->registered = OER_REGISTERCONNECTION_STATUS_NICK;
		return OER_REGISTERCONNECTION_EVERYTHING_OK;
	}
	if(mystate->current_server->registered == OER_REGISTERCONNECTION_STATUS_NICK) {
		if(!mystate->customuser) {
			if((pwd = getpwuid(getuid())) == NULL) {
				return OER_REGISTERCONNECTION_ERR_GETPWUID;
			} else {
				strncpy(mystate->user, pwd->pw_name, USERLEN);
			}
		}
		snprintf(stringbuffer, WRITE_BUFFER_LENGTH, "USER %s %s %s :%s\n", mystate->user, mystate->host, mystate->current_server->serverhost, mystate->realname);
		oer_debug(OER_DEBUG_INFO, "registerconnection->%s", stringbuffer);
		if(write(mystate->sockfd, stringbuffer, strlen(stringbuffer)) < 0) {
			oer_debug(OER_DEBUG_ERROR, "registerconnection->write failed: %s\n", strerror(errno));
			return OER_REGISTERCONNECTION_ERR_WRITE;
		}
		mystate->current_server->lastping = mystate->now;
		mystate->current_server->registered = OER_REGISTERCONNECTION_STATUS_USER;
		return OER_REGISTERCONNECTION_EVERYTHING_OK;
	}
	return OER_REGISTERCONNECTION_STATUS_ERROR;
}

int handleserverdata()
{
	int cont;
	int i;
	char chars[2];
	char stringbuffer[READ_BUFFER_LENGTH + 1];
        char pong[STRINGLEN + 1];
        size_t br;
        char *ptr;

	i = 0;
	cont = 1;
        do {
                if((br = read(mystate->sockfd, chars, 1)) < 0) {
			/* error while reading from server socket */
			oer_debug(OER_DEBUG_ERROR, "handleserverdata->read failed: %s\n", strerror(errno));
			return OER_HANDLESERVERDATA_ERR_READ;
                }
		if(br == 0) {
			/* nothing/eof was returned, bail out */
			cont = 0;
		}
		if(br > 0 && i < READ_BUFFER_LENGTH) {
			/* check for newline */
			chars[1] = '\0';
			stringbuffer[i++] = chars[0];
			if(chars[0] == '\n') {
				cont = 0;
			}
		}
	} while(cont);
	if(!i) {
		/* zero bytes was read, bail out */
		return OER_HANDLESERVERDATA_ERR_NOTHING_READ;
	}
	stringbuffer[i] = '\0';
	/* count both reads from and writes to the server as 
	   server activity */
	mystate->current_server->lastping = mystate->now;
	/* here we log all the server traffic, prefix with timestamp */
	if(index(mystate->flags, (int)'l') != NULL) {
		fprintf(mystate->logfp, "%lu %s", (unsigned long) mystate->now, stringbuffer);
	}
	if((ptr = (char *) strstr(stringbuffer, "PING :")) != NULL) {
		snprintf(pong, STRINGLEN, "PONG :%s", ptr + 6);
		oer_debug(OER_DEBUG_INFO, "handleserverdata->%s", pong);
		if(write(mystate->sockfd, pong, strlen(pong)) < 0) {
			oer_debug(OER_DEBUG_ERROR, "handleserverdata->write failed: %s\n", strerror(errno));
			return OER_HANDLESERVERDATA_ERR_WRITE;
		}
		mystate->current_server->lastping = mystate->now;
	}
	else {
		/* everything else but pings */
		parseirc(stringbuffer);
	}
        return OER_HANDLESERVERDATA_EVERYTHING_OK;
}

int processmmodes()
{
	int mmodes;
	int actions;
	char mode[CHANLEN + 1];
	char target[HUGESTRINGLEN + 1];
	char stringbuffer[WRITE_BUFFER_LENGTH + 1];
	struct channel *this;
	struct modeline *mm;
	struct modeline *tm;

	/* check if connected */
	if(mystate->current_server == NULL) {
		return OER_PROCESSMMODES_EVERYTHING_OK;
	}
	/* check if registered */
	if(mystate->current_server->registered != OER_REGISTERCONNECTION_STATUS_DONE) {
		return OER_PROCESSMMODES_EVERYTHING_OK;
	}
	if(mystate->netjoining) {
		/* don't do any actions while net is joining */
		return OER_PROCESSMMODES_EVERYTHING_OK;
	}
        for(this = mystate->channels, actions = 0; this != NULL; this = this->next) {
		if(actions >= OER_ACTIONS_PER_MAINLOOP) {
			return OER_PROCESSMMODES_EVERYTHING_OK;
		}
		if(!this->joined) {
			/* don't process any modes on channels we
			   aren't on */
			continue;
		}
		/* we like to play on the safe side */
		memset(&mode, 0, CHANLEN + 1);
		memset(&target, 0, HUGESTRINGLEN + 1);
		mmodes = 0;
		mm = this->modelines;
		while(mm != NULL && mmodes < mystate->current_server->servermodes) {
			tm = mm->next;
			if(mystate->now >= mm->at && !mm->done) {
				strcat(mode, mm->mode);
				strcat(target, mm->target);
				strcat(target, " ");
				mmodes++;
				mm->done = 1;
				/* only 1 mmode special case */
				if(mm->prev == NULL && mm->next == NULL) {
					this->modelines = NULL;
					free(mm->mode);
					free(mm->target);
					free(mm);
					tm = NULL;
				} else {
					/* the normal case, >1 mmodes */
					if(mm->prev == NULL) {
						/* 1st mmode */
						this->modelines = mm->next;
						mm->next->prev = NULL;
						tm = mm->next;
					} else {
						if(mm->next == NULL) {
							/* last mmode */
							mm->prev->next = NULL;
							tm = mm->prev;
						} else {
							/* between 1 or more mmodes */
							mm->prev->next = mm->next;
							mm->next->prev = mm->prev;
							tm = mm->next;
						}
					}
					free(mm->mode);
					free(mm->target);
					free(mm);
				}
			}
			mm = tm;
		}
		if(mmodes) {
			snprintf(stringbuffer, WRITE_BUFFER_LENGTH, "MODE %s %s %s\n", this->name, mode, target);
			oer_debug(OER_DEBUG_INFO, "processmmodes->%s", stringbuffer);
			if(write(mystate->sockfd, stringbuffer, strlen(stringbuffer)) < 0) {
				oer_debug(OER_DEBUG_ERROR, "processmodes->write failed: %s\n", strerror(errno));
				return OER_PROCESSMMODES_ERR_WRITE;
			}
			actions++;
			mystate->current_server->lastping = mystate->now;
		}
	}
	return OER_PROCESSMMODES_EVERYTHING_OK;
}

int processtimeds()
{
	int got_timed;
	int actions;
	struct channel *this;
	struct timed *tim;
	struct timed *tim2;
	char stringbuffer[WRITE_BUFFER_LENGTH + 1];

	/* check if connected */
	if(mystate->current_server == NULL) {
		return OER_PROCESSTIMEDS_EVERYTHING_OK;
	}
	/* check if registered */
	if(mystate->current_server->registered != OER_REGISTERCONNECTION_STATUS_DONE) {
		return OER_PROCESSTIMEDS_EVERYTHING_OK;
	}
	if(mystate->netjoining) {
		/* don't do any actions while net is joining */
		return OER_PROCESSTIMEDS_EVERYTHING_OK;
	}
	for(tim = mystate->timeds, actions = 0; tim != NULL; tim = tim2) {
		tim2 = tim->next;
		if(tim->at > mystate->now) {
			continue;
		}
		if(actions >= OER_ACTIONS_PER_MAINLOOP) {
			return OER_PROCESSTIMEDS_EVERYTHING_OK;
		}
		got_timed = 0;
		switch(tim->type) {
		case OER_TIMED_IRC_COMMAND:
			switch(tim->function) {
			case OER_TIMED_IRC_COMMAND_KICK:
				if((this = getchptr(tim->optstring1)) == NULL) {
					return OER_PROCESSTIMEDS_EVERYTHING_OK;
				}
				/* this check is necessary because KICK messages are
				   scheduled before executed and it is possible that
				   there was manual prevention (!unlock) */
				if(!this->locked.unlocked) {
					strncpy(stringbuffer, tim->irc_command, WRITE_BUFFER_LENGTH);
					got_timed = 1;
				}
				break;
			case OER_TIMED_IRC_COMMAND_DUMMY:
				strncpy(stringbuffer, tim->irc_command, WRITE_BUFFER_LENGTH);
				got_timed = 1;
				break;
			}
			break;
		}
		if(got_timed) {
			oer_debug(OER_DEBUG_INFO, "processtimeds->%s", stringbuffer);
			if(write(mystate->sockfd, stringbuffer, strlen(stringbuffer)) < 0) {
				oer_debug(OER_DEBUG_ERROR, "processtimeds->write failed: %s\n", strerror(errno));
				return OER_PROCESSTIMEDS_ERR_WRITE;
			}
			actions++;
			mystate->current_server->lastping = mystate->now;
		}
		/* only 1 timed, special case */
		if(tim->prev == NULL && tim->next == NULL) {
			free(tim->irc_command);
			free(tim->optstring1);
			free(tim->optstring2);
			free(tim->optstring3);
			free(tim);
			mystate->timeds = NULL;
			return OER_PROCESSTIMEDS_EVERYTHING_OK;
		}
		/* the normal case, >1 timeds */
		if(tim->prev == NULL) {
			/* 1st timed */
			mystate->timeds = tim->next;
			tim->next->prev = NULL;
		} else {
			/* in between > 1 timeds */
			if(tim->next == NULL) {
				/* last timed */
				tim->prev->next = NULL;
			} else {
				/* between 1 or more timeds */
				tim->prev->next = tim->next;
				tim->next->prev = tim->prev;
			}
		}
		free(tim->irc_command);
		free(tim->optstring1);
		free(tim->optstring2);
		free(tim->optstring3);
		free(tim);
	}
	return OER_PROCESSTIMEDS_EVERYTHING_OK;
}
