/**
 * Copyright 1998-2013 Epic Games, Inc. All Rights Reserved.
 */
class DecalManager extends Actor
	native(Decal)
	config(Game);

/** template to base pool components off of - should not be used for decals or attached to anything */
var protected DecalComponent DecalTemplate;
/** components currently in the pool */
var array<DecalComponent> PoolDecals;
/** maximum allowed active components - if this is greater than 0 and is exceeded, the oldest active decal is taken */
var int MaxActiveDecals;
/** default lifetime for decals */
var globalconfig float DecalLifeSpan;
/** default depth bias offset */
var float DecalDepthBias;
//@zombie_ps4_begin - TODO - Orbis has a different depth bias. We need a cleaner solution for this.
/** default depth bias offset */
var float OrbisDecalDepthBias;
//@zombie_ps4_end
/** default decal blend range */
var vector2D DecalBlendRange;

/** components currently active in the world and how much longer they will be */
struct native ActiveDecalInfo
{
	var DecalComponent Decal;
	var float LifetimeRemaining;
};
var array<ActiveDecalInfo> ActiveDecals;

cpptext
{
	virtual void TickSpecial(FLOAT DeltaTime);
}

/** @return whether dynamic decals are enabled */
native static final function bool AreDynamicDecalsEnabled();

/**
 * Called when the given decal's lifetime has run out
 * @note: caller responsible for removing from ActiveDecals array (this is to prevent code iterating the array from having dependencies on this function)
 */
event DecalFinished(DecalComponent Decal)
{
	if (Decal != None)
	{
		// clear it and return it to the pool
		Decal.ResetToDefaults();
		PoolDecals[PoolDecals.length] = Decal;
	}
}

/** @return whether spawning decals is allowed right now */
function bool CanSpawnDecals()
{
	return AreDynamicDecalsEnabled();
}


/**
 * This will set all of the various decal parameters.  This is the function that should be updated when there are new
 * Decal Paramaters that exist that should be updated by game code
 *
 * @param InExistingDecal If you have an existing decal that you want to set all the params on.
 * @param DecalMaterial the material to use for the decal
 * @param Width decal width
 * @param Height decal height
 * @param Thickness decal thickness (used to calculate the nearplane/farplane values)
 * @param bNoClip if true, use the bNoClip code path for decal generation (requires DecalMaterial to have clamped texture coordinates)
 * @param DecalRotation rotation of the decal in degrees
 * @param HitComponent if specified, will only project on this component (optimization)
 * @param bProjectOnTerrain whether decal can project on terrain (default true)
 * @param bProjectOnTerrain whether decal can project on skeletal meshes (default false)
 * @param HitBone if HitComponent is a skeletal mesh, the bone that was hit
 * @param HitNodeIndex if HitComponent is BSP, the node that was hit
 * @param HitLevelIndex if HitComponent is BSP, the index of the level whose BSP was hit
 * @param InFracturedStaticMeshComponentIndex The index of the FracturedMesh component.  -1/INDEX_NONE if the decal should project onto both the shell and core of the FracturedMeshActor
 * @param DepthBias depth bias offset to control z-fighting
 * @param BlendRange Start/End blend range specified as an angle in degrees. Controls where to start blending out the decal on a surface
 *
 **/
static final function SetDecalParameters( DecalComponent TheDecal,
						MaterialInterface DecalMaterial,
						vector DecalLocation,
						rotator DecalOrientation,
						float Width,
						float Height,
						float Thickness,
						bool bNoClip,
						float DecalRotation,
						PrimitiveComponent HitComponent,
						bool bProjectOnTerrain,
						bool bProjectOnSkeletalMeshes,
						name HitBone,
						int HitNodeIndex,
						int HitLevelIndex,
						int InFracturedStaticMeshComponentIndex,
						float DepthBias,
//@zombie_ps4_begin - TODO - Orbis has a different depth bias. We need a cleaner solution for this.
						float OrbisDepthBias,
//@zombie_ps4_end
						vector2D BlendRange
						)
{
	// set the decal's data
	TheDecal.Location = DecalLocation;
	TheDecal.Orientation = DecalOrientation;
	TheDecal.DecalRotation = DecalRotation;
	TheDecal.Width = Width;
	TheDecal.Height = Height;
	// the thickness is just divided in half to create the far/near plane
	TheDecal.FarPlane = Thickness * 0.5;
	TheDecal.NearPlane = -TheDecal.FarPlane;
	TheDecal.bNoClip = bNoClip;
	TheDecal.HitComponent = HitComponent;
	TheDecal.HitBone = HitBone;
	TheDecal.HitNodeIndex = HitNodeIndex;
	TheDecal.HitLevelIndex = HitLevelIndex;
	TheDecal.SetDecalMaterial(DecalMaterial);
	TheDecal.bProjectOnTerrain = bProjectOnTerrain;
	TheDecal.bProjectOnSkeletalMeshes = bProjectOnSkeletalMeshes;
	TheDecal.FracturedStaticMeshComponentIndex = InFracturedStaticMeshComponentIndex;
 	TheDecal.DepthBias = DepthBias;
//@zombie_ps4_begin - TODO - Orbis has a different depth bias. We need a cleaner solution for this.
	TheDecal.OrbisDepthBias = OrbisDepthBias;
//@zombie_ps4_end
	TheDecal.BlendRange = BlendRange;
}

/** @return a decal from the pool, or a new one if the pool was empty */
protected function DecalComponent GetPooledComponent()
{
	local int i;
	local DecalComponent Result;

	// try to grab one from the pool
	while (PoolDecals.length > 0)
	{
		i = PoolDecals.length - 1;
		Result = PoolDecals[i];
		PoolDecals.Remove(i, 1);
		if (Result != None && !Result.IsPendingKill() && !Result.IsWaitingForResetToDefaultsToComplete())
		{
			break;
		}
		else
		{
			Result = None;
		}
	}

	if (Result == None)
	{
		// if pool is empty, get started expiring something
		if (MaxActiveDecals > 0 && ActiveDecals.length >= MaxActiveDecals)
		{
			// expire oldest decal
			Result = ActiveDecals[0].Decal;
			Result.ResetToDefaults();
			ActiveDecals.Remove(0, 1);
		}

		Result = new(self) DecalTemplate.Class(DecalTemplate);
	}

	return Result;
}

/**
 * Spawns a decal with the given parameters, taking a component from the pool or creating as necessary.
 *
 * @note: the component is returned so the caller can perform any additional modifications (parameters, etc),
 * 	but it shouldn't keep the reference around as the component will be returned to the pool as soon as the lifetime runs out
 *
 * @param DecalMaterial the material to use for the decal
 * @param Width decal width
 * @param Height decal height
 * @param Thickness decal thickness (used to calculate the nearplane/farplane values)
 * @param bNoClip if true, use the bNoClip code path for decal generation (requires DecalMaterial to have clamped texture coordinates)
 * @param DecalRotation (opt) rotation of the decal in degrees
 * @param HitComponent (opt) if specified, will only project on this component (optimization)
 * @param bProjectOnTerrain (opt) whether decal can project on terrain (default true)
 * @param bProjectOnTerrain (opt) whether decal can project on skeletal meshes (default false)
 * @param HitBone (opt) if HitComponent is a skeletal mesh, the bone that was hit
 * @param HitNodeIndex (opt) if HitComponent is BSP, the node that was hit
 * @param HitLevelIndex (opt) if HitComponent is BSP, the index of the level whose BSP was hit
 * @param InDecalLifeSpan lifetime for the decal
 * @param InFracturedStaticMeshComponentIndex The index of the FracturedMesh component.  -1/INDEX_NONE if the decal should project onto both the shell and core of the FracturedMeshActor
 * @param DepthBias depth bias offset to control z-fighting
 * @param BlendRange Start/End blend range specified as an angle in degrees. Controls where to start blending out the decal on a surface
  * @return the DecalComponent that will be used (may be None if dynamic decals are disabled)
 */
function DecalComponent SpawnDecal( MaterialInterface DecalMaterial,
						vector DecalLocation,
						rotator DecalOrientation,
						float Width,
						float Height,
						float Thickness,
						bool bNoClip,
						optional float DecalRotation = (FRand() * 360.0),
						optional PrimitiveComponent HitComponent,
						optional bool bProjectOnTerrain = TRUE,
						optional bool bProjectOnSkeletalMeshes,
						optional name HitBone,
						optional int HitNodeIndex = INDEX_NONE,
						optional int HitLevelIndex = INDEX_NONE,
						optional float InDecalLifeSpan = DecalLifeSpan,
						optional int InFracturedStaticMeshComponentIndex = INDEX_NONE,
						optional float InDepthBias = DecalDepthBias,
//@zombie_ps4_begin - TODO - Orbis has a different depth bias. We need a cleaner solution for this.
						optional float InOrbisDepthBias = OrbisDecalDepthBias,
//@zombie_ps4_end
						optional vector2D InBlendRange = DecalBlendRange

						)
{
	local DecalComponent Result;
	local ActiveDecalInfo DecalInfo;

	// do nothing if decals are disabled
	if (!CanSpawnDecals())
	{
		return None;
	}

	Result = GetPooledComponent();

	// set the decal's data
	SetDecalParameters(
		Result,
		DecalMaterial,
		DecalLocation,
		DecalOrientation,
		Width,
		Height,
		Thickness,
		bNoClip,
		DecalRotation,
		HitComponent,
		bProjectOnTerrain,
		bProjectOnSkeletalMeshes,
		HitBone,
		HitNodeIndex,
		HitLevelIndex,
		INDEX_NONE,
		InDepthBias,
//@zombie_ps4_begin - TODO - Orbis has a different depth bias. We need a cleaner solution for this.
		InOrbisDepthBias,
//@zombie_ps4_end
		InBlendRange
		);

	AttachComponent(Result);

	// add to list to tick lifetime
	DecalInfo.Decal = Result;
	DecalInfo.LifetimeRemaining = InDecalLifeSpan;
	ActiveDecals.AddItem(DecalInfo);

	return Result;
}

`if(`__TW_)
function Reset()
{
	local int i;

	// Returns all decals to the pool
	for( i = 0; i < ActiveDecals.Length; ++i )
	{
		DecalFinished( ActiveDecals[i].Decal );
	}
	ActiveDecals.Length = 0;
}
`endif

defaultproperties
{
	TickGroup=TG_DuringAsyncWork

	Begin Object Class=DecalComponent Name=BaseDecal
		bIgnoreOwnerHidden=TRUE // this is needed a the owner of this decal is "hidden" as it is a global entity @see UDecalComponent::IsEnabled()
	End Object
	DecalTemplate=BaseDecal

//@zombie_ps4_begin - TODO - Orbis has a different depth bias. We need a cleaner solution for this.
	OrbisDecalDepthBias=-1.6
//@zombie_ps4_end
	DecalDepthBias=-0.00006
	DecalBlendRange=(X=89.5,Y=180)
}
