#pragma semicolon 1
#pragma newdecls required

#include <clientprefs>
#include <sourcemod>
#include <sdkhooks>
#include <PhysHooks>
#undef REQUIRE_PLUGIN
#include <zombiereloaded>
#define REQUIRE_PLUGIN
#include <SelectiveBhop>

ConVar g_CVar_sv_enablebunnyhopping;
ConVar g_CVar_zr_disablebunnyhopping;

enum
{
    LIMITED_NONE = 0,
    LIMITED_GENERAL = 1,
    LIMITED_PLUGIN = 2,

    // Temp
    LIMITED_ZOMBIE = 4
}

bool g_bEnabled = false;
bool g_bZombieEnabled = false;
bool g_bInOnPlayerRunCmd = false;

int g_ClientLimited[MAXPLAYERS + 1] = {LIMITED_NONE, ...};
int g_ActiveLimitedFlags = LIMITED_GENERAL | LIMITED_PLUGIN;

StringMap g_ClientLimitedCache;

Handle g_hCookie_BlockBhop;

public Plugin myinfo =
{
    name = "Selective Bunnyhop",
    author = "BotoX",
    description = "Disables bunnyhop on certain players/groups",
    version = "0.1"
}

public void OnPluginStart()
{
    LoadTranslations("common.phrases");

    g_CVar_sv_enablebunnyhopping = FindConVar("sv_enablebunnyhopping");
    g_CVar_sv_enablebunnyhopping.Flags &= ~FCVAR_REPLICATED;
    g_CVar_sv_enablebunnyhopping.AddChangeHook(OnConVarChanged);
    g_bEnabled = g_CVar_sv_enablebunnyhopping.BoolValue;

    g_CVar_zr_disablebunnyhopping = CreateConVar("zr_disablebunnyhopping", "0", "Disable bhop for zombies.", FCVAR_NOTIFY);
    g_CVar_zr_disablebunnyhopping.AddChangeHook(OnConVarChanged);
    g_bZombieEnabled = g_CVar_zr_disablebunnyhopping.BoolValue;

    g_ClientLimitedCache = new StringMap();

    g_hCookie_BlockBhop = RegClientCookie("blocking_bhop_cookie", "", CookieAccess_Private);

    HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);

    RegAdminCmd("sm_bhop", Command_Bhop, ADMFLAG_GENERIC, "sm_bhop <#userid|name> <0|1>");

    RegConsoleCmd("sm_bhopstatus", Command_Status, "sm_bhopstatus [#userid|name]");

    /* Late load */
    for(int i = 1; i <= MaxClients; i++)
    {
        if(!IsClientInGame(i) || IsFakeClient(i))
            return;

        if(ZR_IsClientZombie(i))
            AddLimitedFlag(i, LIMITED_ZOMBIE);
        if(AreClientCookiesCached(i))
            OnClientCookiesCached(i);
    }

    UpdateLimitedFlags();
    UpdateClients();
}

public void OnClientCookiesCached(int client)
{
	char sBuffer[16];
	GetClientCookie(client, g_hCookie_BlockBhop, sBuffer, sizeof(sBuffer));

	if (StrEqual(sBuffer, "1", false))
	{
        AddLimitedFlag(client, LIMITED_GENERAL);
        g_ClientLimited[client] = 1;
	}
}

public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
    CreateNative("LimitBhop", Native_LimitBhop);
    CreateNative("IsBhopLimited", Native_IsBhopLimited);
    RegPluginLibrary("SelectiveBhop");

    return APLRes_Success;
}

public void OnPluginEnd()
{
    g_CVar_sv_enablebunnyhopping.BoolValue = g_bEnabled;
    g_CVar_sv_enablebunnyhopping.Flags |= FCVAR_REPLICATED|FCVAR_NOTIFY;

    for(int i = 1; i <= MaxClients; i++)
    {
        if(IsClientInGame(i) && !IsFakeClient(i))
        {
            if(g_CVar_sv_enablebunnyhopping.BoolValue)
                g_CVar_sv_enablebunnyhopping.ReplicateToClient(i, "1");
            else
                g_CVar_sv_enablebunnyhopping.ReplicateToClient(i, "0");
        }
    }
}

public void OnMapEnd()
{
    g_ClientLimitedCache.Clear();
}

public void OnClientPutInServer(int client)
{
    TransmitConVar(client);
}

public void OnClientDisconnect(int client)
{
    // Remove temp
    int LimitedFlag = g_ClientLimited[client] & ~(LIMITED_ZOMBIE);

    if(LimitedFlag != LIMITED_NONE)
    {
        char sSteamID[64];
        if(GetClientAuthId(client, AuthId_Engine, sSteamID, sizeof(sSteamID)))
            g_ClientLimitedCache.SetValue(sSteamID, LimitedFlag, true);
    }

    g_ClientLimited[client] = LIMITED_NONE;
}

public void OnClientPostAdminCheck(int client)
{
    char sSteamID[64];
    if(GetClientAuthId(client, AuthId_Engine, sSteamID, sizeof(sSteamID)))
    {
        int LimitedFlag;
        if(g_ClientLimitedCache.GetValue(sSteamID, LimitedFlag))
        {
            AddLimitedFlag(client, LimitedFlag);
            g_ClientLimitedCache.Remove(sSteamID);
        }
    }
}

public void OnConVarChanged(ConVar convar, const char[] oldValue, const char[] newValue)
{
    if(convar == g_CVar_sv_enablebunnyhopping)
    {
        if(g_bInOnPlayerRunCmd)
            return;

        g_bEnabled = convar.BoolValue;
        UpdateClients();
    }
    else if(convar == g_CVar_zr_disablebunnyhopping)
    {
        g_bZombieEnabled = convar.BoolValue;
        UpdateLimitedFlags();
    }
}

public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2])
{
    if(!g_bEnabled)
        return Plugin_Continue;

    bool bEnableBunnyhopping = !(g_ClientLimited[client] & g_ActiveLimitedFlags);
    if(bEnableBunnyhopping == g_CVar_sv_enablebunnyhopping.BoolValue)
        return Plugin_Continue;

    if(!g_bInOnPlayerRunCmd)
    {
        g_CVar_sv_enablebunnyhopping.Flags &= ~FCVAR_NOTIFY;
        g_bInOnPlayerRunCmd = true;
    }

    g_CVar_sv_enablebunnyhopping.BoolValue = bEnableBunnyhopping;

    return Plugin_Continue;
}

public void OnRunThinkFunctionsPost(bool simulating)
{
    if(g_bInOnPlayerRunCmd)
    {
        g_CVar_sv_enablebunnyhopping.BoolValue = g_bEnabled;
        g_CVar_sv_enablebunnyhopping.Flags |= FCVAR_NOTIFY;
        g_bInOnPlayerRunCmd = false;
    }
}

public void ZR_OnClientInfected(int client, int attacker, bool motherInfect, bool respawnOverride, bool respawn)
{
    AddLimitedFlag(client, LIMITED_ZOMBIE);
}

public void ZR_OnClientHumanPost(int client, bool respawn, bool protect)
{
    RemoveLimitedFlag(client, LIMITED_ZOMBIE);
}

public void ZR_OnClientRespawned(int client, ZR_RespawnCondition condition)
{
    if(condition == ZR_Respawn_Human)
        RemoveLimitedFlag(client, LIMITED_ZOMBIE);
    else
        AddLimitedFlag(client, LIMITED_ZOMBIE);
}

public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
    RemoveLimitedFlag(-1, LIMITED_ZOMBIE);
}

void UpdateLimitedFlags()
{
    int Flags = LIMITED_GENERAL;

    if(g_bZombieEnabled)
        Flags |= LIMITED_ZOMBIE;

    if(g_ActiveLimitedFlags != Flags)
    {
        g_ActiveLimitedFlags = Flags;
        UpdateClients();
    }
    g_ActiveLimitedFlags = Flags;
}

stock void AddLimitedFlag(int client, int Flag)
{
    if(client == -1)
    {
        for(int i = 1; i <= MaxClients; i++)
            _AddLimitedFlag(i, Flag);
    }
    else
        _AddLimitedFlag(client, Flag);
}

stock void _AddLimitedFlag(int client, int Flag)
{
    bool bWasLimited = view_as<bool>(g_ClientLimited[client] & g_ActiveLimitedFlags);
    g_ClientLimited[client] |= Flag;
    bool bIsLimited = view_as<bool>(g_ClientLimited[client] & g_ActiveLimitedFlags);

    if(bIsLimited != bWasLimited)
        TransmitConVar(client);
}

stock void RemoveLimitedFlag(int client, int Flag)
{
    if(client == -1)
    {
        for(int i = 1; i <= MaxClients; i++)
            _RemoveLimitedFlag(i, Flag);
    }
    else
        _RemoveLimitedFlag(client, Flag);
}

stock void _RemoveLimitedFlag(int client, int Flag)
{
    bool bWasLimited = view_as<bool>(g_ClientLimited[client] & g_ActiveLimitedFlags);
    g_ClientLimited[client] &= ~Flag;
    bool bIsLimited = view_as<bool>(g_ClientLimited[client] & g_ActiveLimitedFlags);

    if(bIsLimited != bWasLimited)
        TransmitConVar(client);
}

stock void UpdateClients()
{
    for(int i = 1; i <= MaxClients; i++)
    {
        if(IsClientInGame(i))
            TransmitConVar(i);
    }
}

stock void TransmitConVar(int client)
{
    if(!IsClientInGame(client) || IsFakeClient(client))
        return;

    bool bIsLimited = view_as<bool>(g_ClientLimited[client] & g_ActiveLimitedFlags);

    if(g_bEnabled && !bIsLimited)
        g_CVar_sv_enablebunnyhopping.ReplicateToClient(client, "1");
    else
        g_CVar_sv_enablebunnyhopping.ReplicateToClient(client, "0");
}

public Action Command_Bhop(int client, int argc)
{
    if(argc < 2)
    {
        ReplyToCommand(client, "[SM] Usage: sm_bhop <#userid|name> <0|1>");
        return Plugin_Handled;
    }

    char sArg[64];
    char sArg2[2];
    char sTargetName[MAX_TARGET_LENGTH];
    int iTargets[MAXPLAYERS];
    int iTargetCount;
    bool bIsML;
    bool bValue;

    GetCmdArg(1, sArg, sizeof(sArg));
    GetCmdArg(2, sArg2, sizeof(sArg2));

    bValue = sArg2[0] == '1' ? true : false;

    if((iTargetCount = ProcessTargetString(sArg, client, iTargets, MAXPLAYERS, COMMAND_FILTER_NO_MULTI, sTargetName, sizeof(sTargetName), bIsML)) <= 0)
    {
        ReplyToTargetError(client, iTargetCount);
        return Plugin_Handled;
    }

    char sBuffer[16];
    for(int i = 0; i < iTargetCount; i++)
    {
        if(bValue)
        {
            RemoveLimitedFlag(iTargets[i], LIMITED_GENERAL | LIMITED_PLUGIN);
            Format(sBuffer, sizeof(sBuffer), "%s", "0");
            SetClientCookie(iTargets[i], g_hCookie_BlockBhop, sBuffer);
        }
        else
        {
            AddLimitedFlag(iTargets[i], LIMITED_GENERAL);
            Format(sBuffer, sizeof(sBuffer), "%s", "1");
            SetClientCookie(iTargets[i], g_hCookie_BlockBhop, sBuffer);
        }
    }

    ShowActivity2(client, "\x01[SM] \x04", "\x01\x04%s\x01 bunnyhop on target \x04%s", bValue ? "Un-limited" : "Limited", sTargetName);

    if(iTargetCount > 1)
        LogAction(client, -1, "\"%L\" %s bunnyhop on target \"%s\"", client, bValue ? "Un-limited" : "Limited", sTargetName);
    else
        LogAction(client, iTargets[0], "\"%L\" %s bunnyhop on target \"%L\"", client, bValue ? "Un-limited" : "Limited", iTargets[0]);

    return Plugin_Handled;
}

public Action Command_Status(int client, int argc)
{
    if (argc && CheckCommandAccess(client, "", ADMFLAG_BAN, true))
    {
        char sArgument[64];
        GetCmdArg(1, sArgument, sizeof(sArgument));

        int target = -1;
        if((target = FindTarget(client, sArgument, true, false)) == -1)
            return Plugin_Handled;

        bool bLimited = view_as<bool>(g_ClientLimited[target] & (LIMITED_GENERAL | LIMITED_PLUGIN));

        if(bLimited)
        {
            ReplyToCommand(client, "[SM] %N their bhop is currently: limited", target);
            return Plugin_Handled;
        }
        else
        {
            ReplyToCommand(client, "[SM] %N their bhop is currently: unlimited", target);
            return Plugin_Handled;
        }
    }
    else
    {
        bool bLimited = view_as<bool>(g_ClientLimited[client] & (LIMITED_GENERAL | LIMITED_PLUGIN));

        if(bLimited)
        {
            ReplyToCommand(client, "[SM] your bhop is currently: limited");
            return Plugin_Handled;
        }
        else
        {
            ReplyToCommand(client, "[SM] your bhop is currently: unlimited");
            return Plugin_Handled;
        }
    }
}

public int Native_LimitBhop(Handle plugin, int numParams)
{
    int client = GetNativeCell(1);
    bool bLimited = view_as<bool>(GetNativeCell(2));

    if(client > MaxClients || client <= 0)
    {
        ThrowNativeError(SP_ERROR_NATIVE, "Client is not valid.");
        return -1;
    }

    if(!IsClientInGame(client))
    {
        ThrowNativeError(SP_ERROR_NATIVE, "Client is not in-game.");
        return -1;
    }

    if(bLimited)
        AddLimitedFlag(client, LIMITED_PLUGIN);
    else
        RemoveLimitedFlag(client, LIMITED_PLUGIN);

    return 0;
}

public int Native_IsBhopLimited(Handle plugin, int numParams)
{
    int client = GetNativeCell(1);

    if(client > MaxClients || client <= 0)
    {
        ThrowNativeError(SP_ERROR_NATIVE, "Client is not valid.");
        return -1;
    }

    if(!IsClientInGame(client))
    {
        ThrowNativeError(SP_ERROR_NATIVE, "Client is not in-game.");
        return -1;
    }

    int LimitedFlag = g_ClientLimited[client] & LIMITED_PLUGIN;

    return LimitedFlag != LIMITED_NONE;
}
