#pragma semicolon 1
#define PLUGIN_AUTHOR "jenz"
#define PLUGIN_VERSION "1.2"
#include <sourcemod>
#include <cstrike>
#include <PlayerManager>
#include <AFKManager>

Database g_hDatabase;
//check if autismbot
bool is_bot_player[MAXPLAYERS + 1];
Handle g_h_time_activity = null;
char g_csSID[MAXPLAYERS + 1][65];
char g_cTimeRecords[1000][128];
int g_iPlayerTimeServer[MAXPLAYERS + 1];
int g_iPlayerAFKTime;
int g_iPlayerCount_excludeSpec;
int g_iPlayerRTVCapacity;

static Handle g_hForwardPlayerHours;

public Plugin myinfo =
{
    name = "UNLOZE_player_time",
    author = PLUGIN_AUTHOR,
    description = "checks playtime on servers",
    version = PLUGIN_VERSION,
    url = "www.unloze.com"
};


public Action time_query_activity(Handle timer, any data)
{
    if (!g_hDatabase)
    {
        return Plugin_Continue;
    }
    char sServer[64];
    int i_port = GetConVarInt(FindConVar("hostport"));
    if (i_port == 27015 || i_port == 27035) //might as well count play time on ze2. it was not done before but its a bit like why not anyways.
    {
        Format(sServer, sizeof(sServer), "ze_time");
    }
    else if (i_port == 27016)
    {
        Format(sServer, sizeof(sServer), "zr_time");
    }
    else if (i_port == 27017)
    {
        Format(sServer, sizeof(sServer), "mg_time");
    }
    else
    {
        return Plugin_Continue;
    }
    for (int client = 1; client <= MaxClients; client++)
    {
        if (IsValidClient(client) && !IsFakeClient(client) && IsPlayerAlive(client))
        {
            char sIP[32];
            GetClientIP(client, sIP, sizeof(sIP));
            char sQuery[512];
            char sName[MAX_NAME_LENGTH];
            GetClientName(client, sName, sizeof(sName));
            int size2 = 2 * strlen(sName) + 1;
            char[] sEscapedName = new char[size2 + 1];
            g_hDatabase.Escape(sName, sEscapedName, size2 + 1);
            Format(sQuery, sizeof(sQuery), "update unloze_playtimestats.player_time set `%s` = `%s` + 10 where steam_id = '%s' and ipv4 = '%s'", sServer, sServer, g_csSID[client], sIP);
            g_hDatabase.Query(SQL_FinishedQuery, sQuery, _, DBPrio_Low);
            Format(sQuery, sizeof(sQuery), "update unloze_playtimestats.player_time set player_name = '%s' where steam_id = '%s' and player_name != '%s'", sEscapedName, g_csSID[client], sEscapedName);
            g_hDatabase.Query(SQL_FinishedQuery, sQuery, _, DBPrio_Low);
        }
    }
    return Plugin_Continue;
}

public void OnPluginStart()
{
    if (!g_hDatabase)
    {
        Database.Connect(SQL_OnDatabaseConnect, "unloze_playtimestats");
    }
    RegConsoleCmd("sm_playtime", Command_Time, "retreives total connection time on all connected servers");
    RegConsoleCmd("sm_topplaytime", Command_TopTime, "retreives top 1000 playtime highscores on all connected servers");
    
    g_h_time_activity = CreateTimer(10.0, time_query_activity, INVALID_HANDLE, TIMER_REPEAT);
	CreateTimer(600.0, UpdateForward, _, TIMER_REPEAT);

    ConVar cvar;
    HookConVarChange((cvar = CreateConVar("sm_mapchooser_afk_detect_time", "120", "Time in seconds until a player is considered as AFK and therefore excluded from player average time.")), Cvar_playerAFKTime);
    g_iPlayerAFKTime = cvar.IntValue;
    delete cvar;

    ConVar cvar1;
    HookConVarChange((cvar1 = CreateConVar("sm_exclude_specs_avghour", "20", "How many players must be considered active on the server before specs are included in generating the average hour count")), Cvar_playerExcludeSpec);
    g_iPlayerCount_excludeSpec = cvar1.IntValue;
    delete cvar1;

    ConVar cvar2;
    HookConVarChange((cvar2 = CreateConVar("sm_rtv_avg_capacity", "5", "The capacity for how many times the average a players rtv can be worth.")), Cvar_playerRTVAverageCap);
    g_iPlayerRTVCapacity = cvar2.IntValue;
    delete cvar2;
}

public Action UpdateForward(Handle timer)
{
    for (int i = 0; i < MaxClients; i++)
    {
        if (IsValidClient(i) && !IsFakeClient(i) && !IsClientSourceTV(i) && !is_bot_player[i])
        {
            select_client_time_server(i);
        }
    }
    return Plugin_Handled;
}

public void Cvar_playerAFKTime(ConVar convar, const char[] oldValue, const char[] newValue)
{
    g_iPlayerAFKTime = convar.IntValue;
}

public void Cvar_playerExcludeSpec(ConVar convar, const char[] oldValue, const char[] newValue)
{
    g_iPlayerCount_excludeSpec = convar.IntValue;
}

public void Cvar_playerRTVAverageCap(ConVar convar, const char[] oldValue, const char[] newValue)
{
    g_iPlayerRTVCapacity = convar.IntValue;
}

public APLRes AskPluginLoad2(Handle myself, bool late, char [] error, int err_max)
{
	CreateNative("GetAveragePlayerTimeOnServer", Native_GetAveragePlayerActiveTimeServer);
	CreateNative("GetPlayerWorthRTV_boost_", Native_GetPlayerWorthRTV_boost);

	g_hForwardPlayerHours = CreateGlobalForward("GetPlayerHoursServer", ET_Ignore, Param_Cell, Param_Cell, Param_Cell);
	return APLRes_Success;
}

public int GetAveragePlayerActiveTimeServer()
{
    int total_hours = 0;
    int total_players = 0;
    int active_player_count = 0;
    for (int i = 0; i < MaxClients; i++)
    {
        //checking player count before deciding if including or excluding spectators from actual average
        //the purpose of this is me (jenz) not boosting the hour average insanely when the server is very empty. (i often sit spec without playing and that ruins the generated average when low population because the count gets way too high due to me)
        if (IsValidClient(i) && !IsFakeClient(i) && !IsClientSourceTV(i) && !is_bot_player[i] 
            && (GetClientTeam(i) == CS_TEAM_T || GetClientTeam(i) == CS_TEAM_CT))
        {
            active_player_count++;
        }
    }
    for (int i = 0; i < MaxClients; i++)
    {
        if (IsValidClient(i) && !IsFakeClient(i) && !IsClientSourceTV(i) && !is_bot_player[i] && GetClientIdleTime(i) < g_iPlayerAFKTime)
        {
            if (active_player_count < g_iPlayerCount_excludeSpec && GetClientTeam(i) != CS_TEAM_T && GetClientTeam(i) != CS_TEAM_CT)
            {
                continue;
            }

            //re-introducing the hour capacity but putting it here is better than putting it at the mysql query
            //at the time of making this change its only affeting me jenz and nobody else. this is meant to prevent
            //the average hour from being too skewed by high playtime people.
            int added_hours = g_iPlayerTimeServer[i] > 5000 ? 5000 : g_iPlayerTimeServer[i];
            total_hours += added_hours;
            total_players++;
        }
    }
    if (total_hours == 0)
    {
        return 0;
    }
    return total_hours / total_players;
}

public int Native_GetAveragePlayerActiveTimeServer(Handle plugin, int numParams)
{
    return GetAveragePlayerActiveTimeServer();
}

public int Native_GetPlayerWorthRTV_boost(Handle plugin, int numParams)
{
    int client = GetNativeCell(1);
    if(client > MaxClients || client <= 0)
    {
        ThrowNativeError(SP_ERROR_NATIVE, "Client is not valid.");
        return view_as<int>(-1);
    }

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

    int avg = GetAveragePlayerActiveTimeServer();
    if (g_iPlayerTimeServer[client] <= avg || avg == 0 || is_bot_player[client])
    {
        return 1;
    }
    if ((float(g_iPlayerTimeServer[client]) / float(avg)) > g_iPlayerRTVCapacity)
    {
        return g_iPlayerRTVCapacity; //1.0-5.0 booster probably
    }
    int val = RoundToFloor((g_iPlayerTimeServer[client]) / float(avg));
    if (val < 1)
    {
        val = 1;
    }
    return val;
}

public void OnPluginEnd()
{
    if (g_h_time_activity != null)
        delete g_h_time_activity;
	CloseHandle(g_hForwardPlayerHours);
}

public void SQL_OnDatabaseConnect(Database db, const char[] error, any data)
{
    if(!db || strlen(error))
    {
        LogError("Database error: %s", error);
        return;
    }
    g_hDatabase = db;
    for (int i = 1; i <= MaxClients; i++)
    {
        if(IsValidClient(i) && !IsFakeClient(i))
        {
            OnClientPostAdminCheck(i);
        }
    }
    OnMapStart();
}

public void OnClientPostAdminCheck(int client)
{
    GetClientAuthId(client, AuthId_Steam2, g_csSID[client], sizeof(g_csSID[]));
    is_bot_player[client] = false;
    g_iPlayerTimeServer[client] = 0;
    if(!IsValidClient(client) || IsFakeClient(client))
        return;
    if (!g_hDatabase)
    {
        return;
    }
    insert_client(client);
    select_client_time_server(client);

    if (StrEqual("[U:1:1221121532]", g_csSID[client], false) || StrEqual("STEAM_0:0:610560766", g_csSID[client], false))
    {
        is_bot_player[client] = true;
    }
    if (StrEqual("[U:1:408797742]", g_csSID[client], false) || StrEqual("STEAM_0:0:204398871", g_csSID[client], false))
    {
        is_bot_player[client] = true;
    }
    if (StrEqual("[U:1:1036189204]", g_csSID[client], false) || StrEqual("STEAM_0:0:518094602", g_csSID[client], false))
    {
        is_bot_player[client] = true;
    }
    if (StrEqual("[U:1:120378081]", g_csSID[client], false) || StrEqual("STEAM_0:1:60189040", g_csSID[client], false))
    {
        is_bot_player[client] = true;
    }
}

public void select_client_time_server(int client)
{
    char sServer[32];
    int i_port = GetConVarInt(FindConVar("hostport"));
    if (i_port == 27015 || i_port == 27019 || i_port == 27035)
    {
        Format(sServer, sizeof(sServer), "ze_time");
    }
    else if (i_port == 27016)
    {
        Format(sServer, sizeof(sServer), "zr_time");
    }
    else if (i_port == 27017)
    {
        Format(sServer, sizeof(sServer), "mg_time");
    }
    else
    {
        return; 
    }
    char sQuery[512];
    Format(sQuery, sizeof(sQuery), "select sum(%s) as %s_total from unloze_playtimestats.player_time pt where pt.steam_id = '%s' GROUP BY steam_id order by %s_total desc", sServer, sServer, g_csSID[client], sServer);
    g_hDatabase.Query(SQL_OnQueryCompletedTimeServer, sQuery, GetClientSerial(client), DBPrio_High);
}

public void SQL_OnQueryCompletedTimeServer(Database db, DBResultSet results, const char[] error, int iSerial)
{
    if (!db || strlen(error))
    {
        delete results;
        LogError("Query error 3: %s", error);
        return;
    }
    int client;
    if ((client = GetClientFromSerial(iSerial)) == 0)
    {
        delete results;
        return;
    }

    int iTime_Server;
    
    while (results.RowCount && results.FetchRow())
    {
        iTime_Server += results.FetchInt(0);
    }
    delete results;
    int iHours_Server = (iTime_Server / 60) / 60;
    int iMinutes_Server = (iTime_Server / 60) % 60;
    //just a hardcap, maybe it will be adapted somewhen in the future
    //2026, i am removing the hardcap so it wont interfere with play time based restrictions.
    /*
    if (iHours_Server > 5000)
    {
        iHours_Server = 5000;
    }
    */
    g_iPlayerTimeServer[client] = iHours_Server;

    Call_StartForward(g_hForwardPlayerHours);
    Call_PushCell(client);
    Call_PushCell(g_iPlayerTimeServer[client]);
    Call_PushCell(iMinutes_Server);
    Call_Finish();
}

public void OnClientDisconnect(int client)
{
    Format(g_csSID[client], sizeof(g_csSID[]), "");
    is_bot_player[client] = false;
    g_iPlayerTimeServer[client] = 0;
}

public void insert_client(int client)
{
    char sName[MAX_NAME_LENGTH];
    GetClientName(client, sName, sizeof(sName));
    int size2 = 2 * strlen(sName) + 1;
    char[] sEscapedName = new char[size2 + 1];
    g_hDatabase.Escape(sName, sEscapedName, size2 + 1);
    char sIP[32];
    GetClientIP(client, sIP, sizeof(sIP));
    char sQuery[512];
    Format(sQuery, sizeof(sQuery), "INSERT INTO `player_time` (`steam_id`, `ipv4`, `player_name`, `ze_time`, `mg_time`, `zr_time`, `jb_time`) VALUES ('%s', '%s', '%s', 0, 0, 0, 0) ON DUPLICATE KEY UPDATE `player_name` = '%s'", g_csSID[client], sIP, sEscapedName, sEscapedName);
    g_hDatabase.Query(SQL_FinishedQuery, sQuery, _, DBPrio_Low);
}

public void SQL_FinishedQuery(Database db, DBResultSet results, const char[] error, any data)
{
    if (!db || strlen(error))
    {
        LogError("Query error 3: %s", error);
    }
    delete results;
}

stock bool IsValidClient(int client)
{
    if (client > 0 && client <= MaxClients && IsClientConnected(client) && IsClientInGame(client))
        return true;
    return false;
}

public void OnMapStart()
{
    if (!g_hDatabase)
    {
        Database.Connect(SQL_OnDatabaseConnect, "unloze_playtimestats");
        return;
    }
    char sQuery[512];
    char sServer[32];

    int i_port = GetConVarInt(FindConVar("hostport"));
    if (i_port == 27015 || i_port == 27019 || i_port == 27035)
    {
        Format(sServer, sizeof(sServer), "ze_time");
    }
    else if (i_port == 27016)
    {
        Format(sServer, sizeof(sServer), "zr_time");
    }
    else if (i_port == 27017)
    {
        Format(sServer, sizeof(sServer), "mg_time");
    }
    else
    {
        return; 
    }
    Format(sQuery, sizeof(sQuery), "select player_name, sum(%s) as %s_total from unloze_playtimestats.player_time GROUP BY steam_id order by %s_total desc limit 1000", sServer, sServer, sServer);
    g_hDatabase.Query(SQL_OnQueryCompletedTopTime, sQuery);
}

public Action Command_TopTime(int client, int args)
{
    char sTitle[64];
    char sServer[32];
    int i_port = GetConVarInt(FindConVar("hostport"));
    if (i_port == 27015 || i_port == 27019 || i_port == 27035)
    {
        Format(sServer, sizeof(sServer), "ZE");
    }
    else if (i_port == 27016)
    {
        Format(sServer, sizeof(sServer), "ZR");
    }
    else if (i_port == 27017)
    {
        Format(sServer, sizeof(sServer), "MG");
    }
    else if (i_port == 27023)
    {
        Format(sServer, sizeof(sServer), "JB");
    }
    Format(sTitle, sizeof(sTitle), "[UNLOZE Playtime] Top 1000 Record Holders for %s:", sServer);
    Menu menu = new Menu(MenuHandler1);
    menu.SetTitle(sTitle);
    for (int i = 0; i < sizeof(g_cTimeRecords); i++)
    {
        menu.AddItem("-1", g_cTimeRecords[i], ITEMDRAW_DISABLED);
    }
    menu.ExitButton = true;
    menu.Display(client, 0);
    return Plugin_Handled;
}

public Action Command_Time(int client, int args)
{
    if (!g_hDatabase)
    {
        return Plugin_Handled;
    }
    char sQuery[512];
    Format(sQuery, sizeof(sQuery), "select ze_time, mg_time, zr_time, jb_time from unloze_playtimestats.player_time pt where pt.steam_id = '%s'", g_csSID[client]);
    g_hDatabase.Query(SQL_OnQueryCompletedTime, sQuery, GetClientSerial(client));
    return Plugin_Handled;
}

public void SQL_OnQueryCompletedTopTime(Database db, DBResultSet results, const char[] error, int iSerial)
{
    if (!db || strlen(error))
    {
        delete results;
        LogError("Query error 3: %s", error);
        return;
    }

    int iTime;
    char sName[MAX_NAME_LENGTH];
    int counter = 0;
    while (results.RowCount && results.FetchRow())
    {
        char sBuffer[256];
        results.FetchString(0, sName, sizeof(sName));
        iTime = results.FetchInt(1);
        int iHours = (iTime / 60) / 60;
        Format(sBuffer, sizeof(sBuffer), "%i %s %d Hours", counter + 1, sName, iHours);
        Format(g_cTimeRecords[counter], sizeof(g_cTimeRecords[]), sBuffer);
        counter++;
    }
    delete results;
}

public int MenuHandler1(Menu menu, MenuAction action, int param1, int param2)
{
    switch(action)
    {
        case MenuAction_End:
        {
          delete menu;
        }
    }
    return 0;
}

public void SQL_OnQueryCompletedTime(Database db, DBResultSet results, const char[] error, int iSerial)
{
    if (!db || strlen(error))
    {
        delete results;
        LogError("Query error 3: %s", error);
        return;
    }
    int client;
    if ((client = GetClientFromSerial(iSerial)) == 0)
    {
        delete results;
        return;
    }

    int iTime_ze;
    int iTime_mg;
    int iTime_zr;
    int iTime_jb;

    while (results.RowCount && results.FetchRow())
    {
        iTime_ze += results.FetchInt(0);
        iTime_mg += results.FetchInt(1);
        iTime_zr += results.FetchInt(2);
        iTime_jb += results.FetchInt(3);
    }
    delete results;
    int iHours_ze   = (iTime_ze / 60) / 60;
    int iMinutes_ze = (iTime_ze / 60) % 60;
    int iSeconds_ze = (iTime_ze % 60);

    int iHours_mg = (iTime_mg / 60) / 60;
    int iMinutes_mg = (iTime_mg / 60) % 60;
    int iSeconds_mg = (iTime_mg % 60);

    int iHours_zr   = (iTime_zr / 60) / 60;
    int iMinutes_zr = (iTime_zr / 60) % 60;
    int iSeconds_zr = (iTime_zr % 60);

    int iHours_jb   = (iTime_jb / 60) / 60;
    int iMinutes_jb = (iTime_jb / 60) % 60;
    int iSeconds_jb = (iTime_jb % 60);

    char sTime_ze[64];
    char sTime_mg[64];
    char sTime_zr[64];
    char sTime_jb[64];
    char sTitle[64];
    Format(sTitle, sizeof(sTitle), "[UNLOZE Playtime] Player %N:", client);
    Format(sTime_ze, sizeof(sTime_ze), "Zombie Escape: %d Hours %d Minutes %d Seconds", iHours_ze, iMinutes_ze, iSeconds_ze);
    Format(sTime_mg, sizeof(sTime_mg), "MiniGame: %d Hours %d Minutes %d Seconds", iHours_mg, iMinutes_mg, iSeconds_mg);
    Format(sTime_zr, sizeof(sTime_zr), "Zombie Riot: %d Hours %d Minutes %d Seconds", iHours_zr, iMinutes_zr, iSeconds_zr);
    Format(sTime_jb, sizeof(sTime_jb), "Jail Break: %d Hours %d Minutes %d Seconds", iHours_jb, iMinutes_jb, iSeconds_jb);

    Panel mSayPanel = new Panel(GetMenuStyleHandle(MenuStyle_Radio));

    mSayPanel.SetTitle(sTitle);
    mSayPanel.DrawItem("", ITEMDRAW_SPACER);
    mSayPanel.DrawText(sTime_ze);
    mSayPanel.DrawItem("", ITEMDRAW_SPACER);
    mSayPanel.DrawText(sTime_mg);
    mSayPanel.DrawItem("", ITEMDRAW_SPACER);
    mSayPanel.DrawText(sTime_zr);
    mSayPanel.DrawItem("", ITEMDRAW_SPACER);
    mSayPanel.DrawText(sTime_jb);
    mSayPanel.DrawItem("", ITEMDRAW_SPACER);
    mSayPanel.DrawItem("1. Got it!", ITEMDRAW_RAWLINE);
    mSayPanel.SetKeys(1023);

    mSayPanel.Send(client, Handler_Menu, 0);
    delete mSayPanel;
}

public int Handler_Menu(Menu menu, MenuAction action, int param1, int param2)
{
    switch(action)
    {
        case MenuAction_Select, MenuAction_Cancel:
            delete menu;
    }
    return 0;
}
