/*
 * 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 Plugin.Firebase.Firestore;

using DXLib.UI.Alert;
using DXLib.UI.Control;

using DXLib.Log;
using DXLib.Data;
using DXLib.Utils;

using Side = iStatVball3.RecordCourt.Side;

namespace iStatVball3;

/*
 * Provides all data persistence functionality for the state machine including saving stats and undo/restore. 
 */ 
public class RecordDb : RecordHandler
{
	/* Constants */

	// Max points in undo stack 
	private const int UndoDepth = 2;
	
	/* Properties */

	// Reference to stat cache
	public List<Stat> StatCache { get; set; }

	// Current number of stats in cache
	public int StatCount => StatCache?.Count ?? -1;
	
	/* Fields */

	// External refs
	private readonly RecordState sm;
	private readonly Set set;

	// Monotonically increasing stat count
	private short counter;

	// Only persist if admin or not sample
	private readonly bool canRecord;

    /* Methods */
    public RecordDb( RecordState sm )
	{
		this.sm = sm;

		set = sm.Set;
		StatCache = set.StatCache;

		// Setup permissions
		canRecord = (!set.IsSample || Shell.CurrentUser.IsAdmin);
    }

	/* Counter */

	// Returns current counter and advances value
	private short GetCounter()
	{
		short value = counter;

		// Post-increment
		counter++;

		return value;
	}

	// Manually sets counter to specified value, used for restore
	public void SetCounter( short value )
	{
		counter = value;
	}

	// Resets counter to zero
	public void ResetCounter()
	{
		SetCounter( 0 );
	}

	/* Sync */

	// Returns wall clock time to be used for stat timestamp
	private static DateTimeOffset GetTimestamp( RecordData data )
	{
		return data.Timestamp ?? DXUtils.Now();
	}
	
	// Returns offset (ms) from first serve of set
	private int GetOffset( DateTimeOffset timestamp, bool serve = false )
	{
		Stat firstServe = FirstStat( Stats.ServeKey );
	
		// Calculate offset
		if ( firstServe != null )
		{
			DateTimeOffset start = firstServe.Timestamp;
	
			return (int) timestamp.Subtract( start ).TotalMilliseconds;
		}
	
		// -1 for lineup/update before first serve
		return serve ? 0 : -1;
	}

	/* Get */

	// Returns stat at specified index in stack (Handler)
	public Stat GetStat( int index )
	{
		return ((index >= 0) && (index < StatCount)) ? StatCache[ index ] : null;
	}

	// Returns elapsed time from previous to current stat
	private static int GetTempo( Stat prev )
	{
		return (prev == null) ? -1 : DXUtils.ElapsedTime( prev.Timestamp );
	}

	/* Previous */

	// Returns first stat in stack matching specified action
	public Stat FirstStat( string action )
	{
		return StatCache.FirstOrDefault( stat => stat.Action == action );
	}

	// Returns stat N number of places back in stack
	public Stat PreviousStat( int back = 1 )
	{
		return PreviousStat( back, false );
	}

	// Returns stat N number of places back in stack
	public Stat PreviousStat( int back, bool actionOnly )
	{
		int prevCount = 0;

		// Search backwards through stack
		for ( int i = (StatCount - 1); i >= 0; i-- )
		{
			Stat stat = StatCache[i];

			// Optionally skip non-action events
			if ( !actionOnly || stat.IsAction )
			{
				prevCount++;

				// Correct number back
				if ( prevCount == back )
				{
					return stat;
				}
			}
		}

		return null;
	}

	// Returns last stat matching specified action
	public Stat PreviousStat( string action )
	{
		return PreviousStat( action, null );
	}

	// Returns last stat matching specified action and player (Handler)
	public Stat PreviousStat( string action, Player player )
	{
		// Search backwards through stack
		for ( int i = (StatCount - 1); i >= 0; i-- )
		{
			Stat stat = StatCache[i];

			// Found match
			if ( stat.Action == action )
			{
				// Optionally also match player
				if ( (player == null) || player.Equals( stat.Player ) )
				{
					return stat;
				}
			}
		}

		// No match
		return null;
	}

	// Returns last stat matching specified team, action, and rotation (Handler)
	public Stat PreviousStat( string teamId, string action, int rotation )
	{
		// Search backwards through stack
		for ( int i = (StatCount - 1); i >= 0; i-- )
		{
			Stat stat = StatCache[i];

			// Team, action, rotation must all match
			if ( (stat.TeamId == teamId) && (stat.Action == action) && (stat.Rotation == rotation) )
			{
				return stat;
			}
		}

		// No match
		return null;
	}

	// Returns list of all stats since last serve
	public List<Stat> PreviousRally()
	{
        // Most recent stat should be point
        List<Stat> stats = [ PreviousStat() ];

        int count = StatCount;

		// Include all stats back to previous point
		if ( count > 1 )
		{
			for ( int i = (StatCount - 2); i >= 0; i-- )
			{
				Stat stat = StatCache[i];

				if ( stat.IsPoint )
				{
					break;
				}

				stats.Add( stat );
			}
		}

		return stats;
	}

	/* Persist */

	// Persists all stats for one point (rally), along with score data, to the cloud. Write is batched. NO Firestore
	// activity takes place during rally. Undo is NOT permitted after point has been persisted.
	//
	private async Task PersistPoint()
	{
		try
		{
			if ( canRecord )
			{
				// Looking for point immediately before UndoDepth
				int pointIndex = GetPointIndex();

				if ( pointIndex >= 0 )
				{
					int count = StatCount;

					IWriteBatch batch = DXData.StartBatch();

					// Persist each stat until next point
					for ( int index = pointIndex; index < count; index++ )
					{
						Stat stat = StatCache[ index ];

						// Persist
						stat.Persisted = true;
						stat.Create( batch );

						// Found next point
						if ( stat.IsPoint )
						{
							break;
						}
					}

					// Persist starting lineup (first point only)
					set.UpdateStarters( batch, sm.Lineup1 );

					// Persist current match/set score
					set.UpdateScore( batch );

					// WRITE TO CLOUD
					await batch.CommitAsync();
				}
			}
		}
		catch ( Exception ex )
		{
			DXLog.Exception( "record.persist", ex );
			DXAlert.ShowErrorCode( "net.err", "net.err.write", 0 );
		}
	}

	// Returns stat cache index for first stat eligible to be persisted
	private int GetPointIndex()
	{
		int start = (StatCount - 1);
		int pointCount = 0;

		// Search backwards for eligible point
		for ( int index = start; index >= 0; index-- )
		{
			Stat stat = StatCache[ index ];

			// User undid to limit
			if ( stat.Persisted )
			{
				break;
			}
			
			bool setStart = (index == 0);
			
			// Found point
			if ( stat.IsPoint || setStart )
			{
				pointCount++;

				// Far enough back, return eligible index
				if ( pointCount > (UndoDepth + 1) )
				{
					return setStart ? 0 : (index + 1);
				}
			}
		}

		// Not enough points yet
		return -1;
	}
	
	/* Record */

	// Records complete stat event (either RallyFlow or Legacy)
	public async Task RecordStat( RecordData data, bool point = false )
	{
		try
		{
			// Engine specific recording
			Stat stat = data.Legacy ? RecordLegacyStat( data, point ) : await RecordRallyStat( data, point );
			
			// Populate FKs
			stat.Populate();

			// Record additional point-specific fields
			if ( point )
			{
				RecordPoint( stat, data );

				// Score
				set.SetScore( stat.Point.Score1, stat.Point.Score2 );

				// Credit point to server
				if ( !stat.Point.Sideout )
				{
					Stat lastServe = PreviousStat( Stats.ServeKey );

					if ( lastServe != null )
					{
						lastServe.Earned = 1;
					}
				}
			}

			// Record state (deleted later)
			RecordState( stat );

			// Cache
			StatCache.Add( stat );
			
			// End of rally only
			if ( point )
			{
				await PersistPoint();
			}

			// Update undo history, etc
			sm.UpdateUI();

			// DEBUG
			DebugStack();
		}
		catch ( Exception ex )
		{
			DXLog.Exception( "record.stat", ex );
            DXAlert.ShowErrorCode( "net.err", "net.err.write", 100 );
        }
    }

	// Records RallyFlow stat event
	private async Task<Stat> RecordRallyStat( RecordData data, bool point )
	{
		Stat prev = PreviousStat( 1, true );

		double x = data.X;
		double y = data.Y;

		float normX = 0;
		float normY = 0;

		int zone = 0;

		// Convert to device/orientation independent x,y
		Point normalized = sm.Normalize( x, y );

		if ( !normalized.IsEmpty )
		{
			// Faults use previous location
			normX = (float)((x <= Stats.FaultX) ? prev.StartX : normalized.X);
			normY = (float)((y <= Stats.FaultY) ? prev.StartY : normalized.Y);

			// Calc zone (1-6)
			zone = RecordCourt.GetZone( normX, normY );
		}

		// Backpatch (except start of rally)
		if ( !data.IsServe )
		{
			// Update previous stat
			if ( prev != null )
			{
				// Ending location
				prev.EndX = normX;
				prev.EndY = normY;
				prev.EndZone = zone;

				// Rating
				prev.Rating = data.Rating;

				// Result
				prev.Result = data.Result;
				prev.Error = data.Error;
				prev.Fault = data.Fault;

				// Removing player attribution after TRE
				if ( data.IsPrevTeamError )
				{
					prev.Player = null;
					prev.PlayerId = null;

					prev.Number = null;
					prev.Position = null;
				}
			}

			// Some actions update stat before last
			if ( data.PrevResult != null )
			{
				Stat prev2 = PreviousStat( 2, true );

				if ( prev2 != null )
				{
					prev2.Result = data.PrevResult;
					prev2.Error = data.PrevError;
					prev2.Rating = data.PrevRating;
				}
			}

			// Some actions update even further back
			if ( data.Prev2Result != null )
			{
				Stat prev3 = PreviousStat( 3, true );

				if ( prev3 != null )
				{
					prev3.Result = data.Prev2Result;
					prev3.Error = data.Prev2Error;
				}
			}

			// And some go even further
			if ( data.Prev3Result != null )
			{
				Stat prev4 = PreviousStat( 4, true );

				if ( prev4 != null )
				{
					prev4.Result = data.Prev3Result;
				}
			}

			// 2nd ball pass-to-kill conversion
			if ( data.Prev2Value != null )
			{
                Stat prev3 = PreviousStat( 3, true );

                if ( prev3 != null )
                {
	                prev3.Value = data.Prev2Value;
                }
			}

            // 3rd ball pass-to-kill conversion
            if ( data.Prev3Value != null )
			{
                Stat prev4 = PreviousStat( 4, true );

                if ( prev4 != null )
                {
	                prev4.Value = data.Prev3Value;
                }
			}
        }

		// Timestamp as close as possible to initial stat entry tap
		DateTimeOffset timestamp = GetTimestamp( data );
		bool serve = (data.Action == Stats.ServeKey);

		// Create new entity
		return new Stat
		{
			Counter = GetCounter(),
			Timestamp = timestamp,

			Offset = GetOffset( timestamp, serve ),
			VideoOffset = await sm.GetVideoOffset(),

			Number = point ? prev?.Number : data.Number,
			Number2 = data.Number2,
			Number3 = data.Number3,

			Position = point ? prev?.Position : data.Position,
			Position2 = data.Position2,
			Position3 = data.Position3,

			Rotation = sm.BallRotation(),

			Action = data.Action,
			Selector = data.Selector,
			Modifier = data.Modifier,

			// Rating (set later)

			CourtPosition = data.CourtPosition,
			CourtRow = data.CourtRow,

			PrevX = prev?.StartX,
			PrevY = prev?.StartY,

			StartX = normX,
			StartY = normY,
			StartZone = zone,

			// EndX, EndY, EndZone (set later)

			// Result, Error, Fault (set later)

			Tempo = GetTempo( prev ),
			WHP = data.WHP,

			// Context specific
			Value = data.Value,

			// References
			TeamId = data.TeamId,

			PlayerId = point ? prev?.PlayerId : data.PlayerId,
			Player2Id = data.Player2Id,
			Player3Id = data.Player3Id,

			// Set parent
			SetId = set.UniqueId,
			Set = set
		};
	}

	// Records Legacy stat event
	private Stat RecordLegacyStat( RecordData data, bool point )
	{
		Stat prev = PreviousStat( 1, true );

		// Backpatch AutoSet
		if ( (data.PrevResult != null) && (prev != null) )
		{
			switch ( prev.Action )
			{
				case Stats.ReceiveKey:
				case Stats.DefenseKey:
				case Stats.FirstKey:
				case Stats.SecondKey:
				{
					prev.Result = data.PrevResult;
					prev.Auto = true;
					break;
				}
			}
		}

        // 2nd ball pass-to-kill conversion
        if ( (data.PrevValue != null) && (prev != null) )
        {
	        prev.Value = data.PrevValue;
        }

        // 3rd ball pass-to-kill conversion
        if ( data.Prev2Value != null )
        {
            Stat prev2 = PreviousStat( 2, true );

            if ( prev2 != null )
            {
	            prev2.Value = data.Prev2Value;
            }
        }

        DateTimeOffset timestamp = GetTimestamp( data );
		bool serve = (data.Action == Stats.ServeKey);

		// Create new entity
		return new Stat
		{
			Legacy = true,

			Counter = GetCounter(),
			Timestamp = timestamp,

			Offset = GetOffset( timestamp, serve ),
			// Legacy does NOT use VideoOffset

			Number = point ? prev?.Number : data.Number,
			Position = point ? prev?.Position : data.Position,

			Rotation = sm.BallRotation(),

			Action = data.Action,
			Selector = data.Selector,

			CourtPosition = data.CourtPosition,
			CourtRow = data.CourtRow,

			// Saved now (unlike RallyFlow)
			Rating = data.Rating,
			Result = data.Result,
			Error = data.Error,

			// Context specific
			Value = data.Value,

			// Auto Set generated?
			Auto = data.Auto,

			// References
			TeamId = data.TeamId,
			PlayerId = point ? prev?.PlayerId : data.PlayerId,

			// Set parent
			SetId = set.UniqueId,
			Set = set
		};
	}

	// Records additional fields specific to point stats
	private void RecordPoint( Stat stat, RecordData data )
	{
		int duration = DXUtils.ElapsedTime( sm.PointTime );

		// Save in object attached to parent stat
		StatPoint point = new()
		{
			Sideout = data.Sideout,
			Earned = data.Earned,

			ServeCount = sm.ServeCount,
			ServeSide = RecordCourt.KeyFromSide( sm.ServeSide ),

			Score1 = sm.ScoreForTeam( 1 ),
			Score2 = sm.ScoreForTeam( 2 ),

			Rotation = data.Rotation,
			RecvRotation = data.RecvRotation,

			Touches = sm.Touches,
			Transitions = sm.Transitions,

			Duration = duration,

			// FKs
			TeamAId = sm.IdForSide( Side.SideA ),
			TeamBId = sm.IdForSide( Side.SideB ),

			ReceiveId = data.ReceiveId,			// Receiving team
			CausedId = data.CausedId,			// Team causing point
			TeamId = data.TeamId				// Team AWARDED point
		};

		// Remember players on court
		point.SaveLineup( sm.Lineup1 );

		// Watch mode requires error type
		stat.Error = data.Error;

		// Save in parent
		stat.Point = point;
	}

    // Records temporary state object for new stat
    private void RecordState( Stat stat )
    {
        // Save snapshot of entire state
        StatState state = new()
        {
            State = sm.State,
            StateSelector = sm.StateSelector,

            BallSide = (int) sm.BallSide,
            ServeSide = (int) sm.ServeSide,

            ScoreA = sm.ScoreA,
            ScoreB = sm.ScoreB,

            RotationA = sm.RotationA,
            RotationB = sm.RotationB,

            TimeoutsA = sm.TimeoutsA,
            TimeoutsB = sm.TimeoutsB,

            SubsA = sm.SubsA,
            SubsB = sm.SubsB,

            LiberoA = sm.LiberoA,
            LiberoB = sm.LiberoB,

            Touches = sm.Touches,
            Transitions = sm.Transitions,
            ServeCount = sm.ServeCount,

            FaultKey = sm.FaultKey,

            // References
            TeamAId = sm.IdForSide( Side.SideA ),
            TeamBId = sm.IdForSide( Side.SideB )
        };

        stat.State = state;
    }

    // Persists non-action update stat (timeout, side switch, etc)
    public async Task RecordUpdate( RecordData data )
	{
		try
		{
			DateTimeOffset timestamp = GetTimestamp( data );

			// Create new document
			Stat stat = new()
			{
				Counter = GetCounter(),
				Timestamp = timestamp,

				Offset = GetOffset( timestamp ),
				VideoOffset = await sm.GetVideoOffset(),

				Rotation = sm.BallRotation(),

				Action = Stats.UpdateKey,
				Selector = data.Selector,

				// References
				TeamId = data.TeamId,

				// Field hijacked for some updates
				Modifier = data.Modifier,

				// Set parent
				SetId = set.UniqueId,
				Set = set
			};

			// Save state (deleted later)
			RecordState( stat );

			// Cache
			StatCache.Add( stat );

			// Update undo stack
			sm.UpdateUI();

			// DEBUG
			DebugStack();
        }
        catch ( Exception ex )
        {
            DXLog.Exception( "record.update", ex );
            DXAlert.ShowErrorCode( "net.err", "net.err.write", 200 );
        }
    }

    // Persists lineup related state (sub, swap, replace)
    public async Task RecordLineup( RecordData data )
	{
		try
		{
			DateTimeOffset timestamp = GetTimestamp( data );

			// Create new document
			Stat stat = new()
			{
				Counter = GetCounter(),
				Timestamp = timestamp,

				Offset = GetOffset( timestamp ),
				VideoOffset = await sm.GetVideoOffset(),

				Action = Stats.LineupKey,
				Selector = data.Selector,

				Number = data.Number,
				Number2 = data.Number2,

				Position = data.Position,
				Position2 = data.Position2,

				// Used to indicate auto swap-out
				Value = data.Value,

				// References 
				TeamId = data.TeamId,

				PlayerId = data.PlayerId,
				Player2Id = data.Player2Id,

				// Parent
				SetId = set.UniqueId,
				Set = set
			};

			// Populate FKs
			stat.Populate();

			// Save state (deleted later)
			RecordState( stat );

			Stat prev = PreviousStat();
			
			// Save before/after lineup snapshots
			stat.State.SaveLineups( data.Entries_Post );
			prev?.State.SaveLineups( data.Entries_Pre );

			// Cache
			StatCache.Add( stat );

			// Update undo stack
			sm.UpdateUndo();

			// DEBUG
			DebugStack();
        }
        catch ( Exception ex )
        {
            DXLog.Exception( "record.lineup", ex );
            DXAlert.ShowErrorCode( "net.err", "net.err.write", 300 );
        }
	}

	// Updates rating for previous serve event
	public void UpdateServe( int? pass )
	{
		try
		{
			if ( pass != null )
			{
				Stat stat = PreviousStat( 2, true );

				// Only applicable if contact before last was serve
				if ( stat is { IsServe: true } )
				{
					int rating = GetServeRating( (int)pass );

					stat.Rating = rating;
				}
			}
        }
        catch ( Exception ex )
        {
            DXLog.Exception( "record.serve", ex );
            DXAlert.ShowErrorCode( "net.err", "net.err.write", 400 );
        }
    }

    // Returns serve rating based on specified pass rating
    private static int GetServeRating( int pass )
    {
	    // Serve always 0-4, Pass always 0-4 internally
	    int serve = pass switch
	    {
		    0 => 4,
		    1 => 3,
		    2 => 3,
		    3 => 2,
		    4 => 1,
		    
		    _ => 0
	    };

	    return serve;
    }

	// Updates previous stat with player/num/pos of attributable player(s)
	public void UpdateStat( string action, List<LineupEntry> entries )
	{
		try
		{
			// Find last matching event
			Stat stat = PreviousStat( action );

			int count = 0;

			LineupEntry entry = entries[0];

			// Player 1
			stat.Number = entry.Number;
			stat.Position = entry.Position;

			stat.PlayerId = entry.PlayerId;
			stat.Player = entry.Player;

			count++;

			// Player 2 (optional)
			LineupEntry entry2 = entries[1];

			if ( !entry2.IsEmpty )
			{
				stat.Number2 = entry2.Number;
				stat.Position2 = entry2.Position;

				stat.Player2Id = entry2.PlayerId;
				stat.Player2 = entry2.Player;

				count++;
			}

			// Player 3 (optional)
			LineupEntry entry3 = entries[2];

			if ( !entry3.IsEmpty )
			{
				stat.Number3 = entry3.Number;
				stat.Position3 = entry3.Position;

				stat.Player3Id = entry3.PlayerId;
				stat.Player3 = entry3.Player;

				count++;
			}

			// Assign result for block waiting on player selection
			if ( (action == Stats.BlockKey) && (stat.Result == Stats.PreBlockKey) )
			{
				stat.Result = (count > 1) ? Stats.BlockAssistsKey : Stats.BlockKey;
			}

			sm.UpdateUndo();

			// DEBUG
			DebugStack();
        }
        catch ( Exception ex )
        {
            DXLog.Exception( "record.updatestat", ex );
            DXAlert.ShowErrorCode( "net.err", "net.err.write", 500 );
        }
    }

    /* Undo */

    // Callback to initiate an individual stat undo
    public async void OnUndoStat()
	{
		await UndoStat();
	}

	// Undoes last stat (or entire rally), reverts state
	private async Task UndoStat( bool rally = false )
	{
		try
		{
			// Might be empty
			if ( StatCache.Count == 0 )
			{
				return;
			}

			// Retrieve last 3 stats on stack
			Stat prev  = PreviousStat( 1 );		// Stat being undone
			Stat prev2 = PreviousStat( 2 );		// New top of stack
			Stat prev3 = PreviousStat( 3 );

			// Was last stat libero auto swap out?
			bool autoSwapOut = prev.IsLineup && (prev.Selector == Stats.SwapOutKey) && prev.IsValueTrue();

			bool isUpdate = prev.IsUpdate;
			bool isLineup = prev.IsLineup;
			bool isPoint = prev.IsPoint;

			// Delete
			StatCache.Remove( prev );

			// Undo action event
			if ( !isUpdate && !isLineup )
			{
				// Must reset previous result(s)
				if ( prev2 is { IsAction: true } )
				{
					string result = prev2.Result;

					prev2.EndX = -1;
					prev2.EndY = -1;
					prev2.EndZone = 0;

					prev2.Rating = null;

					prev2.Result = null;
					prev2.Error = null;
					prev2.Fault = null;

					// If undo kill, must also undo assist
					if ( result == Stats.KillKey )
					{
						// May not have been an assist
						if ( prev3 is { Result: Stats.AssistKey } )
						{
							prev3.Result = Stats.AttemptKey;
						}
					}
				}

				// Reset set score
				if ( isPoint )
				{
					if ( prev2 == null )
					{
						set.SetScore( 0, 0 );
					}
					else
					{
						StatState state = prev2.State;

						bool team1OnSideA = sm.Team1OnSideA;

						int points1 = team1OnSideA ? state.ScoreA : state.ScoreB;
						int points2 = team1OnSideA ? state.ScoreB : state.ScoreA;

						set.SetScore( points1, points2 );
					}
				}
			}

            // Get new top of stack
            prev = PreviousStat( 1 );

			// Start of set, reset to initial configuration
			if ( prev == null )
			{
				sm.Restart();
			}
			// Restore state from prev stat (do not update UI for rally undo)
			else
			{
				// Mark as restored for debug
				prev.Restored = true;
				
				sm.RestoreState( prev, !rally );
			}

			// DEBUG
			DebugStack();

			// Undo auto swap out and point together
			if ( autoSwapOut )
			{
				await UndoStat( rally );
			}
        }
        catch ( Exception ex )
        {
			DXLog.Exception( "record.undo", ex );
            DXAlert.ShowErrorCode( "net.err", "net.err.write", 600 );
        }
    }

    // Undoes all stats from rally back to previous serve
    public async void OnUndoRally()
	{
		try
		{
			DXSpinner.Start();

            Stat prev = PreviousStat();

            // If rally just ended, undo point first
            if ( prev.IsPoint )
			{
				await UndoStat( true );

				prev = PreviousStat();
			}

			// Undo all stats back to previous point
			while ( (StatCache.Count > 0) && !prev.IsPoint )
			{
				await UndoStat( true );

				prev = PreviousStat();
			}

			// Update UI
			sm.UpdateUI();
        }
        catch ( Exception ex )
        {
            DXLog.Exception( "record.undorally", ex );
            DXAlert.ShowErrorCode( "net.err", "net.err.write", 700 );
        }

        DXSpinner.Stop();
    }

    /* Debug */

    // Outputs debug info for entire stat stack
    [Conditional( "DEBUG" )]
	private void DebugStack()
	{
		const int depth = 20;
		const string anon = "anon";

		DXLog.Trace( "-------- STACK:{0} --------", StatCount );

		int start = (StatCount - 1);
		int end = Math.Max( (start - depth), 0 );

		// Output line for last N stats
		for ( int index = start; index >= end; index-- )
		{
			Stat stat = StatCache[ index ];

			// Update event
			if ( stat.IsUpdate )
			{
				DXLog.Trace( "{0}: UPDATE sel:{1}", stat.Counter, stat.Selector );
			}
			// Normal event
			else
			{
				StatPoint point = stat.Point;
				Match match = set.Match;

				// Normal stat
				if ( point == null )
				{
					string team = match.LabelForTeam( stat.TeamId );
					string teamId = DebugId( stat.TeamId );

					string player = (stat.Player == null) ? anon : stat.Player.LastName;

					DXLog.Trace( "{0}: team:{1} id:{2} player:{3} action:{4} sel:{5} mod:{6} rat:{7} res:{8} err:{9} flt:{10} offset:{11}",
								 stat.Counter, team, teamId, player, stat.Action, stat.Selector, stat.Modifier, stat.Rating, stat.Result, stat.Error, stat.Fault, stat.Offset );
				}
				// Point
				else
				{
					string team = match.LabelForTeam( point.TeamId );
					string teamId = DebugId( point.TeamId );

					DXLog.Trace( "{0}: POINT team:{1} id:{2} sideout:{3} earned:{4} score1:{5} score2:{6}",
								 stat.Counter, team, teamId, point.Sideout, point.Earned, point.Score1, point.Score2 );
				}
			}

			StatState state = stat.State;

			if ( state != null )
			{
				string teamA = DebugId( state.TeamAId );
				string teamB = DebugId( state.TeamBId );

				// Output state info
				DXLog.Trace( "   state:{0} sel:{1} serve:{2} ball:{3} a:{4} b:{5}",
								 state.State, state.StateSelector, (Side)state.ServeSide, (Side)state.BallSide, teamA, teamB );
			}
		}

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

	// Converts team ID to debug display form
	private static string DebugId( string teamId )
	{
		return string.IsNullOrEmpty( teamId ) ? null : teamId[ ..4 ];
	}
}

//
