/* 
 * 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.Gestures;

using DXLib.Data;
using DXLib.Utils;

namespace iStatVball3;

/*
 * A transparent overlay covering the entire recording screen. Used to draw popup menus over both the Skia drawn court
 * and the MAUI drawers. Action menus are pre-created and cached as are LUTs needed to populate the menus.
 */
public class ActionOverlay
{
	/* Constants */

	// Minimum padding against screen used throughout
	public const double Edge = 3;

	// Slight pause before menus fully shown (ms)
	private static readonly int MenuDelay = DXDevice.IsSimulator ? 50 : (DXDevice.IsIOS ? 100 : 0);

	/* Properties */

	// Is a menu currently showing?
	public bool IsActive => (activeMenu != null) || faultActive;

	// Settings
	public string Mode { get; private set; }
	public bool HasHeader { get; private set; }

	// Cached flyout LUTs
	public Dictionary<string,string> Titles { get; }

	public Dictionary<string,List<DXItem>> Ratings { get; }
	public Dictionary<string,List<DXItem>> Modifiers { get; }
	public Dictionary<string,List<DXItem>> Selectors { get; }

	public Dictionary<string,List<DXItem>> Faults { get; }

	/* Fields */

	// Cached action menu LUTs
	private readonly Dictionary<string,string> actions;
	private readonly Dictionary<string,string> actionsAbbrev;

	private readonly Dictionary<string,string> errors;

	// External refs
	private readonly RallyEngine parent;
	private readonly DXAbsoluteGestures layout;

	private RallyState rallyState;

	// Action menus (Team 1/2)
	private readonly List<ActionMenu> menus;

	private ActionMenu serveMenu1;
	private ActionMenu serveMenu2;

	private ActionMenu teamMenu1;
	private ActionMenu teamMenu2;

	private ActionMenu overMenu1;
	private ActionMenu overMenu2;

	private ActionMenu blockMenu1;
	private ActionMenu blockMenu2;

	// Out/net/antennae
	private ActionMenu errorMenu;

	// Invalid marker
	private readonly ActionInvalid invalid;

	// Faults
	private readonly FlyoutBar faultFlyout;
	private Action<RecordData> faultCallback;

	// Currently displayed
	private ActionMenu activeMenu;
	private bool faultActive;

	/* Methods */
	public ActionOverlay( RallyEngine parent, DXAbsoluteGestures layout )
	{
		this.parent = parent;
		this.layout = layout;

		// Allocate container
		menus = [];

		// Load general action menu LUTs
		actions = LoadTable( "action" );
		actionsAbbrev = LoadTable( "action.abbrev" );

		Titles = LoadTable( "title" );
		errors = LoadTable( "error" );

		// Build dictionaries of flyout string LUTs

		// Ratings
		Ratings = new Dictionary<string,List<DXItem>>();

		AddList( Ratings, "rating", Settings.Pass03Key );
		AddList( Ratings, "rating", Settings.Pass04Key );

		AddList( Ratings, "rating", "set" );
		AddList( Ratings, "rating", "serve" );

		// Modifiers
		Modifiers = new Dictionary<string,List<DXItem>>();

		AddList( Modifiers, "modifier", "serve" );
		AddList( Modifiers, "modifier", "set" );
		AddList( Modifiers, "modifier", "attack2" );
		AddList( Modifiers, "modifier", "attack3" );
		AddList( Modifiers, "modifier", "free" );
		AddList( Modifiers, "modifier", "defense" );
		AddList( Modifiers, "modifier", "pass" );
		AddList( Modifiers, "modifier", "over" );
		AddList( Modifiers, "modifier", "block" );

		// Selectors
		Selectors = new Dictionary<string,List<DXItem>>();

		AddList( Selectors, "selector", "second" );
		AddList( Selectors, "selector", "third" );

		// Faults
		Faults = new Dictionary<string,List<DXItem>>();

		AddList( Faults, "fault", "serve" );
		AddList( Faults, "fault", "receive" );
		AddList( Faults, "fault", "set" );
		AddList( Faults, "fault", "attack2" );
		AddList( Faults, "fault", "attack3" );
		AddList( Faults, "fault", "free" );
		AddList( Faults, "fault", "defense" );
		AddList( Faults, "fault", "pass" );
		AddList( Faults, "fault", "block" );

		if ( parent != null )
		{
			// Fault flyout (drawn over scoreboard button)
			faultFlyout = new FlyoutBar
			{
				Type = FlyoutBar.FlyoutType.Fault,

				MaxCount = 9,
				Color = DXColors.Warn
			};

			faultFlyout.Init( layout, null, GetMenuSize() );
			faultFlyout.Add();

			// Invalid 'X' marker
			invalid = new ActionInvalid( layout );
		}
	}

	// Loads resource string lookup table for specified key
	private static Dictionary<string, string> LoadTable( string key )
	{
		return DXString.GetLookupTable( key );
	}

	// Adds resource string list with specified key to existing dictionary
	private static void AddList( Dictionary<string, List<DXItem>> dict, string type, string key )
	{
		List<DXItem> list = DXString.GetLookupList( $"{type}.{key}" );

		dict.Add( key, list );
	}

	// Initializes all menus for both teams
	public void Init( RallyState sm, RecordLineup lineup1, RecordLineup lineup2 )
	{
		rallyState = sm;

		UpdateSettings();

		// Team 1
		serveMenu1 = CreateMenu( ActionMenu.MenuType.Serve, true, lineup1 );
		teamMenu1 = CreateMenu( ActionMenu.MenuType.Team, true, lineup1 );
		overMenu1 = CreateMenu( ActionMenu.MenuType.Block, true, lineup1 );
		blockMenu1 = CreateMenu( ActionMenu.MenuType.Block, true, lineup1, true );

		// Team 2 (currently always anonymous)
		serveMenu2 = CreateMenu( ActionMenu.MenuType.Anonymous, false, lineup2 );
		teamMenu2 = CreateMenu( ActionMenu.MenuType.Anonymous, false, lineup2 );
		overMenu2 = CreateMenu( ActionMenu.MenuType.Anonymous, false, lineup2 );
		blockMenu2 = CreateMenu( ActionMenu.MenuType.Anonymous, false, lineup2 );

		// Out/net/antennae
		errorMenu = CreateMenu( ActionMenu.MenuType.Error, false, null );
	}

	// Creates an action menu of specified type for either team
	private ActionMenu CreateMenu( ActionMenu.MenuType type, bool team1, RecordLineup lineup, bool multi = false )
	{
		ActionMenu menu = new()
		{
			Size = GetMenuSize(),
			SyncMode = parent.IsSyncMode,

			Type = type,
			IsTeam1 = team1,
			LineupType = lineup?.Type ?? RecordLineup.LineupType.Unknown,

			DisplayMode = Mode,
			HasHeader = HasHeader,
			IsMultiSelect = multi
		};

		menu.Init( layout, rallyState );
		menus.Add( menu );

		return menu;
	}

	// Determines menu display size based on device and mode
	private ActionMenu.MenuSize GetMenuSize()
	{
		return parent.IsSyncMode ? ActionMenu.MenuSize.Medium : (DXDevice.IsTablet ? ActionMenu.MenuSize.Large : ActionMenu.MenuSize.Small);
	}

	// Refreshes recording UI related user settings
	private void UpdateSettings()
	{
		Settings settings = Shell.Settings;

		// Refresh settings
		Mode = settings.GridDisplay;
		HasHeader = settings.GridTitle;
	}

	/* Faults */

	// Fault callback constant across all states
	public void SetFaultCallback( Action<RecordData> callback )
	{
		faultCallback = callback;
		faultFlyout.FaultSelected = callback;
	}

	// Set fault button flyout items for current state
	public void SetFaults( string key )
	{
		if ( !string.IsNullOrEmpty( key ) )
		{
			faultFlyout.SetItems( Faults[ key ] );
			faultFlyout.SetError( 0 );
		}
	}

	/* Populate Menus */

	// Populates serve menu for current rally
	public void PopulateServe( bool isTeam1 )
	{
		ActionMenu menu = isTeam1 ? serveMenu1 : serveMenu2;

		RecordLineup lineup = isTeam1 ? rallyState.Lineup1 : rallyState.Lineup2;
		LineupEntry entry = lineup.IsLineup ? rallyState.GetServer( isTeam1 ) : null;

		// Only one team needs serve menu per rally
		menu.SetEntries( [entry] );
	}

	// Populates team menus (non-WHP, static sort, constant for duration of rally)
	public void PopulateTeams()
	{
		RecordLineup lineup1 = rallyState.Lineup1;

		// Team 1
		if ( lineup1.IsLineup )
		{
			string order = rallyState.GridOrder;

			// Rotation is current court order, all others are static sort
			List<LineupEntry> entries = (order == "rot") ? rallyState.GetTeam( true ) : lineup1.SortEntries( Shell.Settings.GridDisplay );
			RecordCourt.Side side = rallyState.SideForTeam( 1 );

			teamMenu1.SetEntries( entries, side, order );
		}

		// Team 2 (currently always anonymous)
		teamMenu2.SetEntries( null );
	}

	// Populates overpass/block menus (non-WHP, constant for duration of rally)
	public void PopulateBlock()
	{
		RecordLineup lineup1 = rallyState.Lineup1;

		// Team 1
		if ( lineup1.IsLineup )
		{
			string order = rallyState.GridOrder;
			List<LineupEntry> frontrow = rallyState.GetFrontrow( true );

			// Rotation is current court order, all others are static sort
			if ( order == "sort" )
			{
				frontrow = RecordLineup.SortEntries( frontrow, Shell.Settings.GridDisplay );
			}

			// Only 3 zones used
			List<LineupEntry> entries =
			[
				new(),
				frontrow[0],
				frontrow[1],
				frontrow[2],
				new(),
				new()
			];

			RecordCourt.Side side = rallyState.SideForTeam( 1 );

			// Overpass/block use same entries
			overMenu1.SetEntries( entries, side );
			blockMenu1.SetEntries( entries, side );
		}

		// Team 2 (currently always anonymous)
		overMenu2.SetEntries( null );
		blockMenu2.SetEntries( null );
	}

	// Dynamically populates team menu with ranked SmartSort (WHP) results
	public void PopulateSmart( ActionMenu menu, bool isTeam1, double x, double y, ActionConfig config )
	{
		// Only valid for full lineup with SmartSort ON
		RecordLineup lineup = isTeam1 ? rallyState.Lineup1 : rallyState.Lineup2;

		if ( lineup.IsLineup )
		{
			// Run WHP
			List<LineupEntry> entries = rallyState.RunWHP( isTeam1, x, y, config.Action );
			RecordCourt.Side side = rallyState.SideForTeam( isTeam1 ? 1 : 2 );

			// Populate menu
			menu.SetEntries( entries, side, rallyState.GridOrder );

			// Disable previous player
			DisablePrevious( menu, isTeam1 );
		}
		else
		{
			menu.SetEntries( null );
		}
	}

	// Shows previously contacting player as disabled in action menu
	private void DisablePrevious( ActionMenu menu, bool isTeam1 )
	{
		// Only valid for primary team
		if ( isTeam1 )
		{
			Stat stat = rallyState.PreviousStat();

			if ( stat != null )
			{
				string action = stat.Action;

				// Previous player can only contact again for block/overpass
				if ( (action != Stats.BlockKey) && (action != Stats.OverpassKey) )
				{
					menu.SetDisabled( WHP.GetInstance().Previous, true );
				}
			}
		}
	}

	// Determines if quick select enabled for a given team/action
	private static bool UseQuickSelect( ActionConfig config )
	{
		// Can be turned on/off in settings (or overridden with swipe)
		if ( RallyState.IsQuickSelect( config ) && !config.Override )
		{
			// Team 1 special cases some actions
			if ( config.IsTeam1 )
			{
				// Must manually select 2nd ball if previous was setter
				if ( config.Action == Stats.SecondKey )
				{
					return !config.PrevSetter;
				}
			}

			return true;
		}

		// Normal selection
		return false;
	}

	/* Show/Hide */

	// Draws serve state action menu (already pre-configured)
	public void ShowServe( double x, double y, ActionConfig config )
	{
		HideAll();

		// Validate lineups before starting rally
		if ( rallyState.StartRally() )
		{
			bool team1 = config.IsTeam1;
			string action = config.Action;

			ActionMenu menu = team1 ? serveMenu1 : serveMenu2;

			activeMenu = menu;

			// Configure
			menu.SetTouch( x, y );
			menu.SetAction( Stats.ServeKey );
			menu.SetCallback( config.Selected );

			// Modifiers can be turned off
			if ( RallyState.HasModifiers( config ) )
			{
				menu.SetModifiers( Modifiers[ "serve" ], "float" );
			}
			else
			{
				menu.SetModifiers( null, null );
			}

			// Determine if quick select turned on for this team/action
			menu.IsQuickSelect = UseQuickSelect( config );

			// Quick select handles event directly
			if ( menu.IsQuickSelect )
			{
				menu.HandleSelected();
			}
			// Otherwise show menu
			else
			{
				menu.SetHeader( actions[ action ], actionsAbbrev[ action ] );

				// Serve always zone 1
				menu.SetDefault( 1 );

				// Prepare menu
				menu.Draw();
				menu.PreShow();

				// Menu shows after slight delay
				DXTimer.Delay( MenuDelay, menu.Show );
			}
		}
	}

	// Shows menu for Team A or B with specified configuration
	public void ShowTeam( double x, double y, ActionConfig config )
	{
		HideAll();

		bool team1 = config.IsTeam1;
		string action = config.Action;

		ActionMenu menu = team1 ? teamMenu1 : teamMenu2;

		activeMenu = menu;

		// Configure
		menu.SetTouch( x, y );
		menu.SetAction( action );
		menu.SetCallback( config.Selected );

		// Not all menus have ratings, and can be turned off
		if ( RallyState.HasRatings( config ) && (config.Ratings != null) )
		{
			menu.SetRatings( Titles[ config.Ratings ], Ratings[ config.Ratings ], config.Rating );
		}
		else
		{
			// Can record pass ratings even when flyout off
			bool useDefault = (config.IsPassRating && Shell.Settings.RallyAuto);

			menu.SetRatings( null, null, (useDefault ? config.Rating : null) );
		}

		// Selectors MUST be set before modifiers
		bool hasSelectors = (config.Selectors != null);

		// Not all menus have selectors
		if ( hasSelectors )
		{
			menu.SetSelectors( Selectors[ config.Selectors ], config.Selector );
		}
		else
		{
			menu.SetSelectors( null, null );
		}

		// Not all menus have modifiers, and can be turned off
		if ( RallyState.HasModifiers( config ) )
		{
			menu.SetModifiers( Modifiers[ config.Modifiers ], config.Modifier );

			// Second modifier list only valid if multiple selectors
			if ( menu.IsMultiSelector() )
			{
				menu.SetModifiers2( Modifiers[ config.Modifiers2 ], config.Modifier2 );
			}
		}
		else
		{
			menu.SetModifiers( null, null );
			menu.SetModifiers2( null, null );
		}

		// Determine if quick select turned on for this team/action
		menu.IsQuickSelect = UseQuickSelect( config );

		// Quick select handles event directly
		if ( menu.IsQuickSelect )
		{
			menu.HandleSelected();
		}
		// Otherwise show menu
		else
		{
			menu.SetHeader( actions[ action ], actionsAbbrev[ action ] );

			// Run WHP
			if ( rallyState.IsSmartOrder )
			{
				PopulateSmart( menu, team1, x, y, config );
			}
			// Highlight tapped zone
			else
			{
				SetDefault( menu, x, y );
			}

			// Prepare menu
			menu.Draw();
			menu.PreShow();

			int delay = menu.IsPhotoMode ? (MenuDelay * 3) : MenuDelay;

			// Menu shows after slight delay
			DXTimer.Delay( delay, menu.Show );
		}
	}

	// Shows overpass or block menu for Team A/B with specified configuration
	public void ShowBlock( double x, double y, ActionConfig config, bool block, bool postSelect = false )
	{
		HideAll();

		bool team1 = config.IsTeam1;
		string action = config.Action;

		// Over/block menu for appropriate team
		ActionMenu menu = block ? (team1 ? blockMenu1 : blockMenu2) : (team1 ? overMenu1 : overMenu2);

		activeMenu = menu;

		// Configure
		menu.SetTouch( x, y );
		menu.SetAction( action );
		menu.SetCallback( config.Selected );

		// Not all menus have modifiers, and can be turned off
		if ( RallyState.HasModifiers( config ) && (config.Modifiers != null) )
		{
			menu.SetModifiers( Modifiers[ config.Modifiers ], config.Modifier );
		}
		else
		{
			menu.SetModifiers( null, null );
		}

		// Determine if quick select turned on for this team/action
		menu.IsQuickSelect = UseQuickSelect( config );

		// Quick select handles event directly
		if ( menu.IsQuickSelect && !postSelect )
		{
			menu.HandleSelected();
		}
		// Otherwise show menu
		else
		{
			menu.SetHeader( actions[ action ], actionsAbbrev[ action ] );

			double blockX = x;
			double blockY = y;

			// Post-select must use location from previous block
			if ( postSelect )
			{
				Stat stat = rallyState.PreviousStat( Stats.BlockKey );
				Point denorm = rallyState.Denormalize( stat.StartX, stat.StartY );

				blockX = denorm.X;
				blockY = denorm.Y;
			}

			// Run WHP
			if ( rallyState.IsSmartOrder )
			{
				PopulateSmart( menu, team1, blockX, blockY, config );
			}

			// Highlight tapped zone
			SetDefault( menu, blockX, blockY );

			// Prepare menu
			menu.Draw();
			menu.PreShow();

			// Menu shows after slight delay
			DXTimer.Delay( MenuDelay, menu.Show );
		}
	}

	// Sets default zone in specified menu for given tap location 
	private void SetDefault( ActionMenu menu, double x, double y )
	{
		string order = rallyState.GridOrder;

		// NA for LOW and all name/number/photo
		if ( (order == "rot") || ((menu.Type == ActionMenu.MenuType.Block) && (order == "smart")) )
		{
			RecordCourt.Side side = rallyState.SideForTeam( menu.IsTeam1 ? 1 : 2 );
			int zone = GetZone( x, y, side );

			// Cell at zone will be highlighted
			menu.SetDefault( zone );
		}
	}

	// Converts tap location to court zone
	private int GetZone( double x, double y, RecordCourt.Side side )
	{
		Point norm = rallyState.Normalize( x, y );

		// Convert normalized location to zone
		return Court.ExtendedCourtZone( norm.X, norm.Y, side );
	}

	// Draws error action menu (single row, red color)
	public void ShowError( double x, double y, ActionConfig config )
	{
		HideAll();

		string key = config.Error;

		// Configure
		errorMenu.SetTouch( x, y );
		errorMenu.SetAction( config.Action );

		errorMenu.SetCallback( config.Selected );
		errorMenu.SetError( key, errors[ key ], config.IsTeamError );

		activeMenu = errorMenu;

		// Prepare menu
		errorMenu.Draw();
		errorMenu.PreShow();

		// Menu shows after slight delay
		DXTimer.Delay( MenuDelay, errorMenu.Show );
	}

	// Draws invalid 'X' marker
	public void ShowInvalid( double x, double y )
	{
		HideAll();

		// Do not draw over drawers or (non-preview) overlays
		if ( rallyState.CourtContains( x, y ) && !parent.IsCourtDisabled )
		{
			invalid.Show( x, y );
		}
	}

	// Ensures that no menus are visible
	public void HideAll()
	{
		activeMenu?.Hide();
		activeMenu = null;

		faultFlyout.Hide();
		faultActive = false;

		invalid.Hide();
	}

	/* Event Callbacks */

	// Forwards down event to active menu (or fault flyout)
	public void OnDown( Point pt )
	{
		// Normal menu
		if ( (activeMenu != null) && activeMenu.Contains( pt ) )
		{
			activeMenu.OnDown( pt );
		}
		// Fault flyout
		else if ( faultActive && faultFlyout.Contains( pt ) )
		{
			faultFlyout.OnDown( pt );
		}
	}

	// Forwards up event to active menu
	public void OnUp( Point pt )
	{
		// Normal menu
		if ( (activeMenu != null) && activeMenu.Contains( pt ) )
		{
			activeMenu.OnUp( pt );
		}
		// Fault flyout
		else if ( faultActive && faultFlyout.Contains( pt ) )
		{
			faultFlyout.OnUp( pt );
		}
		// Hide if not on menu
		else
		{
			HideAll();
		}
	}

	// Forwards long press event to active menu
	public void OnLongPress( Point pt )
	{
		if ( (activeMenu != null) && activeMenu.Contains( pt ) )
		{
			activeMenu.OnLongPress( pt );
		}
	}

	// Called back when fault button on scoreboard (or mobile court) tapped
	public void OnFaultTapped( double x, double y )
	{
		// Tap on flyout button
		if ( rallyState.HasFaults() )
		{
			faultActive = true;

			faultFlyout.Draw( x, y, false );
			faultFlyout.PreShow();
			faultFlyout.Show();
		}
		// Direct button tap
		else
		{
			faultCallback?.Invoke( new RecordData
			{
				X = (float)x,
				Y = (float)y,

				Result = Stats.ErrorKey,
				Error = Stats.FaultKey,
				Fault = null,

				IsError = true,
				IsTeamError = false
			});
		}
	}

	/* Layout */

	// Must re-layout all orientation dependent menus
	public void UpdateLayout( LayoutType type )
	{
		HideAll();

		// Use latest settings
		UpdateSettings();

		// Re-layout
		foreach ( ActionMenu menu in menus )
		{
			menu.DisplayMode = Mode;
			menu.HasHeader = HasHeader;

			menu.UpdateLayout( type );
		}
	}
}

//
