/* Base routines for ChanServ access level handling.
 *
 * IRC Services is copyright (c) 1996-2002 Andrew Church.
 *     E-mail: <achurch@achurch.org>
 * Parts written by Andrew Kempe and others.
 * This program is free but copyrighted software; see the file COPYING for
 * details.
 */

#include "services.h"
#include "modules.h"
#include "language.h"
#include "modules/nickserv/nickserv.h"
#include "modules/operserv/operserv.h"

#include "chanserv.h"
#include "access.h"
#include "cs-local.h"

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

static Module *module;

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

/* Array of all access levels: */
EXPORT_ARRAY(struct levelinfo_, levelinfo)
LevelInfo levelinfo[] = {
    /* Dummy entry for channel owner; flags filled in by init_access */
    { CA_AUTOOWNER, ACCLEV_FOUNDER, "",        -1,
	  CL_SET_MODE,   { cumode: {"", 1} } }, /* FIXME: `field:' gcc-ism */
    { CA_AUTOPROTECT,      100, "AUTOPROTECT", CHAN_LEVEL_AUTOPROTECT,
	  CL_SET_MODE,   { cumode: {"a",0} } },
    { CA_AUTOOP,            50, "AUTOOP",      CHAN_LEVEL_AUTOOP,
	  CL_SET_MODE,   { cumode: {"o",1} } },
    { CA_AUTOHALFOP,        40, "AUTOHALFOP",  CHAN_LEVEL_AUTOHALFOP,
	  CL_SET_MODE,   { cumode: {"h",1} } },
    { CA_AUTOVOICE,         30, "AUTOVOICE",   CHAN_LEVEL_AUTOVOICE,
	  CL_SET_MODE,   { cumode: {"v",0} } },

    /* Internal use; not settable by users.  These two levels change based
     * on the SECUREOPS and RESTRICTED settings. */
    { CA_AUTODEOP,          -1, "",            -1,
	  CL_CLEAR_MODE|CL_LESSEQUAL, { cumode: {"oh",0} } },
    { CA_NOJOIN,          -100, "",            -1,
	  CL_OTHER|CL_LESSEQUAL },

    { CA_INVITE,            50, "INVITE",      CHAN_LEVEL_INVITE,
	  CL_ALLOW_CMD,  { {"INVITE"} } },
    { CA_AKICK,            100, "AKICK",       CHAN_LEVEL_AKICK,
	  CL_ALLOW_CMD,  { {"AKICK"} } },
    { CA_SET,   ACCLEV_FOUNDER, "SET",         CHAN_LEVEL_SET,
	  CL_ALLOW_CMD,  { {"SET"} } },
    { CA_CLEAR,            100, "CLEAR",       CHAN_LEVEL_CLEAR,
	  CL_ALLOW_CMD,  { {"CLEAR"} } },
    { CA_UNBAN,             50, "UNBAN",       CHAN_LEVEL_UNBAN,
	  CL_ALLOW_CMD,  { {"UNBAN"} } },
    { CA_ACCESS_LIST,        0, "ACC-LIST",    CHAN_LEVEL_ACCESS_LIST,
	  CL_ALLOW_CMD,  { {"ACCESS","LIST"} } },
    { CA_ACCESS_CHANGE,     40, "ACC-CHANGE",  CHAN_LEVEL_ACCESS_CHANGE,
	  CL_ALLOW_CMD,  { {"ACCESS"} } },
    { CA_MEMO,             100, "MEMO",        CHAN_LEVEL_MEMO,
	  CL_OTHER },
    { CA_OPDEOP,            50, "OP-DEOP",     CHAN_LEVEL_OPDEOP,
	  CL_ALLOW_CMD,  { {"OP"} } },  /* also includes "DEOP" */
    { CA_VOICE,             30, "VOICE",       CHAN_LEVEL_VOICE,
	  CL_ALLOW_CMD,  { {"VOICE"} } },
    { CA_HALFOP,            40, "HALFOP",      CHAN_LEVEL_HALFOP,
	  CL_ALLOW_CMD,  { {"HALFOP"} } },
    { CA_PROTECT,          100, "PROTECT",     CHAN_LEVEL_PROTECT,
	  CL_ALLOW_CMD,  { {"PROTECT"} } },
    { CA_KICK,              50, "KICK",        CHAN_LEVEL_KICK,
	  CL_ALLOW_CMD,  { {"KICK"} } },
    { CA_TOPIC,             50, "TOPIC",       CHAN_LEVEL_TOPIC,
	  CL_ALLOW_CMD,  { {"TOPIC"} } },
    { CA_STATUS,           100, "STATUS",      CHAN_LEVEL_STATUS,
	  CL_ALLOW_CMD,  { {"STATUS"} } },

    { -1 }
};

/* Default access levels (initialized at runtime): */
int16 def_levels[CA_SIZE];
/* Which levels are "maximums" (CL_LESSEQUAL): */
static int lev_is_max[CA_SIZE];

static int get_access_if_idented(User *user, ChannelInfo *ci);

/*************************************************************************/
/************************** Global-use routines **************************/
/*************************************************************************/

/* Reset channel access level values to their default state.  Does not call
 * put_channelinfo().  If `set' is nonzero, an access level array is
 * allocated and initialized to default values, else any existing array is
 * cleared (implying the default values).
 */

EXPORT_FUNC(reset_levels)
void reset_levels(ChannelInfo *ci, int set)
{
    int i;

    free(ci->levels);
    if (set) {
	ci->levels = scalloc(CA_SIZE, sizeof(*ci->levels));
	for (i = 0; i < CA_SIZE; i++)
	    ci->levels[i] = def_levels[i];
    } else {
	ci->levels = NULL;
    }
}

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

/* Return 1 if the user's access level on the given channel falls into the
 * given category, 0 otherwise.  Note that this may seem slightly confusing
 * in some cases: for example, check_access(..., CA_NOJOIN) returns true if
 * the user does _not_ have access to the channel (i.e. matches the NOJOIN
 * criterion).
 */

EXPORT_FUNC(check_access)
int check_access(User *user, ChannelInfo *ci, int what)
{
    int level = get_access(user, ci);
    int limit;

    if (level == ACCLEV_FOUNDER)
	return lev_is_max[what] ? 0 : 1;
    limit = ci->levels ? ci->levels[what] : def_levels[what];
    /* Hacks to make flags work */
    if (what == CA_AUTODEOP && (ci->flags & CI_SECUREOPS))
	limit = 0;
    if (what == CA_NOJOIN && (ci->flags & CI_RESTRICTED))
	limit = 0;
    if (limit == ACCLEV_INVALID)
	return 0;
    if (lev_is_max[what])
	return level <= limit;
    else
	return level >= limit;
}

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

/* Do like check_access(), but return whether the user would match the
 * given category if they had identified for their nick.  If the nick is
 * not registered, returns the same value as check_access().
 */

EXPORT_FUNC(check_access_if_idented)
int check_access_if_idented(User *user, ChannelInfo *ci, int what)
{
    int level = get_access_if_idented(user, ci);
    int limit;

    if (level == ACCLEV_FOUNDER)
	return lev_is_max[what] ? 0 : 1;
    limit = ci->levels ? ci->levels[what] : def_levels[what];
    /* Hacks to make flags work */
    if (what == CA_AUTODEOP && (ci->flags & CI_SECUREOPS))
	limit = 0;
    if (what == CA_NOJOIN && (ci->flags & CI_RESTRICTED))
	limit = 0;
    if (limit == ACCLEV_INVALID)
	return 0;
    if (lev_is_max[what])
	return level <= limit;
    else
	return level >= limit;
}

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

/* Return positive if the user is permitted access to the given command for
 * the given channel, zero otherwise.  If no level is found that
 * corresponds to the given command, return -1.
 */

EXPORT_FUNC(check_access_cmd)
int check_access_cmd(User *user, ChannelInfo *ci, const char *command,
		     const char *subcommand)
{
    int i;

    /* If we have a subcommand, first check for an exact match */
    if (subcommand) {
	for (i = 0; levelinfo[i].what >= 0; i++) {
	    if ((levelinfo[i].action & CL_TYPEMASK) == CL_ALLOW_CMD
	     && levelinfo[i].target.cmd.sub
	     && stricmp(command, levelinfo[i].target.cmd.main) == 0
	     && stricmp(subcommand, levelinfo[i].target.cmd.sub) == 0
	    ) {
		return check_access(user, ci, levelinfo[i].what);
	    }
	}
    }

    /* No subcommand or no exact match, so match on command name;
     * explicitly skip entries with subcommands (because they didn't
     * match before) */
    for (i = 0; levelinfo[i].what >= 0; i++) {
	if ((levelinfo[i].action & CL_TYPEMASK) == CL_ALLOW_CMD
	 && !levelinfo[i].target.cmd.sub
	 && stricmp(command, levelinfo[i].target.cmd.main) == 0
	) {
	    return check_access(user, ci, levelinfo[i].what);
	}
    }

    /* No level found */
    return -1;
}

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

/* Check a channel user mode change, and return the mask of mode flags
 * which should be changed (i.e. should be the reverse of the new flags
 * given).  The `changemask' parameter indicates which flags are changing,
 * and `newmodes' indicates the value of the changing flags; a new user on
 * a channel, e.g. from JOIN, should have a `changemask' with all bits set
 * (because all bits are changing from "undefined" to either on or off).
 * The returned value will always be a subset of `changemask'.
 *
 * Example: autoprotect/autoop user joins channel
 *     check_access_cumode(..., 0, ~0) -> CUMODE_a | CUMODE_o
 * Example: an autodeop user gets +o'd by somebody
 *     check_access_cumode(..., CUMODE_o, CUMODE_o) -> CUMODE_o
 *         (i.e. "setting CUMODE_o is incorrect" -> send a MODE -o)
 */

EXPORT_FUNC(check_access_cumode)
int check_access_cumode(User *user, ChannelInfo *ci, int32 newmodes,
			int32 changemask)
{
    int i;
    int32 result = 0;

    for (i = 0; levelinfo[i].what >= 0; i++) {
	int type = levelinfo[i].action & CL_TYPEMASK;
	int32 flags = levelinfo[i].target.cumode.flags;
	if ((type == CL_SET_MODE || type == CL_CLEAR_MODE)
	 && (changemask & flags)
	 && check_access(user, ci, levelinfo[i].what)
	) {
	    if (type == CL_SET_MODE && (~newmodes & flags))
		result |= ~newmodes & flags;
	    else if (type == CL_CLEAR_MODE && (newmodes & flags))
		result |= newmodes & flags;
	    while (levelinfo[i].target.cumode.cont)
		i++;
	}
    }
    return result;
}

/*************************************************************************/
/************************** Local-use routines ***************************/
/*************************************************************************/

/* Return the access level the given user has on the channel.  If the
 * channel doesn't exist, the user isn't on the access list, or the channel
 * is CS_SECURE and the user hasn't IDENTIFY'd with NickServ, return 0.
 */

int get_access(User *user, ChannelInfo *ci)
{
    if (is_founder(user, ci))
	return ACCLEV_FOUNDER;
    if (!ci || !valid_ngi(user) || (ci->flags&CI_VERBOTEN) || ci->suspendinfo)
	return 0;
    if (user_identified(user)
     || (user_recognized(user) && !(ci->flags & CI_SECURE))
    ) {
	int32 id = user->ngi->id;
	int i;
	ARRAY_FOREACH (i, ci->access) {
	    if (ci->access[i].nickgroup == id)
		return ci->access[i].level;
	}
    }
    return 0;
}

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

/* Return the access level the given user has on the channel, supposing the
 * user was identified for their current nick (if it's registered).
 */

static int get_access_if_idented(User *user, ChannelInfo *ci)
{
    int i;
    int32 id;

    if (is_identified(user, ci))
	return ACCLEV_FOUNDER;
    if (!ci || !valid_ngi(user) || (ci->flags&CI_VERBOTEN) || ci->suspendinfo)
	return 0;
    if (user->ngi->id == ci->founder)
	return ACCLEV_FOUNDER;
    id = user->ngi->id;
    ARRAY_FOREACH (i, ci->access) {
	if (ci->access[i].nickgroup == id)
	    return ci->access[i].level;
    }
    return 0;
}

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

/* Add an entry `nick' (at `level') to the access list by a user with
 * access `uacc'.
 */

int access_add(ChannelInfo *ci, const char *nick, int level, int uacc)
{
    int i;
    NickInfo *ni;

    if (level >= uacc)
	return RET_PERMISSION;
    ni = get_nickinfo(nick);
    if (!ni)
	return RET_NOSUCHNICK;
    else if (ni->status & NS_VERBOTEN)
	return RET_NICKFORBID;
    else if (!check_ngi(ni))
	return RET_INTERR;
    ARRAY_FOREACH (i, ci->access) {
	if (ci->access[i].nickgroup == ni->nickgroup) {
	    /* Don't allow lowering from a level >= uacc */
	    if (ci->access[i].level >= uacc)
		return RET_PERMISSION;
	    if (ci->access[i].level == level)
		return RET_UNCHANGED;
	    ci->access[i].level = level;
	    put_channelinfo(ci);
	    return RET_CHANGED;
	}
    }
    ARRAY_SEARCH_SCALAR(ci->access, nickgroup, 0, i);
    if (i == ci->access_count) {
	if (i < CSAccessMax) {
	    ARRAY_EXTEND(ci->access);
	} else {
	    return RET_LISTFULL;
	}
    }
    ci->access[i].nickgroup = ni->nickgroup;
    ci->access[i].level = level;
    put_channelinfo(ci);
    return RET_ADDED;
}

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

int init_access(Module *mod)
{
    int i;

    module = mod;

    /* Initialize def_levels[] and lev_is_max[], and convert mode letters
     * to flags */
    for (i = 0; levelinfo[i].what >= 0; i++) {
	int type = levelinfo[i].action & CL_TYPEMASK;
	if (type == CL_SET_MODE || type == CL_CLEAR_MODE) {
	    if (levelinfo[i].what == CA_AUTOOWNER) {
		if (chanusermode_owner)
		    levelinfo[i].target.cumode.flags = chanusermode_owner;
		else
		    levelinfo[i].action = CL_OTHER;  /* make it a no-op */
	    } else {
		/* Use MODE_NOERROR to deal with protocols that don't
		 * support some modes (e.g. +h in AUTODEOP) */
		levelinfo[i].target.cumode.flags =
		    mode_string_to_flags(levelinfo[i].target.cumode.modes,
					 MODE_CHANUSER | MODE_NOERROR);
	    }
	}
	def_levels[levelinfo[i].what] = levelinfo[i].defval;
	lev_is_max[levelinfo[i].what] = levelinfo[i].action & CL_LESSEQUAL;
    }

    /* Delete any levels for features not supported by protocol */
    if (!(protocol_features & PF_HALFOP)) {
	int offset = 0;
	for (i = 0; i == 0 || levelinfo[i-1].what >= 0; i++) {
	    if (levelinfo[i].what == CA_AUTOHALFOP
	     || levelinfo[i].what == CA_HALFOP
	    ) {
		offset++;
	    } else if (offset) {
		memcpy(&levelinfo[i-offset], &levelinfo[i],
		       sizeof(*levelinfo));
	    }
	}
    }
    if (!(protocol_features & PF_CHANPROT)) {
	int offset = 0;
	for (i = 0; i == 0 || levelinfo[i-1].what >= 0; i++) {
	    if (levelinfo[i].what == CA_AUTOPROTECT
	     || levelinfo[i].what == CA_PROTECT
	    ) {
		offset++;
	    } else if (offset) {
		memcpy(&levelinfo[i-offset], &levelinfo[i],
		       sizeof(*levelinfo));
	    }
	}
    }

    return 1;
}

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

void exit_access(void)
{
}

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