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

using DXLib.Log;
using DXLib.Utils;

using Side = iStatVball3.RecordCourt.Side;

namespace iStatVball3;

/*
 * Heart of the stat recording system for both the RallyFlow and Legacy engines. Provides utility methods for accessing
 * and manipulating the current state. Maintains references to UI components to control visual changes in state.
 */
public abstract class RecordState
{
	/* Properties */

	// Master state control
	public string State { get => currentState; set => SetState( value ); }
	public string StateSelector { get; set; }

	public virtual bool IsRallyInProgress => State != "start";

	// Parent set
	public Set Set { get; }
	public int StatCount => Set.StatCache.Count;

	// Live lineups
	public RecordLineup Lineup1 { get; }
	public RecordLineup Lineup2 { get; }

	// Possession, sides of court
	public Team TeamA { get; private set; }
	public Team TeamB { get; private set; }

	public Team ServeTeam { get; private set; }
	public Team ReceiveTeam { get; private set; }

	public Side ServeSide { get; private set; }
	public Side BallSide { get; private set; }

	public Side Team1Side => SideForTeam( 1 );

	// Rotation
	public int RotationA { get; private set; }
	public int RotationB { get; private set; }

	public int Rotation1 => RotationForTeam( 1 );

	// Score
	public int ScoreA { get; private set; }
	public int ScoreB { get; private set; }

	// Timeouts
	public int TimeoutsA { get; private set; }
	public int TimeoutsB { get; private set; }

	// Substitutions
	public int SubsA { get; private set; }
	public int SubsB { get; private set; }

	public int Subs1 => SubsForTeam( 1 );

	// Active libero
	public int LiberoA { get; private set; }
	public int LiberoB { get; private set; }

	public int Libero1 => Team1OnSideA ? LiberoA : LiberoB;

	// Valid/Invalid subs
	public readonly Dictionary<int,List<Player>> ValidSubs;
	
	// Point state
	public int Touches { get; set; }
	public int Transitions { get; private set; }
	public int ServeCount { get; private set; }

	public DateTimeOffset PointTime { get; private set; }

	// Maps Team 1/2 to court side A/B
	public bool Team1OnSideA => Set.Match.Team1.Equals( TeamA );

	public Team Team1 => Team1OnSideA ? TeamA : TeamB;
	public Team Team2 => Team1OnSideA ? TeamB : TeamA;

	// External access to scoreboard
	public bool IsUndoDisabled { set => scoreboard.IsUndoDisabled = (db.StatCount == 0) || value; }

	// Controls scoreboard fault flyout (NA for Legacy)
	public string FaultKey { get; set; }

	/* Fields */

	// Master internal state
	private string currentState;

	// Initial configuration for set
	protected readonly RecordConfig recordConfig;

	// UI references
	private readonly RecordEngine parent;
	protected readonly RecordScore scoreboard;

	private readonly ScoreUndo undoStack;

	// Database handler
	protected readonly RecordDb db;

	// AUTO: sub/swap handler
	protected readonly RecordAuto auto;

	/* Methods */

	// Saves external references, init helpers (start of each set)
	protected RecordState( RecordConfig config, string faultKey = null )
	{
		recordConfig = config;
		FaultKey = faultKey;

		// Save UI references
		parent = config.Parent;
		scoreboard = config.Scoreboard;

		// Save set info
		Set = config.Set;

		// Init helper classes
		db = new RecordDb( this );

		// Prep undo
		undoStack = scoreboard.UndoStack;
		undoStack.StateData = db;

		// Register for callbacks
		scoreboard.UndoTapped = db.OnUndoStat;
		scoreboard.UndoPressed = db.OnUndoRally;

		// Will hold deep copies of starting lineups
		Lineup1 = new RecordLineup();
		Lineup2 = new RecordLineup();

		const int count = Lineup.BaseEntries;

		// Prep for sub validation
		ValidSubs = new Dictionary<int, List<Player>>( count );

		for ( int zone = 1; zone < (count + 1); zone++ )
		{
			ValidSubs.Add( zone, new List<Player>() );
		}

		// AUTO: prep for auto sub/swap
		auto = new RecordAuto( this, Set.Lineup1 );
	}

	// Post construction initialization
	public abstract Task Init();

	/* State Control */

	// Used internally to update master state
	private void SetState( string state )
	{
		currentState = state;
		StateSelector = null;

		DebugState();
	}

	// Initializes state machine at start of each set
	//   restore- starting new set or restoring in-progress?
	//	 update- force UI redraw?
	public virtual Task Start( bool restore, bool update )
	{
		Settings settings = Shell.Settings;

		db.ResetCounter();

		// Reset state
		ServeSide = recordConfig.ServeSide;

		// Set initial sides
		ChooseSides();

		// Set starting values
		int timeouts = settings.TimeoutMax;

		TimeoutsA = timeouts;
		TimeoutsB = timeouts;

		int subs = settings.SubsMax;

		SubsA = subs;
		SubsB = subs;

		bool oneOnA = Team1OnSideA;

		// Optional starting score
		ScoreA = oneOnA ? recordConfig.Points1 : recordConfig.Points2;
		ScoreB = oneOnA ? recordConfig.Points2 : recordConfig.Points1;

		// Set starting lineups (MUST be after choose sides)
		Lineup1.FromLineup( recordConfig.Lineup1 );
		Lineup2.FromLineup( recordConfig.Lineup2 );

		bool hasLibero = Lineup1.HasLibero;

		// Set starting libero for primary team
		LiberoA = (oneOnA ? (hasLibero ? 1 : 0) : 0);
		LiberoB = (oneOnA ? 0 : (hasLibero ? 1 : 0));

		// Init sub validation
		UpdateValidSubs();

		return Task.CompletedTask;
	}

	// Restarts set following undo back to beginning
	public void Restart()
	{
		Start( true, true );

		// MUST reset scores here
		ScoreA = 0;
		ScoreB = 0;

		// Must redraw parent in case sides switched
		parent.UpdateLayout();
	}

	// Initializes state for start of each rally (point)
	public virtual void InitRally( bool sideout, bool update )
	{
		State = "start";
		BallSide = ServeSide;

		// Reset point state
		Touches = 0;
		Transitions = 0;
		PointTime = DXUtils.Now();

		// New series of serves following sideout
		ServeCount = (sideout ? 1 : (ServeCount + 1));

		// Update markers, block zone, etc
		if ( update )
		{
			UpdateUI();
		}
	}

	// Must be called when serve initiated at start of each rally
	public virtual bool StartRally()
	{
		// Support blank lineups for sub validation
		UpdateValidSubs();

		return true;
	}

	// (Re)Initializes interface with current primary team lineup
	public abstract void Populate();

	/* Sides */

	// Places teams on correct sides based on serve team/side
	private void ChooseSides()
	{
		// Is Team 1 on Side A?
		bool oneA = (Equals( recordConfig.ServeFirst, recordConfig.Team1 ) && (ServeSide == Side.SideA)) ||
					(Equals( recordConfig.ServeFirst, recordConfig.Team2 ) && (ServeSide == Side.SideB));

		TeamA = oneA ? recordConfig.Team1 : recordConfig.Team2;
		TeamB = oneA ? recordConfig.Team2 : recordConfig.Team1;

		RotationA = oneA ? recordConfig.Rotation1 : recordConfig.Rotation2;
		RotationB = oneA ? recordConfig.Rotation2 : recordConfig.Rotation1;
	}

	// Updates serve/receive teams, changes serve if specified
	public virtual void ChangeServeSide( bool change = false )
	{
		bool serveA = (ServeSide == Side.SideA);

		// Change serve sides
		if ( change )
		{
			ServeSide = serveA ? Side.SideB : Side.SideA;
			BallSide = ServeSide;

			serveA = (ServeSide == Side.SideA);
		}

		// Update teams
		ServeTeam = serveA ? TeamA : TeamB;
		ReceiveTeam = serveA ? TeamB : TeamA;
	}

	// Transitions ball from one side of court to other
	public virtual void SwitchBallSide()
	{
		// Ball moves to other side
		BallSide = (BallSide == Side.SideA) ? Side.SideB : Side.SideA;

		Transitions++;
	}

	/* Scoreboard */

	// Switches all team state to opposite court side (typically last set of match)
	public void SwitchSides()
	{
        // Teams
        (TeamB, TeamA) = (TeamA, TeamB);

        // Rotations
        (RotationB, RotationA) = (RotationA, RotationB);

        // Scores
        (ScoreB, ScoreA) = (ScoreA, ScoreB);

        // Timeouts
        (TimeoutsB, TimeoutsA) = (TimeoutsA, TimeoutsB);

        // Subs
        (SubsB, SubsA) = (SubsA, SubsB);

        // Liberos
        (LiberoB, LiberoA) = (LiberoA, LiberoB);

        // Court sides
        ChangeServeSide( true );

        // Cache event
        db.RecordUpdate( new RecordData
		{
			Selector = Stats.SwitchKey,

			// Side/serve (hijacked fields)
			TeamId = IdForSide( Side.SideA ),
			Modifier = RecordCourt.KeyFromSide( ServeSide ),
		});

		// Must refresh entire layout to switch teambars
		parent.UpdateLayout();
	}

	// Switches possession to opposite team
	public void ChangePossession()
	{
		ChangeServeSide( true );

		// Cache event
		db.RecordUpdate( new RecordData
		{
			Selector = Stats.PossessionKey
		});
	}

	// Manually changes score for one team
	public void ChangeScore( bool sideA, int score )
	{
		bool change = false;

		// Side A
		if ( sideA && (ScoreA != score) )
		{
			ScoreA = score;
			change = true;
		}
		// Side B
		else if ( !sideA && (ScoreB != score) )
		{
			ScoreB = score;
			change = true;
		}

		// Cache event
		if ( change )
		{
			string teamId = IdForSide( sideA ? Side.SideA : Side.SideB );

			db.RecordUpdate( new RecordData
			{
				TeamId = teamId,
				Selector = Stats.ScoreKey
			});
		}
	}

	// Decrements remaining timeouts for specified side
	public void CallTimeout( Side side )
	{
		bool valid;

		// Side A
		if ( side == Side.SideA )
		{
			valid = (TimeoutsA > 0);

			if ( valid ) TimeoutsA--;
		}
		// Side B
		else
		{
			valid = (TimeoutsB > 0);

			if ( valid ) TimeoutsB--;
		}

		// Cache event
		if ( valid )
		{
			string teamId = IdForSide( side );

			db.RecordUpdate( new RecordData
			{
				TeamId = teamId,
				Selector = Stats.TimeoutKey
			});
		}
	}

	// Forces end to rally and manually awards point
	public abstract Task ForcePoint( Side side );

	// Called by either Legacy or Rally engine to manually award point
	protected async Task ForcePoint( bool legacy, Side side )
	{
		RecordData data = new()
		{
			Legacy = legacy,
			Selector = Stats.ForcedKey
		};

		await ProcessPoint( data, side );
	}

	/* Teambar */

	// Manually changes rotation for one team
	public void ChangeRotation( bool sideA, int rotation )
	{
		bool change = false;

		// Side A
		if ( sideA && (RotationA != rotation) )
		{
			RotationA = rotation;
			change = true;
		}
		// Side B
		else if ( !sideA && (RotationB != rotation) )
		{
			RotationB = rotation;
			change = true;
		}

		// Persist event
		if ( change )
		{
			Side side = sideA ? Side.SideA : Side.SideB;

			db.RecordUpdate( new RecordData
			{
				Selector = Stats.RotationKey,
				TeamId = IdForSide( side )
			});
		}
	}

	// Manually changes number subs remaining for one team
	public void ChangeSubs( bool sideA, int subs )
	{
		// NO persistence
		if ( sideA )
		{
			SubsA = subs;
		}
		else
		{
			SubsB = subs;
		}
	}

	// Persists a manual lineup change (not sub or swap)
	public void ChangeLineup( bool sideA, List<LineupEntry> pre, List<LineupEntry> post )
	{
		Team team = sideA ? TeamA : TeamB;

		// Persist event
		db.RecordLineup( new RecordData
		{
			TeamId = team.UniqueId,
			Selector = Stats.ReplaceKey,

			Entries_Pre = pre,
			Entries_Post = post
		});
	}

	// Changes active libero for specified team
	public void ChangeLibero( bool team1, int number )
	{
		// Not separately persisted
		if ( team1 )
		{
			if ( Team1OnSideA )
			{
				LiberoA = number;
			}
			else
			{
				LiberoB = number;
			}
		}
	}

	/* Substitution */

	// Persists a substitution or libero swap
	public void SubSwap( string selector, bool sideA, LineupEntry subOut, LineupEntry subIn, List<LineupEntry> pre, List<LineupEntry> post, bool autoSwap = false )
	{
		Team team = sideA ? TeamA : TeamB;

		// Decrement subs remaining
		if ( selector == Stats.SubKey )
		{
			if ( sideA )
			{
				SubsA = Math.Max( (SubsA - 1), 0 );
			}
			else
			{
				SubsB = Math.Max( (SubsB - 1), 0 );
			}

			// Add to validation list
			UpdateValidSubs();
		}

		// Persist event
		db.RecordLineup( new RecordData
		{
			TeamId = team?.UniqueId,
			Selector = selector,

			Player = subOut.Player,
			Player2 = subIn.Player,

			Number = subOut.Number,
			Number2 = subIn.Number,

			Position = subOut.Position,
			Position2 = subIn.Position,

			Entries_Pre = pre,
			Entries_Post = post,

			// Auto swap?
			Value = (autoSwap ? 1 : 0)
		});
	}

	// Updates list of players that have been on court in each zone
	private void UpdateValidSubs()
	{
		// Check each zone (1-6)
		for ( int zone = 1; zone <= Lineup.BaseEntries; zone++ )
		{
			List<Player> list = ValidSubs[ zone ];
			LineupEntry entry = Lineup1.Entries[ zone - 1 ];

			// Track any player that has been in zone
			if ( !list.Contains( entry.Player ) )
			{
				list.Add( entry.Player );
			}
		}
	}

	// Determines if specified player is recommended, invalid, or normal sub
	public LineupMenuRow.RowState GetSubState( int zone, Player player )
	{
		int lineupZone = Lineup.GetLineupIndex( (zone - 1), Rotation1 ) + 1;

		// Recommended if player has already been in zone
		if ( ValidSubs[ lineupZone ].Contains( player ) )
		{
			return LineupMenuRow.RowState.Positive;
		}

		// Invalid if player has already been in any other zone
		for ( int z = 1; z <= Lineup.BaseEntries; z++ )
		{
			if ( (z != lineupZone) && ValidSubs[ z ].Contains( player ) )
			{
				return LineupMenuRow.RowState.Negative;
			}
		}

		// Normal state
		return LineupMenuRow.RowState.Normal;
	}

	/* Location */

	// Handles x,y normalization requests
	public virtual Point Normalize( double x, double y )
	{
		return Point.Zero;
	}

	// Handles x,y de-normalization requests
	public virtual Point Denormalize( double x, double y )
	{
		return Point.Zero;
	}

	/* Lineup */

	// Returns current server (zone 1)
	public abstract LineupEntry GetServer( bool isTeam1 );

	// Returns current setter (backrow first)
	public abstract LineupEntry GetSetter( bool isTeam1 );

	// Returns list of all players currently on court (rotation order)
	public abstract List<LineupEntry> GetTeam( bool isTeam1 );

	// Returns list of players currently in frontrow (zones 2-4)
	public abstract List<LineupEntry> GetFrontrow( bool isTeam1 );

	/* Utilities */

	// Returns team responsible for current stat (team that played ball)
	public Team BallTeam()
	{
		return (BallSide == Side.SideA) ? TeamA : TeamB;
	}

	// Returns unique ID of current ball team
	public string BallId()
	{
		Team team = BallTeam();

		// Not first order team
		if ( team == null )
		{
			Opponent opponent = Set.Match.Opponent;

			// Either opponent or anonymous
			return opponent?.UniqueId;
		}

		// Team 1 or first order Team 2
		return team.UniqueId;
	}

	// Returns current rotation for team with ball
	public int BallRotation()
	{
		return Equals( BallTeam(), TeamA ) ? RotationA : RotationB;
	}

	// Determines if specified team ID is for primary team (Team 1)
	public bool IsTeam1( string teamId )
	{
		return Team1.UniqueId.Equals( teamId );
	}

	// Returns unique ID of team that currently has serve possession
	public string ServeId()
	{
		// Opponent (possibly anonymous)
		if ( ServeTeam == null )
		{
			Opponent opponent = Set.Match.Opponent;

			return opponent?.UniqueId;
		}

		// First order team
		return ServeTeam.UniqueId;
	}

	// Returns unique ID of team that is NOT currently serving
	public string ReceiveId()
	{
		// Opponent (possibly anonymous)
		if ( ReceiveTeam == null )
		{
			Opponent opponent = Set.Match.Opponent;

			return opponent?.UniqueId;
		}

		// First order team
		return ReceiveTeam.UniqueId;
	}

	// Determines if Team 1 is currently serving
	public bool Team1Serving()
	{
		return IsTeam1( ServeId() );
	}

	// Does Team 1 currently have ball on their side of court?
	public bool Team1HasBall()
	{
		return (BallSide == SideForTeam( 1 ));
	}

	// Returns side A/B currently corresponding to Team 1/2
	public Side SideForTeam( int number )
	{
		return (number == 1) ? (Team1OnSideA ? Side.SideA : Side.SideB) : (Team1OnSideA ? Side.SideB : Side.SideA);
	}

	// Returns score for Team 1/2 (converted from court side A/B)
	public int ScoreForTeam( int number )
	{
		return (number == 1) ? (Team1OnSideA ? ScoreA : ScoreB) : (Team1OnSideA ? ScoreB : ScoreA);
	}

	// Returns rotation for Team 1/2 (converted from court side A/B)
	public int RotationForTeam( int number )
	{
		return (number == 1) ? (Team1OnSideA ? RotationA : RotationB) : (Team1OnSideA ? RotationB : RotationA);
	}

	// Returns subs remaining for Team 1/2 (converted from court side A/B)
	public int SubsForTeam( int number )
	{
		return (number == 1) ? (Team1OnSideA ? SubsA : SubsB) : (Team1OnSideA ? SubsB : SubsA);
	}

	// Returns display name for specified team
	public string NameForTeam( Team team )
	{
		Match match = Set.Match;

		// Team 2 (anonymous opponent)
		if ( team == null )
		{
			return match.Team2Name;
		}

		// Team 1 (always first order), Team 2 (named or first order)
		return team.Equals( match.Team1 ) ? match.Team1Name : match.Team2Name;
	}

	// Returns team object matching specified unique ID
	public Team TeamForId( string teamId )
	{
		// Team 2 (anonymous or named opponent)
		if ( teamId == null )
		{
			return null;
		}

		Match match = Set.Match;

		// Team 1 (always first order), Team 2 (named or first order)
		return teamId.Equals( match.Team1.UniqueId ) ? match.Team1 : match.Team2;
	}

	// Returns unique ID for specified team
	public string IdForTeam( Team team )
	{
		Match match = Set.Match;

		// Team 2
		if ( team == null )
		{
			// First order
			if ( match.Team2 != null )
			{
				return match.Team2.UniqueId;
			}
			
			// Named
			if ( match.Opponent != null )
			{
				return match.Opponent.UniqueId;
			}
		}
		// Team 1 (always first order)
		else if ( team.Equals( match.Team1 ) )
		{
			return match.Team1.UniqueId;
		}

		// Anonymous
		return null;
	}

	// Returns unique ID for team 1 (primary) or 2 (opponent)
	public string IdForTeam( int number )
	{
		return IdForSide( SideForTeam( number ) );
	}

	// Returns team currently on specified side of court
	public Team TeamForSide( Side side )
	{
		return (side == Side.SideA) ? TeamA : TeamB;
	}

	// Returns unique ID for team on specified side of court
	public string IdForSide( Side side )
	{
		bool oneOnA = Team1OnSideA;

		Match match = Set.Match;
		Opponent opponent = match.Opponent;

		// Side A
		if ( side == Side.SideA )
		{
			// Team 1 (always first order)
			if ( oneOnA )
			{
				return match.Team1.UniqueId;
			}

			// Team 2 (anonymous, named, or first order)
			return (TeamA == null) ? opponent?.UniqueId : TeamA.UniqueId;
		}

		// Side B

		// Team 1 (always first order)
		if ( !oneOnA )
		{
			return match.Team1.UniqueId;
		}

		// Team 2 (anonymous, named, or first order)
		return (TeamB == null) ? opponent?.UniqueId : TeamB.UniqueId;
	}

	/* Video */

	// Returns current time offset (ms) for video sync
	public async Task<int> GetVideoOffset()
	{
		return await parent.GetVideoOffset();
	}

	/* Process */

	// Handles LEGACY point
	public virtual async Task ProcessPoint( RecordData data, Team team )
	{
		await ProcessPoint( data, Side.Unknown, team, true );
	}

	// Handles RALLY point or any manually awarded point
	public virtual async Task ProcessPoint( RecordData data, Side side )
	{
		await ProcessPoint( data, side, null, false );
	}

	// Handles functionality common to all recorded point stats
	private async Task ProcessPoint( RecordData data, Side side, Team team, bool legacy )
	{
		// MUST be here to support undo libero auto swap out
		State = "start";

		// Must track receiving team (for sideout stats) before sideout change
		data.ReceiveId = ReceiveId();

		// Might be forcing the point (direct score tap)
		bool forced = (side != Side.Unknown);

		// Team CAUSING point (error or earned, may NOT be same as team earning point)
		string causedId = forced ? IdForSide( side ) : (legacy ? team?.UniqueId : BallId());

		data.CausedId = causedId;

		// Determine sideout
		bool error = data.IsError;
		bool servingTeam = (causedId == ServeId());

		bool sideout = (error && servingTeam) || (!error && !servingTeam);

		// Earned point?
		bool earned = Stat.IsEarned( data.PrevResult );

		data.Action = Stats.PointKey;
		data.Sideout = sideout;
		data.Earned = earned;

		// NA for points
		data.PrevValue = null;
		data.Prev2Value = null;

		// MUST clear for Legacy only
		if ( legacy )
		{
			data.Prev3Value = null;
		}

		// Determine team actually AWARDED point
		Team pointTeam = sideout ? ReceiveTeam : ServeTeam;

		data.TeamId = IdForTeam( pointTeam );

		// Update score
		bool pointA = Equals( pointTeam, TeamA );

		if ( pointA )
		{
			ScoreA++;
		}
		else
		{
			ScoreB++;
		}

		scoreboard.UpdateScore( ScoreA, ScoreB );
		scoreboard.DisableButtons( false );

		// Rotation of team earning point
		data.Rotation = pointA ? RotationA : RotationB;

		// Rotation of RECEIVING team
		data.RecvRotation = (ServeSide == Side.SideA) ? RotationB : RotationA;

		// Rotation and serve update with sideout
		if ( sideout )
		{
			// Advance rotation (1-based)
			if ( Equals( ReceiveTeam, TeamA ) )
			{
				RotationA = ((RotationA % Lineup.BaseEntries) + 1);
			}
			else
			{
				RotationB = ((RotationB % Lineup.BaseEntries) + 1);
			}

			// Switch serve side
			ChangeServeSide( true );
		}

		// Must switch ball before persist
		BallSide = ServeSide;

		// PERSIST
		await db.RecordStat( data, true );

		// AUTO: process auto subs/swaps (only on sideout)
		if ( sideout )
		{
			auto.Process( RecordAuto.AutoBoth, Rotation1, Team1Serving() );
		}
	}

	// Handles all team/player action stats
	public async Task ProcessAction( RecordData data, string teamId )
	{
		data.TeamId = teamId;

		// Track rotations
		if ( data.IsServe )
		{
			// Must use previous point NOT stat (may have been sub/swap)
			Stat prev = PreviousStat( Stats.PointKey );

			// First serve of set, or first serve after sideout
			data.Value = (((prev == null) || prev.Point.Sideout) ? 1 : 0);
		}

		// Persist
		await db.RecordStat( data );
	}

	// Handles all team/player action stats
	public async Task ProcessAction( RecordData data )
	{
		await ProcessAction( data, BallId() );
	}

	// Handles special end set event
	public void ProcessEnd()
	{
		RecordData data = new()
		{
			Action = Stats.UpdateKey,
			Selector = Stats.EndKey,
			Modifier = "set"
		};

		db.RecordUpdate( data );
	}

	/* Restore */

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

		// Reset counter
		db.SetCounter( (short)(stat.Counter + 1) );

		// Restore all fields
		State = (stat.IsUpdate || stat.IsPoint) ? "start" : state.State;
		StateSelector = state.StateSelector;

		bool oneOnA = Team1OnSideA;

		TeamA = TeamForId( state.TeamAId );
		TeamB = TeamForId( state.TeamBId );

		bool sideSwitch = (Team1OnSideA != oneOnA);

		ServeSide = (Side)state.ServeSide;
		BallSide = (Side)state.BallSide;

		RotationA = state.RotationA;
		RotationB = state.RotationB;

		ScoreA = state.ScoreA;
		ScoreB = state.ScoreB;

		TimeoutsA = state.TimeoutsA;
		TimeoutsB = state.TimeoutsB;

		SubsA = state.SubsA;
		SubsB = state.SubsB;

		LiberoA = state.LiberoA;
		LiberoB = state.LiberoB;

		Touches = state.Touches;
		Transitions = state.Transitions;

		ServeCount = state.ServeCount;

		// Make sure serve/receive correct
		ChangeServeSide();

		// Restore lineup
		if ( state.HasEntries )
		{
			RecordLineup lineup = RecordLineup.Create( state.Entries1 );

			Lineup1.FromLineup( lineup );
		}

		// Restore menus
		if ( stat.IsServe )
		{
			StartRally();
		}

		// Update markers, undo history, etc
		if ( update )
		{
			UpdateUI();

			// Side switch requires full redraw
			if ( sideSwitch )
			{
				parent.UpdateLayout();
			}
		}
	}

	// Replays stats for an in-progress set
	public async Task Restore( Set set, bool populate = true )
	{
		// Must ensure cache populated first
		if ( set.IsInProgress && populate )
		{
			await set.PopulateStats( true );
		}

		db.StatCache = set.StatCache;

		// Must set config first
		await Start( true, false );

		// Restore each stat
		foreach ( Stat stat in set.StatCache )
		{
			RestoreState( stat, false );
		}

		// Update UI
		UpdateUI();
	}

	/* Previous */

	// Passthrough to state data
	public Stat PreviousStat( int back = 1, bool actionOnly = true )
	{
		// Ignore non-action events
		return db.PreviousStat( back, actionOnly );
	}

	// Passthrough to state data
	public Stat PreviousStat( string action )
	{
		return db.PreviousStat( action );
	}

	// Returns action of previous event, if any
	public string PreviousAction()
	{
		return db.PreviousStat()?.Action;
	}

	/* UI Update */

	// Updates scoreboard and undo history
	public virtual void UpdateUI()
	{
		bool startState = (State == "start");
		bool scoreDisabled = !Set.Legacy && startState;

		// Scoreboard
		UpdateScoreboard();

		scoreboard.DisableButtons( !startState );
		scoreboard.DisableScore( scoreDisabled );

		// Undo history
		UpdateUndo();
	}

	// Updates scoreboard with all current state info
	private void UpdateScoreboard()
	{
		// Score
		scoreboard.UpdateScore( ScoreA, ScoreB );

		// Possession
		scoreboard.UpdatePossession( ServeSide );

		// Timeouts
		scoreboard.UpdateTimeouts( TimeoutsA, TimeoutsB );

		Match match = Set.Match;
		bool oneOnA = Team1OnSideA;

		// Sets
		int setsA = oneOnA ? match.Sets1 : match.Sets2;
		int setsB = oneOnA ? match.Sets2 : match.Sets1;

		scoreboard.UpdateSets( setsA, setsB );

		// Teams
		string teamA = oneOnA ? match.Team1Abbrev : match.Team2Abbrev;
		string teamB = oneOnA ? match.Team2Abbrev : match.Team1Abbrev;

		scoreboard.UpdateTeams( teamA, teamB );

		// Team colors
		Color color1 = match.Team1Color;
		Color color2 = match.Team2Color;

		Color colorA = oneOnA ? color1 : color2;
		Color colorB = oneOnA ? color2 : color1;

		scoreboard.UpdateTeamColors( colorA, colorB );
	}

	// Updates team specific display
	public abstract void UpdateTeambar( Side side );

	// Updates number subs remaining
	public abstract void UpdateSubs( Side side );

	// Updates all currently displayed players and libero(s)
	public abstract void UpdatePlayers();

	// Updates scoreboard undo stack
	public void UpdateUndo()
	{
		undoStack.Update();
	}

	/* Debug */

	// Outputs debug info for state machine status
	private void DebugState()
	{
		DXLog.Trace( "STATE:{0} BALL:{1}", State, BallSide );
	}
}

//
