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

using SkiaSharp;
using SkiaSharp.Views.Maui;
using SkiaSharp.Views.Maui.Controls;

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

using DXLib.Utils;

using Color = Microsoft.Maui.Graphics.Color;
using Point = Microsoft.Maui.Graphics.Point;

namespace iStatVball3;

/*
 * Responsible for drawing the volleyball court including net, lines, and block zone. All tap regions are defined here.
 * Methods are provided for converting between normalized x,y coordinates and orientation-dependent draw coordinates.
 */
public class CourtView : SKCanvasView
{
	/* Constants */

	// Non-customizable colors
	private static readonly SKColor NetColor = DXColors.Light4.ToSKColor();
	private static readonly SKColor ShadowColor = SKColors.Black.WithAlpha( 70 );

	// Default zone alpha
	private const byte LightZone = 90;
	private const byte DarkZone = 110;

	// Height:width ratios
	public const double RatioShort = 0.54;
	public const double RatioLong = 1.85;

	// Zone sizes (multiple of net size)
	private const float BlockZoneScale = 1.25f;
	private const float OutZoneScale = 1.00f;

	// Ball markers (proportionate to court size)
	private static readonly float MarkerScale = DXDevice.IsTablet ? 0.10f : 0.11f;

	// Serve marker
	private const float ServeOffset = 0.15f;

	// Used for auto pass rating
	private static readonly Point PerfectRatingPt = new( 0.55, 0.60 );

	/* Properties */

	// Display mode
	public RallyEngine.EngineMode Mode { get; }

	// Disables all touch handling
	public bool IsDisabled { get => disabled; set => SetDisabled( value ); }

	// 10ft lines can be toggled (for z-order issue with team overlay)
	public bool HasLineA { get; set; }
	public bool HasLineB { get; set; }

	// Serve marker locations
	public static Point ServeA { get; private set; }
	public static Point ServeB { get; private set; }

	// Currently showing transient zones?
	public RecordCourt.Side BlockZone { get; set; }
	public RecordCourt.Side OutZone { get; set; }

	public bool HasBlockZone => BlockZone != RecordCourt.Side.Unknown;
	public bool HasOutZone => OutZone != RecordCourt.Side.Unknown;

	// Dynamic net size
	public static float NetSize { get; private set; }

	// Dynamic ball marker sizes
	public static float MarkerSize { get; private set; }
	public static float MarkerSizeSmall => (MarkerSize * 0.75f);

	// Draws all ball markers on court
	public MarkerStack MarkerStack { get; }

	// Converts between dp and pixels
	public static float DpScale { get; private set; }

	// Square court size
	public static float CourtSize { get; private set; }

	// External ref (record mode only)
	public RecordState StateMachine { get; set; }

	// Programmatically set bounds, can be used BEFORE layout
	public Rect LayoutBounds { get; private set; }
	
	/* Fields */

	// Orientation specific
	private float canvasWd;
	private float canvasHt;

	// Tap regions
	private Rect serviceA;
	private Rect serviceB;

	private Rect outA1;
	private Rect outA2;
	private Rect outB1;
	private Rect outB2;

	private Rect net;

	private Rect antennae1;
	private Rect antennae2;

	private Rect backRowA;
	private Rect backRowB;

	private Rect frontRowA;
	private Rect frontRowB;

	// Transient regions
	private Rect blockZone;

	private Rect outZone1;
	private Rect outZone2;
	private Rect outZone3;

	// Input control
	private bool disabled;

	// Controls marker update during redraw
	private bool markerUpdate;

	// Force to draw landscape regardless of orientation?
	private readonly bool forceLandscape;

	// Optional court texture
	private SKBitmap bitmap;

	// Custom colors
	private SKColor zoneColor;
	private SKColor lineColor;
	private SKColor innerColor;
	private SKColor outerColor;

	/* Methods */
	public CourtView( RallyEngine.EngineMode mode )
	{
		Mode = mode;

		// Sync mode always landscape
		forceLandscape = (Mode != RallyEngine.EngineMode.Record);

		// Init ball markers
		MarkerStack = new MarkerStack( this );

		// Defaults
		HasLineA = true;
		HasLineB = true;

		// Register for paint events
		PaintSurface += OnPaintSurface;
	}
	
	// Post construction initialization
	public void Init( Set set )
	{
		MarkerStack.Init( set );
	}

	// Sets bounds for court view within specified layout, saves bounds internally
	public void SetLayoutBounds( DXAbsoluteGestures layout, double x, double y, double wd, double ht )
	{
		LayoutBounds = new Rect( x, y, wd, ht );
		
		layout.SetBounds( this, LayoutBounds );
	}

	// Updates custom court texture/colors, MUST be called before first draw
	public async Task Customize( string texture, Color line, Color inner, Color outer )
	{
		bool textured = (texture != null);

		bitmap?.Dispose();
		bitmap = null;

		// Load background texture
		if ( textured )
		{
			string file = $"Images/{texture}.png";

			await using Stream stream = await DXResource.OpenFile( file );
			bitmap = SKBitmap.Decode( stream );
		}

		// Darker arrows against texture
		MarkerStack.DarkArrows = textured;

		// More opaque zones against texture
		zoneColor = NetColor.WithAlpha( textured ? DarkZone : LightZone );

		// Custom court colors
		lineColor = line.ToSKColor();
		innerColor = inner.ToSKColor();
		outerColor = outer.ToSKColor();

		Redraw();
	}

	// Translates screen x/y to court region
	public Court.Region GetRegion( double screenX, double screenY )
	{
		float x = (float)((screenX - LayoutBounds.X) * DpScale);
		float y = (float)((screenY - LayoutBounds.Y) * DpScale);

		// Block zone (must come first)
		if ( HasBlockZone && blockZone.Contains( x, y ) )
		{
			return Court.Region.BlockZone;
		}

		// Out zone
		if ( HasOutZone && (outZone1.Contains( x, y ) || outZone2.Contains( x, y ) || outZone3.Contains( x, y )) )
		{
			return Court.Region.OutZone;
		}

		// Front Row A/B
		if ( frontRowA.Contains( x, y ) )
		{
			return Court.Region.FrontRowA;
		}
		if ( frontRowB.Contains( x, y ) )
		{
			return Court.Region.FrontRowB;
		}

		// Back Row A/B
		if ( backRowA.Contains( x, y ) )
		{
			return Court.Region.BackRowA;
		}
		if ( backRowB.Contains( x, y ) )
		{
			return Court.Region.BackRowB;
		}

		// Service Area A/B
		if ( serviceA.Contains( x, y ) )
		{
			return Court.Region.ServiceA;
		}
		if ( serviceB.Contains( x, y ) )
		{
			return Court.Region.ServiceB;
		}

		// Net, Antennae (must occur before Out)
		if ( net.Contains( x, y ) )
		{
			return Court.Region.Net;
		}
		if ( antennae1.Contains( x, y ) || antennae2.Contains( x, y ) )
		{
			return Court.Region.Antennae;
		}

		// Out A/B
		if ( outA1.Contains( x, y ) || outA2.Contains( x, y ) )
		{
			return Court.Region.OutA;
		}
		if ( outB1.Contains( x, y ) || outB2.Contains( x, y ) )
		{
			return Court.Region.OutB;
		}
		
		// Should never happen
		return Court.Region.Unknown;
	}

	// Disables all court touch input
	private void SetDisabled( bool value )
	{
		disabled = value;

		// Markers hidden while disabled
		MarkerStack.IsDisabled = disabled;

		Redraw();
	}

	// Determines if court bounds contains specified point
	public bool Contains( double x, double y )
	{
		return LayoutBounds.Contains( x, y );
	}

	// Forces redraw of entire court
	public void Redraw( bool update = true )
	{
		markerUpdate = update;

		InvalidateSurface();
	}

	// Calculates pass rating based on distance between point and ideal location
	//
	// NOTE: Ratings always calculated internally on 0-4 scale
	//
	public static int GetRating( Point pt )
	{
		// Calculate distance between point and ideal
		float distance = DXGraphics.Distance( pt, PerfectRatingPt );

		// Rating 4
		if ( distance < 0.20 )
		{
			return 4;
		}

		// Rating 3, all else Rating 2
		return (distance < 0.30) ? 3 : 2;
	}

	/* X,Y Normalization */

	// Converts an X,Y location from court drawing space to a normalized value that can be used to draw regardless of
	// device size or orientation. The normalized layout is always landscape (horizontal). In-bounds values are always
	// 0.0-1.0. Out-of-bounds are <0.0 or >1.0. Net/Antennae is always 0.5.
	//
	public Point Normalize( double x, double y )
	{
		// Convert to court space
		double canvasX = (x - LayoutBounds.X);
		double canvasY = (y - LayoutBounds.Y);

		// Convert XF to Skia coordinates
		canvasX *= DpScale;
		canvasY *= DpScale;

		bool landscape = (DXDevice.IsLandscape() || forceLandscape);

		return landscape ? NormalizeLandscape( canvasX, canvasY ) : NormalizePortrait( canvasX, canvasY );
	}

	// Returns normalized x,y when in landscape orientation
	private Point NormalizeLandscape( double x, double y )
	{
		double normX;

		// Normalized court is 2:1
		double wd = (CourtSize * 2);
		double ht = CourtSize;

		// X

		// Court A (<0.5)
		if ( x < net.Left )
		{
			normX = (x - backRowA.Left) / wd;
		}
		// Court B (>0.5)
		else if ( x > net.Right )
		{
			normX = (wd - (backRowB.Right - x)) / wd;
		}
		// Net/Antennae (0.5)
		else
		{
			normX = 0.5;
		}

		// Y
		
		// <0.0-1.0>
		double normY = (y - backRowA.Top) / ht;

		return new Point( normX, normY );
	}

	// Returns normalized x,y when in portrait orientation
	private Point NormalizePortrait( double x, double y )
	{
		double normY;

		// Normalized court is 1:2
		double wd = CourtSize;
		double ht = (CourtSize * 2);

		// Y

		// Court A (<0.5)
		if ( y < net.Top )
		{
			normY = (y - backRowA.Top) / ht;
		}
		// Court B (>0.5)
		else if ( y > net.Bottom )
		{
			normY = (ht - (backRowB.Bottom - y)) / ht;
		}
		// Net/Antennae (0.5)
		else
		{
			normY = 0.5;
		}

		// X
		
		// <0.0-1.0>
		double normX = (1.0 - ((x - backRowA.Left) / wd));

		// Flip coordinates to normalize landscape
		return new Point( normY, normX );
	}

	/* X,Y Denormilization */
	
	// Converts a normalized X,Y location that is device and orientation independent back to court drawing space for the
	// current orientation.
	//
	public Point Denormalize( double normX, double normY, bool descale = false )
	{
		bool landscape = (DXDevice.IsLandscape() || forceLandscape);

		return landscape ? DenormalizeLandscape( normX, normY, descale ) : DenormalizePortrait( normX, normY, descale );
	}

	// Converts normalized x,y back to court space when in landscape
	private Point DenormalizeLandscape( double normX, double normY, bool descale )
	{
		double x;

		// Normalized court is 2:1
		double wd = (CourtSize * 2);
		double ht = CourtSize;

		// X

		// Court A (<0.5)
		if ( normX < 0.5 )
		{
			x = (backRowA.Left + (normX * wd));
		}
		// Court B (>0.5)
		else if ( normX > 0.5 )
		{
			x = (backRowB.Right - ((1.0 - normX) * wd));
		}
		// Net/Antennae (0.5)
		else
		{
			x = (net.Left + (net.Width / 2.0));
		}

		// Y
		
		// <0.0-1.0>
		double y = (backRowA.Top + (normY * ht));

		// Optionally descale from Skia to XF coordinate space
		if ( descale )
		{
			x /= DpScale;
			y /= DpScale;

			// Account for scoreboard
			x += X;
			y += Y;
		}

		return new Point( x, y );
	}

	// Converts normalized x,y back to court space when in portrait
	private Point DenormalizePortrait( double normX, double normY, bool descale )
	{
		double y;

		// Normalized court is 1:2
		double wd = CourtSize;
		double ht = (CourtSize * 2);

		// Y

		// Court A (<0.5)
		if ( normX < 0.5 )
		{
			y = Math.Max( (backRowA.Top + (normX * ht)), 0 );
		}
		// Court B (>0.5)
		else if ( normX > 0.5 )
		{
			y = (backRowB.Bottom - ((1.0 - normX) * ht));
		}
		// Net/Antennae (0.5)
		else
		{
			y = (net.Top + (net.Height / 2.0));
		}

		// X
		
		// <0.0-1.0>
		double x = (backRowA.Right - (normY * wd) - 6.71);

		// Optionally descale from Skia to XF coordinate space
		if ( descale )
		{
			x /= DpScale;
			y /= DpScale;

			// Account for scoreboard
			x += X;
			y += Y;
		}

		return new Point( x, y );
	}

	/* Event Handlers */

	// Called for each draw loop
	private void OnPaintSurface( object sender, SKPaintSurfaceEventArgs args )
	{
		SKCanvas canvas = args.Surface.Canvas;

		// Canvas dimensions
		canvasWd = CanvasSize.Width;
		canvasHt = CanvasSize.Height;

		// Pixel density
		DpScale = (float) (CanvasSize.Width / Width);

		// Draw based on orientation
		if ( (canvasWd > canvasHt) || forceLandscape )
		{
			PaintLandscape( canvas );
		}
		else
		{
			PaintPortrait( canvas );
		}

		// Optionally recalc marker locations for current orientation
		if ( markerUpdate )
		{
			MarkerStack.Update();
		}

		// Redraw marker stack
		MarkerStack.Paint( canvas );
	}

	/* Paint */

	// Draws court and all lines for landscape orientation
	private void PaintLandscape( SKCanvas canvas )
	{
		/* Layout Calculations */

		float wd = canvasWd;
		float ht = canvasHt;

		// Minimum out-of-bounds area (depends on display mode)
		float minBoundary = GetMinBoundary( wd );

		// Line thickness relative to court size
		const float lineScale = 0.008f;

		// Net size (proportional to court)
		NetSize = GetNetSize( wd );
		float netWd = NetSize;

		// Court size determined by minimum out-of-bounds
		float maxWd = (wd - (minBoundary * 2) - netWd) / 2;
		float maxHt = (ht - (minBoundary * 2));

		CourtSize = Math.Min( maxWd, maxHt );

		// Ball marker size (proportional to court)
		MarkerSize = (CourtSize * MarkerScale);

		// Courts always perfect square
		float courtX = (wd - (CourtSize * 2) - netWd) / 2;
		float courtX2 = (courtX + CourtSize + netWd);

		float courtY = (ht - CourtSize) / 2;
		float courtY2 = (courtY + CourtSize);

		float fullWd = (CourtSize + netWd + CourtSize);

		float netX = (courtX + CourtSize);
		float netY = courtY;

		float antennaeSize = netWd;

		float frontrowWd = (CourtSize / 3);
		float backrowWd = (frontrowWd * 2);

		// Line thickness proportional to court size
		float lineSize = (CourtSize * lineScale);

		/* Drawing */

		// Normal inner/outer colors
		if ( bitmap == null )
		{
			// Clear to outer court color
			canvas.Clear( outerColor );

			// Fill inner court area
			using SKPaint courtFill = new();
			
			courtFill.Style = SKPaintStyle.Fill;
			courtFill.Color = innerColor;
			courtFill.IsAntialias = false;

			canvas.DrawRect( courtX, courtY, fullWd, CourtSize, courtFill );
		}
		// Tiled texture background
		else
		{
			PaintBackground( canvas );
		}

		// Draw court lines
		using ( SKPaint lineStroke = new() )
		{
			lineStroke.Style = SKPaintStyle.Stroke;
			lineStroke.StrokeWidth = lineSize;
			lineStroke.Color = lineColor;
			lineStroke.IsAntialias = false;
			
			canvas.DrawRect( courtX, courtY, fullWd, CourtSize, lineStroke );

			// Draw net (with shadow)
			SKImageFilter shadow = SKImageFilter.CreateDropShadow( 10, 10, 20, 20, ShadowColor );

			using ( SKPaint netFill = new() )
			{
				netFill.Style = SKPaintStyle.Fill;
				netFill.Color = NetColor;
				netFill.ImageFilter = shadow;
				netFill.IsAntialias = true;
				
				canvas.DrawRect( netX, netY, netWd, CourtSize, netFill );

				// Draw antennae (with shadow)
				canvas.DrawRect( netX, (courtY - antennaeSize), antennaeSize, antennaeSize, netFill );
				canvas.DrawRect( netX, courtY2, antennaeSize, antennaeSize, netFill );
			}

			// Draw outline around entire net/antennae
			using ( SKPaint outlineStroke = new() )
			{
				outlineStroke.Style = SKPaintStyle.Stroke;
				outlineStroke.StrokeWidth = 1;
				outlineStroke.Color = SKColors.Black;
				outlineStroke.IsAntialias = true;
				
				canvas.DrawRect( netX, (courtY - antennaeSize), netWd, (antennaeSize + CourtSize + antennaeSize), outlineStroke );
			}

			// Draw 10ft lines (if enabled)
			float lineX = (courtX + backrowWd);
			float lineX2 = (courtX2 + frontrowWd);

			if ( HasLineA )
			{
				canvas.DrawLine( lineX, courtY, lineX, courtY2, lineStroke );
			}

			if ( HasLineB )
			{
				canvas.DrawLine( lineX2, courtY, lineX2, courtY2, lineStroke );
			}

			// Draw 10ft line extenders (3 dashes)
			const int numDash = 3;

			float gapHt = (lineSize * (DXDevice.IsIOS ? 2.0f : 2.5f));
			float dashHt = (courtY - lineSize - (gapHt * (numDash + 1))) / numDash;

			for ( int i = 0; i < numDash; i++ )
			{ 
				float dashY = (gapHt * (i + 1)) + (dashHt * i);
				float dashY2 = (dashY + dashHt);

				canvas.DrawLine( lineX, dashY, lineX, dashY2, lineStroke );
				canvas.DrawLine( lineX2, dashY, lineX2, dashY2, lineStroke );

				dashY = (courtY2 + dashY);
				dashY2 = (dashY + dashHt);

				canvas.DrawLine( lineX, dashY, lineX, dashY2, lineStroke );
				canvas.DrawLine( lineX2, dashY, lineX2, dashY2, lineStroke );
			}

			// Draw service area markers
			float gapWd = (lineSize * 3);
			float markerWd = (lineSize * 5);

			float markerX = (courtX - gapWd - markerWd);
			float markerX2 = (markerX + markerWd);

			canvas.DrawLine( markerX, courtY, markerX2, courtY, lineStroke );
			canvas.DrawLine( markerX, courtY2, markerX2, courtY2, lineStroke );

			markerX = (courtX + fullWd + gapWd);
			markerX2 = (markerX + markerWd);

			canvas.DrawLine( markerX, courtY, markerX2, courtY, lineStroke );
			canvas.DrawLine( markerX, courtY2, markerX2, courtY2, lineStroke );
		}

		/* Tap Regions */

		float serviceWd = courtX;
		float serviceHt = CourtSize;

		float outWd = (wd / 2);
		float outHt = courtY;

		float courtHt = (CourtSize + lineSize);

		// Calculate bounds for each region
		serviceA = new Rect( 0, courtY, serviceWd, serviceHt );
		serviceB = new Rect( (courtX + fullWd), courtY, serviceWd, serviceHt );

		outA1 = new Rect( 0, 0, outWd, outHt );
		outA2 = new Rect( 0, courtY2, outWd, outHt );

		outB1 = new Rect( outWd, 0, outWd, outHt );
		outB2 = new Rect( outWd, courtY2, outWd, outHt );

		net = new Rect( netX, netY, netWd, CourtSize );

		antennae1 = new Rect( netX, (courtY - antennaeSize), antennaeSize, antennaeSize );
		antennae2 = new Rect( netX, courtY2, antennaeSize, antennaeSize );

		backRowA = new Rect( courtX, courtY, backrowWd, courtHt );
		backRowB = new Rect( (courtX2 + frontrowWd), courtY, backrowWd, courtHt );

		frontRowA = new Rect( (courtX + backrowWd), courtY, frontrowWd, courtHt );
		frontRowB = new Rect( courtX2, courtY, frontrowWd, courtHt );

		/* Serve Markers */

		float serveOffsetX = (MarkerSize * 1.0f);
		const float servePad = 24;

		float halfOffset = (serveOffsetX / 2);

		float serveX = Math.Max( (courtX - serveOffsetX), (servePad + halfOffset) );
		float serveX2 = Math.Min( (courtX + fullWd + serveOffsetX), (wd - servePad - halfOffset) );

		float serveY = (courtY + (courtHt * (1.0f - ServeOffset)));
		float serveY2 = (courtY + (courtHt * ServeOffset));

		ServeA = new Point( serveX, serveY );
		ServeB = new Point( serveX2, serveY2 );

		/* Transient Zones */

		// Translucent fill
		using SKPaint zoneFill = new();
		
		zoneFill.Style = SKPaintStyle.Fill;
		zoneFill.Color = zoneColor;
		zoneFill.IsAntialias = false;

		// Block zone
		if ( HasBlockZone )
		{
			float zoneSize = (netWd * BlockZoneScale);

			// Tap region
			blockZone = (BlockZone == RecordCourt.Side.SideA) ? new Rect( (net.Left - zoneSize), net.Top, zoneSize, net.Height ) :
																new Rect( net.Right, net.Top, zoneSize, net.Height );
			// Draw
			canvas.DrawRect( blockZone.ToSKRect(), zoneFill );
		}

		// Out zone
		if ( HasOutZone )
		{
			float zoneSize = (netWd * OutZoneScale);

			float zoneWd = outWd;
			float zoneHt = ht - (zoneSize * 2);

			// Left
			if ( OutZone == RecordCourt.Side.SideA )
			{
				outZone1 = new Rect( 0, 0, zoneWd, zoneSize );
				outZone2 = new Rect( 0, zoneSize, zoneSize, zoneHt );
				outZone3 = new Rect( 0, (zoneSize + zoneHt), zoneWd, zoneSize );
			}
			// Right
			else
			{
				outZone1 = new Rect( zoneWd, 0, zoneWd, zoneSize );
				outZone2 = new Rect( (wd - zoneSize), zoneSize, zoneSize, zoneHt );
				outZone3 = new Rect( zoneWd, (zoneSize + zoneHt), zoneWd, zoneSize );
			}

			// Draw
			canvas.DrawRect( outZone1.ToSKRect(), zoneFill );
			canvas.DrawRect( outZone2.ToSKRect(), zoneFill );
			canvas.DrawRect( outZone3.ToSKRect(), zoneFill );
		}
	}

	// Draws court and all lines for portrait orientation
	private void PaintPortrait( SKCanvas canvas )
	{
		/* Constants */

		float wd = canvasWd;
		float ht = canvasHt;

		// Minimum out-of-bounds area (depends on display mode)
		float minBoundary = GetMinBoundary( ht );

		// Line thickness relative to court size
		const float lineRatio = 0.008f;

		// Net size (proportional to court)
		NetSize = GetNetSize( ht );
		float netHt = NetSize;

		/* Layout Calculations */

		// Court size determined by minimum out-of-bounds
		float maxWd = (wd - (minBoundary * 2));
		float maxHt = (ht - (minBoundary * 2) - netHt) / 2;

		CourtSize = Math.Min( maxWd, maxHt );

		// Ball marker size (proportional to court)
		MarkerSize = (CourtSize * MarkerScale);

		// Courts always perfect square
		float courtY = (ht - (CourtSize * 2) - netHt) / 2;
		float courtY2 = (courtY + CourtSize + netHt);

		float courtX = (wd - CourtSize) / 2;
		float courtX2 = (courtX + CourtSize);

		float fullHt = (CourtSize + netHt + CourtSize);

		float netX = courtX;
		float netY = (courtY + CourtSize);

		float antennaeSize = netHt;

		float frontrowHt = (CourtSize / 3);
		float backrowHt = (frontrowHt * 2);

		// Line thickness proportional to court size
		float lineSize = (CourtSize * lineRatio);

		/* Drawing */

		// Normal inner/outer colors
		if ( bitmap == null )
		{
			// Clear to outer court color
			canvas.Clear( outerColor );

			// Fill inner court area
			using SKPaint courtFill = new();
			
			courtFill.Style = SKPaintStyle.Fill;
			courtFill.Color = innerColor;
			courtFill.IsAntialias = false;

			canvas.DrawRect( courtX, courtY, CourtSize, fullHt, courtFill );
		}
		// Tiled texture background
		else
		{
			PaintBackground( canvas );
		}

		// Draw court lines
		using ( SKPaint lineStroke = new() )
		{
			lineStroke.Style = SKPaintStyle.Stroke;
			lineStroke.StrokeWidth = lineSize;
			lineStroke.Color = lineColor;
			lineStroke.IsAntialias = false;
			
			canvas.DrawRect( courtX, courtY, CourtSize, fullHt, lineStroke );

			// Draw net (with shadow)
			SKImageFilter shadow = SKImageFilter.CreateDropShadow( 10, 10, 20, 20, ShadowColor );

			using ( SKPaint netFill = new() )
			{
				netFill.Style = SKPaintStyle.Fill;
				netFill.Color = NetColor;
				netFill.ImageFilter = shadow;
				netFill.IsAntialias = true;
				
				canvas.DrawRect( netX, netY, CourtSize, netHt, netFill );

				// Draw antennae (with shadow)
				canvas.DrawRect( (netX - antennaeSize), netY, antennaeSize, antennaeSize, netFill );
				canvas.DrawRect( courtX2, netY, antennaeSize, antennaeSize, netFill );
			}

			// Draw outline around entire net/antennae
			using ( SKPaint outlineStroke = new() )
			{
				outlineStroke.Style = SKPaintStyle.Stroke;
				outlineStroke.StrokeWidth = 1;
				outlineStroke.Color = SKColors.Black;
				outlineStroke.IsAntialias = true;
				
				canvas.DrawRect( (netX - antennaeSize), netY, (antennaeSize + CourtSize + antennaeSize), netHt, outlineStroke );
			}

			// Draw 10ft lines (if enabled)
			float lineY = (courtY + backrowHt);
			float lineY2 = (courtY2 + frontrowHt);

			if ( HasLineA )
			{
				canvas.DrawLine( courtX, lineY, courtX2, lineY, lineStroke );
			}

			if ( HasLineB )
			{
				canvas.DrawLine( courtX, lineY2, courtX2, lineY2, lineStroke );
			}

			// Draw 10ft line extenders (3 dashes)
			const int numDash = 3;

			float gapWd = (lineSize * (DXDevice.IsIOS ? 2.0f : 2.5f));
			float dashWd = (courtX - lineSize - (gapWd * (numDash + 1))) / numDash;

			for ( int i = 0; i < numDash; i++ )
			{
				float dashX = (gapWd * (i + 1)) + (dashWd * i);
				float dashX2 = (dashX + dashWd);

				canvas.DrawLine( dashX, lineY, dashX2, lineY, lineStroke );
				canvas.DrawLine( dashX, lineY2, dashX2, lineY2, lineStroke );

				dashX = (courtX2 + dashX);
				dashX2 = (dashX + dashWd);

				canvas.DrawLine( dashX, lineY, dashX2, lineY, lineStroke );
				canvas.DrawLine( dashX, lineY2, dashX2, lineY2, lineStroke );
			}

			// Draw service area markers
			float gapHt = (lineSize * 3);
			float markerHt = (lineSize * 5);

			float markerY = (courtY - gapHt - markerHt);
			float markerY2 = (markerY + markerHt);

			canvas.DrawLine( courtX, markerY, courtX, markerY2, lineStroke );
			canvas.DrawLine( courtX2, markerY, courtX2, markerY2, lineStroke );

			markerY = (courtY + fullHt + gapHt);
			markerY2 = (markerY + markerHt);

			canvas.DrawLine( courtX, markerY, courtX, markerY2, lineStroke );
			canvas.DrawLine( courtX2, markerY, courtX2, markerY2, lineStroke );
		}

		/* Tap Regions */

		float serviceWd = CourtSize;
		float serviceHt = courtY;

		float outWd = courtX;
		float outHt = (ht / 2);

		float courtWd = (CourtSize + lineSize);
		
		// Calculate bounds for each region
		serviceA = new Rect( courtX, 0, serviceWd, serviceHt );
		serviceB = new Rect( courtX, (courtY + fullHt), serviceWd, serviceHt );

		outA1 = new Rect( 0, 0, outWd, outHt );
		outA2 = new Rect( courtX2, 0, outWd, outHt );

		outB1 = new Rect( 0, outHt, outWd, outHt );
		outB2 = new Rect( courtX2, outHt, outWd, outHt );

		net = new Rect( netX, netY, CourtSize, netHt );

		antennae1 = new Rect( (netX - antennaeSize), netY, antennaeSize, antennaeSize );
		antennae2 = new Rect( courtX2, netY, antennaeSize, antennaeSize );

		backRowA = new Rect( courtX, courtY, courtWd, backrowHt );
		backRowB = new Rect( courtX, (courtY2 + frontrowHt), courtWd, backrowHt );

		frontRowA = new Rect( courtX, (courtY + backrowHt), courtWd, frontrowHt );
		frontRowB = new Rect( courtX, courtY2, courtWd, frontrowHt );

		/* Serve Markers */

		float serveOffsetY = (MarkerSize * 1.0f) - 5;
		const float servePad = 20;

		float halfOffset = (serveOffsetY / 2);

		float serveX = (courtX + (courtWd * ServeOffset));
		float serveX2 = (courtX + (courtWd * (1.0f - ServeOffset)));

		float serveY = Math.Max( (courtY - serveOffsetY), (servePad + halfOffset) );
		float serveY2 = Math.Min( (courtY + fullHt + serveOffsetY), (ht - servePad - halfOffset) );

		ServeA = new Point( serveX, serveY );
		ServeB = new Point( serveX2, serveY2 );

		/* Transient Zones */

		// Translucent fill
		using SKPaint zoneFill = new();
		
		zoneFill.Style = SKPaintStyle.Fill;
		zoneFill.Color = zoneColor;
		zoneFill.IsAntialias = false;

		// Block zone
		if ( HasBlockZone )
		{
			float zoneSize = (netHt * BlockZoneScale);

			// Tap region
			blockZone = (BlockZone == RecordCourt.Side.SideA) ? new Rect( net.Left, (net.Top - zoneSize), net.Width, zoneSize ) :
																new Rect( net.Left, net.Bottom, net.Width, zoneSize );
			// Draw
			canvas.DrawRect( blockZone.ToSKRect(), zoneFill );
		}

		// Out zone
		if ( HasOutZone )
		{
			float zoneSize = (netHt * OutZoneScale);

			float zoneWd = wd - (zoneSize * 2);
			float zoneHt = outHt;

			// Top
			if ( OutZone == RecordCourt.Side.SideA )
			{
				outZone1 = new Rect( 0, 0, zoneSize, zoneHt );
				outZone2 = new Rect( zoneSize, 0, zoneWd, zoneSize );
				outZone3 = new Rect( (wd - zoneSize), 0, zoneSize, zoneHt );
			}
			// Bottom
			else
			{
				outZone1 = new Rect( 0, zoneHt, zoneSize, zoneHt );
				outZone2 = new Rect( zoneSize, (ht - zoneSize), zoneWd, zoneSize );
				outZone3 = new Rect( (wd - zoneSize), zoneHt, zoneSize, zoneHt );
			}

			// Draw
			canvas.DrawRect( outZone1.ToSKRect(), zoneFill );
			canvas.DrawRect( outZone2.ToSKRect(), zoneFill );
			canvas.DrawRect( outZone3.ToSKRect(), zoneFill );
		}
	}

	// Draws tiled background using currently defined texture
	private void PaintBackground( SKCanvas canvas )
	{
		float wd = bitmap.Width;
		float ht = bitmap.Height;

		// Calc number tiles
		float tilesX = (canvasWd / wd);
		float tilesY = (canvasHt / ht);

		// Horizontal texture in landscape with staggered X for each row
		if ( DXDevice.IsLandscape() )
		{
			tilesX += 1;

			float offset = (1.0f / tilesY);

			// Draw bitmap at each x,y location
			for ( int y = 0; y < tilesY; y++ )
			{
				float yPos = (y * ht);

				// Staggered X for better tiling
				float offsetX = -(wd * (y * offset));

				// Draw across row
				for ( int x = 0; x < tilesX; x++ )
				{
					float xPos = (offsetX + (x * wd));

					canvas.DrawBitmap( bitmap, xPos, yPos );
				}
			}
		}
		// Vertical texture in portrait with staggered Y for each column
		else
		{
			tilesY += 1;

			float offset = (1.0f / tilesX);

			// Draw bitmap at each x,y location
			for ( int x = 0; x < tilesX; x++ )
			{
				float xPos = (x * wd);

				// Staggered Y for better tiling
				float offsetY = -(ht * (x * offset));

				// Draw down column
				for ( int y = 0; y < tilesY; y++ )
				{
					float yPos = (offsetY + (y * ht));

					canvas.DrawBitmap( bitmap, xPos, yPos );
				}
			}
		}
	}

	// Calculates minimum out-of-bounds size based on current display mode
	private float GetMinBoundary( float dim )
	{
		return Mode switch
		{
			RallyEngine.EngineMode.Record => (dim * (DXDevice.IsMobile ? (DXDevice.IsPlusSize() ? 0.062f : 0.070f) : (DXDevice.IsLargeTablet() ? 0.060f : 0.068f))),
			RallyEngine.EngineMode.Sync => (dim * 0.072f),

			_ => 0
		};
	}

	// Calculates net size based on current display mode
	private float GetNetSize( float dim )
	{
		return Mode switch
		{
			RallyEngine.EngineMode.Record => (dim * (DXDevice.IsMobile ? 0.032f : 0.029f)),
			RallyEngine.EngineMode.Sync => (dim * 0.033f),

			_ => 0
		};
	}
}

//
