// BLANKART
//-----------------------------------------------------------------------------
// Copyright (C) 2012-2016 by John "JTE" Muniz.
// Copyright (C) 2012-2022 by Sonic Team Junior.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file  lua_hooklib.c
/// \brief hooks for Lua scripting

#include "doomdef.h"
#include "doomstat.h"
#include "p_mobj.h"
#include "g_game.h"
#include "r_skins.h"
#include "k_bot.h"
#include "z_zone.h"

#include "lua_script.h"
#include "lua_libs.h"
#include "lua_hook.h"
#include "lua_hud.h" // hud_running errors

#include "m_perfstats.h"
#include "d_netcmd.h" // for cv_perfstats
#include "i_system.h" // I_GetPreciseTime

#include "v_video.h" // V_ClearClipRect

/* =========================================================================
                                  ABSTRACTION
   ========================================================================= */

#define LIST(id, M) \
	static const char * const id [] = { M (TOSTR)  NULL }

LIST   (mobjHookNames,   MOBJ_HOOK_LIST);
LIST       (hookNames,        HOOK_LIST);
LIST    (hudHookNames,    HUD_HOOK_LIST);
LIST (stringHookNames, STRING_HOOK_LIST);

#undef LIST

typedef struct {
	int numHooks;
	int *ids;
} hook_t;

typedef struct {
	int numGeneric;
	int ref;
} stringhook_t;

static hook_t hookIds[HOOK(MAX)];
static hook_t hudHookIds[HUD_HOOK(MAX)];
static hook_t mobjHookIds[NUMMOBJTYPES][MOBJ_HOOK(MAX)];

// Lua tables are used to lookup string hook ids.
static stringhook_t stringHooks[STRING_HOOK(MAX)];

// This will be indexed by hook id, the value of which fetches the registry.
static int * hookRefs;
static int   nextid;

// After a hook errors once, don't print the error again.
static UINT8 * hooksErrored;

// compat mode for each hook
static UINT8 * hookCompat;

static int errorRef;

static boolean mobj_hook_available(int hook_type, mobjtype_t mobj_type)
{
	return
		(
				mobjHookIds [MT_NULL] [hook_type].numHooks > 0 ||
				mobjHookIds[mobj_type][hook_type].numHooks > 0
		);
}

static int hook_in_list
(
		const char * const         name,
		const char * const * const list
){
	int type;

	for (type = 0; list[type] != NULL; ++type)
	{
		if (fastcmp(name, list[type]))
			break;
	}

	return type;
}

static void get_table(lua_State *L)
{
	lua_pushvalue(L, -1);
	lua_rawget(L, -3);

	if (lua_isnil(L, -1))
	{
		lua_pop(L, 1);
		lua_createtable(L, 1, 0);
		lua_pushvalue(L, -2);
		lua_pushvalue(L, -2);
		lua_rawset(L, -5);
	}

	lua_remove(L, -2);
}

static void add_hook_to_table(lua_State *L, int n)
{
	lua_pushnumber(L, nextid);
	lua_rawseti(L, -2, n);
}

static void add_string_hook(lua_State *L, int type)
{
	stringhook_t * hook = &stringHooks[type];

	char * string = NULL;

	switch (type)
	{
		case STRING_HOOK(ShouldJingleContinue):
			if (lua_isstring(L, 3))
			{ // lowercase copy
				string = Z_StrDup(lua_tostring(L, 3));
				strlwr(string);
			}
			break;

		case STRING_HOOK(LinedefExecute):
		case STRING_HOOK(SpecialExecute):
			string = Z_StrDup(luaL_checkstring(L, 3));
			strupr(string);
			break;
	}

	if (hook->ref > 0)
		lua_getref(L, hook->ref);
	else
	{
		lua_newtable(L);
		lua_pushvalue(L, -1);
		hook->ref = luaL_ref(L, LUA_REGISTRYINDEX);
	}

	if (string)
	{
		lua_pushstring(L, string);
		get_table(L);
		add_hook_to_table(L, 1 + lua_objlen(L, -1));
	}
	else
		add_hook_to_table(L, ++hook->numGeneric);
}

static void add_hook(hook_t *map)
{
	Z_Realloc(map->ids, (map->numHooks + 1) * sizeof *map->ids,
			PU_STATIC, &map->ids);
	map->ids[map->numHooks++] = nextid;
}

static void add_mobj_hook(lua_State *L, int hook_type)
{
	mobjtype_t   mobj_type = luaL_optnumber(L, 3, MT_NULL);

	luaL_argcheck(L, mobj_type < NUMMOBJTYPES, 3, "invalid mobjtype_t");

	add_hook(&mobjHookIds[mobj_type][hook_type]);
}

static void add_hud_hook(lua_State *L, int idx)
{
	add_hook(&hudHookIds[luaL_checkoption(L,
				idx, "game", hudHookNames)]);
}

static void add_hook_ref(lua_State *L, int idx)
{
	if (!(nextid & 7))
	{
		Z_Realloc(hooksErrored,
				BIT_ARRAY_SIZE (nextid + 1) * sizeof *hooksErrored,
				PU_STATIC, &hooksErrored);
		hooksErrored[nextid >> 3] = 0;
		Z_Realloc(hookCompat,
				BIT_ARRAY_SIZE (nextid + 1) * sizeof *hookCompat,
				PU_STATIC, &hookCompat);
		hookCompat[nextid >> 3] = 0;
	}

	Z_Realloc(hookRefs, (nextid + 1) * sizeof *hookRefs, PU_STATIC, &hookRefs);

	if (lua_compatmode)
		set_bit_array(hookCompat, nextid);

	// set the hook function in the registry.
	lua_pushvalue(L, idx);
	hookRefs[nextid++] = luaL_ref(L, LUA_REGISTRYINDEX);
}

// Takes hook, function, and additional arguments (mobj type to act on, etc.)
static int lib_addHook(lua_State *L)
{
	const char * name;
	int type;

	/* LIMITS ARE MEANT TO BE BROKEN!
	if (!lua_lumploading)
		return luaL_error(L, "This function cannot be called from within a hook or coroutine!");
	*/

	name = luaL_checkstring(L, 1);
	luaL_checktype(L, 2, LUA_TFUNCTION);

	/* this is a very special case */
	if (( type = hook_in_list(name, stringHookNames) ) < STRING_HOOK(MAX))
	{
		add_string_hook(L, type);
	}
	else if (( type = hook_in_list(name, mobjHookNames) ) < MOBJ_HOOK(MAX))
	{
		add_mobj_hook(L, type);
	}
	else if (( type = hook_in_list(name, hookNames) ) < HOOK(MAX))
	{
		add_hook(&hookIds[type]);
	}
	else if (fastcmp(name, "HUD"))
	{
		add_hud_hook(L, 3);
	}
	else
	{
		return luaL_argerror(L, 1, lua_pushfstring(L, "invalid hook " LUA_QS, name));
	}

	add_hook_ref(L, 2);/* the function */

	return 0;
}

int LUA_HookLib(lua_State *L)
{
	lua_pushcfunction(L, LUA_GetErrorMessage);
	errorRef = luaL_ref(L, LUA_REGISTRYINDEX);

	lua_register(L, "addHook", lib_addHook);

	return 0;
}

/* TODO: remove in next backwards incompatible release */
int lib_hudadd(lua_State *L);/* yeah compiler */
int lib_hudadd(lua_State *L)
{
	/*
	if (!lua_lumploading)
		return luaL_error(L, "This function cannot be called from within a hook or coroutine!");
	*/

	luaL_checktype(L, 1, LUA_TFUNCTION);

	add_hud_hook(L, 2);
	add_hook_ref(L, 1);

	return 0;
}

typedef struct Hook_State Hook_State;
typedef void (*Hook_Callback)(Hook_State *);

struct Hook_State {
	INT32        status;/* return status to calling function */
	void       * userdata;
	int          hook_type;
	mobjtype_t   mobj_type;/* >0 if mobj hook */
	const char * string;/* used to fetch table, ran first if set */
	int          top;/* index of last argument passed to hook */
	int          id;/* id to fetch ref */
	int          values;/* num arguments passed to hook */
	int          results;/* num values returned by hook */
	Hook_Callback results_handler;/* callback when hook successfully returns */
};

enum {
	EINDEX = 1,/* error handler */
	SINDEX = 2,/* string itself is pushed in case of string hook */
};

static void push_error_handler(void)
{
	lua_getref(gL, errorRef);
}

/* repush hook string */
static void push_string(void)
{
	lua_pushvalue(gL, SINDEX);
}

static boolean begin_hook_values(Hook_State *hook)
{
	hook->top = lua_gettop(gL);
	return true;
}

static void start_hook_stack(void)
{
	lua_settop(gL, 0);
	push_error_handler();
}

static boolean init_hook_type
(
		Hook_State * hook,
		int          status,
		int          hook_type,
		mobjtype_t   mobj_type,
		const char * string,
		int          nonzero
){
	hook->status = status;

	if (nonzero)
	{
		start_hook_stack();
		hook->hook_type = hook_type;
		hook->mobj_type = mobj_type;
		hook->string = string;
		return begin_hook_values(hook);
	}
	else
		return false;
}

static boolean prepare_hook
(
		Hook_State * hook,
		int default_status,
		int hook_type
){
	return init_hook_type(hook, default_status,
			hook_type, 0, NULL,
			hookIds[hook_type].numHooks);
}

static boolean prepare_mobj_hook
(
		Hook_State * hook,
		int          default_status,
		int          hook_type,
		mobjtype_t   mobj_type
){
#ifdef PARANOIA
	if (mobj_type == MT_NULL)
		I_Error("MT_NULL has been passed to a mobj hook\n");
#endif
	return init_hook_type(hook, default_status,
			hook_type, mobj_type, NULL,
			mobj_hook_available(hook_type, mobj_type));
}

static boolean prepare_string_hook
(
		Hook_State * hook,
		int          default_status,
		int          hook_type,
		const char * string
){
	if (init_hook_type(hook, default_status,
				hook_type, 0, string,
				stringHooks[hook_type].ref))
	{
		lua_pushstring(gL, string);
		return begin_hook_values(hook);
	}
	else
		return false;
}

static void init_hook_call
(
		Hook_State * hook,
		int    results,
		Hook_Callback results_handler
){
	const int top = lua_gettop(gL);
	hook->values = (top - hook->top);
	hook->top = top;
	hook->results = results;
	hook->results_handler = results_handler;
}

static void get_hook(Hook_State *hook, const int *ids, int n)
{
	hook->id = ids[n];
	lua_getref(gL, hookRefs[hook->id]);
}

static void get_hook_from_table(Hook_State *hook, int n)
{
	lua_rawgeti(gL, -1, n);
	hook->id = lua_tonumber(gL, -1);
	lua_pop(gL, 1);
	lua_getref(gL, hookRefs[hook->id]);
}

static int call_single_hook_no_copy(Hook_State *hook)
{
	boolean compat = in_bit_array(hookCompat, hook->id);
	boolean printerror = cht_debug & DBG_LUA || !in_bit_array(hooksErrored, hook->id);
	if (LUA_Call(gL, hook->values, hook->results, EINDEX, compat, printerror) == 0)
	{
		if (hook->results > 0)
		{
			(*hook->results_handler)(hook);
			lua_pop(gL, hook->results);
		}
	}
	else if (printerror)
	{
		set_bit_array(hooksErrored, hook->id);
	}

	return 1;
}

static int call_single_hook(Hook_State *hook)
{
	int i;

	for (i = -(hook->values) + 1; i <= 0; ++i)
		lua_pushvalue(gL, hook->top + i);

	return call_single_hook_no_copy(hook);
}

static int call_hook_table_for(Hook_State *hook, int n)
{
	int k;

	for (k = 1; k <= n; ++k)
	{
		get_hook_from_table(hook, k);
		call_single_hook(hook);
	}

	return n;
}

static int call_hook_table(Hook_State *hook)
{
	return call_hook_table_for(hook, lua_objlen(gL, -1));
}

static int call_mapped(Hook_State *hook, const hook_t *map)
{
	int k;

	for (k = 0; k < map->numHooks; ++k)
	{
		get_hook(hook, map->ids, k);
		call_single_hook(hook);
	}

	return map->numHooks;
}

static int call_mapped_hud(Hook_State *hook, const hook_t *map)
{
	int k;

	for (k = 0; k < map->numHooks; ++k)
	{
		hud_interpolate = hud_interpstring = hud_interplatch = false;
		hud_interpcounter++;

		get_hook(hook, map->ids, k);
		call_single_hook(hook);

		// Catch runaway clipping rectangles.
		LUA_ClearClipRect();
	}

	return map->numHooks;
}

static int call_mapped_gamemap(Hook_State *hook, const hook_t *map)
{
	int k;

	for (k = 0; k < map->numHooks; ++k)
	{
		get_hook(hook, map->ids, k);
		boolean compat = in_bit_array(hookCompat, hook->id);
		lua_pushvalue(gL, hook->top - !compat);
		call_single_hook_no_copy(hook);
	}

	return map->numHooks;
}

static int call_string_hooks(Hook_State *hook)
{
	const stringhook_t *map = &stringHooks[hook->hook_type];

	int calls = 0;

	lua_getref(gL, map->ref);

	/* call generic string hooks first */
	calls += call_hook_table_for(hook, map->numGeneric);

	push_string();
	lua_rawget(gL, -2);
	calls += call_hook_table(hook);

	return calls;
}

static int call_mobj_type_hooks(Hook_State *hook, mobjtype_t mobj_type)
{
	return call_mapped(hook, &mobjHookIds[mobj_type][hook->hook_type]);
}

static int call_hooks
(
		Hook_State * hook,
		int        results,
		Hook_Callback results_handler
){
	int calls = 0;

	init_hook_call(hook, results, results_handler);

	if (hook->string)
	{
		calls += call_string_hooks(hook);
	}
	else if (hook->mobj_type > 0)
	{
		/* call generic mobj hooks first */
		calls += call_mobj_type_hooks(hook, MT_NULL);
		calls += call_mobj_type_hooks(hook, hook->mobj_type);

		ps_lua_mobjhooks += calls;
	}
	else
		calls += call_mapped(hook, &hookIds[hook->hook_type]);

	lua_settop(gL, 0);

	return calls;
}

/* =========================================================================
                            COMMON RESULT HANDLERS
   ========================================================================= */

#define res_none NULL

static void res_true(Hook_State *hook)
{
	if (lua_toboolean(gL, -1))
		hook->status = true;
}

static void res_false(Hook_State *hook)
{
	if (!lua_isnil(gL, -1) && !lua_toboolean(gL, -1))
		hook->status = false;
}

static void res_force(Hook_State *hook)
{
	if (!lua_isnil(gL, -1))
	{
		if (lua_toboolean(gL, -1))
			hook->status = 1; // Force yes
		else
			hook->status = 2; // Force no
	}
}

/* =========================================================================
                               GENERALISED HOOKS
   ========================================================================= */

int LUA_HookMobj(mobj_t *mobj, int hook_type)
{
	Hook_State hook = {};
	if (prepare_mobj_hook(&hook, false, hook_type, mobj->type))
	{
		LUA_PushUserdata(gL, mobj, META_MOBJ);
		call_hooks(&hook, 1, res_true);
	}
	return hook.status;
}

int LUA_Hook2Mobj(mobj_t *t1, mobj_t *t2, int hook_type)
{
	Hook_State hook = {};
	if (prepare_mobj_hook(&hook, 0, hook_type, t1->type))
	{
		LUA_PushUserdata(gL, t1, META_MOBJ);
		LUA_PushUserdata(gL, t2, META_MOBJ);
		call_hooks(&hook, 1, res_force);
	}
	return hook.status;
}

void LUA_HookVoid(int type)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, 0, type))
		call_hooks(&hook, 0, res_none);
}

void LUA_HookInt(INT32 number, int hook_type)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, 0, hook_type))
	{
		lua_pushinteger(gL, number);
		call_hooks(&hook, 0, res_none);
	}
}

void LUA_HookGamemap(int hook_type)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, 0, hook_type))
	{
		lua_pushinteger(gL, gamemap);
		lua_pushinteger(gL, G_NativeMapToKart(gamemap));
		init_hook_call(&hook, 0, res_none);
		hook.values = 1;
		call_mapped_gamemap(&hook, &hookIds[hook.hook_type]);
		lua_settop(gL, 0);
	}
}

void LUA_HookBool(boolean value, int hook_type)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, 0, hook_type))
	{
		lua_pushboolean(gL, value);
		call_hooks(&hook, 0, res_none);
	}
}

int LUA_HookPlayer(player_t *player, int hook_type)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, false, hook_type))
	{
		LUA_PushUserdata(gL, player, META_PLAYER);
		call_hooks(&hook, 1, res_true);
	}
	return hook.status;
}

int LUA_HookPlayerForceResults(player_t *player, int hook_type)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, 0, hook_type))
	{
		LUA_PushUserdata(gL, player, META_PLAYER);
		call_hooks(&hook, 1, res_force);
	}
	return hook.status;
}

int LUA_HookTiccmd(player_t *player, ticcmd_t *cmd, int hook_type)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, false, hook_type))
	{
		LUA_PushUserdata(gL, player, META_PLAYER);
		LUA_PushUserdata(gL, cmd, META_TICCMD);

		if (hook_type == HOOK(PlayerCmd))
			hook_cmd_running = true;

		call_hooks(&hook, 1, res_true);

		if (hook_type == HOOK(PlayerCmd))
			hook_cmd_running = false;
	}
	return hook.status;
}

void LUA_HookHUD(huddrawlist_h list, int hook_type)
{
	const hook_t * map = &hudHookIds[hook_type];
	Hook_State hook = {};
	if (map->numHooks > 0)
	{
		start_hook_stack();
		begin_hook_values(&hook);

		LUA_SetHudHook(hook_type, list);
		hud_interpcounter = 0;

		hud_running = true; // local hook

		init_hook_call(&hook, 0, res_none);
		call_mapped_hud(&hook, map);

		hud_running = false;
	}
}

/* =========================================================================
                               SPECIALIZED HOOKS
   ========================================================================= */

void LUA_HookThinkFrame(void)
{
	const int type = HOOK(ThinkFrame);

	// variables used by perf stats
	int hook_index = 0;
	precise_t time_taken = 0;

	Hook_State hook = {};

	const hook_t * map = &hookIds[type];
	int k;

	if (prepare_hook(&hook, 0, type))
	{
		init_hook_call(&hook, 0, res_none);

		for (k = 0; k < map->numHooks; ++k)
		{
			get_hook(&hook, map->ids, k);

			if (cv_perfstats.value == PS_THINKFRAME)
			{
				lua_pushvalue(gL, -1);/* need the function again */
				time_taken = I_GetPreciseTime();
			}

			call_single_hook(&hook);

			if (cv_perfstats.value == PS_THINKFRAME)
			{
				lua_Debug ar;
				time_taken = I_GetPreciseTime() - time_taken;
				lua_getinfo(gL, ">S", &ar);
				PS_SetThinkFrameHookInfo(hook_index, time_taken, ar.short_src);
				hook_index++;
			}
		}

		lua_settop(gL, 0);
	}
}

int LUA_HookMobjLineCollide(mobj_t *mobj, line_t *line)
{
	Hook_State hook = {};
	if (prepare_mobj_hook(&hook, 0, MOBJ_HOOK(MobjLineCollide), mobj->type))
	{
		LUA_PushUserdata(gL, mobj, META_MOBJ);
		LUA_PushUserdata(gL, line, META_LINE);
		call_hooks(&hook, 1, res_force);
	}
	return hook.status;
}

int LUA_HookTouchSpecial(mobj_t *special, mobj_t *toucher)
{
	Hook_State hook = {};
	if (prepare_mobj_hook(&hook, false, MOBJ_HOOK(TouchSpecial), special->type))
	{
		LUA_PushUserdata(gL, special, META_MOBJ);
		LUA_PushUserdata(gL, toucher, META_MOBJ);
		call_hooks(&hook, 1, res_true);
	}
	return hook.status;
}

static int damage_hook
(
		mobj_t *target,
		mobj_t *inflictor,
		mobj_t *source,
		INT32   damage,
		UINT8   damagetype,
		int     hook_type,
		Hook_Callback results_handler
){
	Hook_State hook = {};
	if (prepare_mobj_hook(&hook, 0, hook_type, target->type))
	{
		LUA_PushUserdata(gL, target, META_MOBJ);
		LUA_PushUserdata(gL, inflictor, META_MOBJ);
		LUA_PushUserdata(gL, source, META_MOBJ);
		if (hook_type != MOBJ_HOOK(MobjDeath))
			lua_pushinteger(gL, damage);
		lua_pushinteger(gL, damagetype);
		call_hooks(&hook, 1, results_handler);
	}
	return hook.status;
}

static int playerdamage_hook
(
	mobj_t *target,
 mobj_t *inflictor,
 mobj_t *source,
 INT32   damage,
 UINT8   damagetype,
 int     hook_type,
 Hook_Callback results_handler
){
	(void)damage;
	(void)damagetype;
	Hook_State hook = {};
	if (prepare_mobj_hook(&hook, 0, hook_type, target->type))
	{
		LUA_PushUserdata(gL, target->player, META_PLAYER);
		LUA_PushUserdata(gL, inflictor, META_MOBJ);
		LUA_PushUserdata(gL, source, META_MOBJ);
		call_hooks(&hook, 1, results_handler);
	}
	return hook.status;
}

int LUA_HookShouldDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
{
	return damage_hook(target, inflictor, source, damage, damagetype,
			MOBJ_HOOK(ShouldDamage), res_force);
}

int LUA_HookShouldSpin(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
{
		return playerdamage_hook(target, inflictor, source, damage, damagetype,
					   MOBJ_HOOK(ShouldSpin), res_force);
}

int LUA_HookShouldExplode(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
{
		return playerdamage_hook(target, inflictor, source, damage, damagetype,
					   MOBJ_HOOK(ShouldExplode), res_force);
}

int LUA_HookShouldSquish(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
{
		return playerdamage_hook(target, inflictor, source, damage, damagetype,
				MOBJ_HOOK(ShouldSquish), res_force);
}

int LUA_HookMobjDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
{
	return damage_hook(target, inflictor, source, damage, damagetype,
					   MOBJ_HOOK(MobjDamage), res_true);
}

int LUA_HookPlayerSpin(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
{
		return playerdamage_hook(target, inflictor, source, damage, damagetype,
					   MOBJ_HOOK(PlayerSpin), res_true);
}

int LUA_HookPlayerExplode(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
{
		return playerdamage_hook(target, inflictor, source, damage, damagetype,
						MOBJ_HOOK(PlayerExplode), res_true);
}

int LUA_HookPlayerSquish(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
{
		return playerdamage_hook(target, inflictor, source, damage, damagetype,
					   MOBJ_HOOK(PlayerSquish), res_true);
}

int LUA_HookMobjDeath(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damagetype)
{
	return damage_hook(target, inflictor, source, 0, damagetype,
			MOBJ_HOOK(MobjDeath), res_true);
}

int LUA_HookMobjMoveBlocked(mobj_t *t1, mobj_t *t2, line_t *line)
{
	Hook_State hook = {};
	if (prepare_mobj_hook(&hook, 0, MOBJ_HOOK(MobjMoveBlocked), t1->type))
	{
		LUA_PushUserdata(gL, t1, META_MOBJ);
		LUA_PushUserdata(gL, t2, META_MOBJ);
		LUA_PushUserdata(gL, line, META_LINE);
		call_hooks(&hook, 1, res_true);
	}
	return hook.status;
}

// Backwards Compatability For older scripts
void LUA_HookLinedefExecute(line_t *line, mobj_t *mo, sector_t *sector)
{
	Hook_State hook = {};
	if (prepare_string_hook
		(&hook, 0, STRING_HOOK(LinedefExecute), line->stringargs[0]))
	{
		LUA_PushUserdata(gL, line, META_LINE);
		LUA_PushUserdata(gL, mo, META_MOBJ);
		LUA_PushUserdata(gL, sector, META_SECTOR);
#if 0
		// but this isn't a mobj hook...?
		ps_lua_mobjhooks += call_hooks(&hook, 0, res_none);
#else
		call_hooks(&hook, 0, res_none);
#endif
	}
}

void LUA_HookSpecialExecute(activator_t *activator, INT32 *args, char **stringargs)
{
	Hook_State hook = {};

	if (activator->line)
	{
		// Run the old hook if possible.
		LUA_HookLinedefExecute(activator->line, activator->mo, activator->sector);
	}

	if (prepare_string_hook
			(&hook, 0, STRING_HOOK(SpecialExecute), stringargs[0]))
	{
		LUA_PushUserdata(gL, activator, META_ACTIVATOR);
		LUA_PushUserdata(gL, args, META_LINEARGS);
		LUA_PushUserdata(gL, stringargs, META_LINESTRINGARGS);
#if 0
		// but this isn't a mobj hook...?
		ps_lua_mobjhooks += call_hooks(&hook, 0, res_none);
#else
		call_hooks(&hook, 0, res_none);
#endif
	}
}

int LUA_HookPlayerMsg(int source, int target, int flags, char *msg, int mute)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, false, HOOK(PlayerMsg)))
	{
		LUA_PushUserdata(gL, &players[source], META_PLAYER); // Source player
		if (flags & 2 /*HU_CSAY*/) { // csay TODO: make HU_CSAY accessible outside hu_stuff.c
			lua_pushinteger(gL, 3); // type
			lua_pushnil(gL); // target
		} else if (target == -1) { // sayteam
			lua_pushinteger(gL, 1); // type
			lua_pushnil(gL); // target
		} else if (target == 0) { // say
			lua_pushinteger(gL, 0); // type
			lua_pushnil(gL); // target
		} else { // sayto
			lua_pushinteger(gL, 2); // type
			LUA_PushUserdata(gL, &players[target-1], META_PLAYER); // target
		}
		lua_pushstring(gL, msg); // msg
		lua_pushboolean(gL, mute); // the message was supposed to be eaten by spamprotecc.

		call_hooks(&hook, 1, res_true);
	}
	return hook.status;
}

int LUA_HookHurtMsg(player_t *player, mobj_t *inflictor, mobj_t *source, UINT8 damagetype)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, false, HOOK(HurtMsg)))
	{
		LUA_PushUserdata(gL, player, META_PLAYER);
		LUA_PushUserdata(gL, inflictor, META_MOBJ);
		LUA_PushUserdata(gL, source, META_MOBJ);
		lua_pushinteger(gL, damagetype);
		call_hooks(&hook, 1, res_true);
	}
	return hook.status;
}

void LUA_HookNetArchive(lua_CFunction archFunc, savebuffer_t *save)
{
	const hook_t * map = &hookIds[HOOK(NetVars)];
	Hook_State hook = {};
	/* this is a remarkable case where the stack isn't reset */
	if (map->numHooks > 0)
	{
		// stack: tables
		I_Assert(lua_gettop(gL) > 0);
		I_Assert(lua_istable(gL, -1));

		push_error_handler();
		lua_insert(gL, EINDEX);

		begin_hook_values(&hook);

		// tables becomes an upvalue of archFunc
		lua_pushvalue(gL, -1);
		lua_pushlightuserdata(gL, save);
		lua_pushcclosure(gL, archFunc, 2);
		// stack: tables, savebuffer_t, archFunc

		init_hook_call(&hook, 0, res_none);
		call_mapped(&hook, map);

		lua_pop(gL, 1); // pop archFunc
		lua_remove(gL, EINDEX); // pop error handler
		// stack: tables
	}
}

int LUA_HookMapThingSpawn(mobj_t *mobj, mapthing_t *mthing)
{
	Hook_State hook = {};
	if (prepare_mobj_hook(&hook, false, MOBJ_HOOK(MapThingSpawn), mobj->type))
	{
		LUA_PushUserdata(gL, mobj, META_MOBJ);
		LUA_PushUserdata(gL, mthing, META_MAPTHING);
		call_hooks(&hook, 1, res_true);
	}
	return hook.status;
}

int LUA_HookFollowMobj(player_t *player, mobj_t *mobj)
{
	Hook_State hook = {};
	if (prepare_mobj_hook(&hook, false, MOBJ_HOOK(FollowMobj), mobj->type))
	{
		LUA_PushUserdata(gL, player, META_PLAYER);
		LUA_PushUserdata(gL, mobj, META_MOBJ);
		call_hooks(&hook, 1, res_true);
	}
	return hook.status;
}

int LUA_HookPlayerCanDamage(player_t *player, mobj_t *mobj)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, 0, HOOK(PlayerCanDamage)))
	{
		LUA_PushUserdata(gL, player, META_PLAYER);
		LUA_PushUserdata(gL, mobj, META_MOBJ);
		call_hooks(&hook, 1, res_force);
	}
	return hook.status;
}

void LUA_HookPlayerQuit(player_t *plr, kickreason_t reason)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, 0, HOOK(PlayerQuit)))
	{
		LUA_PushUserdata(gL, plr, META_PLAYER); // Player that quit
		lua_pushinteger(gL, reason); // Reason for quitting
		call_hooks(&hook, 0, res_none);
	}
}

int LUA_HookNameChange(player_t *plr, const char *name)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, true, HOOK(NameChange)))
	{
		LUA_PushUserdata(gL, plr, META_PLAYER); // Player that changed name
		lua_pushstring(gL, name);   // New player name
		call_hooks(&hook, 1, res_false);
	}
	return hook.status;
}

int LUA_HookTeamSwitch(player_t *player, int newteam, boolean fromspectators, boolean tryingautobalance, boolean tryingscramble)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, true, HOOK(TeamSwitch)))
	{
		LUA_PushUserdata(gL, player, META_PLAYER);
		lua_pushinteger(gL, newteam);
		lua_pushboolean(gL, fromspectators);
		lua_pushboolean(gL, tryingautobalance);
		lua_pushboolean(gL, tryingscramble);
		call_hooks(&hook, 1, res_false);
	}
	return hook.status;
}

int LUA_HookViewpointSwitch(player_t *player, player_t *newdisplayplayer, boolean forced)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, 0, HOOK(ViewpointSwitch)))
	{
		LUA_PushUserdata(gL, player, META_PLAYER);
		LUA_PushUserdata(gL, newdisplayplayer, META_PLAYER);
		lua_pushboolean(gL, forced);

		hud_running = true; // local hook
		call_hooks(&hook, 1, res_force);
		hud_running = false;
	}
	return hook.status;
}

int LUA_HookSeenPlayer(player_t *player, player_t *seenfriend)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, true, HOOK(SeenPlayer)))
	{
		LUA_PushUserdata(gL, player, META_PLAYER);
		LUA_PushUserdata(gL, seenfriend, META_PLAYER);

		hud_running = true; // local hook
		call_hooks(&hook, 1, res_false);
		hud_running = false;
	}
	return hook.status;
}

int LUA_HookShouldJingleContinue(player_t *player, const char *musname)
{
	Hook_State hook = {};
	if (prepare_string_hook
			(&hook, false, STRING_HOOK(ShouldJingleContinue), musname))
	{
		LUA_PushUserdata(gL, player, META_PLAYER);
		push_string();

		hud_running = true; // local hook
		call_hooks(&hook, 1, res_true);
		hud_running = false;
	}
	return hook.status;
}

static void res_gprankpoints(Hook_State *hook)
{
	if (!lua_isnil(gL, -1))
	{
		INT16 *points = (INT16*)hook->userdata;
		*points = lua_tointeger(gL, -1);
		hook->status = true;
	}
}

int LUA_HookGPRankPoints(UINT8 position, UINT8 numplayers, INT16 *points)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, 0, HOOK(GPRankPoints)))
	{
		hook.userdata = points;
		lua_pushinteger(gL, position);
		lua_pushinteger(gL, numplayers);
		lua_pushinteger(gL, *points);
		call_hooks(&hook, 1, res_gprankpoints);
	}
	return hook.status;
}

boolean hook_cmd_running = false;

static void update_music_name(struct MusicChange *musicchange)
{
	size_t length;
	const char * new = lua_tolstring(gL, -6, &length);

	if (length < 7)
	{
		strcpy(musicchange->newname, new);
		lua_pushvalue(gL, -6);/* may as well keep it for next call */
	}
	else
	{
		memcpy(musicchange->newname, new, 6);
		musicchange->newname[6] = '\0';
		lua_pushlstring(gL, new, 6);
	}

	lua_replace(gL, -7);
}

static void res_musicchange(Hook_State *hook)
{
	struct MusicChange *musicchange = hook->userdata;

	// output 1: true, false, or string musicname override
	if (lua_isstring(gL, -6))
		update_music_name(musicchange);
	else if (lua_isboolean(gL, -6) && lua_toboolean(gL, -6))
		hook->status = true;

	// output 2: mflags override
	if (lua_isnumber(gL, -5))
		*musicchange->mflags = lua_tonumber(gL, -5);
	// output 3: looping override
	if (lua_isboolean(gL, -4))
		*musicchange->looping = lua_toboolean(gL, -4);
	// output 4: position override
	if (lua_isnumber(gL, -3))
		*musicchange->position = lua_tonumber(gL, -3);
	// output 5: prefadems override
	if (lua_isnumber(gL, -2))
		*musicchange->prefadems = lua_tonumber(gL, -2);
	// output 6: fadeinms override
	if (lua_isnumber(gL, -1))
		*musicchange->fadeinms = lua_tonumber(gL, -1);
}

int LUA_HookMusicChange(const char *oldname, struct MusicChange *param)
{
	const int type = HOOK(MusicChange);
	const hook_t * map = &hookIds[type];

	Hook_State hook = {};

	int k;

	if (prepare_hook(&hook, false, type))
	{
		init_hook_call(&hook, 6, res_musicchange);
		hook.values = 7;/* values pushed later */
		hook.userdata = param;

		lua_pushstring(gL, oldname);/* the only constant value */
		lua_pushstring(gL, param->newname);/* semi constant */

		for (k = 0; k < map->numHooks; ++k)
		{
			get_hook(&hook, map->ids, k);

			lua_pushvalue(gL, -3);
			lua_pushvalue(gL, -3);
			lua_pushinteger(gL, *param->mflags);
			lua_pushboolean(gL, *param->looping);
			lua_pushinteger(gL, *param->position);
			lua_pushinteger(gL, *param->prefadems);
			lua_pushinteger(gL, *param->fadeinms);

			call_single_hook_no_copy(&hook);
		}

		lua_settop(gL, 0);
	}

	return hook.status;
}

// Allows to both override default behavior and force action. I don't like it but thats how it works
// in CEP and neptune. Fuckal whyyyyyyyyy
typedef struct {
	boolean override;
	boolean force;
} TrueForce_State;

static void res_trueforce(Hook_State *hook)
{
	TrueForce_State *state = (TrueForce_State*)hook->userdata;

	if (lua_isboolean(gL, -2))
		state->override = lua_toboolean(gL, -2);

	if (lua_isboolean(gL, -1))
		state->force = lua_toboolean(gL, -1);
};

boolean LUA_HookPlayerItem(player_t *player, UINT8 itemType, boolean wasHoldingItem, boolean *force)
{
	Hook_State hook = {};
	TrueForce_State state = {0};

	if (prepare_hook(&hook, 0, HOOK(PlayerItem)))
	{
		LUA_PushUserdata(gL, player, META_PLAYER);
		lua_pushinteger(gL, itemType);
		lua_pushboolean(gL, wasHoldingItem);

		hook.userdata = &state;

		call_hooks(&hook, 2, res_trueforce);
	}

	*force = state.force;
	return state.override;
}

typedef struct {
	INT32 *target;
	boolean force_sink;
} KartHyudoro_State;

static void res_karthyudoro(Hook_State *hook)
{
	KartHyudoro_State *state = (KartHyudoro_State*)hook->userdata;

	if (lua_isnumber(gL, -2))
		*state->target = lua_tonumber(gL, -2);
	else if (lua_isuserdata(gL, -2))
	{
		player_t *player = *(player_t**)luaL_checkudata(gL, -2, META_PLAYER);
		*state->target = player - players;
	}

	if (lua_isboolean(gL, -1))
		state->force_sink = lua_toboolean(gL, -1);
}

boolean LUA_HookKartHyudoro(player_t *player, INT32 *target, boolean sink)
{
	Hook_State hook = {};
	KartHyudoro_State state = {0};
	state.target = target;

	if (prepare_hook(&hook, 0, HOOK(KartHyudoro)))
	{
		LUA_PushUserdata(gL, player, META_PLAYER);
		if (*target >= 0)
			LUA_PushUserdata(gL, &players[*target], META_PLAYER);
		else
			lua_pushnil(gL);
		lua_pushboolean(gL, sink);

		hook.userdata = &state;

		call_hooks(&hook, 2, res_karthyudoro);
	}

	return state.force_sink;

}

boolean LUA_HookMobjScaleChange(mobj_t *target, fixed_t newscale, fixed_t oldscale)
{
	Hook_State hook = {};
	if (prepare_mobj_hook(&hook, false, MOBJ_HOOK(MobjScaleChange), target->type))
	{
		LUA_PushUserdata(gL, target, META_MOBJ);
		lua_pushfixed(gL, newscale);
		lua_pushfixed(gL, oldscale);

		call_hooks(&hook, 1, res_true);
	}
	return hook.status;
}

boolean LUA_HookKartSneaker(player_t *player, int type)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, false, HOOK(KartSneaker)))
	{
		LUA_PushUserdata(gL, player, META_PLAYER);
		// Type of sneaker: 0 - perfect/panel, 1 - regular sneaker, 2 - rocket sneaker.
		lua_pushinteger(gL, type);

		call_hooks(&hook, 1, res_true);
	}
	
	return hook.status;
}

boolean LUA_HookKartStripItems(player_t* player, UINT8 item)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, false, HOOK(KartStripItems)))
	{
		LUA_PushUserdata(gL, player, META_PLAYER);
		lua_pushinteger(gL, item);

		call_hooks(&hook, 1, res_true);
	}
	
	return hook.status;
}

boolean LUA_HookKartStripOther(player_t* player)
{
	Hook_State hook = {};
	if (prepare_hook(&hook, false, HOOK(KartStripOther)))
	{
		LUA_PushUserdata(gL, player, META_PLAYER);

		call_hooks(&hook, 1, res_true);
	}
	
	return hook.status;
}

boolean LUA_HookCanPickupItem(player_t *player, UINT8 weapon, boolean *force)
{
	Hook_State hook = {};
	TrueForce_State state = {0};

	if (prepare_hook(&hook, 0, HOOK(CanPickupItem)))
	{
		LUA_PushUserdata(gL, player, META_PLAYER);
		lua_pushinteger(gL, weapon);

		hook.userdata = &state;

		call_hooks(&hook, 2, res_trueforce);
	}

	*force = state.force;
	return state.override;
}

typedef struct {
	mobjtype_t droptype;
	boolean *orbit;
	boolean *dropall;
} KartDropHnextList_State;

static void res_kartdrophnext(Hook_State *hook)
{
	KartDropHnextList_State *state = (KartDropHnextList_State*)hook->userdata;
	if (!lua_isnil(gL, -3))
	{
		state->droptype = luaL_checkinteger(gL, -3);

		if (lua_isboolean(gL, -2))
		{
			*state->orbit = lua_toboolean(gL, -2);
		}
		if (lua_isboolean(gL, -1))
		{
			*state->dropall = lua_toboolean(gL, -1);
		}
	}
}

int LUA_HookDropHnextList(mobj_t *mobj, mobjtype_t droptype, boolean *orbit, boolean *dropall)
{
	Hook_State hook = {};
	KartDropHnextList_State state = {0};
	state.droptype = droptype;
	state.orbit = orbit;
	state.dropall = dropall;

	if (prepare_mobj_hook(&hook, 0, MOBJ_HOOK(DropHnextList), mobj->type))
	{
		LUA_PushUserdata(gL, mobj, META_MOBJ);
		hook.userdata = &state;

		call_hooks(&hook, 3, res_kartdrophnext);
	}
	return state.droptype;
}
