//=============================================================================
// KFDroppedPickup
//=============================================================================
// Implements dropped weapon/item pickups
//=============================================================================
// Killing Floor 2
// Copyright (C) 2015 Tripwire Interactive LLC
// - Christian "schneidzekk" Schneider
//=============================================================================
// Based on GearDroppedPickup
// Copyright 1998-2011 Epic Games, Inc. All Rights Reserved.
//=============================================================================
class KFDroppedPickup extends DroppedPickup
	native
	nativereplication;

var const RigidBodyState	RBState;
var	const float				AngErrorAccumulator;

/** True if a collision should trigger audio, false otherwise. */
var transient bool			bCollisionSoundsEnabled;

var protectedwrite MeshComponent MyMeshComp;

/** Cache a reference to the cylinder component */
var protectedwrite CylinderComponent MyCylinderComp;

/** (TW) Setup rigid body physics for static meshes components */
var const bool bEnableStaticMeshRBPhys;
/** (TW) If set this pickups passes (and can be picked) through blocking volumes) */
var const bool bIgnoreBlockingVolumes;

/** When TRUE, there is a delay before the owner is allowed to re-pickup if they are low on health */
var protected const bool bUseLowHealthDelay;

/** Skin assigned from dropped weapon */
var int SkinItemId;

/** Whether the weapon skin has finished loading */
var bool bWaitingForWeaponSkinLoad;

/** Whether or not to use the authority's rigid body update */
var protected bool bUseAuthorityRBUpdate;

/** Life span after changing authority update status */
var protected float PostAuthorityChangeLifeSpan;

/** Amount of delay before anything can pick this up */
var protected float PickupDelay;

// Visuals
var             bool                        bEmptyPickup;       // pickup has an inventory with no ammo
var             LinearColor                 EmptyPickupColor;


var				bool						bUpgradedPickup; //if the item has an upgrade level of > 0
cpptext
{
	// AActor interface
	virtual void physRigidBody(FLOAT DeltaTime);
	virtual INT* GetOptimizedRepList(BYTE* InDefault, FPropertyRetirement* Retire, INT* Ptr, UPackageMap* Map, UActorChannel* Channel);
}

replication
{
	if ( Role == ROLE_Authority )
		RBState, bUseAuthorityRBUpdate;

	if ( bNetInitial )
		SkinItemId, bEmptyPickup, bUpgradedPickup;
}

/*********************************************************************************************
 Constructors, Destructors, and Loading
********************************************************************************************* */

simulated native function StaticMesh GetPickupMesh(class<KFWeapon> ItemClass);
/**
 * sets the pickups mesh and makes it the collision component so we can run rigid body physics on it
 */
simulated function SetPickupMesh(PrimitiveComponent NewPickupMesh)
{
	local ActorComponent Comp;
	local SkeletalMeshComponent SkelMC;
	local StaticMeshComponent StaticMC;
	local KFGameInfo KFGI;

	if (Role == ROLE_Authority )
	{
		if ( Inventory != None && Inventory.IsA('KFWeapon') )
		{
			SkinItemId = KFWeapon(Inventory).SkinItemId;
            bEmptyPickup = !KFWeapon(Inventory).HasAnyAmmo();
			bUpgradedPickup = KFWeapon(Inventory).CurrentWeaponUpgradeIndex > 0;

		}

		KFGI = KFGameInfo(WorldInfo.Game);
		if (KFGI != none && KFGI.OutbreakEvent != none && KFGI.OutbreakEvent.ActiveEvent.DroppedItemLifespan >= 0.0f)
		{
			LifeSpan = KFGI.OutbreakEvent.ActiveEvent.DroppedItemLifespan;
		}

		SetTimer(LifeSpan, false, nameof(TryFadeOut));
	}

	if (NewPickupMesh != None)
	{
		if (CollisionComponent == None || CollisionComponent.Class != NewPickupMesh.Class)
		{
			Comp = new(self) NewPickupMesh.Class(NewPickupMesh);
			MyMeshComp = MeshComponent(Comp);
			AttachComponent(Comp);
			MyCylinderComp = CylinderComponent(CollisionComponent);
			AlignMeshToCylinder();

			CollisionComponent = PrimitiveComponent(Comp);
		}
		else
		{
			Comp = CollisionComponent;
		}

		//Weapons packages are async loaded from string.  At this point the packages will be loaded into root,
		//		but the asset still has to be grabbed and set.
		if (class<KFWeapon>(InventoryClass) != none)
		{
			StaticMeshComponent(MyMeshComp).SetStaticMesh(GetPickupMesh(class<KFWeapon>(InventoryClass)));
		}

		// Set up physics on the cloned component
		CollisionComponent.SetScale3D(NewPickupMesh.Scale3D);

		CollisionComponent.SetBlockRigidBody(TRUE);
		CollisionComponent.SetActorCollision(TRUE,FALSE);
		CollisionComponent.SetRBChannel( RBCC_Pickup );
		CollisionComponent.SetRBCollidesWithChannel( RBCC_Default, TRUE );
		if ( !bIgnoreBlockingVolumes )
		{
			CollisionComponent.SetRBCollidesWithChannel( RBCC_BlockingVolume, TRUE );
		}

		CollisionComponent.SetNotifyRigidBodyCollision( TRUE );
		CollisionComponent.ScriptRigidBodyCollisionThreshold = 100;
		CollisionComponent.WakeRigidBody();

		SkelMC = SkeletalMeshComponent(CollisionComponent);
		if (SkelMC != None)
		{
			SkelMC.bUpdateSkelWhenNotRendered=FALSE;
			SkelMC.bComponentUseFixedSkelBounds=FALSE; // using physics when dropped, can't use fixed bounds

			SkelMC.PhysicsWeight = 1.f;
			SkelMC.SetHasPhysicsAssetInstance(TRUE);
			if (Role == ROLE_Authority)
			{
				SetPhysics(PHYS_RigidBody);
			}

			SkelMC.PhysicsAssetInstance.SetAllBodiesFixed(false);

			bCallRigidBodyWakeEvents = true;
		}
		else
		{
			StaticMC = StaticMeshComponent(CollisionComponent);
			if (StaticMC != none && bEnableStaticMeshRBPhys)
			{
				CollisionComponent.InitRBPhys();
				if (Role == ROLE_Authority && CollisionComponent.BodyInstance != None)
				{
					SetPhysics(PHYS_RigidBody);
				}

				bCallRigidBodyWakeEvents = true;

				// PhysX3 fix - Inherit RB velocity from actor (StaticMesh only)
				MyMeshComp.SetRBLinearVelocity(Velocity);
				Velocity = vect(0,0,0);
			}
		}

		SetPickupSkin(SkinItemId);
	}
}

native function bool StartLoadPickupSkin(int SkinId);

simulated event SetPickupSkin(int ItemId, bool bFinishedLoading = false)
{
	local array<MaterialInterface> SkinMICs;

	if (ItemId > 0 && WorldInfo.NetMode != NM_DedicatedServer && !bWaitingForWeaponSkinLoad)
	{
		if (bFinishedLoading || !StartLoadPickupSkin(ItemId))
		{
			SkinMICs = class'KFWeaponSkinList'.static.GetWeaponSkin(ItemId, WST_Pickup);
			if ( SkinMICs.Length > 0 )
			{
				MyMeshComp.SetMaterial(0, SkinMICs[0]);
			}
		}
	}

	if (bUpgradedPickup)
	{
		SetUpgradedMaterial();
	}
	if (bEmptyPickup)
    {
        SetEmptyMaterial();
    }
}

simulated function SetUpgradedMaterial()
{
	local MaterialInstanceConstant MeshMIC;

	if (MyMeshComp != None)
	{
		MeshMIC = MyMeshComp.CreateAndSetMaterialInstanceConstant(0);
		if (MeshMIC != None)
		{
			MeshMIC.SetScalarParameterValue('Upgrade', 1);
		}
	}
}

simulated function SetEmptyMaterial()
{
    local MaterialInstanceConstant MeshMIC;

    if (MyMeshComp != None)
    {
        MeshMIC = MyMeshComp.CreateAndSetMaterialInstanceConstant(0);
        if (MeshMIC != None)
        {
            MeshMIC.SetVectorParameterValue('GlowColor', EmptyPickupColor);
        }
    }
}


/**
 * returns the display name of the item to be picked up
 */
simulated function String GetDisplayName()
{
	if ( InventoryClass != None )
	{
		return InventoryClass.Default.ItemName;
	}

	return "";
}

/** Moves the mesh to be centered and sized correctly since most of our weapon meshes are way off center */
simulated function AlignMeshToCylinder()
{
	local vector MeshCenterLocal;
	local SkeletalMeshComponent SkelComp;

	// translate mesh so that the bounds origin is at the actor position
	SkelComp = SkeletalMeshComponent(MyMeshComp);
	if (SkelComp != None)
	{
		MeshCenterLocal = (SkelComp.Bounds.Origin - Location) << Rotation;
		SkelComp.SetTranslation(-MeshCenterLocal);
	}
}

/*********************************************************************************************
 PHYS_RigidBody
********************************************************************************************* */

 /** RigidBody went to sleep after being awake - only valid if bCallRigidBodyWakeEvents==TRUE */
event OnSleepRBPhysics()
{
	local SkeletalMeshComponent SkelComp;

	//`log( "turning off RB collision" );
	CollisionComponent.SetNotifyRigidBodyCollision(false);
	ClearTimer( 'CheckForRigidBodySleepState' );
	CollisionComponent.SetBlockRigidBody(false);
	CollisionComponent.SetActorCollision(false, false);

	// Fix in place so nothing can move it.
	SkelComp = SkeletalMeshComponent(CollisionComponent);
	if (SkelComp != None && SkelComp.PhysicsAssetInstance != None)
	{
		SkelComp.PhysicsAssetInstance.SetAllBodiesFixed(true);
		SkelComp.bUpdateKinematicBonesFromAnimation = false;
	}

	if (SkelComp != None)
	{
		SkelComp.bNoSkeletonUpdate = true;
		SkelComp.bUpdateSkelWhenNotRendered = false;
		SkelComp.SetAnimTreeTemplate(None);
	}
	else if ( bEnableStaticMeshRBPhys )
	{
		CollisionComponent.GetRootBodyInstance().SetFixed(true);
	}

	// reduce to dirty replication like DroppedPickup
	bOnlyDirtyReplication = true;
	bNetDirty = true;

	// pretend we were rendered this frame to make sure everything (bounds, physics, etc) gets updated with our final pose
	CollisionComponent.LastRenderTime = WorldInfo.TimeSeconds;

	AlignCollisionCylinder();
}

/** moves the cylinder to be centered and sized correctly since most of our weapon meshes are way off center */
simulated final function AlignCollisionCylinder()
{
	local vector MeshCenter;
	local vector MeshTranslationLocal;

	MeshCenter = (MyMeshComp.Bounds.Origin - Location);
	// Added cylinder CollisionHeight so that cylinder rests on the floor
	MeshTranslationLocal = (MyCylinderComp.default.CollisionHeight * vect(0,0,1) + MeshCenter) << Rotation;

	MyCylinderComp.SetTranslation(MeshTranslationLocal);
}

/** @see GearWeapon.DropFrom  for the ScriptRigidBodyCollisionThreshold value we use **/
event RigidBodyCollision(PrimitiveComponent HitComponent, PrimitiveComponent OtherComponent,
    const out CollisionImpactData RigidCollisionData, int ContactIndex)
{
    PlayCollisionSound();
}

/*********************************************************************************************
 Sound Effects
********************************************************************************************* */

/**
 * Overridden to prevent adding to navigation network.
 */
event Landed(vector HitNormal, actor FloorActor)
{
	bForceNetUpdate = TRUE;
	NetUpdateFrequency = 3;

	PlayCollisionSound();
}

function PlayCollisionSound()
{
	//local SoundCue Cue;
	//local float Duration;

	if (bCollisionSoundsEnabled)
	{
		//Cue = class<KFWeapon>(InventoryClass).default.WeaponDropSound;

		//if (Cue != None)
		//{
		//	Duration = Cue.GetCueDuration();
		//	SetTimer( Duration, FALSE, nameof(ReenableCollisionSounds) );
		//	PlaySound(Cue);
		//	bCollisionSoundsEnabled = FALSE;
		//}
	}
}

function ReenableCollisionSounds()
{
	bCollisionSoundsEnabled = TRUE;
}

/*********************************************************************************************
 Inventory
********************************************************************************************* */

/**
 * Give pickup to player
 */
function GiveTo(Pawn P)
{
    local KFWeapon KFW;
    local class<KFWeapon> KFWInvClass;
    local Inventory NewInventory;
    local KFInventoryManager KFIM;

    KFIM = KFInventoryManager(P.InvManager);
    if (KFIM != None)
    {
        KFWInvClass = class<KFWeapon>(InventoryClass);
        foreach KFIM.InventoryActors(class'KFWeapon', KFW)
        {
            if (KFW.Class == InventoryClass)
            {
                // if this isn't a dual-wield class, then we can't carry another
                if (KFW.DualClass == none)
                {
                    PlayerController(P.Owner).ReceiveLocalizedMessage(class'KFLocalMessage_Game', GMT_AlreadyCarryingWeapon);
                    return;
                }
                break;
            }
            // if we already have the dual version of this single, then we can't carry another
            else if (KFWInvClass != none && KFW.Class == KFWInvClass.default.DualClass)
            {
                PlayerController(P.Owner).ReceiveLocalizedMessage(class'KFLocalMessage_Game', GMT_AlreadyCarryingWeapon);
                return;
            }
        }

		if (KFWInvClass != none && KFWeapon(Inventory) != none && !KFIM.CanCarryWeapon(KFWInvClass, KFWeapon(Inventory).CurrentWeaponUpgradeIndex))
		{
			PlayerController(P.Owner).ReceiveLocalizedMessage(class'KFLocalMessage_Game', GMT_TooMuchWeight);
			return;
		}

        NewInventory = KFIM.CreateInventory(InventoryClass, true);
        if (NewInventory != none)
        {
            // Added extra check in case we want to pick up a non-weapon based pickup
            KFW = KFWeapon(NewInventory);
            if (KFW != none)
            {
                KFW.SetOriginalValuesFromPickup(KFWeapon(Inventory));
                KFW = KFIM.CombineWeaponsOnPickup(KFW);
                KFW.NotifyPickedUp();
            }

            Destroy();
        }
    }

    if (Role == ROLE_Authority)
    {
        //refresh weapon hud here
        NotifyHUDofWeapon(P);
    }
}

event Destroyed()
{
    super.Destroyed();

    Inventory.Destroy();
    Inventory = none;
}

function NotifyHUDofWeapon(Pawn P)
{
    local KFPlayerController KFPC;

    KFPC = KFPlayerController(P.Owner);

    if (KFPC != none && KFPC.MyGFxHUD != none)
    {
        KFPC.MyGFxHUD.NotifyHUDofWeapon();
    }
}

/*********************************************************************************************
 * State Pickup
 * Pickup is active
 *********************************************************************************************/

function bool ValidTouch(Pawn Other);

auto state Pickup
{
    /**
     * Validate touch (if valid return true to let other pick me up and trigger event).
     * Overridden to remove call to WorldInfo.Game.PickupQuery(Other, InventoryType, self) since we need this to be client side
     */
    function bool ValidTouch(Pawn Other)
    {
        local Actor HitA;
        local vector HitLocation, HitNormal;
        local TraceHitInfo HitInfo;
        local bool bHitWall;

        // make sure its a live player
        if (Other == None || !Other.bCanPickupInventory || !Other.IsAliveAndWell() || (Other.DrivenVehicle == None && Other.Controller == None))
        {
            return false;
        }

        if (`TimeSince(CreationTime) < PickupDelay)
        {
            return false;
        }

		// prevent picking up as soon as it's spawned
		if ( Other == Instigator )
		{
			// If low on health, wait a little longer to allow pickup again
			if( (bUseLowHealthDelay && (Instigator.Health / Instigator.HealthMax) <= 0.2f && `TimeSince(CreationTime) < 1.f)
				|| `TimeSince(CreationTime) < 0.1f )
			{
				return false;
			}
		}

		// make sure not touching through wall
		foreach Other.TraceActors( class'Actor', HitA, HitLocation, HitNormal, MyCylinderComp.GetPosition() + vect(0,0,10), Other.Location, vect(1,1,1), HitInfo )
		{
			if ( IsTouchBlockedBy(HitA, HitInfo.HitComponent) )
			{
				if (MyMeshComp == None)
				{
					return false;
				}
				else
				{
					bHitWall = true;
					break;
				}
			}
		}
		if (bHitWall)
		{
			// fail only if wall between both cylinder and mesh center
			foreach Other.TraceActors( class'Actor', HitA, HitLocation, HitNormal, MyMeshComp.Bounds.Origin + vect(0,0,10), Other.Location, vect(1,1,1), HitInfo )
			{
				if ( IsTouchBlockedBy(HitA, HitInfo.HitComponent) )
				{
					return false;
				}
			}
		}

		// make sure game will let player pick me up
		if (WorldInfo.Game.PickupQuery(Other, Inventory.class, self))
		{
			return true;
		}
		return false;
	}

	/**
	* Overridden to prevent adding to navigation network.
	*/
	function BeginState(Name PreviousStateName)
	{
		SetTimer(LifeSpan - 1, false);
	}
}

/** helper for ValidTouch */
simulated function bool IsTouchBlockedBy( Actor A, PrimitiveComponent HitComponent )
{
	if( A != self && A.bWorldGeometry && HitComponent != none && HitComponent.BlockActors )
	{
		if ( BlockingVolume(A) != None )
		{
			return false;
		}

		return true;
	}

	return false;
}

/** Level was reset without reloading */
function Reset()
{
	Destroy();
}

/** Handle any behavior associated with disabling the authority override for RB simulation.  Also
 *      flip the replicated flag to let the clients take over.
 */
function DisableAuthorityRBSim()
{
    bUseAuthorityRBUpdate = false;

    //Since we're now doing client simulation, keep it around long enough to
    //      be useful, but no longer.  Reset fadeout timer.
    ClearTimer(nameof(TryFadeOut));
    SetTimer(PostAuthorityChangeLifeSpan, false, nameof(TryFadeOut));
}

/*********************************************************************************************
 * state FadeOut extends Pickup
 * Pickup is about to disappear
 *********************************************************************************************/

State FadeOut
{
	function Tick(float DeltaTime)
	{
		SetDrawScale(FMax(0.01, DrawScale - Default.DrawScale * DeltaTime));
		Global.Tick(DeltaTime);
	}

	simulated event BeginState(Name PreviousStateName)
	{
		bFadeOut = true;
		RotationRate.Yaw=60000;
		SetPhysics(PHYS_Rotating);
		LifeSpan = 1.0;
	}

	/** disable normal touching. we require input from the player to pick it up */
	event Touch( Actor Other, PrimitiveComponent OtherComp, vector HitLocation, vector HitNormal )
	{
	}
}

/** Called on the server, try to find out when item is not close to player */
function TryFadeOut()
{
	local Pawn P;

    //Only do this if we're letting server run the RB updates
    if (bUseAuthorityRBUpdate)
    {
        // don't fade out yet if a player is nearby
        foreach WorldInfo.AllPawns(class'Pawn', P, Location, 1024.0)
        {
            if (P.IsPlayerOwned())
            {
                SetTimer(5.0, false, nameof(TryFadeOut));
                return;
            }
        }
    }

	GotoState('FadeOut');
}

defaultproperties
{
	//TickGroup=TG_DuringAsyncWork  // can't turn this on here as we get a TickGroup error when trying to update bones

	bOnlyDirtyReplication=FALSE
	bOnlyRelevantToOwner=FALSE
	bAlwaysRelevant=TRUE
	bKillDuringLevelTransition=TRUE
	bCanStepUpOn=false
	Lifespan=300.f
    PostAuthorityChangeLifeSpan=5.f

	Begin Object NAME=CollisionCylinder
		CollisionRadius=+00075.000000
		CollisionHeight=+00040.000000
		CollideActors=true
		Translation=(Z=40) // offset by CollisionHeight so that cylinder is on floor
	End Object

	bUseLowHealthDelay=TRUE

	bCollisionSoundsEnabled=TRUE

	// custom collision settings
	bEnableStaticMeshRBPhys=TRUE

    bUseAuthorityRBUpdate=TRUE

    EmptyPickupColor=(R=0.75)
	bWaitingForWeaponSkinLoad=false
}