/*
 *   IRC - Internet Relay Chat, src/modules/watch-backend.c
 *   (C) 2021 The UnrealIRCd Team
 *
 *   See file AUTHORS in IRC package for additional names of
 *   the programmers.
 *
 *   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 1, 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include "unrealircd.h"

#define WATCH_HASH_TABLE_SIZE 32768

#define WATCHES(client) (moddata_local_client(client, watchCounterMD).i)
#define WATCH(client) (moddata_local_client(client, watchListMD).ptr)

ModDataInfo *watchCounterMD;
ModDataInfo *watchListMD;

static Watch **watchTable = NULL;
static char *siphashkey_watch = NULL;

void dummy_free(ModData *md);
void watch_free(ModData *md);

int watch_backend_user_quit(Client *client, MessageTag *mtags, const char *comment);
int _watch_add(const char *nick, Client *client, int flags);
int _watch_check(Client *client, int event, void *data, int (*watch_notify)(Client *client, Watch *watch, Link *lp, int event, void *data));
Watch *_watch_get(const char *nick);
int _watch_del(const char *nick, Client *client, int flags);
int _watch_del_list(Client *client, int flags);
uint64_t hash_watch_nick_name(const char *name);

ModuleHeader MOD_HEADER
= {
	"watch-backend",
	"6.0.3",
	"backend for /WATCH",
	"UnrealIRCd Team",
	"unrealircd-6",
};

MOD_TEST()
{
	MARK_AS_OFFICIAL_MODULE(modinfo);

	EfunctionAdd(modinfo->handle, EFUNC_WATCH_ADD, _watch_add);
	EfunctionAdd(modinfo->handle, EFUNC_WATCH_DEL, _watch_del);
	EfunctionAdd(modinfo->handle, EFUNC_WATCH_DEL_LIST, _watch_del_list);
	EfunctionAddPVoid(modinfo->handle, EFUNC_WATCH_GET, TO_PVOIDFUNC(_watch_get));
	EfunctionAdd(modinfo->handle, EFUNC_WATCH_CHECK, _watch_check);
	return MOD_SUCCESS;
}

void watch_generic_free(ModData *m)
{
	safe_free(m->ptr);
}

MOD_INIT()
{	
	ModDataInfo mreq;

	MARK_AS_OFFICIAL_MODULE(modinfo);
	ModuleSetOptions(modinfo->handle, MOD_OPT_PERM_RELOADABLE, 1); /* or do a complex memory freeing algorithm instead */

	LoadPersistentPointer(modinfo, siphashkey_watch, watch_generic_free);
	if (siphashkey_watch == NULL)
	{
		siphashkey_watch = safe_alloc(SIPHASH_KEY_LENGTH);
		siphash_generate_key(siphashkey_watch);
	}
	LoadPersistentPointer(modinfo, watchTable, watch_generic_free);
	if (watchTable == NULL)
		watchTable = safe_alloc(sizeof(Watch *) * WATCH_HASH_TABLE_SIZE);

	memset(&mreq, 0 , sizeof(mreq));
	mreq.type = MODDATATYPE_LOCAL_CLIENT;
	mreq.name = "watchCount",
	mreq.free = dummy_free;
	watchCounterMD = ModDataAdd(modinfo->handle, mreq);
	if (!watchCounterMD)
	{
		config_error("[%s] Failed to request user watchCount moddata: %s", MOD_HEADER.name, ModuleGetErrorStr(modinfo->handle));
		return MOD_FAILED;
	}

	memset(&mreq, 0 , sizeof(mreq));
	mreq.type = MODDATATYPE_LOCAL_CLIENT;
	mreq.name = "watchList",
	mreq.free = watch_free;
	watchListMD = ModDataAdd(modinfo->handle, mreq);
	if (!watchListMD)
	{
		config_error("[%s] Failed to request user watchList moddata: %s", MOD_HEADER.name, ModuleGetErrorStr(modinfo->handle));
		return MOD_FAILED;
	}

	HookAdd(modinfo->handle, HOOKTYPE_LOCAL_QUIT, 0, watch_backend_user_quit);

	return MOD_SUCCESS;
}

MOD_LOAD()
{
	return MOD_SUCCESS;
}

MOD_UNLOAD()
{
	SavePersistentPointer(modinfo, siphashkey_watch);
	SavePersistentPointer(modinfo, watchTable);
	return MOD_SUCCESS;
}

void dummy_free(ModData *md)
{
}

void watch_free(ModData *md)
{
	/* it should have been never requested to free as the module is PERM */
	if (md)
	{
		unreal_log(ULOG_ERROR, "watch-backend", "BUG_WATCH_FREE_MEMORY_LEAK", NULL,
		           "[BUG] watchList moddata was not freed -- memory leak!");
	}
}

int watch_backend_user_quit(Client *client, MessageTag *mtags, const char *comment)
{
	/* Clean out list and watch structures -Donwulff */
	watch_del_list(client, 0);
	return 0;
}

int _watch_add(const char *nick, Client *client, int flags)
{
	unsigned int hashv;
	Watch *watch;
	Link *lp;
	
	
	/* Get the right bucket... */
	hashv = hash_watch_nick_name(nick);
	
	/* Find the right nick (header) in the bucket, or NULL... */
	if ((watch = watchTable[hashv]))
		while (watch && mycmp(watch->nick, nick))
		 watch = watch->hnext;
	
	/* If found NULL (no header for this nick), make one... */
	if (!watch) {
		watch = safe_alloc(sizeof(Watch)+strlen(nick)+1);
		watch->lasttime = timeofday;
		strcpy(watch->nick, nick);
		
		watch->watch = NULL;
		
		watch->hnext = watchTable[hashv];
		watchTable[hashv] = watch;
	}
	/* Is this client already on the watch-list? */
	if ((lp = watch->watch))
		while (lp && (lp->value.client != client))
		 lp = lp->next;
	
	/* No it isn't, so add it in the bucket and client addint it */
	if (!lp) {
		lp = watch->watch;
		watch->watch = make_link();
		watch->watch->value.client = client;
		watch->watch->flags = flags;
		watch->watch->next = lp;
		
		lp = make_link();
		lp->next = WATCH(client);
		lp->value.wptr = watch;
		lp->flags = flags;
		WATCH(client) = lp;
		WATCHES(client)++;
		
		RunHook(HOOKTYPE_WATCH_ADD, nick, client, flags);
	}
	
	return 0;
}

int _watch_check(Client *client, int event, void *data, int (*watch_notify)(Client *client, Watch *watch, Link *lp, int event, void *data))
{
	unsigned int hashv;
	Watch *watch;
	Link *lp;

	/* Get us the right bucket */
	hashv = hash_watch_nick_name(client->name);
	
	/* Find the right header in this bucket */
	if ((watch = watchTable[hashv]))
		while (watch && mycmp(watch->nick, client->name))
		 watch = watch->hnext;
	if (!watch)
		return 0;	 /* This nick isn't on watch */
	
	/* Update the time of last change to item */
	watch->lasttime = TStime();
	
	/* Send notifies out to everybody on the list in header */
	for (lp = watch->watch; lp; lp = lp->next)
	{
		watch_notify(client, watch, lp, event, data);
	}
	
	return 0;
}

Watch *_watch_get(const char *nick)
{
	unsigned int hashv;
	Watch *watch;
	
	hashv = hash_watch_nick_name(nick);
	
	if ((watch = watchTable[hashv]))
		while (watch && mycmp(watch->nick, nick))
		 watch = watch->hnext;
	
	return watch;
}

int _watch_del(const char *nick, Client *client, int flags)
{
	unsigned int hashv;
	Watch **watch, *wprev;
	Link **lp, *prev;

	/* Get the bucket for this nick... */
	hashv = hash_watch_nick_name(nick);
	
	/* Find the right header, maintaining last-link pointer... */
	watch = (Watch **)&watchTable[hashv];
	while (*watch && mycmp((*watch)->nick, nick))
		watch = &(*watch)->hnext;
	if (!*watch)
		return 0;	 /* No such watch */
	
	/* Find this client from the list of notifies... with last-ptr. */
	lp = &(*watch)->watch;
	while (*lp)
	{
		if ((*lp)->value.client == client && ((*lp)->flags & flags) == flags)
			break;
		lp = &(*lp)->next;
	}
	if (!*lp)
		return 0;	 /* No such client to watch */
	
	/* Fix the linked list under header, then remove the watch entry */
	prev = *lp;
	*lp = prev->next;
	free_link(prev);
	
	/* Do the same regarding the links in client-record... */
	lp = (Link **)&WATCH(client);
	while (*lp && ((*lp)->value.wptr != *watch))
		lp = &(*lp)->next;
	
	/*
	 * Give error on the odd case... probobly not even neccessary
	 * No error checking in ircd is unneccessary ;) -Cabal95
	 */
	if (!*lp)
	{
		unreal_log(ULOG_WARNING, "watch", "BUG_WATCH_DEL", client,
		           "[BUG] watch_del found a watch entry with no client counterpoint, "
		           "while processing nick $nick on client $client.details",
		           log_data_string("nick", nick));
	} else {
		prev = *lp;
		*lp = prev->next;
		free_link(prev);
	}
	/* In case this header is now empty of notices, remove it */
	if (!(*watch)->watch) {
		wprev = *watch;
		*watch = wprev->hnext;
		safe_free(wprev);
	}
	
	/* Update count of notifies on nick */
	WATCHES(client)--;
	
	RunHook(HOOKTYPE_WATCH_DEL, nick, client, flags);
	
	return 0;
}

int _watch_del_list(Client *client, int flags)
{
	unsigned int hashv;
	Watch *watch;
	Link **np, **lp, *prev;
	
	np = (Link **)&WATCH(client);
	
	while (*np) {
		if (((*np)->flags & flags) != flags)
		{
			/* this entry is not fitting requested flags */
			np = &(*np)->next;
			continue;
		}
		
		WATCHES(client)--;
		
		/* Find the watch-record from hash-table... */
		watch = (*np)->value.wptr;
		lp = &(watch->watch);
		while (*lp && ((*lp)->value.client != client))
			lp = &(*lp)->next;

		/* Not found, another "worst case" debug error */
		if (!*lp)
		{
			unreal_log(ULOG_WARNING, "watch", "BUG_WATCH_DEL_LIST", client,
				   "[BUG] watch_del_list found a watch entry with no table counterpoint, "
				   "while processing client $client.details");
		} else {
			/* Fix the watch-list and remove entry */
			Link *prev = *lp;
			*lp = prev->next;
			free_link(prev);
			
			/*
			 * If this leaves a header without notifies,
			 * remove it. Need to find the last-pointer!
			 */
			if (!watch->watch) {
				Watch **np2, *wprev;
				
				hashv = hash_watch_nick_name(watch->nick);
				
				np2 = &watchTable[hashv];
				while (*np2 && *np2 != watch)
					np2 = &(*np2)->hnext;

				*np2 = watch->hnext;

				safe_free(watch);
			}
		}
		
		prev = *np; /* Save last pointer processed */
		*np = prev->next; /* Jump to the next pointer */
		free_link(prev); /* Free the previous */
	}
	
	if (!flags)
		WATCHES(client) = 0;
	
	return 0;
}

uint64_t hash_watch_nick_name(const char *name)
{
	return siphash_nocase(name, siphashkey_watch) % WATCH_HASH_TABLE_SIZE;
}

