#include <sourcemod>
#include <dhooks>
#include <sdktools>
#include <geoip>

#pragma semicolon 1
#pragma newdecls required

GameData gamedatafile; //Handle to the gamedata
Handle hPlayerSlot = INVALID_HANDLE;
KeyValues nodeConfig; //main node file

char originalConVar[256]; //original sv_downloadurl value
char clientIPAddress[64]; //IP of the connecting client

int url_length = 512;

ConVar downloadurl; // sv_downloadurl

//unloze is just using geoip2, no reason to keep SypexGeo for us.

public Plugin myinfo = 
{
    name        = "Smart Fast Downloads",
    description = "Routes clients to the closest Fast Download server available. removed IP lookup feature because its not useable with S3 objects",
    author      = "Nolo001, edits by jenz for unloze",
    url         = "original is at: https://github.com/Nolo001-Aha/sourcemod_smart_fastdownloads",
    version     = "1.2"
};

public void OnPluginStart()
{
    PrintToServer("[Smart Fast Downloads] Initializing...");
    gamedatafile = LoadGameConfigFile("betterfastdl.games");
    
    if(gamedatafile == null)
        SetFailState("Cannot load betterfastdl.games.txt! Make sure you have it installed!");

    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!");

    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!");
  
    if(!DHookEnableDetour(detourBuildConVarMessage, true, buildConVarMessageDetCallback_Post))
        SetFailState("Failed to detour Host_BuildConVarUpdateMessage PostHook!");
        
    StartPrepSDKCall(SDKCall_Raw);
    PrepSDKCall_SetFromConf(gamedatafile, SDKConf_Virtual, "CBaseClient::GetPlayerSlot");
    PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
    hPlayerSlot = EndPrepSDKCall();

    nodeConfig = new KeyValues("FastDL Settings"); //Load the main node config file
    char config[PLATFORM_MAX_PATH];
    BuildPath(Path_SM, config, sizeof(config), "configs/fastdlmanager.cfg");
    nodeConfig.ImportFromFile(config);
    nodeConfig.Rewind();
    
    BuildPath(Path_SM, config, sizeof(config), "fastdl_debug.log");
    
    AutoExecConfig(true, "SmartFastDownloads");
        
    downloadurl = FindConVar("sv_downloadurl"); //Save original downloadurl, so we can send it to clients who we cant locate
    downloadurl.GetString(originalConVar, sizeof(originalConVar));  
}

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

public MRESReturn sendServerInfoDetCallback_Pre(Address pointer, Handle hReturn, Handle hParams) //First callback in chain, derive client and find their IP
{
    int client;
    client = view_as<int>(SDKCall(hPlayerSlot, pointer)) + 1; //we just use linux anyways, no reason to check OS.
    GetClientIP(client, clientIPAddress, sizeof(clientIPAddress));
    return MRES_Ignored;
}

public MRESReturn buildConVarMessageDetCallback_Pre(Handle hParams) //Second callback in chain, call our main function and get a node link in response
{
    char[] url = new char[url_length];
    getLocationSettings(url);
    setConVarValue(url);
    return MRES_Ignored;
}

void getLocationSettings(char[] link) //Main function
{
    nodeConfig.Rewind();
    float clientLongitude, clientLatitude;
    if(nodeConfig.JumpToKey("Nodes", false)) 
    {
        char[] nodeURL = new char[url_length];
        char[] finalurl = new char[url_length];
        float distance, currentDistance; //1 - distance to the closest server, may change in iterations. 2 - distance between client and current iteration node 
        clientLatitude = GetLatitude(clientIPAddress); //clients coordinates
        clientLongitude = GetLongitude(clientIPAddress);

        //if look up of client failed just give them the default fastdl value instead. 
        if(clientLatitude == 0 || clientLongitude == 0)
        {
            //LogMessage("Failed distance calculation. Sending default values. Client(%f %f IP: %s).", clientLatitude, clientLongitude, clientIPAddress);
            strcopy(link, url_length, "EMPTY");
            return;
        }

        char section[64];
        nodeConfig.GotoFirstSubKey(false);
        do
        {
            float nodeLongitude, nodeLatitude;
            nodeConfig.GetSectionName(section, sizeof(section));
            nodeLatitude = nodeConfig.GetFloat("latitude");
            nodeLongitude = nodeConfig.GetFloat("longitude");             
            currentDistance = GetDistance(nodeLatitude, nodeLongitude, clientLatitude, clientLongitude);    

            if((currentDistance < distance) || distance == 0)
            {
                nodeConfig.GetString("link", nodeURL, url_length, "EMPTY");
                strcopy(finalurl, url_length, nodeURL);
                distance = currentDistance;
            }                   
        }
        while(nodeConfig.GotoNextKey(false));
        strcopy(link, url_length, nodeURL);
        //LogMessage("Sending final: Distance is %f. Client IP Address: %s. selected link is: %s", distance, clientIPAddress, link);
    }
}

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

public MRESReturn buildConVarMessageDetCallback_Post(Handle hParams) //Reverts the ConVar to its original value
{
    setConVarValue("EMPTY");
    return MRES_Ignored;
}

float GetLatitude(char[] lookupIP)
{
    return GeoipLatitude(lookupIP);
}

float GetLongitude(char[] lookupIP)
{
    return GeoipLongitude(lookupIP);  
}

float GetDistance(float nodeLatitude, float nodeLongitude, float clientLatitude, float clientLongitude)
{
    return GeoipDistance(nodeLatitude,  nodeLongitude,  clientLatitude, clientLongitude);  
}
