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

using System.Diagnostics;

using DXLib.Log;
using DXLib.Utils;

namespace iStatVball3;

/*
 * Implementation of the Weighted Historic Proximity (WHP) machine learning algorithm. WHP ranks all players currently
 * on the court based on predicted likelihood of being selected for the next volleyball action.
 *
 * WHP produces a score for each zone by utilizing a Weighted Sum Model based on multiple variables including: Rotation,
 * Action, Court Position, and Location. Each new stat is scored against all stats from the current set as well as some
 * number of historic stats from the current season.
 *
 * The resulting score will be combined with a weighted default seed score to help ensure that the total score meets a
 * minimum confidence threshold.
 */
public class WHP
{
	/* Constants */
	public const int ZoneCount = 6;

	// Zone sort order LUTs

	// Normal
	private static readonly int[][] ZoneOrders =
	[
		[ 1, 5, 6, 4, 2, 3 ],
		[ 2, 4, 3, 5, 1, 6 ],
		[ 3, 4, 2, 5, 1, 6 ],

		[ 4, 2, 3, 5, 1, 6 ],
		[ 5, 1, 6, 4, 2, 3 ],
		[ 6, 5, 1, 4, 2, 3 ]
	];

	// Receive
	private static readonly int[][] ZoneOrdersReceive =
	[
		[ 1, 6, 5, 2, 3, 4 ],
		[ 2, 1, 6, 5, 3, 4 ],
		[ 3, 6, 5, 1, 2, 4 ],

		[ 4, 5, 6, 1, 3, 2 ],
		[ 5, 6, 1, 4, 3, 2 ],
		[ 6, 5, 1, 3, 2, 4 ]
	];

	// Block/Overpass
	private static readonly int[] ZoneOrdersBlock = [ 3, 2, 4 ];

	// Maximum number of sets for stat cache
	private const int MaxCachedSets = 4;

	/* Properties */

	// Previous contacting player
	public Player Previous { get; private set; }

	/* Fields */

	// Singleton instance
	private static WHP instance;

	// All stats from season and current set
	private List<Stat> statCache;
	private Set currentSet;

	// Players currently on court
	private List<LineupEntry> courtEntries;
	private List<LineupEntry> frontCourtEntries;

	// Accumulated results
	private List<WHPScore> scores;

	// Profiling
	private int totalRuns;

	private int totalEval;
	private int totalEvalCache;

	private int totalScored;
	private int totalScoredCache;

	private int totalExact;
	private float totalDistance;

	// External ref
	private RallyState state;

	/* Methods */
	private WHP()
	{}

	// Returns singleton instance
	public static WHP GetInstance()
	{
		// Lazily create
		return instance ??= new WHP();
	}

	/* Init */

	// Initializes WHP (MUST be called at start of each set)
	public void Init( RallyState sm, Set set )
	{
		state = sm;
		currentSet = set;

		// Prep for profiling
		totalRuns = 0;
		totalExact = 0;
		totalDistance = 0;

		// Cache historic stats
		CacheStats();
	}

	// Cache historic stats to be used for WHP
	private async void CacheStats()
	{
		// Allocate container
		statCache = [];

		Season season = currentSet.Match.Season;
		Team team = season.Team;

		int setsCached = 0;

		Lineup lineup = currentSet.Lineup1;

		// Caching currently requires pre-created lineup
		if ( lineup != null )
		{
			// Check backwards through all matches in season
			for ( int m = (season.Matches.Count - 1); m >= 0; m-- )
			{
				Match match = season.Matches[m];

				// Check backwards through each set of match
				for ( int s = (match.Sets.Count- 1); s >= 0; s-- )
				{
					Set set = match.Sets[s];

					// Ignore current and Legacy sets
					if ( !set.Equals( currentSet ) && !set.Legacy )
					{
						// Lineups must equal
						if ( lineup.Equals( set.Lineup1 ) )
						{
							// Read stats from cache/cloud
							await set.ReadCache();

							int added = 0;

							if ( set.StatCache != null )
							{
								// Add stats from set
								foreach ( Stat stat in set.StatCache )
								{
									string teamId = stat.TeamId;

									// Ignore opposing team stats
									if ( (teamId != null) && teamId.Equals( team.UniqueId ) )
									{
										// Ignore irrelevant actions
										if ( WHPScore.IsRelevant( stat.Action ) )
										{
											statCache.Add( stat );

											added++;
										}
									}
								}

								setsCached++;

								#if DEBUG
									DXLog.Trace( "WHP count:{0} added:{1}", set.StatCache.Count, added );
								#endif
							}

							// Limit cache size
							if ( setsCached == MaxCachedSets )
							{
								return;
							}
						}
					}
				}
			}
		}
	}

	// Initializes WHP for next rally (MUST be called at start of each rally)
	public void InitRally( List<LineupEntry> entries )
	{
		courtEntries = entries;

		// Save frontrow (zones 2-4) for block/over
		frontCourtEntries =
		[
			entries[1],
			entries[2],
			entries[3]
		];

		// New score list each rally
		scores = new List<WHPScore>( Lineup.BaseEntries );

		// Create each zone score list
		for ( int zone = 0; zone < ZoneCount; zone++ )
		{
			scores.Add( new WHPScore
			{
				Entry = entries[ zone ]
			});
		}
	}

	/* Algorithm */

	// Scores a new stat event using the WHP algorithm. A list of players will be returned arranged in court zones based
	// on prediction confidence. This method must only be called for valid WHP actions.
	//
	public List<LineupEntry> Score( WHPConfig config )
	{
		// Reset profiling
		totalRuns++;

		totalEvalCache = 0;
		totalEval = 0;

		totalScoredCache = 0;
		totalScored = 0;

		DXProfiler.Start( true );

		// Must remember previous contacting player
		SavePrevious( config );

		// Clear scores
		Clear();

		// Score against both historic stats and current set
		ScoreStats( config, statCache, true );
		ScoreStats( config, currentSet.StatCache, false );

		// Calculate all totals
		foreach ( WHPScore score in scores )
		{
			score.CalculateTotal( config );
		}

		// Arrange players in court zones based on prediction confidence
		List<LineupEntry> zones = config.Action switch
		{
			// Frontrow zones only
			Stats.BlockKey or 
			Stats.OverpassKey => PopulateBlockZones( config ),
			
			_ => PopulateZones( config )
		};

		// Profiling
		DebugGeneral( config, DXProfiler.Elapsed );
		DebugScores( scores );

		DXProfiler.Stop();

		return zones;
	}

	// Populates court zones (1-6) based on prediction confidence
	private List<LineupEntry> PopulateZones( WHPConfig config )
	{
		LineupEntry[] zones = new LineupEntry[ ZoneCount ];

		// User tapped this zone
		int index = (config.Zone - 1);

		// Population order depends on zone (and action)
		int[] zoneOrder = (config.Action == Stats.ReceiveKey) ? ZoneOrdersReceive[ index ] : ZoneOrders[ index ];

		// Fill results in specified order, always start with default zone
		foreach ( int zone in zoneOrder )
		{
			zones[ zone - 1 ] = GetZoneEntry( zones, zone, null );
		}

		// Highlight default in UI
		zones[ index ].IsDefault = true;

		return zones.ToList();
	}

	// Populates frontrow zones (2-4) based on prediction confidence
	private List<LineupEntry> PopulateBlockZones( WHPConfig config )
	{
		// Only 3 frontrow zones used
		LineupEntry[] zones = new LineupEntry[ ZoneCount ];

		// User tapped this zone
		int index = (config.Zone - 1);

		// Population order always 3,2,4
		int[] zoneOrder = ZoneOrdersBlock;

		// Scope limited to frontrow only
		LineupEntry[] scope = frontCourtEntries.ToArray();

		// Fill results in specified order, always start with default zone
		foreach ( int zone in zoneOrder )
		{
			zones[ zone - 1 ] = GetZoneEntry( zones, zone, scope );
		}

		// Highlight default in UI
		zones[ index ].IsDefault = true;

		return zones.ToList();
	}

	// Returns highest scored player for specified zone
	private LineupEntry GetZoneEntry( LineupEntry[] zones, int zone, LineupEntry[] scope )
	{
		// Sort scores for zone
		List<WHPScore> sorted = scores.OrderByDescending( s => s.TotalScores[ zone - 1 ] ).ToList();

		// Return highest result not already in list
		foreach ( WHPScore score in sorted )
		{
			LineupEntry entry = score.Entry;

			if ( !zones.Contains( entry ) )
			{
				// Optionally limit to players in specified list
				if ( (scope == null) || scope.Contains( entry ) )
				{
					return entry;
				}
			}
		}

		// Should never happen
		return null;
	}

	// Clears all scores for start of new algorithm pass
	private void Clear()
	{
		foreach ( WHPScore score in scores )
		{
			score.Clear();
		}
	}

	// Saves reference to previous contacting player
	private void SavePrevious( WHPConfig config )
	{
		Previous = null;

		Stat stat = config.Previous;

		// May not be a previous player
		if ( stat != null )
		{
			string teamId = config.TeamId;

			// Must be from same team
			if ( (teamId != null) && teamId.Equals( stat.TeamId ) )
			{
				string action = stat.Action;

				// May not be applicable action (serve/third/block/over)
				if ( WHPScore.IsRelevant( action ) && (action != Stats.ThirdKey) )
				{
					Previous = stat.Player;
				}
			}
		}
	}

	/* WHP Scoring */

	// Scores current stat against specified list of historic stats
	private void ScoreStats( WHPConfig config, IList<Stat> stats, bool cached )
	{
		// Score against all stats in list
		foreach ( Stat stat in stats )
		{
			if ( cached )
			{
				totalEvalCache++;
			}
			else
			{
				totalEval++;
			}

			// Only score if stat eligible for WHP
			if ( IsEligible( config, stat, cached ) )
			{
				WHPScore score = WHPScore.FindScore( scores, stat.Player );

				// Accumulate score for corresponding player
				score.Score( config, stat );

				if ( cached )
				{
					totalScoredCache++;
				}
				else
				{
					totalScored++;
				}
			}
		}
	}

	// Determines if target stat eligible for WHP scoring against current stat
	private bool IsEligible( WHPConfig config, Stat stat, bool cached )
	{
		// Ignore irrelevant actions (cached stats already validated)
		if ( cached || WHPScore.IsRelevant( stat.Action ) )
		{
			string teamId = config.TeamId;

			// Must be from same team (may already be validated)
			if ( cached || ((teamId != null) && teamId.Equals( stat.TeamId )) )
			{
				// Rotation must match
				if ( config.Rotation == stat.Rotation )
				{
					// Action must match
					if ( config.EqualsAction( stat.Action ) )
					{
						Player player = stat.Player;

						// Player must be on court
						if ( LineupEntry.Contains( courtEntries, player ) )
						{
							// Ignore previous contacting player
							if ( !player.Equals( Previous ) )
							{
								// Eligible
								return true;
							}
						}
					}
				}
			}					
		}

		// NOT eligible
		return false;
	}

	/* Accuracy */

	// Updates profiling accuracy metrics
	public float UpdateAccuracy( string action, int predicted, int selected )
	{
		float distance = -1;

		if ( state.IsSmartOrder )
		{
			// Must be relevant stat
			if ( WHPScore.IsRelevant( action ) )
			{
				Point predictedPt = WHPScore.ZoneAnchors[ predicted - 1 ];
				Point selectedPt = WHPScore.ZoneAnchors[ selected - 1 ];

				// Calculate distance between prediction and actual selection
				distance = DXGraphics.Distance( predictedPt, selectedPt );

				// Track exact hits and miss distance
				if ( predicted == selected )
				{
					totalExact++;
				}
				else
				{
					totalDistance += distance;
				}

				// Profiling
				DebugAccuracy( predicted, selected, distance );
			}
		}

		return distance;
	}

	// Updates accuracy metrics following an undo
	[Conditional( "DEBUG" )]
	public void UndoAccuracy( float? distance )
	{
		// Ignore opponent and irrelevant stats
		if ( distance is >= 0 )
		{
			totalRuns--;

			// Subtract last value
			if ( distance == 0 )
			{
				totalExact--;
			}
			else
			{
				totalDistance -= (float)distance;
			}

			DebugAccuracy( -1, -1, -1 );
		}
	}

	/* Debug */

	// Outputs general algorithm and performance profiling data
	[Conditional( "DEBUG" )]
	private void DebugGeneral( WHPConfig config, long elapsed )
	{
		DXLog.Trace( "---------------------------------------------------------------" );

		// General info
		DXLog.Trace( "Evaluated: {0} cached: {1}", totalEval, totalEvalCache );
		DXLog.Trace( "Scored: {0} cached: {1}", totalScored, totalScoredCache );
		DXLog.Trace( "Elapsed: {0}ms", elapsed );
		DXLog.Trace();

		DXLog.Trace( "Zone: {0}", config.Zone );
		DXLog.Trace( "Rotation: {0}", config.Rotation );
		DXLog.Trace( "Action: {0}", config.Action.ToUpper() );
		DXLog.Trace();
	}

	// Outputs scoring results for last algorithm run
	[Conditional( "DEBUG" )]
	private void DebugScores( List<WHPScore> list )
	{
		DXLog.Trace( "Scores:" );

		for ( int i = 0; i < list.Count; i++ )
		{
			WHPScore score = list[i];

			DXLog.Trace( "-------------------------------------" );
			DXLog.Trace( "{0} p:{1} pos:{2} scored:{3}", (i + 1), score.Entry.Number, score.Entry.Position?.ToUpper(), score.TotalScored );
			DXLog.Trace( "-------------------------------------" );

			DXLog.Trace( "Zones:" );

			for ( int zone = 0; zone < ZoneCount; zone++ )
			{
				DXLog.Trace( "{0}: count:{1,2} score:{2:N2} zone:{3:N2} rot:{4:N2} seed:{5:N2} total:{6:N2}",
							 (zone + 1), score.ZoneCounts[ zone ], score.ZoneScores[ zone ], score.SeedScores[ zone ],
							 score.RotSeedScores[ zone ], score.TotalSeedScores[ zone ], score.TotalScores[ zone ] );
			}
		}
	}

	// Outputs algorithm accuracy data
	[Conditional( "DEBUG" )]
	private void DebugAccuracy( int predicted, int selected, float distance )
	{
		DXLog.Trace( "---------------------------------------------------------------" );

		// -1 indicates undo
		if ( predicted >= 0 )
		{
			DXLog.Trace( "Predicted: {0}", predicted );
			DXLog.Trace( "Selected: {0}", selected );
			DXLog.Trace( "Distance: {0:F3}", distance );
			DXLog.Trace();
		}

		float exactPct = float.NaN;
		float avgDistance = float.NaN;

		if ( totalRuns > 0 )
		{
			// Percentage of exact prediction hits
			exactPct = ((float)totalExact / totalRuns) * 100.0f;

			// Average distance between prediction and selected
			avgDistance = (totalDistance / totalRuns);
		}

		DXLog.Trace( "Runs: {0}", totalRuns );
		DXLog.Trace( "Exact: {0} {1:F1}%", totalExact, exactPct );
		DXLog.Trace( "Avg Dist: {0:F3}", avgDistance );

		DXLog.Trace( "---------------------------------------------------------------" );
	}
}

//
