/* 
 * Copyright (c)2009-2025 DemiVision, LLC. All Rights Reserved. The information 
 * herein is the CONFIDENTIAL and PROPRIETARY information of DemiVision, LLC. 
 */

namespace iStatVball3;

/*
 * Provides methods for calculating all metrics necessary to generate a heat map for a specific analysis report. Multiple
 * pre-defined heat map types are supported including efficiency, ratings, and count per set.
 */
public class HeatCalculator
{
	/* Properties */
	public bool IsTeam1 { get; set; }

	// Based on team and start/end direction
	public RecordCourt.Side Side { get; private set; }

	// Passing stats?
	public bool IsPassing { get; set; }

	/* Fields */
	private JsonHeat jsonHeat;
	private DataStats dataStats;

	// Map size
	private int areasX;
	private int areasY;
	private int areaCount;

	// Result for each area
	private float[] results;

	/* Methods */

	// Post-construction initialization
	public void Init( JsonHeat json )
	{
		jsonHeat = json;
	}

	// Dynamically updates XxY map size
	public void SetAreas( int x, int y )
	{
		areasX = x;
		areasY = y;

		areaCount = (x * y);

		// Allocate results container
		results = new float[ areaCount ];
	}

	/* Calculate */

	// Calculates metrics needed to generate heat map for current report
	public float[] Calculate( DataStats stats )
	{
		dataStats = stats;

		// Pre-defined heat map types
		return jsonHeat.Type switch
		{
			HeatMap.EfficiencyType => CalculateEfficiency(),
			HeatMap.RatingType => CalculateRating(),
			HeatMap.CountType => CalculateCount(),

			_ => null,
		};
	}

	// Calculates efficiency result for each map area as:
	//
	// (EarnedPoints - Errors) / Attempts
	//
	private float[] CalculateEfficiency()
	{
		int[] zeros = new int[ areaCount ];
		int[] points = new int[ areaCount ];
		int[] errors = new int[ areaCount ];

		int[] attempts = new int[ areaCount ];

		// Primary/opponent teams always on opposite sides
		Side = GetSide();

		// Accumulate each pre-filtered stat
		foreach ( Stat stat in dataStats )
		{
			// Convert to map area
			int area = GetArea( stat );

			// Accumulate result
			switch ( stat.Result )
			{
				// Attempts/errors handled same for all categories
				case Stats.AttemptKey: zeros[ area ]++; break;
				case Stats.ErrorKey: errors[ area ]++; break;
				default:
				{
					// Earned point stat differs by category
					switch ( jsonHeat.Category )
					{
						case "serving": if ( stat.Result == Stats.AceKey ) points[ area ]++; break;
						case "setting": if ( stat.Result == Stats.AssistKey ) points[ area ]++; break;
						case "hitting": if ( stat.Result == Stats.KillKey ) points[ area ]++; break;
					}

					break;
				}
			}

			attempts[ area ]++;
		}

		// Calculate result for each area
		for ( int area = 0; area < areaCount; area++ )
		{
			int att = attempts[ area ];

			results[ area ] = (att == 0) ? DataMetrics.FloatNA : (points[ area ] - errors[ area ]) / (float)att;
		}

		return results;
	}

	// Calculates average rating (0-3 or 0-4) for each map area
	private float[] CalculateRating()
	{
		int[] ratings = new int[ areaCount ];
		int[] attempts = new int[ areaCount ];

		// Primary/opponent teams always on opposite sides
		Side = GetSide();

		// Stats must be pre-filtered for category
		foreach ( Stat stat in dataStats )
		{
			// Convert to map area
			int area = GetArea( stat );

			// Accumulate totals
			ratings[ area ] += Stat.MapRating( stat.Rating, IsPassing );
			attempts[ area ]++;
		}

		// Calculate average rating for each area
		for ( int area = 0; area < areaCount; area++ )
		{
			int att = attempts[ area ];

			results[ area ] = (att == 0) ? DataMetrics.FloatNA : (ratings[ area ] / (float)att);
		}

		return results;
	}

	// Calculates avg count of earned point results per set for each map area
	private float[] CalculateCount()
	{
		float[] points = new float[ areaCount ];

		// Primary/opponent teams always on opposite sides
		Side = GetSide();

		// Stats must be pre-filtered for category
		foreach ( Stat stat in dataStats )
		{
			// Convert to map area
			int area = GetArea( stat );

			// Accumulate earned point count
			switch ( jsonHeat.Category )
			{
				case "serving":
				{
					if ( stat.Result == Stats.AceKey )
					{
						points[ area ] += 1.0f;
					} 
					
					break;
				}
				case "setting":
				{
					if ( stat.Result == Stats.AssistKey )
					{
						points[ area ] += 1.0f;
					}

					break;
				}
				case "hitting":
				{
					if ( stat.Result == Stats.KillKey )
					{
						points[ area ] += 1.0f;
					}
					
					break;
				}
				case "blocking":
				{
					switch ( stat.Result )
					{
						case Stats.BlockKey:
						{
							points[ area ] += 1.0f;
							break;
						}
						case Stats.BlockAssistsKey:
						{
							points[ area ] += 0.5f;
							break;
						}
					}

					break;
				}
				case "defense":
				{
					if ( stat.Result == Stats.DigKey ) points[ area ] += 1.0f; 
					break;
				}
			}
		}

		int setCount = dataStats.SetCount;

		// Calculate average count for each area
		for ( int area = 0; area < areaCount; area++ )
		{
			float pts = points[ area ];

			results[ area ] = ((pts < 1.0) || (setCount == 0)) ? DataMetrics.FloatNA : (pts / (float)setCount);
		}

		return results;
	}

	// Determines side of court map will be displayed on (from JSON settings)
	private RecordCourt.Side GetSide()
	{
		bool start = jsonHeat.Start;

		// Assume start/end same side of court
		if ( jsonHeat.SameSide )
		{
			return IsTeam1 ? RecordCourt.Side.SideA : RecordCourt.Side.SideB;
		}

		// Primary team flowing left-to-right (opponent opposite)
		return IsTeam1 ? (start ? RecordCourt.Side.SideA : RecordCourt.Side.SideB) : (start ? RecordCourt.Side.SideB : RecordCourt.Side.SideA);
	}

	// Returns heat map area matching specified normalized location
	private int GetArea( Stat stat )
	{
		bool start = jsonHeat.Start;

		// Using serve start or end location?
		float x = start ? stat.StartX : stat.EndX;
		float y = start ? stat.StartY : stat.EndY;

		// Must normalize to map side of court
		Point point = RecordCourt.NormalizeSide( x, y, Side );

		float normX = (float)point.X;
		float normY = (float)point.Y;

		int resultX = 0;
		int resultY = 0;

		// Cap left/right edges of court
		if ( normX <= 0.0 )
		{
			resultX = 0;
		}
		else if ( normX >= 1.0 )
		{
			resultX = (areasX - 1);
		}
		// Otherwise find matching area along length of court
		else
		{
			bool sideA = (Side == RecordCourt.Side.SideA);

			// Court length 0.0-0.5 per side
			float rangeX = (0.5f / areasX);
			float startX = sideA ? 0.0f : 0.5f;

			// Scan areas
			for ( int areaX = 0; areaX < areasX; areaX++ )
			{
				float x1 = startX + (areaX * rangeX);
				float x2 = (x1 + rangeX);

				// Match found
				if ( (normX >= x1) && (normX <= x2) )
				{
					resultX = areaX;
					break;
				}
			}
		}

		// Cap top/bottom edges of court
		if ( normY <= 0.0 )
		{
			resultY = 0;
		}
		else if ( normY >= 1.0 )
		{
			resultY = (areasY - 1);
		}
		// Otherwise find matching area along height of court
		else
		{
			// Court width always 0.0-1.0
			float rangeY = (1.0f / areasY);

			// Scan areas
			for ( int areaY = 0; areaY < areasY; areaY++ )
			{
				float y1 = (areaY * rangeY);
				float y2 = (y1 + rangeY);

				// Match found
				if ( (normY >= y1) && (normY <= y2) )
				{
					resultY = areaY;
					break;
				}
			}
		}

		// Convert to map index
		return (resultY * areasX) + resultX;
	}
}

//
