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

using DXLib.UI;
using DXLib.UI.Layout;
using DXLib.UI.Gestures;

using DXLib.UI.Control;
using DXLib.UI.Control.Button;

using DXLib.Log;
using DXLib.Utils;

namespace iStatVball3;

/*
 * Implementation of the stat recording interface for the RallyFlow engine. Primary child containers are the scoreboard,
 * court, and team bars.
 */ 
public class RallyEngine : RecordEngine
{
	/* Constants */

	// RallyFlow engine can be used in multiple modes
	public enum EngineMode
	{
		Record,
		Sync
	}

	// Touch handling
	private const int LongPressDelay = 500;
	private const double SwipeDistance = 30;

	// Mobile drawer animation
	private const int DrawerTime = 333;
	private readonly Easing DrawerEasing = Easing.CubicOut;

	/* Properties */

	// Record/Sync modes
	public EngineMode Mode { get; }
	public bool IsSyncMode => (Mode == EngineMode.Sync);

	// Disable control
	public override bool IsFaultDisabled { set { if ( faultBtn != null ) faultBtn.IsDisabled = value; } }
	public override bool IsUndoDisabled { set { if ( undoBtn != null ) undoBtn.IsDisabled = value; } }

	public bool IsCourtDisabled { get => court.IsDisabled; set => court.IsDisabled = value; }

	// Used for overlay display
	public Rect CourtBounds => court.LayoutBounds;

	// Sizing
	public double TeambarSize { get; private set; }

	// Used to prevent overlay tap throughs onto court
	private bool IsDrawerOpen => (scoreOpen || teamsOpen);
	private bool IsTeamOverlayOpen => (TeambarA.IsModalOpen || TeambarB.IsModalOpen);

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

	/* Fields */

	// Child containers
	private readonly CourtView court;

	private readonly TeamBar teambar1;
	private readonly TeamBar teambar2;

	// Owned here
	private readonly ActionOverlay overlay;

	// Mobile buttons
	private readonly DXIconButton scoreBtn;
	private readonly DXIconButton teamsBtn;
	private readonly DXIconButton faultBtn;
	private readonly DXIconButton undoBtn;

	private readonly ScoreSmall scoreABtn;
	private readonly ScoreSmall scoreBBtn;

	private readonly DXLabel teamALbl;
	private readonly DXLabel teamBLbl;

	private readonly DXAbsoluteLayout cover1;
	private readonly DXAbsoluteLayout cover2;
	
	private bool scoreOpen;
	private bool teamsOpen;

	// Touch handling
	private Point touchPt;
	private Timer touchTimer;
	private bool touchPress;

	// Engine specific ref
	private RallyState rallyState;
	
	/* Methods */
	public RallyEngine( DXAbsoluteGestures layout, EngineMode mode ) : base( layout )
	{
		Mode = mode;

		// Create children
		Scoreboard = new RallyScore( this );
		court = new CourtView( mode );

		teambar1 = new TeamBar( this, layout );
		teambar2 = new TeamBar( this, layout );
		
		// ONLY source of events in recording UI
		layout.Down += OnDown;
		layout.Up += OnUp;

		bool mobile = DXDevice.IsMobile;
		
		// Court must be added first
		layout.Add( court );

		// Create and add mobile buttons (above court)
		if ( mobile )
		{
			scoreBtn = CreateButton( "home", OnScoreTapped );
			teamsBtn = CreateButton( "player", OnTeamsTapped );
			faultBtn = CreateButton( "fault", OnFaultTapped, OnFaultDown );
			undoBtn = CreateButton( "undo", OnUndoTapped );

			// Scorecards
			scoreABtn = new ScoreSmall { ButtonTapped = OnScoreATapped };
			scoreBBtn = new ScoreSmall { ButtonTapped = OnScoreBTapped };

			// Team labels
			teamALbl = CreateLabel();
			teamBLbl = CreateLabel();

			// Hides drawers in safe area
			cover1 = CreateCover();
			cover2 = CreateCover();
				
			// Undo has long press action
			undoBtn.ButtonPressed = OnUndoPressed;

			faultBtn.IsDisabled = true;
			undoBtn.IsDisabled = true;

			layout.Add( scoreBtn );
			layout.Add( teamsBtn );
			layout.Add( faultBtn );
			layout.Add( undoBtn );

			layout.Add( scoreABtn );
			layout.Add( scoreBBtn );

			layout.Add( teamALbl );
			layout.Add( teamBLbl );
		}

		// Court overlays (below drawers)
		teambar1.AddCourt();
		teambar2.AddCourt();

		// Libero overlays
		teambar1.AddLibero();
		teambar2.AddLibero();

		// Drawers (above buttons)
		layout.Add( Scoreboard );
		layout.Add( teambar1 );
		layout.Add( teambar2 );

		// Covers (above drawers)
		if ( mobile )
		{
			layout.Add( cover1 );
			layout.Add( cover2 );
		}
		
		// Action menus (above all)
		overlay = new ActionOverlay( this, layout );

		((RallyScore)Scoreboard).FaultTapped = overlay.OnFaultTapped;

		// Swipe config
		MR.Gestures.Settings.MinimumDeltaDistance = 2;
	}

	// Used internally to create mobile corner buttons
	private static DXIconButton CreateButton( string resource, Action callback, Action downCallback = null )
	{
		return new DXIconButton
		{
			Resource = resource,

			ButtonColor = DXColors.Dark2,
			CornerRadius = 0,
			HasShadow = false,

			IconColor = DXColors.Light4,
			IconScale = 0.65f,

			IsSticky = false,

			ButtonDown = downCallback,
			ButtonTapped = callback
		};
	}

	// Used internally to create mobile team name label
	private static DXLabel CreateLabel()
	{
		return new DXLabel
		{
			TextColor = DXColors.Light4,
			Font = DXFonts.RobotoBoldItalic,
			FontSize = 12
		};
	}

	// Used internally to create cover over drawers in safe area
	private static DXAbsoluteLayout CreateCover()
	{
		return new DXAbsoluteLayout
		{
			IgnoreSafeArea = true,
			BackgroundColor = DXColors.Dark2,
				
			Horizontal = LayoutOptions.Fill,
			Vertical = LayoutOptions.Fill
		};
	}
	
	// Configures recording UI for new set
	public override async Task Init( Set set )
	{
		await base.Init( set );

		// Init children
		court.Init( set );

		if ( lineup1.IsLineup )
		{
			teambar1.Init( team1, set.Roster1 );
		}

		if ( lineup2.IsLineup )
		{
			teambar2.Init( team2, Set.Roster2 );
		}

		// Mobile specific
		if ( DXDevice.IsMobile )
		{
			scoreBtn.Init();
			teamsBtn.Init();
			faultBtn.Init();
			undoBtn.Init();
		}
		
		// UI references
		config.Overlay = overlay;
		config.Court = court;
		config.Teambar1 = teambar1;
		config.Teambar2 = teambar2;

		// Init master state machine
		StateMachine = new RallyState( config );

		rallyState = (RallyState)StateMachine;

		Scoreboard.StateMachine = rallyState;
		court.StateMachine = rallyState;
	}

	// Ends stat recording for a set
	public override async Task End( int points1, int points2 )
	{
		// Must make sure overlays are closed
		teambar1.HideOverlay();
		teambar2.HideOverlay();

		// Persist end data
		await base.End( points1, points2 );
	}

	/* Mobile */

	// Updates scorecards on mobile
	public override void UpdateScore( int scoreA, int scoreB )
	{
		if ( DXDevice.IsMobile )
		{
			scoreABtn.Score = scoreA;
			scoreBBtn.Score = scoreB;
		}
	}

	// Updates scorecard team names on mobile
	public override void UpdateTeams( string teamA, string teamB )
	{
		if ( DXDevice.IsMobile )
		{
			teamALbl.Text = teamA;
			teamBLbl.Text = teamB;
		}
	}

	// Updates score card colors on mobile
	public override void UpdateTeamColors( Color colorA, Color colorB )
	{
		if ( DXDevice.IsMobile )
		{
			scoreABtn.BarColor = colorA;
			scoreBBtn.BarColor = colorB;
		}
	}

	/* Drawers */

	// Closes drawers if any are open (MOBILE ONLY)
	public override async Task<bool> CloseDrawers()
	{
		if ( DXDevice.IsMobile )
		{
			// Scoreboard
			if ( scoreOpen )
			{
				await CloseScore();
				return true;
			}

			// Teams
			if ( teamsOpen )
			{
				await CloseTeams();
				return true;
			}
		}

		return false;
	}

	// TEAMS

	// Opens both team drawers (MOBILE ONLY)
	public async Task OpenTeams()
	{
		if ( DXDevice.IsMobile )
		{
			overlay.HideAll();
			
			TeamBar barA = TeambarA;
			TeamBar barB = TeambarB;

			barA.IsVisible = true;
			barB.IsVisible = true;

			// Landscape
			if ( DXDevice.IsLandscape() )
			{
				await Task.WhenAll
				(
					barA.TranslateTo( barA.LayoutWidth, 0, DrawerTime, DrawerEasing ),
					barB.TranslateTo( -barB.LayoutWidth, 0, DrawerTime, DrawerEasing )
				);
			}
			// Portrait
			else
			{
				await Task.WhenAll
				(
					barA.TranslateTo( 0, barA.LayoutHeight, DrawerTime, DrawerEasing ),
					barB.TranslateTo( 0, -barB.LayoutHeight, DrawerTime, DrawerEasing )
				);
			}

			teamsOpen = true;
		}
	}
	
	// Closes both team drawers
	private async Task CloseTeams()
	{
		TeamBar barA = TeambarA;
		TeamBar barB = TeambarB;

		// Animate closed
		if ( DXDevice.IsLandscape() )
		{
			await Task.WhenAll
			(
				barA.TranslateTo( -barA.LayoutWidth, 0, DrawerTime, DrawerEasing ),
				barB.TranslateTo( barB.LayoutWidth, 0, DrawerTime, DrawerEasing )
			);
		}
		else
		{
			await Task.WhenAll
			(
				barA.TranslateTo( 0, -barA.LayoutHeight, DrawerTime, DrawerEasing ),
				barB.TranslateTo( 0, barB.LayoutHeight, DrawerTime, DrawerEasing )
			);
		}

		teamsBtn.Reset();

		teambar1.IsVisible = false;
		teambar2.IsVisible = false;

		teamsOpen = false;
	}

	// Tests if specified touch point is within teambar a/b bounds
	private bool TeambarContains( Point touch, bool barA )
	{
		TeamBar bar = barA ? TeambarA : TeambarB;

		if ( teamsOpen )
		{
			bool landscape = DXDevice.IsLandscape();
			Rect bounds = bar.LayoutBounds;

			double wd = bounds.Width;
			double ht = bounds.Height;

			// Calc bar location
			Point adjusted = new()
			{
				X = landscape ? (touch.X - (barA ? wd : -wd)) : touch.X,
				Y = landscape ? touch.Y : (touch.Y - (barA ? ht : -ht))
			};

			// Hit?
			return bounds.Contains( adjusted );
		}

		return false;
	}

	// SCOREBOARD

	// Opens scoreboard drawer (MOBILE ONLY)
	private async Task OpenScore()
	{
		if ( DXDevice.IsMobile )
		{
			overlay.HideAll();
			
			Scoreboard.IsVisible = true;

			// Animate open
			if ( DXDevice.IsLandscape() )
			{
				await Scoreboard.TranslateTo( 0, Scoreboard.LayoutHeight - (DXDevice.IsIOS ? 0 : 1), DrawerTime, DrawerEasing );
			}
			else
			{
				await Scoreboard.TranslateTo( -Scoreboard.LayoutWidth, 0, DrawerTime, DrawerEasing );
			}

			scoreOpen = true;
		}
	}

	// Closes scoreboard drawer
	private async Task CloseScore()
	{
		// Animate closed
		if ( DXDevice.IsLandscape() )
		{
			await Scoreboard.TranslateTo( 0, -Scoreboard.LayoutHeight, DrawerTime, DrawerEasing );
		}
		else
		{
			await Scoreboard.TranslateTo( Scoreboard.LayoutWidth, 0, DrawerTime, DrawerEasing );
		}

		scoreBtn.Reset();

		Scoreboard.IsVisible = false;
		scoreOpen = false;
	}

	// Tests if specified touch point is within scoreboard bounds
	private bool ScoreboardContains( Point touch )
	{
		if ( scoreOpen )
		{
			bool landscape = DXDevice.IsLandscape();

			Rect bounds = Scoreboard.LayoutBounds;

			// Adjust for hit detection
			Point adjusted = new()
			{
				X = landscape ? touch.X : (touch.X + bounds.Width),
				Y = landscape ? (touch.Y - bounds.Height) : touch.Y
			};

			// Hit?
			return bounds.Contains( adjusted );
		}

		return false;
	}

	/* Event Callbacks */

	// User touched down anywhere in recording UI
	private void OnDown( object sender, MR.Gestures.DownUpEventArgs args )
	{
		// NA while report/log open
		if ( !Scoreboard.IsDrawerOpen && !IsTeamOverlayOpen )
		{
			touchPress = false;

			// Remember touch location
			touchPt = args.Touches[0];

			// Touch down on open menu
			if ( overlay.IsActive )
			{
				overlay.OnDown( touchPt );
			}
		
			// Might be starting long press
			touchTimer = new Timer( OnLongPress );
			touchTimer.Change( LongPressDelay, 0 );
		}
	}

	// Sufficient time elapsed for touch down to be long press
	private void OnLongPress( object timer )
	{
		touchPress = true;
		touchTimer.Dispose();

		// Call must occur on main UI thread
		MainThread.BeginInvokeOnMainThread( () => HandleLongPress( touchPt ) );
	}

	// User released finger after touch down
	private async void OnUp( object sender, MR.Gestures.DownUpEventArgs args )
	{
		try
		{ 
			// NA while report/log open
			if ( !Scoreboard.IsDrawerOpen && !IsTeamOverlayOpen )
			{
				// No longer eligible for long press
				if ( touchTimer != null )
				{
					await touchTimer.DisposeAsync();
				}
	
				// Ignore long press release
				if ( touchPress )
				{
					return;
				}
	
				// Release on open menu
				if ( overlay.IsActive )
				{
					overlay.OnUp( args.Center );
				}
				// Release on court
				else
				{
					// Distance between tap and release point
					double distance = DXGraphics.Distance( touchPt, args.Touches[0] );
	
					// Traveled enough for swipe gesture
					if ( distance > SwipeDistance )
					{
						HandleSwipe( touchPt );
					}
					// Normal tap
					else
					{
						await HandleTap( touchPt );
					}
				}
			}
		}
		catch ( Exception ex )
		{
			DXLog.Exception( "rally.onup", ex );
		}
	}

	// Main tap handling entry point for recording UI
	private async Task HandleTap( Point touch )
	{
		if ( DXDevice.IsMobile )
		{
			// Scoreboard swallows touch
			if ( ScoreboardContains( touch ) )
			{
				return;
			}

			// Both team bars swallow touch
			if ( TeambarContains( touch, true ) || TeambarContains( touch, false ) )
			{
				return;
			}

			// Any tap not on drawer closes drawers
			if ( await CloseDrawers() )
			{
				return;
			}

			// Ignore button taps (handled by buttons)
			if ( scoreBtn.Contains( touch ) || teamsBtn.Contains( touch ) ||
			     faultBtn.Contains( touch ) || undoBtn.Contains( touch ) ||
				 scoreABtn.Contains( touch ) || scoreBBtn.Contains( touch ) )
			{
				return;
			}
		}

		// Feed tap on court into state machine
		if ( !court.IsDisabled && CourtBounds.Contains( touch ) )
		{
			rallyState.HandleTouch( touch.X, touch.Y, RallyUI.TouchType.Tap );
		}
	}

	// Entry point for special swipe gesture
	private void HandleSwipe( Point touch )
	{
		rallyState.HandleTouch( touch.X, touch.Y, RallyUI.TouchType.Swipe );
	}

	// Entry point for long press gesture
	private void HandleLongPress( Point touch )
	{
		// Long press on open menu
		if ( overlay.IsActive )
		{
			overlay.OnLongPress( touch );
		}
		// Long press directly on court
		else
		{
			if ( !IsDrawerOpen )
			{
				rallyState.HandleTouch( touch.X, touch.Y, RallyUI.TouchType.LongPress );
			}
		}
	}

	/* Mobile Callbacks */

	// Opens both team drawers on mobile
	private async void OnTeamsTapped()
	{
		await OpenTeams();
	}

	// Opens score drawer on mobile
	private async void OnScoreTapped()
	{
		await OpenScore();
	}

	// Show fault menu, optionally with flyout
	private void OnFaultDown()
	{
		if ( rallyState.HasFaults() )
		{
			Rect bounds = faultBtn.Bounds;
			const double edge = ActionOverlay.Edge;

			// Calc location for popup
			double maxX = (DXDevice.GetScreenWd() - edge - FlyoutBar.DefaultBarWd);
			double minY = (DXDevice.GetSafeTop() + edge);

			double x = Math.Min( bounds.X, maxX );
			double y = Math.Max( bounds.Y, minY );

			// Show popup
			overlay.OnFaultTapped( x, y );
		}
	}

	// Fault acts like normal button when flyouts off
	private void OnFaultTapped()
	{
		if ( !rallyState.HasFaults() )
		{
			overlay.OnFaultTapped( 0, 0 );
		}
	}

	// Handles undo tap on mobile
	private void OnUndoTapped()
	{
		Scoreboard.OnUndoTapped();
	}

	// Handles undo rally long press on mobile
	private void OnUndoPressed()
	{
		Scoreboard.OnUndoPressed();

		undoBtn.Reset();
	}

	// Handles forced point for Team A on mobile
	private void OnScoreATapped()
	{
		Scoreboard.OnScoreATapped();
	}

	// Handles forced point for Team B on mobile
	private void OnScoreBTapped()
	{
		Scoreboard.OnScoreBTapped();
	}

	/* Layout */

	// Some controls require special handling on rotate
	public override void Rotate()
	{
		base.Rotate();

		teambar1.Rotate();
		teambar2.Rotate();
	}

	// Returns RallyFlow specific layout orientation
	public LayoutType GetLayoutType()
	{
		// Sync mode forces landscape
		LayoutType type = IsSyncMode ? LayoutType.Landscape : DXDevice.GetLayoutType();

		// Special handling for widescreen
		if ( DXDevice.IsTabletWide )
		{
			type = (type == LayoutType.Landscape) ? LayoutType.WideLandscape : LayoutType.WidePortrait;
		}

		return type;
	}

	// Redraws entire RallyFlow recording UI
	public override async void UpdateLayout( LayoutType type )
	{
		bool mobile = DXDevice.IsMobile;

		// Do NOT use param type
		LayoutType customType = GetLayoutType();

		// Update for orientation
		base.UpdateLayout( customType );

		// Update children
		Scoreboard.UpdateLayout( customType );
		
		teambar1.UpdateLayout( customType );
		teambar2.UpdateLayout( customType );

		// Action menu must respond to rotation
		overlay.UpdateLayout( customType );

		// Update court texture/colors
		await Venue.UpdateCourt( court, recordSet.Match.Venue );

		// Reset in case rotated while open
		if ( mobile )
		{
			scoreBtn.Reset();
			teamsBtn.Reset();
		}
	}

	// Landscape (4:3) [dynamic]
	protected override void Landscape()
	{
		// Support sync mode
		double x = LayoutBounds.X;
		double y = LayoutBounds.Y;

		double wd = LayoutBounds.Width;
		double ht = LayoutBounds.Height;

		double safeTop = DXDevice.GetSafeTop();
		double safeBottom = DXDevice.SafeArea().Bottom;

		ScoreboardWd = wd;
		ScoreboardHt = (ht * 0.169);

		TeambarSize = (ht * 0.096);

		// Calc locations
		double scoreHt = (ScoreboardHt + (IsSyncMode ? 0 : safeTop));

		double courtY = (y + scoreHt);
		double courtHt = (ht - safeBottom - TeambarSize - scoreHt);

		double teamY = (courtY + courtHt);
		double teamWd = (wd / 2);
		double teamHt = TeambarSize;

		// Place views
		Scoreboard.SetLayoutBounds( layout, x, y, wd, scoreHt );
		court.SetLayoutBounds( layout, x, courtY, wd, courtHt );
		TeambarA.SetLayoutBounds( layout, x, teamY, teamWd, teamHt );
		TeambarB.SetLayoutBounds( layout, teamWd, teamY, teamWd, teamHt );
	}

	// Widescreen Landscape (16:10)
	protected override void WideLandscape()
	{
		double wd = DXDevice.GetScreenWd();
		double ht = DXDevice.GetScreenHt();

		double statusHt = DXDevice.GetSafeTop();

		ScoreboardWd = wd;
		ScoreboardHt = 120;

		TeambarSize = 85;

		// Calc locations
		double scoreHt = (ScoreboardHt + statusHt);

		double teamsWd = TeambarSize;
		double teamsHt = (ht - scoreHt - statusHt);

		double courtWd = wd - (teamsWd * 2);
		double courtHt = teamsHt;

		// Place views
		layout.SetBounds( Scoreboard, 0, 0, wd, scoreHt );
		layout.SetBounds( court, teamsWd, scoreHt, courtWd, courtHt );
		layout.SetBounds( TeambarA, 0, scoreHt, teamsWd, teamsHt );
		layout.SetBounds( TeambarB, (teamsWd + courtWd), scoreHt, teamsWd, teamsHt );
	}

	// Portrait (3:4) [dynamic]
	protected override void Portrait()
	{
		bool ios = DXDevice.IsIOS;

		double wd = DXDevice.GetScreenWd();
		double ht = DXDevice.GetScreenHt();

		double safeTop = DXDevice.GetSafeTop();

		// Calc location
		const double courtX = 0;
		double courtY = ios ? safeTop : 0;

		double scoreWd =  (ht * 0.199);
		double courtWd = (wd - scoreWd);

		double courtHt = (ht - safeTop);
		double scoreHt = courtHt;

		double teamY1 = (ht * 0.129) + (ios ? safeTop : -5);
		double teamY2 = (ht * 0.642) + (ios ? safeTop : -5);
		double teamHt = (ht * 0.197);

		// Place views
		court.SetLayoutBounds( layout, courtX, courtY, courtWd, courtHt );
		TeambarA.SetLayoutBounds( layout, courtWd, teamY1, scoreWd, teamHt );
		Scoreboard.SetLayoutBounds( layout, courtWd, courtY, scoreWd, scoreHt );
		TeambarB.SetLayoutBounds( layout, courtWd, teamY2, scoreWd, teamHt );
	}

	// Widescreen Portrait (10:16)
	protected override void WidePortrait()
	{
		double wd = DXDevice.GetScreenWd();
		double ht = DXDevice.GetScreenHt();

		double statusHt = DXDevice.GetSafeTop();

		ScoreboardWd = 178;
		ScoreboardHt = ht;

		TeambarSize = 75;

		// Calc locations
		double teamsHt = TeambarSize;

		double scoreWd = ScoreboardWd;
		double scoreHt = ht - (teamsHt * 2) - statusHt;

		double courtWd = wd - scoreWd;
		double courtHt = scoreHt;

		// Place views
		layout.SetBounds( court, 0, teamsHt, courtWd, courtHt );
		layout.SetBounds( Scoreboard, courtWd, teamsHt, scoreWd, scoreHt );
		layout.SetBounds( TeambarA, 0, 0, wd, teamsHt );
		layout.SetBounds( TeambarB, 0, (teamsHt + courtHt), wd, teamsHt );
	}

	// Mobile Landscape
	protected override void MobileLandscape()
	{
		bool ios = DXDevice.IsIOS;

		double screenWd = DXDevice.GetScreenWd();
		double screenHt = DXDevice.GetScreenHt();
		
		Thickness safeArea = DXDevice.SafeArea();
		double cornerArea = DXDevice.CornerArea();

		// Calc bounds (minus safe areas)
		double safeLeft = Math.Max( safeArea.Left, cornerArea );
		double safeRight = Math.Max( safeArea.Right, cornerArea );

		double x = safeLeft;
		double x2 = (screenWd - safeRight);

		double y = (ios ? 0 : safeArea.Top);

		double wd = (screenWd - safeRight - safeLeft);
		double ht = (screenHt - safeArea.Bottom);

		// Court always full screen
		court.SetLayoutBounds( layout, x, y, wd, ht );

		// Calc button locations
		const double size = 40;
		const double pad = 4;

		double btnX = x;
		double btnY = y;

		double btnX2 = (x + wd - size + 1);
		double btnY2 = (y + ht - size + 1);

		// Icon sizes
		scoreBtn.Size = size;
		undoBtn.Size = size;
		teamsBtn.Size = size;
		faultBtn.Size = size;

		// Buttons in all 4 corners
		layout.SetBounds( undoBtn, btnX, btnY, size, size );		// top-left
		layout.SetBounds( scoreBtn, btnX2, btnY, size, size );		// top-right
		layout.SetBounds( faultBtn, btnX, btnY2, size, size );		// bottom-left
		layout.SetBounds( teamsBtn, btnX2, btnY2, size, size );		// bottom-right

		double midX = x + (wd / 2);
		double netPad = (wd * 0.026);

		double scoreX = (midX - netPad - size);
		double scoreY = (btnY + pad);
		
		double scoreX2 = (midX + netPad);

		// Scorecards
		layout.SetBounds( scoreABtn, scoreX, scoreY, size, size );
		layout.SetBounds( scoreBBtn, scoreX2, scoreY, size, size );

		double labelAX = (scoreX - pad - size);
		double labelBX = (scoreX2 + size + pad);

		double labelHt = ios ? 12 : 14;

		// Team labels
		teamALbl.HAlign = TextAlignment.End;
		teamBLbl.HAlign = TextAlignment.Start;

		layout.SetBounds( teamALbl, labelAX, scoreY, size, labelHt );
		layout.SetBounds( teamBLbl, labelBX, scoreY, size, labelHt );
		
		// Close drawers during rotation
		Scoreboard.IsVisible = false;
		
		teambar1.IsVisible = false;
		teambar2.IsVisible = false;

		const double teamWd = 68;
		double scoreHt = 117 + (ios ? 0 : safeArea.Top);
		
		// Drawers ready offscreen
		Scoreboard.SetLayoutBounds( layout, x, -scoreHt, wd, scoreHt );
		
		TeambarA.SetLayoutBounds( layout, (x - teamWd), y, teamWd, ht );
		TeambarB.SetLayoutBounds( layout, x2, y, teamWd, ht );
		
		// Covers hide teambar animation in safe areas
		cover1.SetLayoutBounds( layout, 0, 0, safeLeft, screenHt );
		cover2.SetLayoutBounds( layout, x2, 0, safeRight, screenHt );
	}

	// Mobile Portrait
	protected override void MobilePortrait()
	{
		bool ios = DXDevice.IsIOS;
		
		double screenWd = DXDevice.GetScreenWd();
		double screenHt = DXDevice.GetScreenHt();

		Thickness safeArea = DXDevice.SafeArea();

		double safeTop = safeArea.Top;
		double safeBottom = safeArea.Bottom;
		
		// Calc bounds
		const double x = 0;
		
		double y = safeTop;
		double y2 = (screenHt - safeBottom);
		
		double wd = screenWd;
		double ht = (y2 - safeTop);

		// Court always full screen
		court.SetLayoutBounds( layout, x, y, wd, ht );

		// Calc button locations
		const double size = 40;
		const double pad = 3;

		const double btnX = (x - 1);
		double btnY = y;

		double btnX2 = (wd - size + 1);
		double btnY2 = (y + ht - size + 1);

		// Icon sizes
		faultBtn.Size = size;
		undoBtn.Size = size;
		teamsBtn.Size = size;
		scoreBtn.Size = size;

		// Buttons in all 4 corners
		teamsBtn.SetLayoutBounds( layout, btnX, btnY, size, size );			// top-left
		scoreBtn.SetLayoutBounds( layout, btnX2, btnY, size, size );		// top-right
		faultBtn.SetLayoutBounds( layout, btnX, btnY2, size, size );		// bottom-left
		undoBtn.SetLayoutBounds( layout, btnX2, btnY2, size, size );		// bottom-right

		double midY = y + (ht / 2);
		double netPad = (ht * 0.025);

		double scoreX = (wd - pad - size);

		double scoreY = (midY - netPad - size);
		double scoreY2 = (midY + netPad);

		// Scorecards
		layout.SetBounds( scoreABtn, scoreX, scoreY, size, size );
		layout.SetBounds( scoreBBtn, scoreX, scoreY2, size, size );

		const double labelWd = size;
		double labelHt = ios ? 12 : 14;

		double labelX = scoreX;

		double labelAY = (scoreY - pad - labelHt);
		double labelBY = (scoreY2 + size + pad - 2);

		// Team labels
		teamALbl.HAlign = TextAlignment.Center;
		teamBLbl.HAlign = TextAlignment.Center;

		layout.SetBounds( teamALbl, labelX, labelAY, labelWd, labelHt );
		layout.SetBounds( teamBLbl, labelX, labelBY, labelWd, labelHt );

		// Close drawers during rotation
		Scoreboard.IsVisible = false;

		teambar1.IsVisible = false;
		teambar2.IsVisible = false;

		const double scoreWd = 208;
		double scoreHt = ht;
		const double teamHt = 70;

		// Drawers ready offscreen
		Scoreboard.SetLayoutBounds( layout, wd, y, scoreWd, scoreHt );
		
		TeambarA.SetLayoutBounds( layout, 0, (y - teamHt), wd, teamHt );
		TeambarB.SetLayoutBounds( layout, 0, y2, wd, teamHt );
		
		// Covers hide teambar animation in safe areas
		cover1.SetLayoutBounds( layout, 0, 0, wd, safeTop );
		cover2.SetLayoutBounds( layout, 0, y2, wd, safeBottom );
	}
}

//
