#include <sourcemod>
#include <dhooks>
#include <SteamWorks>
#include <sdktools>

#pragma semicolon 1
#pragma newdecls required

Handle hPlayerSlot = INVALID_HANDLE;
GameData gamedatafile; //Handle to the gamedata

char originalConVar[1024]; //original sv_downloadurl value
char g_cBackupURLS[2][1024];
char g_cIPaddresses[MAXPLAYERS + 1][256];
int g_icurrentClient;
StringMap g_SteamIDRotations;
bool g_bDoesIndexHaveBZ2[sizeof(g_cBackupURLS)]; //must have same size as g_cBackupURLS
ConVar downloadurl; // sv_downloadurl

public Plugin myinfo = 
{
    name        = "Fastdownload rotater",
    description = "Rotates fastdl used for clients when missing map errors happen",
    author      = "jenz",
    url         = "",
    version     = "1.0"
};

public void OnPluginStart()
{
    g_SteamIDRotations = new StringMap();
    ConVar cvar;
    HookConVarChange((cvar = CreateConVar("sm_first_backup_fastdl_url", "https://uk-fastdl.unloze.com/css_ze/", "the first backup fastdl URL")), Cvar_backupurl1);
    cvar.GetString(g_cBackupURLS[0], sizeof(g_cBackupURLS[]));
    delete cvar;

    ConVar cvar1;
    HookConVarChange((cvar1 = CreateConVar("sm_second_backup_fastdl_url", "https://uk-fastdl.unloze.com/css_ze/", "the second backup fastdl URL")), Cvar_backupurl2);
    cvar1.GetString(g_cBackupURLS[1], sizeof(g_cBackupURLS[]));
    delete cvar1;

    //we only use one backup fastdl but you could potentially have several.

    downloadurl = FindConVar("sv_downloadurl"); //Save original downloadurl, so we can send it to clients who we cant locate
    downloadurl.GetString(originalConVar, sizeof(originalConVar));  

    gamedatafile = LoadGameConfigFile("fastdl_rotater.games");
    
    if(gamedatafile == null)
        SetFailState("Cannot load fastdl_rotater.games.txt! Make sure you have it installed!");
   
    Handle detourBuildConVarMessage = DHookCreateDetour(Address_Null, CallConv_CDECL, ReturnType_Void, ThisPointer_Ignore);
    if(detourBuildConVarMessage == null)
        SetFailState("Failed to create detour for Host_BuildConVarUpdateMessage!");
    
    if(!DHookSetFromConf(detourBuildConVarMessage, gamedatafile, SDKConf_Signature, "Host_BuildConVarUpdateMessage"))
        SetFailState("Failed to load Host_BuildConVarUpdateMessage signature from gamedata!");

    DHookAddParam(detourBuildConVarMessage, HookParamType_Unknown);
    DHookAddParam(detourBuildConVarMessage, HookParamType_Int);
    DHookAddParam(detourBuildConVarMessage, HookParamType_Bool);    
    
    if(!DHookEnableDetour(detourBuildConVarMessage, false, buildConVarMessageDetCallback_Pre))
        SetFailState("Failed to detour Host_BuildConVarUpdateMessage PreHook!");
    

    Handle detourSendServerInfo = DHookCreateDetour(Address_Null, CallConv_THISCALL, ReturnType_Bool, ThisPointer_Address);
    if(detourSendServerInfo == null)
        SetFailState("Failed to create detour for CBaseClient::SendServerInfo!");
        
    if(!DHookSetFromConf(detourSendServerInfo, gamedatafile, SDKConf_Signature, "CBaseClient::SendServerInfo"))
        SetFailState("Failed to load CBaseClient::SendServerInfo signature from gamedata!");
   
    if(!DHookEnableDetour(detourSendServerInfo, false, sendServerInfoDetCallback_Pre))
        SetFailState("Failed to detour CBaseClient::SendServerInfo PreHook!");

    HookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre);

    StartPrepSDKCall(SDKCall_Raw);
    PrepSDKCall_SetFromConf(gamedatafile, SDKConf_Virtual, "CBaseClient::GetPlayerSlot");
    PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
    hPlayerSlot = EndPrepSDKCall();

    OnMapStart();
}

public Action Event_PlayerDisconnect(Handle event, const char[] name, bool dontBroadcast)
{
    int client = GetClientOfUserId(GetEventInt(event, "userid"));
    char reason[256];
    GetEventString(event, "reason", reason, sizeof(reason));
    //requires net_disconnect_reason 1
    if (StrContains(reason, "Map is missing") != -1)
    {
        GetClientIP(client, g_cIPaddresses[client], sizeof(g_cIPaddresses[]));
        
        int rotated = -1;
        g_SteamIDRotations.GetValue(g_cIPaddresses[client], rotated);
        rotated++;
        g_SteamIDRotations.SetValue(g_cIPaddresses[client], rotated, true);
    }
    return Plugin_Continue;
}

public void OnMapStart()
{
    //http request to the backup fastdl urls to verify if they have the bz2 files.
	char sRequest[512];
    char map[256];
    GetCurrentMap(map, sizeof(map));
    for (int i = 0; i < sizeof(g_cBackupURLS);i++)
    {
        g_bDoesIndexHaveBZ2[i] = false; //resetting to false until the http header finished
        FormatEx(sRequest, sizeof(sRequest), "%s%s%s%s", g_cBackupURLS[i], "maps/", map, ".bsp.bz2");

        Handle hRequest = SteamWorks_CreateHTTPRequest(k_EHTTPMethodHEAD, sRequest);
        if (!hRequest ||
            !SteamWorks_SetHTTPCallbacks(hRequest, OnTransferComplete) ||
            !SteamWorks_SetHTTPRequestContextValue(hRequest, i) ||
            !SteamWorks_SendHTTPRequest(hRequest))
        {
            delete hRequest;
        }
    }
}

//----------------------------------------------------------------------------------------------------
// Purpose:
//----------------------------------------------------------------------------------------------------
public int OnTransferComplete(Handle hRequest, bool bFailure, bool bSuccessful, EHTTPStatusCode eStatusCode, int url_index)
{
    if (bFailure || !bSuccessful || eStatusCode != k_EHTTPStatusCode200OK)
    {
        g_bDoesIndexHaveBZ2[url_index] = false;
    }
    else
    {
        g_bDoesIndexHaveBZ2[url_index] = true;
    }
    delete hRequest;
    return 0;
}

public void OnMapEnd()
{
    for (int i = 0; i <= MaxClients; i++)
    {
        Format(g_cIPaddresses[i], sizeof(g_cIPaddresses[]), "");
    }
    g_SteamIDRotations = new StringMap();
}

public void Cvar_backupurl1(ConVar convar, const char[] oldValue, const char[] newValue)
{
    convar.SetString(newValue);
}

public void Cvar_backupurl2(ConVar convar, const char[] oldValue, const char[] newValue)
{
    convar.SetString(newValue);
}

public void OnConfigsExecuted()
{
    downloadurl = FindConVar("sv_downloadurl");
    downloadurl.GetString(originalConVar, sizeof(originalConVar));
}

public MRESReturn sendServerInfoDetCallback_Pre(Address pointer, Handle hReturn, Handle hParams) //First callback
{
    int client;
    client = view_as<int>(SDKCall(hPlayerSlot, pointer)) + 1; //we just use linux anyways, no reason to check OS.
    g_icurrentClient = client;
    return MRES_Ignored;
}

public MRESReturn buildConVarMessageDetCallback_Pre(Handle hParams) //Second callback
{
    if (g_icurrentClient > 0 && g_icurrentClient <= MaxClients && IsClientConnected(g_icurrentClient))
    {
        //LogMessage("client %N in buildConVarMessageDetCallback_Pre", g_icurrentClient);
        char clientIPAddress[64]; //IP of the connecting client
        GetClientIP(g_icurrentClient, clientIPAddress, sizeof(clientIPAddress));

        for (int i = 0; i < sizeof(g_cIPaddresses); i++)
        {
            if (StrEqual(g_cIPaddresses[i], clientIPAddress))
            {
                int rotated = -1;
                g_SteamIDRotations.GetValue(clientIPAddress, rotated);

                //0 = ovh uk unloze. 1 = ovh uk unloze. 2 = default sv_downloadurl again. so not overwriting anything
                rotated = rotated % 3;

                //the bz2 file does not exist on the backup urls so rotate to the next working one. 
                while (rotated < sizeof(g_cBackupURLS) && !g_bDoesIndexHaveBZ2[rotated])
                {
                    rotated++;
                }
                if (rotated < sizeof(g_cBackupURLS))
                {
                    char map[256];
                    GetCurrentMap(map, sizeof(map));
                    g_SteamIDRotations.SetValue(clientIPAddress, rotated, true); //adapting in case of increment.
                    LogMessage("client %N had missing map with default sv_downloadurl on %s. using %s instead.", g_icurrentClient, map, g_cBackupURLS[rotated]);
                    setConVarValue(g_cBackupURLS[rotated]);
                }
                break;
            }
        }
    }
    return MRES_Ignored;
}

void setConVarValue(char[] value) //Sets the actual ConVar value
{
    int oldflags = GetConVarFlags(downloadurl);
    SetConVarFlags(downloadurl, oldflags &~ FCVAR_REPLICATED);
    SetConVarString(downloadurl, value, true, false); 
    SetConVarFlags(downloadurl, oldflags|FCVAR_REPLICATED);
}
