/* BotServ functions
 *
 * (C) 2003-2011 Anope Team
 * Contact us at team@anope.org
 *
 * Please read COPYING and README for further details.
 *
 * Based on the original code of Epona by Lara.
 * Based on the original code of Services by Andy Church.
 */

/*************************************************************************/

#include "services.h"
#include "modules.h"

static UserData *get_user_data(Channel *c, User *u);

E void moduleAddBotServCmds();

/*************************************************************************/

void moduleAddBotServCmds()
{
	ModuleManager::LoadModuleList(Config->BotServCoreModules);
}

/*************************************************************************/
/*************************************************************************/

/* Return information on memory use.  Assumes pointers are valid. */

void get_botserv_stats(long *nrec, long *memuse)
{
	long count = 0, mem = 0;

	for (Anope::insensitive_map<BotInfo *>::const_iterator it = BotListByNick.begin(), it_end = BotListByNick.end(); it != it_end; ++it)
	{
		BotInfo *bi = it->second;

		++count;
		mem += sizeof(*bi);
		mem += bi->nick.length() + 1;
		mem += bi->GetIdent().length() + 1;
		mem += bi->host.length() + 1;
		mem += bi->realname.length() + 1;
	}

	*nrec = count;
	*memuse = mem;
}

/*************************************************************************/
/*************************************************************************/

/* BotServ initialization. */

void bs_init()
{
	if (!Config->s_BotServ.empty())
		moduleAddBotServCmds();
}

/*************************************************************************/

/* Handles all messages that are sent to registered channels where a
 * bot is on.
 */

void botchanmsgs(User *u, ChannelInfo *ci, const Anope::string &buf)
{
	if (!u || !ci || !ci->c || buf.empty())
		return;

	/* Answer to ping if needed */
	if (buf.substr(0, 6).equals_ci("\1PING ") && buf[buf.length() - 1] == '\1')
	{
		Anope::string ctcp = buf;
		ctcp.erase(ctcp.begin());
		ctcp.erase(ctcp.length() - 1);
		ircdproto->SendCTCP(ci->bi, u->nick, "%s", ctcp.c_str());
	}

	bool was_action = false;
	Anope::string realbuf = buf;

	/* If it's a /me, cut the CTCP part because the ACTION will cause
	 * problems with the caps or badwords kicker
	 */
	if (realbuf.substr(0, 8).equals_ci("\1ACTION ") && realbuf[realbuf.length() - 1] == '\1')
	{
		realbuf.erase(0, 8);
		realbuf.erase(realbuf.length() - 1);
		was_action = true;
	}

	if (realbuf.empty())
		return;

	/* Now we can make kicker stuff. We try to order the checks
	 * from the fastest one to the slowest one, since there's
	 * no need to process other kickers if a user is kicked before
	 * the last kicker check.
	 *
	 * But FIRST we check whether the user is protected in any
	 * way.
	 */

	bool Allow = true;
	if (check_access(u, ci, CA_NOKICK))
		Allow = false;
	else if (ci->botflags.HasFlag(BS_DONTKICKOPS) && (ci->c->HasUserStatus(u, CMODE_HALFOP) || ci->c->HasUserStatus(u, CMODE_OP) || ci->c->HasUserStatus(u, CMODE_PROTECT) || ci->c->HasUserStatus(u, CMODE_OWNER)))
		Allow = false;
	else if (ci->botflags.HasFlag(BS_DONTKICKVOICES) && ci->c->HasUserStatus(u, CMODE_VOICE))
		Allow = false;
	
	EventReturn MOD_RESULT;
	FOREACH_RESULT(I_OnPrivmsg, OnPrivmsg(u, ci, realbuf, Allow));
	if (MOD_RESULT == EVENT_STOP)
		return;

	if (Allow)
	{
		/* Bolds kicker */
		if (ci->botflags.HasFlag(BS_KICK_BOLDS) && realbuf.find(2) != Anope::string::npos)
		{
			check_ban(ci, u, TTB_BOLDS);
			bot_kick(ci, u, _("Don't use bolds on this channel!"));
			return;
		}

		/* Color kicker */
		if (ci->botflags.HasFlag(BS_KICK_COLORS) && realbuf.find(3) != Anope::string::npos)
		{
			check_ban(ci, u, TTB_COLORS);
			bot_kick(ci, u, _("Don't use colors on this channel!"));
			return;
		}

		/* Reverses kicker */
		if (ci->botflags.HasFlag(BS_KICK_REVERSES) && realbuf.find(22) != Anope::string::npos)
		{
			check_ban(ci, u, TTB_REVERSES);
			bot_kick(ci, u, _("Don't use reverses on this channel!"));
			return;
		}

		/* Italics kicker */
		if (ci->botflags.HasFlag(BS_KICK_ITALICS) && realbuf.find(29) != Anope::string::npos)
		{
			check_ban(ci, u, TTB_ITALICS);
			bot_kick(ci, u, _("Don't use italics on this channel!"));
			return;
		}

		/* Underlines kicker */
		if (ci->botflags.HasFlag(BS_KICK_UNDERLINES) && realbuf.find(31) != Anope::string::npos)
		{
			check_ban(ci, u, TTB_UNDERLINES);
			bot_kick(ci, u, _("Don't use underlines on this channel!"));
			return;
		}

		/* Caps kicker */
		if (ci->botflags.HasFlag(BS_KICK_CAPS) && realbuf.length() >= ci->capsmin)
		{
			int i = 0, l = 0;

			for (unsigned j = 0, end = realbuf.length(); j < end; ++j)
			{
				if (isupper(realbuf[j]))
					++i;
				else if (islower(realbuf[j]))
					++l;
			}

			/* i counts uppercase chars, l counts lowercase chars. Only
			 * alphabetic chars (so islower || isupper) qualify for the
			 * percentage of caps to kick for; the rest is ignored. -GD
			 */

			if ((i || l) && i >= ci->capsmin && i * 100 / (i + l) >= ci->capspercent)
			{
				check_ban(ci, u, TTB_CAPS);
				bot_kick(ci, u, _("Turn caps lock OFF!"));
				return;
			}
		}

		/* Bad words kicker */
		if (ci->botflags.HasFlag(BS_KICK_BADWORDS))
		{
			bool mustkick = false;

			/* Normalize the buffer */
			Anope::string nbuf = normalizeBuffer(realbuf);

			for (unsigned i = 0, end = ci->GetBadWordCount(); i < end; ++i)
			{
				BadWord *bw = ci->GetBadWord(i);

				if (bw->type == BW_ANY && ((Config->BSCaseSensitive && nbuf.find(bw->word) != Anope::string::npos) || (!Config->BSCaseSensitive && nbuf.find_ci(bw->word) != Anope::string::npos)))
					mustkick = true;
				else if (bw->type == BW_SINGLE)
				{
					size_t len = bw->word.length();

					if ((Config->BSCaseSensitive && bw->word.equals_cs(nbuf)) || (!Config->BSCaseSensitive && bw->word.equals_ci(nbuf)))
						mustkick = true;
					else if (nbuf.find(' ') == len && ((Config->BSCaseSensitive && bw->word.equals_cs(nbuf)) || (!Config->BSCaseSensitive && bw->word.equals_ci(nbuf))))
						mustkick = true;
					else
					{
						if (nbuf.rfind(' ') == nbuf.length() - len - 1 && ((Config->BSCaseSensitive && nbuf.find(bw->word) == nbuf.length() - len) || (!Config->BSCaseSensitive && nbuf.find_ci(bw->word) == nbuf.length() - len)))
							mustkick = true;
						else
						{
							Anope::string wordbuf = " " + bw->word + " ";

							if ((Config->BSCaseSensitive && nbuf.find(wordbuf) != Anope::string::npos) || (!Config->BSCaseSensitive && nbuf.find_ci(wordbuf) != Anope::string::npos))
								mustkick = true;
						}
					}
				}
				else if (bw->type == BW_START)
				{
					size_t len = bw->word.length();

					if ((Config->BSCaseSensitive && nbuf.substr(0, len).equals_cs(bw->word)) || (!Config->BSCaseSensitive && nbuf.substr(0, len).equals_ci(bw->word)))
						mustkick = true;
					else
					{
						Anope::string wordbuf = " " + bw->word;

						if ((Config->BSCaseSensitive && nbuf.find(wordbuf) != Anope::string::npos) || (!Config->BSCaseSensitive && nbuf.find_ci(wordbuf) != Anope::string::npos))
							mustkick = true;
					}
				}
				else if (bw->type == BW_END)
				{
					size_t len = bw->word.length();

					if ((Config->BSCaseSensitive && nbuf.substr(nbuf.length() - len).equals_cs(bw->word)) || (!Config->BSCaseSensitive && nbuf.substr(nbuf.length() - len).equals_ci(bw->word)))
						mustkick = true;
					else
					{
						Anope::string wordbuf = bw->word + " ";

						if ((Config->BSCaseSensitive && nbuf.find(wordbuf) != Anope::string::npos) || (!Config->BSCaseSensitive && nbuf.find_ci(wordbuf) != Anope::string::npos))
							mustkick = true;
					}
				}

				if (mustkick)
				{
					check_ban(ci, u, TTB_BADWORDS);
					if (Config->BSGentleBWReason)
						bot_kick(ci, u, _("Watch your language!"));
					else
						bot_kick(ci, u, _("Don't use the word \"%s\" on this channel!"), bw->word.c_str());

					return;
				}
			}
		}

		/* Flood kicker */
		if (ci->botflags.HasFlag(BS_KICK_FLOOD))
		{
			UserData *ud = get_user_data(ci->c, u);
			if (!ud)
				return;

			if (Anope::CurTime - ud->last_start > ci->floodsecs)
			{
				ud->last_start = Anope::CurTime;
				ud->lines = 0;
			}

			++ud->lines;
			if (ud->lines >= ci->floodlines)
			{
				check_ban(ci, u, TTB_FLOOD);
				bot_kick(ci, u, _("Stop flooding!"));
				return;
			}
		}

		/* Repeat kicker */
		if (ci->botflags.HasFlag(BS_KICK_REPEAT))
		{
			UserData *ud = get_user_data(ci->c, u);
			if (!ud)
				return;

			if (!ud->lastline.empty() && !ud->lastline.equals_ci(buf))
			{
				ud->lastline = buf;
				ud->times = 0;
			}
			else
			{
				if (ud->lastline.empty())
					ud->lastline = buf;
				++ud->times;
			}

			if (ud->times >= ci->repeattimes)
			{
				check_ban(ci, u, TTB_REPEAT);
				bot_kick(ci, u, _("Stop repeating yourself!"));
				return;
			}
		}
	}

	/* Fantaisist commands */
	if (ci->botflags.HasFlag(BS_FANTASY) && buf[0] == Config->BSFantasyCharacter[0] && !was_action)
	{
		Anope::string message = buf;
		/* Strip off the fantasy character */
		message.erase(message.begin());

		size_t space = message.find(' ');
		Anope::string command, rest;
		if (space == Anope::string::npos)
			command = message;
		else
		{
			command = message.substr(0, space);
			rest = message.substr(space + 1);
		}

		if (check_access(u, ci, CA_FANTASIA))
		{

			Command *cmd = FindCommand(ChanServ, command);

			/* Command exists and can be called by fantasy */
			if (cmd && !cmd->HasFlag(CFLAG_DISABLE_FANTASY))
			{
				Anope::string params = rest;
				/* Some commands don't need the channel name added.. eg !help */
				if (!cmd->HasFlag(CFLAG_STRIP_CHANNEL))
					params = ci->name + " " + params;
				params = command + " " + params;

				mod_run_cmd(ChanServ, u, ci, params);
			}

			FOREACH_MOD(I_OnBotFantasy, OnBotFantasy(command, u, ci, rest));
		}
		else
		{
			FOREACH_MOD(I_OnBotNoFantasyAccess, OnBotNoFantasyAccess(command, u, ci, rest));
		}
	}
}

/*************************************************************************/

BotInfo *findbot(const Anope::string &nick)
{
	BotInfo *bi = NULL;
	if (isdigit(nick[0]) && ircd->ts6)
	{
		Anope::map<BotInfo *>::iterator it = BotListByUID.find(nick);
		if (it != BotListByUID.end())
			bi = it->second;
	}
	else
	{
		Anope::insensitive_map<BotInfo *>::iterator it = BotListByNick.find(nick);
		if (it != BotListByNick.end())
			bi = it->second;
	}
	
	FOREACH_MOD(I_OnFindBot, OnFindBot(nick));
	
	return bi;
}

/*************************************************************************/

/* Returns ban data associated with a user if it exists, allocates it
   otherwise. */

static BanData *get_ban_data(Channel *c, User *u)
{
	if (!c || !u)
		return NULL;

	Anope::string mask = u->GetIdent() + "@" + u->GetDisplayedHost();
	/* XXX This should really be on some sort of timer/garbage collector, and use std::map */
	for (std::list<BanData *>::iterator it = c->bd.begin(), it_end = c->bd.end(), it_next; it != it_end; it = it_next)
	{
		it_next = it;
		++it_next;

		if (Anope::CurTime - (*it)->last_use > Config->BSKeepData)
		{
			delete *it;
			c->bd.erase(it);
			continue;
		}
		if ((*it)->mask.equals_ci(mask))
		{
			(*it)->last_use = Anope::CurTime;
			return *it;
		}
	}

	/* If we fall here it is that we haven't found the record */
	BanData *bd = new BanData();
	bd->mask = mask;
	bd->last_use = Anope::CurTime;
	for (int x = 0; x < TTB_SIZE; ++x)
		bd->ttb[x] = 0;

	c->bd.push_front(bd);

	return bd;
}

/*************************************************************************/

/* Returns BotServ data associated with a user on a given channel.
 * Allocates it if necessary.
 */

static UserData *get_user_data(Channel *c, User *u)
{
	if (!c || !u)
		return NULL;

	for (CUserList::iterator it = c->users.begin(), it_end = c->users.end(); it != it_end; ++it)
	{
		UserContainer *uc = *it;

		if (uc->user == u)
		{
			/* Checks whether data is obsolete */
			if (Anope::CurTime - uc->ud.last_use > Config->BSKeepData)
			{
				/* We should not free and realloc, but reset to 0
				   instead. */
				uc->ud.Clear();
				uc->ud.last_use = Anope::CurTime;
			}

			return &uc->ud;
		}
	}

	return NULL;
}

/*************************************************************************/

/** Check if a user should be banned by botserv
 * @param ci The channel the user is on
 * @param u The user
 * @param ttbtype The type of bot kicker the user should be checked against
 */
void check_ban(ChannelInfo *ci, User *u, int ttbtype)
{
	BanData *bd = get_ban_data(ci->c, u);

	if (!bd)
		return;

	/* Don't ban ulines */
	if (u->server->IsULined())
		return;

	++bd->ttb[ttbtype];
	if (ci->ttb[ttbtype] && bd->ttb[ttbtype] >= ci->ttb[ttbtype])
	{
		/* Should not use == here because bd->ttb[ttbtype] could possibly be > ci->ttb[ttbtype]
		 * if the TTB was changed after it was not set (0) before and the user had already been
		 * kicked a few times. Bug #1056 - Adam */
		Anope::string mask;

		bd->ttb[ttbtype] = 0;

		get_idealban(ci, u, mask);

		if (ci->c)
			ci->c->SetMode(NULL, CMODE_BAN, mask);
		FOREACH_MOD(I_OnBotBan, OnBotBan(u, ci, mask));
	}
}

/*************************************************************************/

/* This makes a bot kick a user. Works somewhat like notice_lang in fact ;) */

void bot_kick(ChannelInfo *ci, User *u, const char *message, ...)
{
	va_list args;
	char buf[1024];

	if (!ci || !ci->bi || !ci->c || !u)
		return;

	Anope::string fmt = GetString(u->Account(), message);
	va_start(args, message);
	if (fmt.empty())
		return;
	vsnprintf(buf, sizeof(buf), fmt.c_str(), args);
	va_end(args);

	ci->c->Kick(ci->bi, u, "%s", buf);
}

/*************************************************************************/

/* Makes a simple ban and kicks the target
 * @param requester The user requesting the kickban
 * @param ci The channel
 * @param u The user being kicked
 * @param reason The reason
 */
void bot_raw_ban(User *requester, ChannelInfo *ci, User *u, const Anope::string &reason)
{
	if (!u || !ci)
		return;

	if (ModeManager::FindUserModeByName(UMODE_PROTECTED) && u->IsProtected() && requester != u)
	{
		ircdproto->SendPrivmsg(ci->bi, ci->name, "%s", GetString(requester->Account(), _(ACCESS_DENIED)).c_str());
		return;
	}

	ChanAccess *u_access = ci->GetAccess(u), *req_access = ci->GetAccess(requester);
	int16 u_level = u_access ? u_access->level : 0, req_level = req_access ? req_access->level : 0;
	if (ci->HasFlag(CI_PEACE) && !requester->nick.equals_ci(u->nick) && u_level >= req_level)
		return;

	if (matches_list(ci->c, u, CMODE_EXCEPT))
	{
		ircdproto->SendPrivmsg(ci->bi, ci->name, "%s", GetString(requester->Account(), _("User matches channel except.")).c_str());
		return;
	}

	Anope::string mask;
	get_idealban(ci, u, mask);

	ci->c->SetMode(NULL, CMODE_BAN, mask);

	/* Check if we need to do a signkick or not -GD */
	if (ci->HasFlag(CI_SIGNKICK) || (ci->HasFlag(CI_SIGNKICK_LEVEL) && !check_access(requester, ci, CA_SIGNKICK)))
		ci->c->Kick(ci->bi, u, "%s (%s)", !reason.empty() ? reason.c_str() : ci->bi->nick.c_str(), requester->nick.c_str());
	else
		ci->c->Kick(ci->bi, u, "%s", !reason.empty() ? reason.c_str() : ci->bi->nick.c_str());
}

/*************************************************************************/

/* Makes a kick with a "dynamic" reason ;)
 * @param requester The user requesting the kick
 * @param ci The channel
 * @param u The user being kicked
 * @param reason The reason for the kick
 */
void bot_raw_kick(User *requester, ChannelInfo *ci, User *u, const Anope::string &reason)
{
	if (!u || !ci || !ci->c || !ci->c->FindUser(u))
		return;

	if (ModeManager::FindUserModeByName(UMODE_PROTECTED) && u->IsProtected() && requester != u)
	{
		ircdproto->SendPrivmsg(ci->bi, ci->name, "%s", GetString(requester->Account(), _(ACCESS_DENIED)).c_str());
		return;
	}

	ChanAccess *u_access = ci->GetAccess(u), *req_access = ci->GetAccess(requester);
	int16 u_level = u_access ? u_access->level : 0, req_level = req_access ? req_access->level : 0;
	if (ci->HasFlag(CI_PEACE) && !requester->nick.equals_ci(u->nick) && u_level >= req_level)
		return;

	if (ci->HasFlag(CI_SIGNKICK) || (ci->HasFlag(CI_SIGNKICK_LEVEL) && !check_access(requester, ci, CA_SIGNKICK)))
		ci->c->Kick(ci->bi, u, "%s (%s)", !reason.empty() ? reason.c_str() : ci->bi->nick.c_str(), requester->nick.c_str());
	else
		ci->c->Kick(ci->bi, u, "%s", !reason.empty() ? reason.c_str() : ci->bi->nick.c_str());
}

/*************************************************************************/

/**
 * Normalize buffer stripping control characters and colors
 * @param A string to be parsed for control and color codes
 * @return A string stripped of control and color codes
 */
Anope::string normalizeBuffer(const Anope::string &buf)
{
	Anope::string newbuf;

	for (unsigned i = 0, end = buf.length(); i < end; ++i)
	{
		switch (buf[i])
		{
			/* ctrl char */
			case 1:
				break;
			/* Bold ctrl char */
			case 2:
				break;
			/* Color ctrl char */
			case 3:
				/* If the next character is a digit, its also removed */
				if (isdigit(buf[i + 1]))
				{
					++i;

					/* not the best way to remove colors
					 * which are two digit but no worse then
					 * how the Unreal does with +S - TSL
					 */
					if (isdigit(buf[i + 1]))
						++i;

					/* Check for background color code
					 * and remove it as well
					 */
					if (buf[i + 1] == ',')
					{
						++i;

						if (isdigit(buf[i + 1]))
							++i;
						/* not the best way to remove colors
						 * which are two digit but no worse then
						 * how the Unreal does with +S - TSL
						 */
						if (isdigit(buf[i + 1]))
							++i;
					}
				}

				break;
			/* line feed char */
			case 10:
				break;
			/* carriage returns char */
			case 13:
				break;
			/* Reverse ctrl char */
			case 22:
				break;
			/* Underline ctrl char */
			case 31:
				break;
			/* Italic ctrl char */
			case 29:
				break;
			/* A valid char gets copied into the new buffer */
			default:
				newbuf += buf[i];
		}
	}

	return newbuf;
}
