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

namespace iStatVball3;

/*
 * Implements the complete RallyFlow state machine logic flow. Each state has a touch handler and an error handler. 
 */ 
public class RallyFlow
{
	/* Fields */
	private readonly RallyState sm;
	private readonly RallyUI ui;

	private readonly RecordDb db;

	// Required for post-block
	private RecordData pointData;

	// Callback LUTs
	private readonly Dictionary<string,Action<Court.Region,double,double,RallyUI.TouchType>> touchCallbacks;
	private readonly Dictionary<string,Action<RecordData>> errorCallbacks;

	/* Methods */
	public RallyFlow( RallyState sm, RecordDb db, RallyUI ui )
	{
		this.sm = sm;
		this.db = db;
		this.ui = ui;

		// Build callback LUTs
		touchCallbacks = new Dictionary<string,Action<Court.Region,double,double,RallyUI.TouchType>>
		{
			{ "start", StartTouch },
			{ "serve", ServeTouch },
			{ "receive", ReceiveTouch },
			{ "first", FirstTouch },
			{ "second", SecondTouch },
			{ "third", ThirdTouch },
			{ "defense", DefenseTouch },
			{ "putback", PutbackTouch },
			{ "freeball", FreeballTouch },
			{ "block", BlockTouch },
			{ "over", OverTouch },
		};

		errorCallbacks = new Dictionary<string,Action<RecordData>>
		{
			{ "serve", ServeError },
			{ "receive", ReceiveError },
			{ "first", FirstError },
			{ "second", SecondError },
			{ "third", ThirdError },
			{ "defense", DefenseError },
			{ "putback", PutbackError },
			{ "freeball", FreeballError },
			{ "block", BlockError },
			{ "over", OverError }
		};
	}

	// Used to evaluate conditions for an NCAA ball handling error (BHE)
	private static string ValidateBHE( string error, string fault )
	{
		string validated = error;

		// Only applicable for faults
		if ( error == Stats.FaultKey )
		{
			// Generic fault counted as BHE
			if ( fault is null or "double" or "throw" )
			{
				validated = Stats.BHEKey;
			}
		}

		return validated;
	}

	// Determines if automatic ratings should be used for specified state
	private bool AutoRate( string action )
	{
		Settings settings = Shell.Settings;
		string level = settings.RallyLevel;

		// Some detail levels always have auto ratings
		bool autoRatings = level is Settings.HighKey or Settings.MaximumKey;

		return settings.RallyAuto || sm.HasRatings( action ) || autoRatings;
	}

	// Awards kill to previous player, possibly assist to player before that
	private void AwardKill( RecordData data, bool touch = false )
	{
        Stat stat3 = sm.PreviousStat( 3 );
        Stat stat4 = sm.PreviousStat( 4 );

        // Touch off block (block attempt, kill)
        if ( touch )
		{
			data.PrevResult = Stats.AttemptKey;
			data.Prev2Result = Stats.KillKey;

			// Assist only if same team
            if ( (stat3 != null) && (stat4 != null) )
			{
				if ( stat3.TeamId == stat4.TeamId )
				{
					data.Prev3Result = Stats.AssistKey;
				}
			}
		}
		// No touch
		else
		{
            Stat stat2 = sm.PreviousStat( 2 );

            // Previous contact counts as kill (except overpass)
            data.PrevResult = Stats.KillKey;

            // Contact before that counts as assist
            if ( (stat2 != null) && (stat3 != null) )
			{
				string teamId = stat2.TeamId;

				// Only if same team
				if ( stat3.TeamId == teamId )
				{
					data.Prev2Result = Stats.AssistKey;

					// 2nd ball pass-to-kill conversion
					if ( stat3.IsPassing() )
					{
						data.Prev2Value = 1;
					}
				}

                // 3rd ball pass-to-kill conversion
                if ( (stat4 != null) && (stat4.TeamId == teamId) && stat4.IsPassing() )
				{
					data.Prev3Value = 1;
				}
			}
		}
	}

	/* Event Handling */

	// Forwards touch event to appropriate state
	public void HandleTouch( Court.Region region, double x, double y, RallyUI.TouchType type )
	{
		touchCallbacks[ sm.State ]?.Invoke( region, x, y, type );
	}

	// Forwards error fault to appropriate state
	public void HandleFault( RecordData data )
	{
		errorCallbacks[ sm.State ]?.Invoke( data );
	}

	// Handle post-processing of block point (QuickSelect only)
	private async void HandlePostBlock( RecordData data )
	{
		await sm.ProcessPoint( pointData );

		// Update block event with selected player(s)
		sm.ProcessPostBlock( data );
	}

	/* State Machine */

	// START

	// Touch while in start state
	private void StartTouch( Court.Region region, double x, double y, RallyUI.TouchType type )
	{
		// Only valid location is same side service area
		if ( Court.SameServiceArea( sm.ServeSide, region ) )
		{
			ui.ShowServe( x, y, type, ServeSelect );
		}
		// Anywhere else invalid
		else
		{
			ui.ShowInvalid( x, y );
		}
	}

	// SERVE

	// Server selected
	private async void ServeSelect( RecordData data )
	{
		// Update state
		sm.State = "serve";

		sm.Touches++;
		sm.SetFaults( "serve" );

		Stat prev = db.PreviousStat();
		Stat prevServe = db.PreviousStat( Stats.ServeKey );

		// First Serve if first stat, first serve, or first serve following sideout
		data.Value = ((prev == null) || (prevServe == null) || prev.IsSideout) ? 1 : 0;
		
		// Persist data
		await sm.ProcessAction( data );
	}

	// Touch after serve
	private void ServeTouch( Court.Region region, double x, double y, RallyUI.TouchType type )
	{
		// In-bounds opposite side -> Receive
		if ( Court.OppositeCourt( sm.ServeSide, region ) )
		{
			ui.ShowReceive( x, y, type, ReceiveSelect );
		}
		// Serve resulted in out/net/ant (same side also considered out)
		else
		{
			ui.ShowError( region, x, y, type, ServeError );
		}
	}

	// Serve resulted in out/net/antennae or fault
	private async void ServeError( RecordData data )
	{
		// Rotation errors not counted against server
		if ( data.Fault == "rot" )
		{
			data.Result = Stats.AttemptKey;
		}
		// 0 rating
		else
		{
			if ( AutoRate( Stats.ReceiveKey ) )
			{
				data.Rating = 0;
			}
		}

		// Persist
		await sm.ProcessPoint( data );
	}

	// RECEIVE

	// Receive passer selected
	private async void ReceiveSelect( RecordData data )
	{
		// Update state
		sm.State = "receive";
		sm.SwitchBallSide();

		sm.Touches++;
		sm.SetFaults( "receive" );

		// 0-Serve
		data.Result = Stats.AttemptKey;

		// Persist data
		await sm.ProcessAction( data );

		// Error (this player or TRE)
		if ( data.IsError )
		{
			sm.Touches--;

			data.Error = data.IsTeamError ? Stats.TREKey : Stats.DownKey;

			ReceiveError( data );
		}
	}

	// Touch for destination of receive
	private void ReceiveTouch( Court.Region region, double x, double y, RallyUI.TouchType type )
	{
		// In-bounds same side -> 2nd ball
		if ( Court.SameSide( sm.BallSide, region ) )
		{
			ui.ShowSecond( x, y, type, SecondSelect, ReceiveError );
		}
		// In-bounds other side -> 1st ball
		else if ( Court.OppositeCourt( sm.BallSide, region ) )
		{
			ui.ShowFirst( x, y, type, FirstSelect );
		}
		// Block zone other side -> Overpass
		else if ( Court.BlockZone( region ) )
		{
			ui.ShowOver( x, y, type, OverSelect );
		}
		// Receive resulted in out/net/ant
		else
		{
			ui.ShowError( region, x, y, type, ReceiveError );
		}
	}

	// Receive resulted in out/net/ant/down or fault
	private async void ReceiveError( RecordData data )
	{
		data.Result = Stats.ErrorKey;

		// NOT counted as BHE

		// Previous serve is Ace
		data.PrevResult = "ace";

		bool autoRate = AutoRate( Stats.ReceiveKey );

		// Overlap/Rotation faults counted as TRE
		if ( data.IsFault && data.Fault is "olap" or "rot" )
		{
			data.Error = Stats.TREKey;
			data.IsPrevTeamError = true;
		}
		// Player specific error/fault (0 pass)
		else
		{
			if ( autoRate )
			{
				data.Rating = 0;
			}
		}

		// Rating 4 serve
		if ( autoRate )
		{
			data.PrevRating = 4;
		}

		// Persist
		await sm.ProcessPoint( data );
	}

	// FIRST BALL

	// Passer selected for first ball contact
	private async void FirstSelect( RecordData data )
	{
		bool touch = (sm.StateSelector == "touch");
		bool defense = (sm.State == "defense");

		// Update state
		sm.State = "first";
		sm.StateSelector = null;

		sm.Touches++;
		sm.SetFaults( "pass" );

		// Coming from touch off overpass, ball already on this side
		if ( !touch )
		{
			sm.SwitchBallSide();
		}

		// Defense always dig
		data.Result = defense ? Stats.DigKey : Stats.AttemptKey;

		// Any pass or set over is 1 rating
		if ( AutoRate( data.Action ) )
		{
			string prev = sm.PreviousAction();

			// Do NOT rate overpass
			if ( prev != Stats.OverpassKey )
			{
				data.Rating = 1;
			}
		}

		// Serve receive pass over must update serve rating
		db.UpdateServe( 1 );

		// Persist data
		await sm.ProcessAction( data );

		// Error (this player or team)
		if ( data.IsError )
		{
			sm.Touches--;

			data.Error = data.IsTeamError ? Stats.TeamKey : Stats.DownKey;
			data.PrevRating = 1;

			FirstError( data );
		}
	}

	// Destination touched for first ball contact
	private void FirstTouch( Court.Region region, double x, double y, RallyUI.TouchType type )
	{
		// In-bounds same side as first -> 2nd ball
		if ( Court.SameSide( sm.BallSide, region ) )
		{
			ui.ShowSecond( x, y, type, SecondSelect, FirstError );
		}
		// In-bounds other side -> 1st ball
		else if ( Court.OppositeCourt( sm.BallSide, region ) )
		{
			ui.ShowFirst( x, y, type, FirstSelect );
		}
		// Block zone other side -> Overpass
		else if ( Court.BlockZone( region ) )
		{
			ui.ShowOver( x, y, type, OverSelect );
		}
		// Pass resulted in out/net/ant
		else
		{
			ui.ShowError( region, x, y, type, FirstError );
		}
	}

	// First ball resulted in out/net/ant/down or fault
	private async void FirstError( RecordData data )
	{
		// Error
		data.Result = Stats.ErrorKey;

		// 0 pass
		if ( AutoRate( Stats.FirstKey ) )
		{
			data.Rating = 0;
		}

		// Check for BHE
		data.Error = ValidateBHE( data.Error, data.Fault );

		// Previous contact awarded kill
		AwardKill( data );

		// Persist
		await sm.ProcessPoint( data );
	}

	// SECOND BALL

	// Setter selected
	private async void SecondSelect( RecordData data )
	{
		bool defense = (sm.State == "defense");

		// Update state
		sm.State = "second";
		sm.StateSelector = data.Selector;

		sm.Touches++;
		sm.SetFaults( (data.Selector == Stats.SetKey) ? "set" : "attack2" );

		// Dig or pass attempt
		data.Result = defense ? Stats.DigKey : Stats.AttemptKey;

		// 2nd ball after receive must update serve rating (inverse of pass rating)
		db.UpdateServe( data.Rating );

		// Persist data
		await sm.ProcessAction( data );

		// Error (this player, passer goes prev)
		if ( data.IsError )
		{
			sm.Touches--;

			data.Error = Stats.DownKey;

			SecondError( data );
		}
	}

	// Touch for destination of set/attack
	private void SecondTouch( Court.Region region, double x, double y, RallyUI.TouchType type )
	{
		// In-bounds same side as second -> 3rd ball
		if ( Court.SameSide( sm.BallSide, region ) )
		{
			ui.ShowThird( x, y, type, ThirdSelect, SecondError );
		}
		// In-bounds other side
		else if ( Court.OppositeCourt( sm.BallSide, region ) )
		{
			// Set -> 1st Ball
			if ( sm.StateSelector == Stats.SetKey )
			{
				ui.ShowFirst( x, y, type, FirstSelect );
			}
			// Attack -> Defense
			else if ( sm.StateSelector == Stats.AttackKey )
			{
				ui.ShowDefense( x, y, type, DefenseSelect );
			}
		}
		// Block zone other side
		else if ( Court.BlockZone( region ) )
		{
			// Set -> Overpass
			if ( sm.StateSelector == Stats.SetKey )
			{
				ui.ShowOver( x, y, type, OverSelect );
			}
			// Attack -> Block
			else if ( sm.StateSelector == Stats.AttackKey )
			{
				ui.ShowBlock( x, y, type, BlockSelect );
			}
		}
		// Set/attack resulted in out/net/ant
		else
		{
			ui.ShowError( region, x, y, type, SecondError );
		}
	}

	// Set/attack resulted in out/net/ant/down or fault
	private async void SecondError( RecordData data )
	{
		data.Result = Stats.ErrorKey;

		// Set error always rated 0
		if ( sm.StateSelector == Stats.SetKey )
		{
			if ( sm.HasRatings( Stats.SecondKey ) )
			{
				data.Rating = 0;
			}
		}

		// Check for BHE
		data.Error = ValidateBHE( data.Error, data.Fault );

		// Persist
		await sm.ProcessPoint( data );
	}

	// THIRD BALL

	// Hitter selected
	private async void ThirdSelect( RecordData data )
	{
		// Update state
		sm.State = "third";
		sm.StateSelector = data.Selector;

		bool attack = (data.Selector == Stats.AttackKey);

		sm.Touches++;
		sm.SetFaults( attack ? "attack3" : "free" );
		
		// Starts as attempt
		data.Result = Stats.AttemptKey;

		if ( attack )
		{
			// Associated setter
			data.Player2 = db.PreviousStat( 1 ).Player;

			// Receive rating
			Stat stat = db.PreviousStat( 2 );

			if ( (stat.Action == Stats.ReceiveKey) && stat.Rating.HasValue )
			{
				data.Value = (int)stat.Rating;
			}
		}

		// Persist data
		await sm.ProcessAction( data );

		// Error (this player, setter goes prev)
		if ( data.IsError )
		{
			sm.Touches--;

			data.Error = Stats.DownKey;

			ThirdError( data );
		}
	}

	// Touch for destination of attack/free
	private void ThirdTouch( Court.Region region, double x, double y, RallyUI.TouchType type )
	{
		bool attack = (sm.StateSelector == "attack");

		// In-bounds other side
		if ( Court.OppositeCourt( sm.BallSide, region ) )
		{
			// Attack -> Defense
			if ( attack )
			{
				ui.ShowDefense( x, y, type, DefenseSelect );
			}
			// Free -> FreeBall
			else
			{
				ui.ShowFreeball( x, y, type, FreeballSelect );
			}
		}
		// Block zone other side
		else if ( Court.BlockZone( region ) )
		{
			// Attack -> Block
			if ( attack )
			{
				ui.ShowBlock( x, y, type, BlockSelect );
			}
			// Free -> Overpass
			else
			{
				ui.ShowOver( x, y, type, OverSelect );
			}
		}
		// In-bounds same side is 4 hits
		else if ( Court.SameSide( sm.BallSide, region ) )
		{
			ui.ShowError( region, x, y, type, ThirdError );
		}
		// Attack/Free resulted in out/net/ant
		else
		{
			ui.ShowError( region, x, y, type, ThirdError );
		}
	}

	// Attack/Free resulted in out/net/ant/down or fault
	private async void ThirdError( RecordData data )
	{
		data.Result = Stats.ErrorKey;

		// Attack NOT counted as BHE
		if ( data.Selector == Stats.FreeKey )
		{
			data.Error = ValidateBHE( data.Error, data.Fault );
		}

		// Persist
		await sm.ProcessPoint( data );
	}

	// DEFENSE

	// Dig player selected
	private async void DefenseSelect( RecordData data )
	{
		data.Touch = (sm.State == "block");

		// Update state
		sm.State = "defense";

		sm.Touches++;
		sm.SetFaults( "defense" );

		// Might be touch off block, already on this side
		if ( !data.Touch )
		{
			sm.SwitchBallSide();
		}

		// Previous hit is 0-Attack
		data.Result = Stats.AttemptKey;

		// Persist data
		await sm.ProcessAction( data );

		// Error (this player or team)
		if ( data.IsError )
		{
			sm.Touches--;

			data.Error = data.IsTeamError ? Stats.TeamKey : Stats.DownKey;

			DefenseError( data );
		}
	}

	// Destination touched for dig
	private void DefenseTouch( Court.Region region, double x, double y, RallyUI.TouchType type )
	{
		// In-bounds same side -> 2nd ball
		if ( Court.SameSide( sm.BallSide, region ) )
		{
			ui.ShowSecond( x, y, type, SecondSelect, DefenseError );
		}
		// In-bounds other side -> 1st ball
		else if ( Court.OppositeCourt( sm.BallSide, region ) )
		{
			ui.ShowFirst( x, y, type, FirstSelect );
		}
		// Block zone other side -> Overpass
		else if ( Court.BlockZone( region ) )
		{
			ui.ShowOver( x, y, type, OverSelect );
		}
		// Dig resulted in out/net/ant
		else
		{
			ui.ShowError( region, x, y, type, DefenseError );
		}
	}

	// Dig resulted in out/net/ant/down or fault
	private async void DefenseError( RecordData data )
	{
		// Faults are errors, team/out/net/ant/down is just dig-0
		data.Result = data.IsFault ? Stats.ErrorKey : Stats.AttemptKey;

		// Check for BHE
		data.Error = ValidateBHE( data.Error, data.Fault );

		// 0 pass
		if ( AutoRate( Stats.DefenseKey ) )
		{
			data.Rating = 0;
		}

		// Award kill and assist (may be touch off block)
		AwardKill( data, data.Touch );

		// Persist
		await sm.ProcessPoint( data );
	}

	// PUTBACK

	// Putback passer selected
	private async void PutbackSelect( RecordData data )
	{
		// Update state
		sm.State = "putback";

		sm.Touches++;
		sm.SetFaults( "pass" );

		sm.SwitchBallSide();

		// Previous block is 0-Block
		data.Result = Stats.AttemptKey;

		// Persist data
		await sm.ProcessAction( data );

		// Error (this player or team)
		if ( data.IsError )
		{
			sm.Touches--;

			data.Error = data.IsTeamError ? Stats.TeamKey : Stats.DownKey;

			PutbackError( data );
		}
	}

	// Destination touched for putback pass
	private void PutbackTouch( Court.Region region, double x, double y, RallyUI.TouchType type )
	{
		// In-bounds same side as first -> 2nd ball
		if ( Court.SameSide( sm.BallSide, region ) )
		{
			ui.ShowSecond( x, y, type, SecondSelect, PutbackError );
		}
		// In-bounds other side -> 1st ball
		else if ( Court.OppositeCourt( sm.BallSide, region ) )
		{
			ui.ShowFirst( x, y, type, FirstSelect );
		}
		// Block zone other side -> Overpass
		else if ( Court.BlockZone( region ) )
		{
			ui.ShowOver( x, y, type, OverSelect );
		}
		// Putback resulted in out/net/ant
		else
		{
			ui.ShowError( region, x, y, type, PutbackError );
		}
	}

	// Putback resulted in out/net/ant/down or fault
	private async void PutbackError( RecordData data )
	{
		bool isTeam1 = !sm.IsTeam1( sm.BallId() );

		// Error
		data.Result = Stats.ErrorKey;

		// 0 pass
		if ( AutoRate( Stats.PutbackKey ) )
		{
			data.Rating = 0;
		}

		// Check for BHE
		data.Error = ValidateBHE( data.Error, data.Fault );

		// Either block/assist now or quick select with block/assist later
		bool preBlock = RallyState.IsQuickSelect( Stats.BlockKey ) && !RallyState.RallyLow;

		// Primary team
		if ( isTeam1 )
		{
			if ( preBlock )
			{
				data.PrevResult = Stats.PreBlockKey;
			}
			else
			{
				Stat stat = db.PreviousStat( Stats.BlockKey );
				bool multiBlock = (stat.Player2Id != null);

				data.PrevResult = multiBlock ? Stats.BlockAssistsKey : Stats.BlockKey;
			}
		}
		// Opponent always just block
		else
		{
			data.PrevResult = Stats.BlockKey;
		}

		// Attacker given block error
		data.Prev2Result = Stats.ErrorKey;
		data.Prev2Error = Stats.BlockKey;

		// QuickSelect blocker selection
		if ( isTeam1 && preBlock )
		{
			pointData = data;

			ui.ShowPostBlock( isTeam1, data.X, data.Y, HandlePostBlock );
		}
		// Persist now
		else
		{
			await sm.ProcessPoint( data );
		}
	}

	// FREEBALL

	// Passer selected
	private async void FreeballSelect( RecordData data )
	{
		// Update state
		sm.State = "freeball";

		sm.Touches++;
		sm.SetFaults( "pass" );

		sm.SwitchBallSide();

		// Previous free is attempt only
		data.Result = Stats.AttemptKey;

		// Persist data
		await sm.ProcessAction( data );

		// Error (this player or team)
		if ( data.IsError )
		{
			sm.Touches--;

			data.Error = data.IsTeamError ? Stats.TeamKey : Stats.DownKey;

			FreeballError( data );
		}
	}

	// Destination touched for freeball pass
	private void FreeballTouch( Court.Region region, double x, double y, RallyUI.TouchType type )
	{
		// In-bounds same side as first -> 2nd ball
		if ( Court.SameSide( sm.BallSide, region ) )
		{
			ui.ShowSecond( x, y, type, SecondSelect, FreeballError );
		}
		// In-bounds other side -> 1st ball
		else if ( Court.OppositeCourt( sm.BallSide, region ) )
		{
			ui.ShowFirst( x, y, type, FirstSelect );
		}
		// Block zone other side -> Overpass
		else if ( Court.BlockZone( region ) )
		{
			ui.ShowOver( x, y, type, OverSelect );
		}
		// Freeball resulted in out/net/ant
		else
		{
			ui.ShowError( region, x, y, type, FreeballError );
		}
	}

	// Freeball resulted in out/net/ant/down or fault
	private async void FreeballError( RecordData data )
	{
		// Error
		data.Result = Stats.ErrorKey;

		// 0 pass
		if ( AutoRate( Stats.FreeballKey ) )
		{
			data.Rating = 0;
		}

		// Check for BHE
		data.Error = ValidateBHE( data.Error, data.Fault );

		// Previous contact awarded kill
		AwardKill( data );

		// Persist
		await sm.ProcessPoint( data );
	}

	// OVERPASS

	// Overpass blocker selected (solo only)
	private async void OverSelect( RecordData data )
	{
		bool defense = (sm.State == "defense");

		// Update state
		sm.State = "over";

		sm.Touches++;
		sm.SetFaults( "block" );

		sm.SwitchBallSide();

		// Previous contact is attempt, 1 rating (might be dig)
		data.Result = defense ? Stats.DigKey : Stats.AttemptKey;

		if ( AutoRate( data.Action ) )
		{
			data.Rating = 1;
		}

		// Overpass after receive must update serve rating
		db.UpdateServe( 1 );

		// Persist data
		await sm.ProcessAction( data );

		// Error (team only)
		if ( data.IsTeamError )
		{
			data.Error = Stats.TeamKey;

			OverError( data );
		}
	}

	// Destination touched for overpass contact
	private void OverTouch( Court.Region region, double x, double y, RallyUI.TouchType type )
	{
		// In-bounds same side -> 1st Ball (touch)
		if ( Court.SameSide( sm.BallSide, region ) )
		{
			sm.StateSelector = "touch";

			ui.ShowFirst( x, y, type, FirstSelect, true );
		}
		// In-bounds other side -> Defense
		else if ( Court.OppositeCourt( sm.BallSide, region ) )
		{
			ui.ShowDefense( x, y, type, DefenseSelect );
		}
		// Block zone other side -> Block
		else if ( Court.BlockZone( region ) )
		{
			ui.ShowBlock( x, y, type, BlockSelect );
		}
		// Overpass resulted in out/net/ant
		else
		{
			ui.ShowError( region, x, y, type, OverError );
		}
	}

	// Overpass contact resulted in out/net/ant or fault
	private async void OverError( RecordData data )
	{
		data.Result = Stats.ErrorKey;

		// NOT counted as BHE

		// Previous contact awarded kill
		AwardKill( data );

		// Persist
		await sm.ProcessPoint( data );
	}

	// BLOCK

	// Block selected
	private async void BlockSelect( RecordData data )
	{
		// Update state
		sm.State = "block";

		sm.Touches++;
		sm.SetFaults( "block" );

		sm.SwitchBallSide();

		// Previous attack is attempt only
		data.Result = Stats.AttemptKey;

		// Persist data
		await sm.ProcessAction( data );

		// Error (team only)
		if ( data.IsTeamError )
		{
			data.Error = Stats.TeamKey;

			BlockError( data );
		}
	}

	// Destination touched for block
	private void BlockTouch( Court.Region region, double x, double y, RallyUI.TouchType type )
	{
		// In-bounds same side -> Defense (touch)
		if ( Court.SameSide( sm.BallSide, region ) )
		{
			ui.ShowDefense( x, y, type, DefenseSelect, true );
		}
		// In-bounds other side -> Putback
		else if ( Court.OppositeCourt( sm.BallSide, region ) )
		{
			ui.ShowPutback( x, y, type, PutbackSelect );
		}
		// Block zone other side -> Overpass
		else if ( Court.BlockZone( region ) )
		{
			ui.ShowOver( x, y, type, OverSelect );
		}
		// Block resulted in out/net/ant
		else
		{
			ui.ShowError( region, x, y, type, BlockError );
		}
	}

	// Block resulted in out/net/ant or fault
	private async void BlockError( RecordData data )
	{
		bool fault = data.IsFault;

		// Only faults considered as error
		data.Result = fault ? Stats.ErrorKey : Stats.AttemptKey;

		// NOT counted as BHE

		// Previous contact awarded kill
		AwardKill( data );

		bool isTeam1 = sm.IsTeam1( sm.BallId() );
		bool preBlock = RallyState.IsQuickSelect( Stats.BlockKey ) && !RallyState.RallyLow;

		// Post-selection of players (QuickSelect only)
		if ( isTeam1 && preBlock )
		{
			double x = data.X;
			double y = data.Y;

			// Faults anchored to scoreboard button
			if ( fault )
			{
				Point center = sm.GetFaultBounds().Center;

				x = center.X;
				y = center.Y;
			}

			pointData = data;

			ui.ShowPostBlock( isTeam1, x, y, HandlePostBlock );
		}
		// Persist now
		else
		{
			await sm.ProcessPoint( data );
		}
	}
}

//
