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

namespace iStatVball3;

using Side = RecordCourt.Side;

/*
 * Implements state machine functionality specific to the RallyFlow engine.
 */
public class RallyState : RecordState
{
	/* Properties */

	// RallyFlow level
	public static bool RallyLow => Shell.Settings.IsLowLevel;
	public static bool RallyPreview => Shell.Settings.RallyPreview;

	// Action menu sorting
	public string GridOrder { get; private set; }
	public bool IsSmartOrder => GridOrder == "smart";

	// Used internally to map team bars to court side
	private TeamBar TeambarA => Team1OnSideA ? teambar1 : teambar2;
	private TeamBar TeambarB => Team1OnSideA ? teambar2 : teambar1;

	/* Fields */

	// UI references
	private readonly ActionOverlay overlay;
	private readonly CourtView court;

	private readonly TeamBar teambar1;
	private readonly TeamBar teambar2;

	private readonly MarkerStack markerStack;

	// Engine specific refs
	private readonly RallyFlow flow;

	/* Methods */

	// Saves external references, init helpers (start of each set)
	public RallyState( RecordConfig config ) : base( config )
	{
		// Save UI references
		overlay = config.Overlay;
		court = config.Court;

		teambar1 = config.Teambar1;
		teambar2 = config.Teambar2;

		// Create overlays (based on starting lineups)
		overlay?.Init( this, config.Lineup1, config.Lineup2 );
		overlay?.SetFaultCallback( HandleFault );

		// Init helper classes
		RallyUI ui = new( this, overlay );
		flow = new RallyFlow( this, db, ui );

		// Prep UI
		markerStack = court.MarkerStack;
		markerStack.Handler = db;
	}

	// Post construction initialization
	public override Task Init()
	{
		return Task.CompletedTask;
	}

	/* Action Options */

	// Determines if QuickSelect turned ON for specified action and team
	public static bool IsQuickSelect( string action, bool team1 = true )
	{
		return HasAction( Settings.QuickKey, action, null, team1 );
	}

	// Determines if QuickSelect turned ON for specified configuration
	public static bool IsQuickSelect( ActionConfig config )
	{
		return HasAction( Settings.QuickKey, config.Action, config.PrevAction, config.IsTeam1 );
	}

	// Determines if specified action has Ratings turned ON
	public bool HasRatings( string action )
	{
		return HasRatings( action, Team1HasBall() );
	}

	// Determines if specified action has Ratings turned ON
	public static bool HasRatings( ActionConfig config )
	{
		return HasRatings( config.PrevAction, config.IsTeam1 );
	}

	// Determines if specified action has Ratings turned ON
	public static bool HasRatings( string action, bool team1 )
	{
		return HasAction( Settings.RatingKey, action, null, team1 );
	}

	// Determines if specified action has Modifiers turned ON
	public static bool HasModifiers( ActionConfig config )
	{
		return HasAction( Settings.ModifierKey, config.Action, null, config.IsTeam1 );
	}

	// Determines if specified action has Faults turned ON
	public bool HasFaults()
	{
		return HasAction( Settings.FaultKey, State, null, Team1HasBall() );
	}

	// Used internally to determine if Action Option ON for specified action
	private static bool HasAction( string key, string action, string prevAction, bool team1 )
	{
		Settings settings = Shell.Settings;

		// Opponent 2nd Ball not QuickSelect if rating primary Serve (must rate Receive)
		if ( !team1 )
		{
			if ( settings.RallyCustom.RatingKeys.Contains( Stats.ServeKey ) )
			{
				if ( (key == Settings.QuickKey) && (prevAction == Stats.ReceiveKey) )
				{
					return false;
				}

				if ( (key == Settings.RatingKey) && (action == Stats.ReceiveKey) )
				{
					return true;
				}
			}
		}

		// Primary or opponent team
		return (team1 && settings.RallyCustom.Contains( key, action )) ||
			   (!team1 && settings.RallyCustomOpp.Contains( key, action ));
	}

	/* State Control */

	// Initializes state machine (start of each set)
	public override async Task Start( bool restore, bool update )
	{
		await base.Start( restore, update );

		// Init grid order setting
		GridOrder = Shell.Settings.GridOrder;

		// Init WHP
		if ( Lineup1.IsLineup )
		{
			WHP.GetInstance().Init( this, Set );
		}

		bool oneOnA = Team1OnSideA;

		// Init team bars
		teambar1.IsSideA = oneOnA;
		teambar2.IsSideA = !oneOnA;

		teambar1.SetLineup( Lineup1 );
		teambar2.SetLineup( Lineup2 );

		// MUST be after setting lineups and WHP init
		ChangeServeSide();

		// Ready for first rally
		InitRally( true, update );

		// Show initial blank lineup or preview overlay
		if ( !restore && update )
		{
			await StartOverlay();
		}

		// AUTO: process auto swap at start of set
		auto.Process( RecordAuto.AutoSwap, Rotation1, Team1Serving() );
	}

	// Must be called when serve initiated at start of each rally
	public override bool StartRally()
	{
		base.StartRally();

        // Force end to any in-progress timeout
        recordConfig.Scoreboard.EndTimeout( null );

        // Low detail level does not require lineup
        if ( RallyLow )
		{
			return true;
		}

		// Remove translucent lineup preview
		HidePreview();

		List<LineupEntry> entries = teambar1.GetEntries();

		// Validate and populate lineups (currently only Team 1)
		if ( RecordLineup.Validate( entries ) )
		{
			Populate();
			return true;
		}

		return false;
	}

	// Updates action overlays with current primary team lineup
	public override void Populate()
	{
		overlay?.HideAll();

		bool isTeam1 = Team1.Equals( ServeTeam );

		// Refresh grid order setting
		GridOrder = Shell.Settings.GridOrder;

		// Populate serve menu for serving team (never Smart)
		overlay?.PopulateServe( isTeam1 );

		// Prep WHP
		if ( IsSmartOrder )
		{
			WHP.GetInstance().InitRally( GetTeam( true ) );
		}
		// Non-WHP, pre-populate team/block menus
		else
		{
			overlay?.PopulateTeams();
			overlay?.PopulateBlock();
		}
	}

	// Forces end to rally and manually awards point
	public override async Task ForcePoint( Side side )
	{
		await ForcePoint( false, side );
	}

	/* Preview */

	// Shows initial blank lineup or preview overlay
	private async Task StartOverlay()
	{
		if ( RallyPreview && !RallyLow )
		{
			bool empty = recordConfig.Lineup1.IsEmpty();

			// Blank lineup
			if ( empty )
			{
				await teambar1.OpenTeams();
				
				teambar1.ShowOverlay( TeamOverlay.LineupMode.Blank );
			}
			// Preview
			else
			{
				ShowPreview();
			}
		}
	}

	// Shows team overlay in preview mode
	private void ShowPreview()
	{
		if ( RallyPreview && !RallyLow )
		{
			teambar1.ShowOverlay( TeamOverlay.LineupMode.Preview );
		}
	}

	// Hides preview overlay if open
	private void HidePreview()
	{
		// OK to not check setting, always call
		teambar1.HideOverlay();
	}

	// Required workaround for court 10ft line z-order issue
	public void UpdateLines( bool bothOn )
	{
		// Both 10ft lines on
		if ( bothOn )
		{
			court.HasLineA = true;
			court.HasLineB = true;
		}
		// Only line NOT under overlay
		else
		{
			court.HasLineA = !Team1OnSideA;
			court.HasLineB = Team1OnSideA;
		}
	}

	/* Sides */

	// Updates serve/receive teams, changes serve if specified
	public override void ChangeServeSide( bool change = false )
	{
		base.ChangeServeSide( change );

		// Ball same side as serve
		markerStack.ServeSide = ServeSide;

		// Update UI
		teambar1?.UpdateServer();
	}

	// Transitions ball from one side of court to other
	public override void SwitchBallSide()
	{
		base.SwitchBallSide();

		// Block/out zones dependent on ball side
		UpdateZones();
	}

	/* Scoreboard */

	// Updates scoreboard faults flyout
	public void SetFaults( string key )
	{
		FaultKey = key;

		overlay?.SetFaults( key );
	}

	// Returns bounds of fault button, used for anchoring multi-block menu
	public Rect GetFaultBounds()
	{
		Rect scoreBounds = scoreboard.Bounds;
		Rect faultBounds = scoreboard.FaultBounds;

		return new Rect( (scoreBounds.X + faultBounds.X), (scoreBounds.Y + faultBounds.Y), faultBounds.Width, faultBounds.Height );
	}

	/* Location */

	// Forwards x,y normalization requests to court view
	public override Point Normalize( double x, double y )
	{
		return court.Normalize( x, y );
	}

	// Forwards x,y de-normalization requests to court view (XF coordinates)
	public override Point Denormalize( double x, double y )
	{
		return court.Denormalize( x, y, true );
	}

	/* WHP */

	// Configures WHP call, executes algorithm, returns ranked results
	public List<LineupEntry> RunWHP( bool isTeam1, double x, double y, string action )
	{
		Settings settings = Shell.Settings;

		// Must normalize touch location
		Point norm = Normalize( x, y );

		int rotation = RotationForTeam( isTeam1 ? 1 : 2 );
		Side side = SideForTeam( isTeam1 ? 1 : 2 );

		int zone = Court.ExtendedCourtZone( norm.X, norm.Y, side );

		// All algorithm params passed via config
		return WHP.GetInstance().Score( new WHPConfig
		{
			TeamId = isTeam1 ? Team1.UniqueId : Team2.UniqueId,

			Action = action,
			Rotation = rotation,
			Zone = zone,

			X = (float)norm.X,
			Y = (float)norm.Y,

			Previous = db.PreviousStat( 1, true ),

			UseML = settings.SmartAI,
			LiberoLB = (settings.SmartLibero == Settings.LiberoLBKey),

			StateMachine = this
		});
	}

	// Returns auto-calculated default pass rating based on specified location
	public int GetDefaultRating( double x, double y )
	{
		// Normalize location
		Point norm = Normalize( x, y );
		Point normSide = RecordCourt.NormalizeSide( norm );

		// Map location to rating
		return CourtView.GetRating( normSide );
	}

	/* Lineup */

	// Returns current server (zone 1)
	public override LineupEntry GetServer( bool isTeam1 )
	{
		return isTeam1 ? teambar1?.GetServer() : teambar2?.GetServer();
	}

	// Returns current setter (backrow first)
	public override LineupEntry GetSetter( bool isTeam1 )
	{
		return isTeam1 ? teambar1?.GetSetter() : teambar2?.GetSetter();
	}

	// Returns list of all players currently on court (rotation order)
	public override List<LineupEntry> GetTeam( bool isTeam1 )
	{
		return isTeam1 ? teambar1?.GetTeam() : teambar2?.GetTeam();
	}

	// Returns list of players currently in frontrow (zones 2-4)
	public override List<LineupEntry> GetFrontrow( bool isTeam1 )
	{
		return isTeam1 ? teambar1?.GetFrontrow() : teambar2?.GetFrontrow();
	}

	/* Utilities */

	// Pass-through method to determine if specified point is on court
	public bool CourtContains( double x, double y )
	{
		return court.Contains( x, y );
	}

    /* Persistence */

    // Handles functionality common to all recorded point stats
    public async Task ProcessPoint( RecordData data )
	{
		await ProcessPoint( data, Side.Unknown );
	}

	// Handles functionality common to all recorded point stats
	public override async Task ProcessPoint( RecordData data, Side side )
	{
		// MUST be first
		await base.ProcessPoint( data, side );

		bool sideout = data.Sideout;

		int swapA = 0;
		int swapB = 0;

		// Update team bars
		if ( sideout )
		{
			// IMPORTANT: Receiving team already toggled in base call
			if ( Equals( ReceiveTeam, TeamB ) )
			{
				TeambarA.UpdateRotation( RotationA );
				swapA = TeambarA.FindLibero();
			}
			else
			{
				TeambarB.UpdateRotation( RotationB );
				swapB = TeambarB.FindLibero();
			}
		}

		// Possibly auto swap libero
		if ( LineupEntry.ZoneIsFrontRow( swapA ) )
		{
			TeambarA.SwapOut( swapA, true );
		}
		else if ( LineupEntry.ZoneIsFrontRow( swapB ) )
		{
			TeambarB.SwapOut( swapB, true );
		}

		// Ready for next rally
		InitRally( sideout, true );

		// Show translucent lineup preview between rallies
		ShowPreview();
	}

	// Handles special post-block player selection
	public void ProcessPostBlock( RecordData data )
	{
		List<LineupEntry> entries = new( Lineup.RowEntries )
		{
			new LineupEntry
			{
				Position = data.Position,
				IsAltNumber = (data.Player != null) && data.Player.IsAltNumber( data.Number ),

				PlayerId = data.Player?.UniqueId,
				Player = data.Player,
			},
			new LineupEntry
			{
				Position = data.Position2,
				IsAltNumber = (data.Player2 != null) && data.Player2.IsAltNumber( data.Number2 ),

				PlayerId = data.Player2?.UniqueId,
				Player = data.Player2
			},
			new LineupEntry
			{
				Position = data.Position3,
				IsAltNumber = (data.Player3 != null) && data.Player3.IsAltNumber( data.Number3 ),

				PlayerId = data.Player3?.UniqueId,
				Player = data.Player3
			}
		};

		// Update last block stat with 1-3 blockers
		db.UpdateStat( Stats.BlockKey, entries );

		overlay?.HideAll();
	}

	// Restores state snapshot following an undo
	public override void RestoreState( Stat stat, bool update )
	{
		StatState state = stat.State;

		base.RestoreState( stat, update );

		// Restore extended fields
		FaultKey = state.FaultKey;

		// Restore lineup
		if ( state.HasEntries )
		{
			teambar1?.SetLineup( Lineup1 );
		}

		// Update markers, undo history, etc
		if ( update )
		{
			if ( stat.IsAction )
			{
				teambar1?.HideOverlay();
			}
		}
	}

	/* UI Update */

	// Updates scoreboard, undo history, and court
	public override void UpdateUI()
	{
		base.UpdateUI();

		scoreboard.IsFaultDisabled = (State == "start");

		// Team bars
		UpdateTeambar( Side.SideA );
		UpdateTeambar( Side.SideB );

		teambar1?.Update();
		teambar2?.Update();

		// Court
		UpdateZones();

		// Markers
		markerStack.Update();

		court.Redraw();

		// Overlay (last)
		overlay?.HideAll();
		overlay?.SetFaults( FaultKey );
	}

	// Updates active, disabled, rotation, etc status for a teambar
	public override void UpdateTeambar( Side side )
	{
		bool oneOnA = Team1OnSideA;

		// Teambar A
		if ( side == Side.SideA )
		{
			TeamBar teambar = TeambarA;

			if ( teambar is { IsActive: true } )
			{
				teambar.IsSideA = true;

				teambar.UpdateType( oneOnA ? Set.Type1 : Set.Type2 );
				teambar.UpdateRotation( RotationA );
				teambar.UpdateLibero();
			}
		}
		// Teambar B
		else
		{
			TeamBar teambar = TeambarB;

			if ( teambar is { IsActive: true } )
			{
				teambar.IsSideA = false;

				teambar.UpdateType( oneOnA ? Set.Type2 : Set.Type1 );
				teambar.UpdateRotation( RotationB );
				teambar.UpdateLibero();
			}
		}

		// Also update subs
		UpdateSubs( side );
	}

	// Updates number subs remaining in teambar for specified side
	public override void UpdateSubs( Side side )
	{
		bool sideA = (side == Side.SideA);

		TeamBar teamBar = sideA ? TeambarA : TeambarB;
		int subs = sideA ? SubsA : SubsB;

		// Update UI
		teamBar.UpdateSubs( subs );
	}

	// Forces team overlay to update all players (preview or normal mode)
	public override void UpdatePlayers()
	{
		teambar1.UpdateOverlay();
		teambar1.UpdateLibero();
	}

	// Updates all transient zones (block and out)
	private void UpdateZones()
	{
		UpdateBlockZone();
		UpdateOutZone();
	}

	// Updates block zone given current state
	private void UpdateBlockZone()
	{
		court.BlockZone = State switch
		{
			// Can't block serve
			"start" or 
			"serve" => Side.Unknown,
			
			_ => (BallSide == Side.SideA) ? Side.SideB : Side.SideA
		};
	}

	// Updates out zone given current state
	private void UpdateOutZone()
	{
		court.OutZone = State switch
		{
			// Do not track serve out same side
			"start" or 
			"serve" => Side.Unknown,
			
			_ => BallSide
		};
	}

	/* Event Handling */

	// State machine driven by touch gestures on court
	public void HandleTouch( double x, double y, RallyUI.TouchType type )
	{
		Court.Region region = court.GetRegion( x, y );

		flow.HandleTouch( region, x, y, type );
	}

	// Direct fault callbacks to handler based on state
	private void HandleFault( RecordData data )
	{
		flow.HandleFault( data );
	}
}

//
