player_list = {};

class_viewheight_map = {
    [TF_CLASS_SCOUT]        = 65,
    [TF_CLASS_SOLDIER]      = 68,
    [TF_CLASS_PYRO]         = 68,
    [TF_CLASS_DEMOMAN]      = 68,
    [TF_CLASS_HEAVYWEAPONS] = 75,
    [TF_CLASS_ENGINEER]     = 68,
    [TF_CLASS_MEDIC]        = 75,
    [TF_CLASS_SNIPER]       = 75,
    [TF_CLASS_SPY]          = 75,
};

-- Grab the CEntity table
local _       = Entity("info_target", false, false);
local CEntity = getmetatable(_);
_:Remove();

-- Get player eye angles
CEntity.GetEyeAngles = function(self)
    if (self and self:IsValid() and self:IsPlayer()) then
        return Vector(self["m_angEyeAngles[0]"], self["m_angEyeAngles[1]"], 0);
    end
end

-- Get player eye position
CEntity.GetEyePos = function(self)
	if (not self or not self:IsValid() or not self:IsPlayer()) then return; end
	
	local eyepos = self:GetAbsOrigin();
	eyepos.z = eyepos.z + class_viewheight_map[self.m_iClass];
	
	return eyepos
end

-- Check whether the passed player is in our FOV (not necessarily whether we can see them)
CEntity.IsPlayerInFOV = function(self, player)
	if (not self or not self:IsValid() or not self:IsPlayer()) then return; end
	if (not player or not player:IsValid() or not player:IsPlayer()) then return; end
	
	local tolerance    = 0.5736; -- cos(110/2)
	local eyepos       = self:GetEyePos();
	local eyefwd       = self:GetEyeAngles():GetForward();
	local playerorigin = player:GetAbsOrigin()
	
	-- We go eyepos -> origin because you're more likely to see the top of a player
	-- rather than their feet due to map geometry
	
	-- What can we see from [eyepos, origin) ?
	-- Checks target eyepos and center
	for i=1,2 do
		playerorigin.z = playerorigin.z + class_viewheight_map[player.m_iClass] / i;
		delta = (playerorigin - eyepos):Normalize();
		
		if (eyefwd:Dot(delta) >= tolerance) then return true; end
	end
	
	-- Can we see target's origin?
	local delta = (player:GetAbsOrigin() - eyepos):Normalize();
	if (eyefwd:Dot(delta) >= tolerance) then return true; end
	
	-- Not in our FOV
	return false;
end

function PlayerParry(player, wep, viewmodel, idleseq, inspectseq, playbackrate, idletime, attacktime, parrytimemin, parrytimemax)
	local playerdata = player_list[player:GetUserId()];
	local curtime    = CurTime();

	-- We only want to parry when idle
	-- We also check next attack time for if we idle early
	if (viewmodel.m_nSequence == idleseq and curtime >= wep.m_flNextPrimaryAttack) then
		player:AddCond(TF_COND_CANNOT_SWITCH_FROM_MELEE, 1);
		player:PlaySoundToSelf("weapons/demo_sword_swing"..math.random(1,3)..".wav");
		
		wep.m_flTimeWeaponIdle     = curtime + idletime;
		wep.m_flNextPrimaryAttack  = curtime + attacktime;
		viewmodel.m_flPlaybackRate = playbackrate;
		viewmodel.m_flCycle        = 0.0;
		viewmodel.m_nSequence      = inspectseq;
		
		timer.Create(parrytimemin, function()
			if (not playerdata) then return; end
			
			playerdata.demo_parry_count = 0;
			playerdata.demo_parrying = true;
		end, 1);
		timer.Create(parrytimemax, function()
			if (not playerdata) then return; end
			
			playerdata.demo_parry_count = 0;
			playerdata.demo_parrying = false;
		end, 1);
	end
end

-- Called on player key press
function OnPlayerKey(player, key)
    if (not player or not player:IsValid() or not player:IsAlive()) then return end;

    local userid     = player:GetUserId();
    local playerdata = player_list[userid];
    local wep        = player.m_hActiveWeapon;

    -- Mouse2
    if (key == IN_ATTACK2) then
        if (player:GetPlayerItemBySlot(LOADOUT_POSITION_MELEE) == wep) then				
			if (player.m_iClass == TF_CLASS_DEMOMAN) then
				if (not wep or not wep:IsValid()) then return; end
				
				local viewmodel = player.m_hViewModel[1] or player.m_hViewModel[2];
				
				-- Persian Persuader
				if (wep.m_iItemDefinitionIndex == 404) then
					PlayerParry(player, wep, viewmodel, 8, 48, 1.5, 0.7, 1, 0.5, 1.5);
				
				-- Eyelander, Skullcutter, Claidheamohmor, Katana
				elseif (wep.m_iClassname == "tf_weapon_sword" or wep.m_iClassname == "tf_weapon_katana") then
					PlayerParry(player, wep, viewmodel, 13, 53, 5, 1, 1, 0.5, 1.5);
				end
            end
        end
	end
end

-- Called before player takes damage
function OnPlayerDamagedPre(player, damageinfo)
    local damagecustom = damageinfo.DamageCustom;
    local attacker     = damageinfo.Attacker;
	
	local playerdata = player_list[player:GetUserId()];
	
	-- Should we parry our attacker?
	if (playerdata.demo_parrying and attacker and attacker:IsValid() and attacker:IsPlayer() and
		attacker.m_iTeamNum ~= player.m_iTeamNum and damageinfo.DamageType & DMG_CLUB == DMG_CLUB) then
			
		-- We're parrying a valid melee attack, can we see our attacker and are we able to parry?
		if (playerdata.demo_parry_count < playerdata.demo_max_parry_count and player:IsPlayerInFOV(attacker)) then
			player:AddCond(TF_COND_SPEED_BOOST, 4);
			
			if (attacker.m_bIsMiniBoss == 1) then
				attacker:StunPlayer(2, 0.4, TF_STUNFLAG_SLOWDOWN | TF_STUNFLAG_NOSOUNDOREFFECT, player);
				damageinfo.Damage = damageinfo.Damage * 0.5;
			else
				attacker:StunPlayer(1.5, 1, TF_STUNFLAGS_NORMALBONK, player);
				damageinfo.Damage = 0;
			end
			
			player:PlaySoundToSelf("weapons/samurai/tf_katana_impact_object_0"..math.random(1,3)..".wav");
			playerdata.demo_parry_count = playerdata.demo_parry_count + 1;
			
			return true;
		end
    end
end

-- Called on player connected to server
function OnPlayerConnected(player)
    local userid = player:GetUserId();

    if (player:IsRealPlayer()) then
        player_list[userid] = {
			demo_parrying                = false, -- Are we parrying attacks at the moment?
			demo_parry_count             = 0,     -- How many melee attacks have we blocked this parry?
			demo_max_parry_count         = 5,     -- How many melee attacks are we allowed to block per parry?
        };

        -- Callbacks
        player:AddCallback(ON_KEY_PRESSED,         OnPlayerKey);
        player:AddCallback(ON_DAMAGE_RECEIVED_PRE, OnPlayerDamagedPre);
	end
end

-- Called on player disconnected from server
function OnPlayerDisconnected(player)
    local userid     = player:GetUserId()
    local playerdata = player_list[userid];

    if (player:IsRealPlayer() and playerdata) then
        player_list[userid] = nil;
    end
end