#include <sourcemod>
#include <sdktools>
#include <cstrike>
#include <multicolors>
#include <zombiereloaded>

#pragma semicolon 1
#pragma newdecls required

#define PARACHUTE_MDL "models/mark2580/pubg/carepackage/parachutecare.mdl"

ConVar g_cvParachuteFallSpeed;
ConVar g_cvParachuteLinear;
ConVar g_cvParachuteDecrease;
ConVar g_cvDelayPlaneLanded;

Handle g_hTimerTriggersHook = INVALID_HANDLE;
Handle g_hTimerPlaneDropTrigger = INVALID_HANDLE;
Handle g_hPlaneDoorOpen = INVALID_HANDLE;
Handle g_hTimerPlaneEndPath = INVALID_HANDLE;

int g_iVelocity = -1;
int g_iParachuteEntity[MAXPLAYERS+1];
int g_iTriggerTeleportID = 0;
int g_iTriggerTemplateID = 0;
int g_iTriggerMultipleID = 0;
int g_iPlaneEndPathID = 0;

bool g_bFallSpeed[MAXPLAYERS +1] = {false, ...};
bool g_bInUse[MAXPLAYERS+1] = {false, ...};
bool g_bHasParachuteModel[MAXPLAYERS+1] = {false, ...};
bool g_bRoundStarted = false;
bool g_bPlayerJumpedFormPlane[MAXPLAYERS+1] = {false, ...};
bool g_bPlayerLanded[MAXPLAYERS+1] = {false, ...};

bool g_bParaLinearFallSpeed = false;
float g_fFallSpeed = 100.0;
float g_fDecrease = 0.0;
float g_fAfterExplodeDelay = 45.0;

// Based on SWAT_88's plugin from SRCDSLAB
public Plugin myinfo =
{
	name = "VScript - ze_stardust_battleground",
	author = ".Rushaway & SWAT_88",
	description = "Give a single parachute to player for jumping out of the plane.",
	version = "1.0.0",
	url = "https://github.com/srcdslab/sm-plugin-Parachute"
};

public void OnPluginStart()
{
	g_cvParachuteLinear = CreateConVar("sm_stardust_parachute_linear", "1", "0: disables linear fallspeed - 1: enables it");
	g_cvParachuteFallSpeed = CreateConVar("sm_stardust_parachute_fallspeed", "135", "Speed of the fall when you use the parachute");
	g_cvParachuteDecrease = CreateConVar("sm_stardust_parachute_decrease", "75", "0: dont use Realistic velocity-decrease - x: sets the velocity-decrease");
	g_cvDelayPlaneLanded = CreateConVar("sm_stardust_parachute_delay", "45", "Remove parachute after x seconds of the plane exploded");

	g_iVelocity = FindSendPropInfo("CBasePlayer", "m_vecVelocity[0]");
	g_bParaLinearFallSpeed = g_cvParachuteLinear.BoolValue;
	g_fFallSpeed = g_cvParachuteFallSpeed.FloatValue;
	g_fDecrease = g_cvParachuteDecrease.FloatValue;
	g_fAfterExplodeDelay = g_cvDelayPlaneLanded.FloatValue;

	HookConVarChange(g_cvParachuteLinear, OnConVarChanged);
	HookConVarChange(g_cvParachuteFallSpeed, OnConVarChanged);
	HookConVarChange(g_cvParachuteDecrease, OnConVarChanged);
	HookConVarChange(g_cvDelayPlaneLanded, OnConVarChanged);

	AutoExecConfig(true);
}

public void OnMapStart()
{
	VerifyMap(false);
}

public void VerifyMap(bool bForceUnload)
{
	char sCurMap[256];
	GetCurrentMap(sCurMap, sizeof(sCurMap));
	bool bValidMap = strncmp(sCurMap, "ze_stardust_battleground", 24, false) == 0;
	LogMessage("bValidMap %d", bValidMap);
	if (bForceUnload || !bValidMap || g_iVelocity == -1)
	{
		if (bValidMap && g_iVelocity == -1)
			SetFailState("Failed to find m_vecVelocity[0] for CBasePlayer");
        //better to let the server control loading and unloading than the plugin itself. 
        //having many plugins unload/load on mapchange can cause very slow mapchanges.
	}
	else
	{
		// Is it needed if the model is already present in the map?
		// PrecacheModel("models/mark2580/pubg/carepackage");
		// AddFileToDownloadsTable(Line);

		HookEvent("player_death", Event_PlayerDeath);
		HookEvent("round_start", Event_OnRoundStart);
		HookEvent("round_end", Event_OnRoundEnd);
	}
}

public void OnMapEnd()
{
	EndRoundCleanUp();

	g_hTimerTriggersHook = INVALID_HANDLE;
	g_hTimerPlaneDropTrigger = INVALID_HANDLE;
	g_hPlaneDoorOpen = INVALID_HANDLE;
	g_hTimerPlaneEndPath = INVALID_HANDLE;

	UnhookEvent("player_death", Event_PlayerDeath);
	UnhookEvent("round_start", Event_OnRoundStart);
	UnhookEvent("round_end", Event_OnRoundEnd);

	// Force plugin to unload
	VerifyMap(true);
}

public void OnConVarChanged(ConVar convar, const char[] oldValue, const char[] newValue)
{
	if (convar == g_cvParachuteLinear)
		g_bParaLinearFallSpeed = g_cvParachuteLinear.BoolValue;
	else if (convar == g_cvParachuteFallSpeed)
		g_fFallSpeed = g_cvParachuteFallSpeed.FloatValue;
	else if (convar == g_cvParachuteDecrease)
		g_fDecrease = g_cvParachuteDecrease.FloatValue;
	else if (convar == g_cvDelayPlaneLanded)
		g_fAfterExplodeDelay = g_cvDelayPlaneLanded.FloatValue;
}

public void OnClientDisconnect(int client)
{
	StopParachute(client);
}

public void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast)
{
	int client = GetClientOfUserId(event.GetInt("userid"));
	StopParachute(client);
}

public void Event_OnRoundStart(Event event, const char[] name, bool dontBroadcast)
{
	g_bRoundStarted = false;
	// We use 5.0 seconds to let the map initialize the entities and dont stress the server
	g_hTimerTriggersHook = CreateTimer(5.0, Timer_HookTriggers, _, TIMER_FLAG_NO_MAPCHANGE);
}

public void Event_OnRoundEnd(Event event, const char[] name, bool dontBroadcast)
{
	EndRoundCleanUp();
}

public Action ZR_OnClientInfect(int &client, int &attacker, bool &motherInfect, bool &respawnOverride, bool &respawn)
{
	MarkPlayerAsLanded(client);
	StopParachute(client);
	return Plugin_Stop;
}

// We need to use OnGameFrame instead of OnPlayerRunCmd for more accurate parachute position rendering
public void OnGameFrame()
{
	if (!g_bRoundStarted)
		return;

	for (int client = 1; client <= MaxClients; client++)
	{
		if (IsClientInGame(client) && !g_bPlayerLanded[client] && IsPlayerAlive(client))
		{
			if (GetClientButtons(client) & IN_USE)
			{
				if (!g_bInUse[client])
				{
					g_bInUse[client] = true;
					g_bFallSpeed[client] = false;
					StartParachute(client, true);
				}
				// else
				// 	StartParachute(client, false);

				TeleportParachute(client);
			}
			else
			{
				// Player released the key, parachute will be released
				if (g_bInUse[client])
				{
					MarkPlayerAsLanded(client);
					StopParachute(client);
					CPrintToChat(client, "{pink}[VScripts] {white}You released your parachute.");
				}
			}
			CheckClientLocation(client);
		}
	}
}

stock void StartParachute(int client, bool bOpen)
{
	float fVelocity[3];
	GetEntDataVector(client, g_iVelocity, fVelocity);

	float fFallSpeed = g_fFallSpeed * (-1.0);

	if (fVelocity[2] >= fFallSpeed)
		g_bFallSpeed[client] = true;

	if (fVelocity[2] < 0.0)
	{
		if ((g_bParaLinearFallSpeed && g_bFallSpeed[client]) || g_fDecrease == 0.0)
			fVelocity[2] = fFallSpeed;
		else
			fVelocity[2] = fVelocity[2] + g_fDecrease;

		TeleportEntity(client, NULL_VECTOR, NULL_VECTOR, fVelocity);
		SetEntDataVector(client, g_iVelocity, fVelocity);
		SetEntityGravity(client, 0.1);

		if (bOpen)
			OpenParachute(client);
	}
}

stock void OpenParachute(int client)
{
	g_iParachuteEntity[client] = CreateEntityByName("prop_dynamic");
	if (!g_iParachuteEntity[client])
		return;

	DispatchKeyValue(g_iParachuteEntity[client], "model", PARACHUTE_MDL);
	DispatchKeyValue(g_iParachuteEntity[client], "DefaultAnim", "default");
	DispatchKeyValue(g_iParachuteEntity[client], "solid", "0");
	DispatchKeyValue(g_iParachuteEntity[client], "spawnflags", "256");
	DispatchKeyValue(g_iParachuteEntity[client], "rendermode", "1");
	DispatchKeyValue(g_iParachuteEntity[client], "rendercolor", "255 255 255");
	DispatchKeyValue(g_iParachuteEntity[client], "renderamt", "255");
	// Original packed model is big++ if we dont resize it, players views will be blocked when they jump off from the plane
	DispatchKeyValue(g_iParachuteEntity[client], "modelscale", "0.7");
	DispatchKeyValue(g_iParachuteEntity[client], "disablereceiveshadows", "1");
	DispatchKeyValue(g_iParachuteEntity[client], "disableshadows", "1");
	SetEntityMoveType(g_iParachuteEntity[client], MOVETYPE_NOCLIP);
	DispatchSpawn(g_iParachuteEntity[client]);
	g_bHasParachuteModel[client] = true;
	TeleportParachute(client);
}

stock void TeleportParachute(int client)
{
	if (HasValidParachute(client))
	{
		float fClient_Origin[3], fClient_Angles[3], fParachute_Angles[3];

		GetClientAbsOrigin(client, fClient_Origin);
		// Adjust the position of the parachute model
		fClient_Origin[2] += 60.0;
		GetClientAbsAngles(client, fClient_Angles);
		fParachute_Angles[1] = fClient_Angles[1];
		TeleportEntity(g_iParachuteEntity[client], fClient_Origin, fParachute_Angles, NULL_VECTOR);
	}
}

stock void StopParachute(int client)
{
	SetEntityGravity(client, 1.0);
	DeleteParachute(client);
	g_bInUse[client] = false;
	g_bFallSpeed[client] = false;
}

stock void DeleteParachute(int client)
{
	if (HasValidParachute(client))
	{
		AcceptEntityInput(g_iParachuteEntity[client], "Kill");
		g_bHasParachuteModel[client] = false;
	}
}

stock void CheckClientLocation(int client)
{
	float fSpeed[3];
	GetEntDataVector(client, g_iVelocity, fSpeed);
	int iFlag = GetEntityFlags(client);

	if (fSpeed[2] >= 0 || iFlag & FL_ONGROUND)
	{
		StopParachute(client);
		if (g_bPlayerJumpedFormPlane[client] && iFlag & FL_ONGROUND)
		{
			MarkPlayerAsLanded(client);
			CPrintToChat(client, "{pink}[VScripts] {white}You have landed.");
		}
	}
}

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

	if (g_hTimerTriggersHook != INVALID_HANDLE && CloseHandle(g_hTimerTriggersHook))
		g_hTimerTriggersHook = INVALID_HANDLE;

	if (g_hTimerPlaneDropTrigger != INVALID_HANDLE && CloseHandle(g_hTimerPlaneDropTrigger))
		g_hTimerPlaneDropTrigger = INVALID_HANDLE;

	if (g_hPlaneDoorOpen != INVALID_HANDLE && CloseHandle(g_hPlaneDoorOpen))
		g_hPlaneDoorOpen = INVALID_HANDLE;

	if (g_hTimerPlaneEndPath != INVALID_HANDLE && CloseHandle(g_hTimerPlaneEndPath))
		g_hTimerPlaneEndPath = INVALID_HANDLE;

	g_iTriggerTemplateID = 0;
	g_iTriggerTeleportID = 0;
	g_iTriggerMultipleID = 0;
	g_iPlaneEndPathID = 0;

	VerifyStartHookedEntity();
	VerifyPlaneJumpHookedEntity();
}

/* TIMERS TO HOOKS ENTITIES */
public Action Timer_HookTriggers(Handle timer)
{
	g_hTimerTriggersHook = INVALID_HANDLE;

	// Credits
	char sBuffer[64];
	Handle hPlugin = GetMyHandle();
	GetPluginInfo(hPlugin, PlInfo_Author, sBuffer, sizeof(sBuffer));
	CPrintToChatAll("{pink}[VScripts] {white}Map using VScripts made by %s", sBuffer);
	delete hPlugin;

	// Plante template is spawned
	g_iTriggerTemplateID = FindEntityByTargetname(0, "player_drop", "point_template");
	if (g_iTriggerTemplateID != 0)
		HookSingleEntityOutput(g_iTriggerTemplateID, "OnUser1", CallBack_OnPlaneTemplateSpawn);

	// Humans getting teleported inside the plane
	g_iTriggerTeleportID = FindEntityByTargetname(0, "teleport_spawn_ct", "trigger_teleport");
	if (g_iTriggerTeleportID != 0)
		HookSingleEntityOutput(g_iTriggerTeleportID, "OnUser1", CallBack_OnPlayerTeleportInPlane);

	if (!VerifyStartHookedEntity())
		CPrintToChatAll("{pink}[VScripts] {red}An error was caught. No parachute this round.");

	return Plugin_Stop;
}

public Action Timer_HookPlaneDropTrigger(Handle timer)
{
	g_hTimerPlaneDropTrigger = INVALID_HANDLE;

	g_iTriggerMultipleID = FindEntityByTargetname(0, "plane_player_drop", "trigger_multiple");
	if (g_iTriggerMultipleID != 0)
		HookSingleEntityOutput(g_iTriggerMultipleID, "OnEndTouch", CallBack_OnPlayerJumpedFromPlane);

	g_iPlaneEndPathID = FindEntityByTargetname(0, "player_drop_path02", "path_track");
	if (g_iPlaneEndPathID != 0)
		HookSingleEntityOutput(g_iPlaneEndPathID, "OnPass", CallBack_OnPlaneEndPath);

	if (!VerifyPlaneJumpHookedEntity())
		CPrintToChatAll("{pink}[VScripts] {red}An error was caught. No parachute this round.");

	return Plugin_Stop;
}

public Action Timer_PlaneHasExploded(Handle timer)
{
	g_hTimerPlaneEndPath = INVALID_HANDLE;
	g_bRoundStarted = false;

	for (int i = 1; i <= MaxClients; i++)
	{
		if (IsClientInGame(i) && !g_bPlayerLanded[i])
		{
			g_bPlayerLanded[i] = true;
			StopParachute(i);
			CPrintToChat(i, "{pink}[VScripts] {white}You parachute have a problem, prepare for impact!");
		}
	}

	return Plugin_Stop;
}

public Action Timer_OnPlayerTeleportInPlane(Handle timer)
{
	g_hPlaneDoorOpen = INVALID_HANDLE;
	g_bRoundStarted = true;
	for (int i = 1; i <= MaxClients; i++)
	{
		if (!IsClientInGame(i))
			continue;

		if (GetClientTeam(i) != CS_TEAM_CT)
			continue;

		InitPlayerToJumpFromPlane(i);
	}
	return Plugin_Stop;
}

/* HOOKS ENTITY CALLBACKS */
stock void CallBack_OnPlaneTemplateSpawn(const char[] output, int caller, int activator, float delay)
{
	// "OnUser1" "player_drop_prop,FireUser1,,5,1"
	// So we add a delay of 0.1 seconds to hook the players dropping from the plane trigger
	g_hTimerPlaneDropTrigger = CreateTimer(5.1, Timer_HookPlaneDropTrigger, _, TIMER_FLAG_NO_MAPCHANGE);
}

stock void CallBack_OnPlayerTeleportInPlane(const char[] output, int caller, int activator, float delay)
{
	g_hPlaneDoorOpen = CreateTimer(8.0, Timer_OnPlayerTeleportInPlane, _, TIMER_FLAG_NO_MAPCHANGE);
}

stock void CallBack_OnPlayerJumpedFromPlane(const char[] output, int caller, int activator, float delay)
{
	g_bPlayerJumpedFormPlane[activator] = true;
}

stock void CallBack_OnPlaneEndPath(const char[] output, int caller, int activator, float delay)
{
	// Plane reached the end of the path_track - Some players may still in the air with parachute
	// Give them 45 seconds to land
	if (g_fAfterExplodeDelay <= 0.0)
		g_fAfterExplodeDelay = 45.0;

	g_hTimerPlaneEndPath = CreateTimer(g_fAfterExplodeDelay, Timer_PlaneHasExploded, _, TIMER_FLAG_NO_MAPCHANGE);
}

/* UTILS */
stock bool VerifyStartHookedEntity()
{
	if (g_iTriggerTeleportID != 0)
		return true;

	UnhookSingleEntityOutput(g_iTriggerTemplateID, "OnUser1", CallBack_OnPlaneTemplateSpawn);
	UnhookSingleEntityOutput(g_iTriggerTeleportID, "OnUser1", CallBack_OnPlayerTeleportInPlane);

	return false;
}

stock bool VerifyPlaneJumpHookedEntity()
{
	if (g_iTriggerMultipleID != 0 || g_iPlaneEndPathID != 0)
		return true;

	UnhookSingleEntityOutput(g_iTriggerMultipleID, "OnEndTouch", CallBack_OnPlayerJumpedFromPlane);
	UnhookSingleEntityOutput(g_iPlaneEndPathID, "OnPass", CallBack_OnPlaneEndPath);
	return false;
}

stock bool HasValidParachute(int client)
{
	return g_bHasParachuteModel[client] && IsValidEntity(g_iParachuteEntity[client]);
}

stock void InitPlayerToJumpFromPlane(int client)
{
	g_bPlayerLanded[client] = false;
	g_bPlayerJumpedFormPlane[client] = false;
}

stock void MarkPlayerAsLanded(int client)
{
	g_bPlayerLanded[client] = true;
}

public int FindEntityByTargetname(int entity, const char[] sTargetname, const char[] sClassname)
{
	if(sTargetname[0] == '#') // HammerID
	{
		int HammerID = StringToInt(sTargetname[1]);

		while((entity = FindEntityByClassname(entity, sClassname)) != 0)
		{
			if(GetEntProp(entity, Prop_Data, "m_iHammerID") == HammerID)
				return entity;
		}
	}
	else // Targetname
	{
		int Wildcard = FindCharInString(sTargetname, '*');
		char sTargetnameBuf[64];

		while((entity = FindEntityByClassname(entity, sClassname)) != 0)
		{
			if(GetEntPropString(entity, Prop_Data, "m_iName", sTargetnameBuf, sizeof(sTargetnameBuf)) <= 0)
				continue;

			if(strncmp(sTargetnameBuf, sTargetname, Wildcard) == 0)
				return entity;
		}
	}
	return 0;
}
