class MeleeTargetData
{	
	Object Obj;
	vector HitPos;
	int HitComponent;
	
	void MeleeTargetData(Object o, vector p, int c)
	{
		Obj = o;
		HitPos = p;
		HitComponent = c;
	}
}

class MeleeTargetSettings
{
	vector ConeOrigin;
	float ConeLength;
	float ConeHalfAngle;
	float ConeHalfAngleRad;
	float ConeMinHeight;
	float ConeMaxHeight;
	
	vector ConeLeftPoint;
	vector ConeRightPoint;
	
	vector RayStart;
	vector RayEnd;
	vector Dir;
	vector XZDir;
	float MaxDist;
	
	EntityAI Attacker;
	ref array<typename> TargetableObjects
	
	void MeleeTargetSettings(vector coneOrigin, float coneLength, float coneHalfAngle, float coneMinHeight, float coneMaxHeight, vector rayStart, vector dir, float maxDist, EntityAI pToIgnore, array<typename> targetableObjects)
	{
		ConeOrigin 			= coneOrigin;
		ConeLength 			= coneLength;
		ConeHalfAngle 		= coneHalfAngle;
		ConeHalfAngleRad 	= Math.DEG2RAD * coneHalfAngle;
		ConeMinHeight 		= coneMinHeight;
		ConeMaxHeight 		= coneMaxHeight;
		
		RayStart 			= rayStart;
		RayEnd 				= rayStart + Math.SqrFloat(coneLength) * dir;
		
		Dir 				= dir; 
		
		XZDir 				= dir;
		XZDir[1] 			= 0;
		XZDir.Normalize();
		
		MaxDist 			= maxDist;
		
		Attacker 			= pToIgnore;
		TargetableObjects 	= targetableObjects;
		
		// Calculate cone points
		Math3D.ConePoints(ConeOrigin, ConeLength, ConeHalfAngleRad, -Math.Atan2(XZDir[0], XZDir[2]), ConeLeftPoint, ConeRightPoint);
	}
}

class ComponentResult
{
	vector ComponentPos;
	float ComponentAngle;
	float ComponentDistance2;
	int ComponentIdx;
}

class MeleeTargeting
{
	const static PhxInteractionLayers MELEE_TARGET_OBSTRUCTION_LAYERS = PhxInteractionLayers.BUILDING|PhxInteractionLayers.DOOR|PhxInteractionLayers.VEHICLE|PhxInteractionLayers.ROADWAY|PhxInteractionLayers.TERRAIN|PhxInteractionLayers.ITEM_SMALL|PhxInteractionLayers.ITEM_LARGE|PhxInteractionLayers.FENCE;
	
	MeleeTargetData GetMeleeTargetEx(MeleeTargetSettings settings, out array<Object> allTargets = null, array<string> blacklistedDamageZones = null)
	{
		MeleeTargetData ret;
		
		// Easier access to MeleeTargetSettings variables
		vector 	coneOrigin 					= settings.ConeOrigin;
		float 	coneLength 					= settings.ConeLength;
		float 	coneHalfAngle 				= settings.ConeHalfAngle;
		float	radAngle 					= settings.ConeHalfAngleRad;
		float 	coneMinHeight 				= settings.ConeMinHeight;
		float 	coneMaxHeight 				= settings.ConeMaxHeight;
		
		vector coneLeft 					= settings.ConeLeftPoint;
		vector coneRight 					= settings.ConeRightPoint;
		
		vector rayStart 					= settings.RayStart;
		vector rayEnd 						= settings.RayEnd;
		vector dir 							= settings.Dir;
		vector xzDir 						= settings.XZDir;
		float maxDist 						= settings.MaxDist;
		
		EntityAI pToIgnore 					= settings.Attacker;
		array<typename> targetableObjects 	= settings.TargetableObjects;
		
		// Calculate box size
		float boxWidth = vector.Distance(coneLeft, coneRight);		
		float boxHeight = coneMaxHeight - coneMinHeight;
		
		vector boxSize = Vector(boxWidth, boxHeight, coneLength);
		
		// Calculate box center
		vector centerHeight = Vector(0, Math.Lerp(coneMinHeight, coneMaxHeight, 0.5), 0);
		vector offset = xzDir * coneLength * 0.5;
		vector boxCenter = coneOrigin + offset + centerHeight;
		
		// Gather all targets
		BoxCollidingParams params = new BoxCollidingParams();
		params.SetParams(boxCenter, xzDir.VectorToAngles(), boxSize, ObjIntersect.Fire, ObjIntersect.Fire, true);
		array<ref BoxCollidingResult> results = new array<ref BoxCollidingResult>;
		array<Object> toIgnore = { pToIgnore };
		if (GetGame().IsBoxCollidingGeometryProxy(params, toIgnore, results))
		{
			//
			float 	retVal 	= float.MAX;
			float 	tgAngle = Math.Tan(radAngle);	
		
			#ifdef DIAG_DEVELOPER
			if (DiagMenu.GetBool(DiagMenuIDs.MELEE_DRAW_RANGE) && DiagMenu.GetBool(DiagMenuIDs.MELEE_DEBUG))
				Debug.DrawLine(rayStart, rayEnd, COLOR_GREEN, ShapeFlags.ONCE);
			#endif
			
			// Find the most suitable target
			foreach (BoxCollidingResult bResult : results)
			{
				Object obj = bResult.obj;
				
				// Check for targetable objects
				if (!obj.IsAnyInherited(targetableObjects) || !obj.IsAlive())
				{
					continue;
				}
				
				// Ready the defaults for the object
				vector targetPos = obj.GetPosition();							
				float targetAngle = Math.RAD2DEG * Math.AbsFloat(Math3D.AngleFromPosition(coneOrigin, dir, targetPos));	
				float targetDistance2 = vector.DistanceSq(targetPos, Math3D.NearestPoint(rayStart, rayEnd, targetPos));	
				int hitComponent = -1;

				float csSum = float.MAX;
				
				// Find the most suitable component
				ComponentResult result;
				if (FindMostSuitableComponentEx(obj, bResult, settings, csSum, result, blacklistedDamageZones))
				{
					targetPos 		= result.ComponentPos;
					targetAngle 	= result.ComponentAngle;
					targetDistance2 = result.ComponentDistance2;
					hitComponent 	= result.ComponentIdx;
				}

				// ProxyInfo
				if (bResult.proxyInfo)
				{
					foreach (BoxCollidingResult pInfo : bResult.proxyInfo)
					{
						if (FindMostSuitableComponentEx(obj, pInfo, settings, csSum, result, blacklistedDamageZones))
						{
							targetPos 		= result.ComponentPos;							
							targetAngle 	= result.ComponentAngle;	
							targetDistance2 = result.ComponentDistance2;	
							hitComponent 	= result.ComponentIdx;
						}
					}
				}
				
				// No suitable component found
				if (hitComponent == -1 || csSum == float.MAX)
				{
					continue;
				}

				// Check if it is a better fit than what has been found previously
				float sum  = targetDistance2;
				if (sum < retVal)
				{
					ret 	= new MeleeTargetData(obj, targetPos, hitComponent);
					retVal 	= sum;
				}
	
				allTargets.Insert(obj);
			}
		}
		
		return ret;
	}
	
	MeleeTargetData GetMeleeTarget(MeleeTargetSettings settings, out array<Object> allTargets = null)
	{
		return GetMeleeTargetEx(settings, allTargets);
	}
	

	bool FindMostSuitableComponentEx(Object obj, BoxCollidingResult bResult, MeleeTargetSettings settings, out float sum, out ComponentResult result, array<string> blacklistedDamageZones)
	{
		foreach (ComponentInfo cInfo : bResult.componentInfo)
		{
			ComponentResult cResult = new ComponentResult;
			
			if (!EvaluateComponentEx(obj, cInfo, settings, cResult, blacklistedDamageZones))
			{
				continue;
			}
			
			// Smallest number is a winner
			float cSum = cResult.ComponentDistance2;
			if (cSum < sum)
			{
				sum = cSum;				
				result = cResult;				
			}
		}
		
		return result != null;
	}

	bool FindMostSuitableComponent(Object obj, BoxCollidingResult bResult, MeleeTargetSettings settings, out float sum, out ComponentResult result)
	{
		return FindMostSuitableComponentEx(obj, bResult, settings, sum, result, null);
	}
	
	bool EvaluateComponentEx(Object obj, ComponentInfo cInfo, MeleeTargetSettings settings, out ComponentResult result, array<string> blacklistedDamageZones)
	{
		//! check if the component is on blacklist, if so, continue in lookup
		foreach (string zoneName: blacklistedDamageZones)
		{
			if (obj.GetDamageZoneNameByComponentIndex(cInfo.component) == zoneName)
			{
				return false;
			}
		}

		vector componentPos = cInfo.componentCenter;
		
		// Too far away! (fast reject)
		float componentMaxDist2 = Math.SqrFloat(settings.MaxDist + cInfo.componentRadius * obj.GetScale());
		vector nearestPoint = Math3D.NearestPoint(settings.RayStart, settings.RayEnd, componentPos);
		float componentDistance2 = vector.DistanceSq(componentPos, nearestPoint);
		if (componentDistance2 > componentMaxDist2)
		{
			return false;
		}
		
		// Here some more accurate check could be placed that would adjust the componentPos
		
		// Outside of the cone angle!
		float componentAngle = Math.RAD2DEG * Math.AbsFloat(Math3D.AngleFromPosition(settings.ConeOrigin, settings.XZDir, componentPos));	
		if (componentAngle > settings.ConeHalfAngle)
		{
			return false;
		}
			
		// Obstructed!
		if (IsMeleeTargetObstructed(settings.RayStart, componentPos))
		{
			return false;
		}

		// We found something, fill it in!
		result.ComponentPos = componentPos;
		result.ComponentAngle = componentAngle;
		result.ComponentDistance2 = componentDistance2;
		result.ComponentIdx = cInfo.component;
		
		return true;
	}
	
	bool EvaluateComponent(Object obj, ComponentInfo cInfo, MeleeTargetSettings settings, out ComponentResult result)
	{
		return EvaluateComponentEx(obj, cInfo, settings, result, {});
	}
	
	bool IsMeleeTargetObstructed(vector rayStart, vector rayEnd)
	{
		if (rayStart == rayEnd)
		{
			return true; // Not possible to trace when this happens (zero length raycast)
		}
		
		Object hitObject;
		vector hitPos, hitNormal;
		float hitFraction;

		return DayZPhysics.RayCastBullet( rayStart, rayEnd, MELEE_TARGET_OBSTRUCTION_LAYERS, null, hitObject, hitPos, hitNormal, hitFraction);
	}
}