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

using System.Reflection;
using System.ComponentModel;
using System.Collections.ObjectModel;

using Syncfusion.Maui.Data;
using Syncfusion.Maui.DataGrid;

using DXLib.UI;
using DXLib.Data;
using DXLib.Utils;

namespace iStatVball3;

/*
 * Data grid report dynamically configured from JSON and populated with pre-calculated statistic variables. 
 */
public class Grid : SfDataGrid
{
	// Undefined column display
	public const string NA = DXData.NaN;

	// Pseudo Constants
	private Thickness CellPadding;
	private static string TotalStr;

	// Manually controlled totals row
	private DataGridUnboundRow totalRow;

	/* Events */
	public Action LoadComplete { get; set; }

	/* Properties */

	// Grid handling custom first col, last row, etc?
	public bool IsNonStandard { get; private set; }

	// Grid rows statically defined?
	public bool IsStatic { get; private set; }

	// Grid has stacked header row?
	public bool IsStacked { get; private set; }

	// Grid has inner dimension (e.g. rotation by player)
	public bool HasInner { get; private set; }

	// Forced static width of first column (0 for NA)
	public double FixedFirstWd { get; set; }

	// Font size for all stacked, header, and internal cells
	public double TextSize { get; set; }

	// Header/cell height
	public double RowHt { set => SetRowHt( value ); }

	// External JSON configuration
	public JsonGrid Json { get; private set; }

	// Is column sorting currently enabled?
	private bool IsSortingOn => (SortingMode != DataGridSortingMode.None);

	/* Fields */

	// Dynamic grid sizing
	private int rowCount;

	// Customizable decimal precision
	private static int digits;

	// Underlying scope/dimension configuration
	private DataConfig dataConfig;

	private readonly Style dataGridCellStyle;
	
	/* Methods */
	public Grid()
	{
		HorizontalOptions = LayoutOptions.Start;
		VerticalOptions = LayoutOptions.Fill;
		
		BackgroundColor = DXColors.Light4;

		// Set pseudo-constants
		TotalStr = DXString.GetUpper( "grid.total" );

		// Columns auto fit to content
		AutoGenerateColumnsMode = AutoGenerateColumnsMode.None;
		ColumnWidthMode = ColumnWidthMode.Auto;

		// Fixed row height, alternating row colors
		ShowRowHeader = false;
		AlternationRowCount = 2;

		SetRowHt( 35 );

		// Scrolling behavior
		HorizontalScrollBarVisibility = ScrollBarVisibility.Never;
		VerticalScrollBarVisibility = ScrollBarVisibility.Never;

		// Resizing OFF
		AllowResizingColumns = false;
		
		// Sorting ON, one column at a time
		SortingMode = DataGridSortingMode.Single;
		AllowTriStateSorting = false;
		ShowSortNumbers = false;

		// Editing, gestures, selection OFF
		AllowEditing = false;
		AllowDraggingRow = false;
		AllowDraggingColumn = false;

		// Grid lines
		GridLinesVisibility = GridLinesVisibility.Both;
		HeaderGridLinesVisibility = GridLinesVisibility.Both;
		
		// Not selectable
		SelectionMode = DataGridSelectionMode.None;

		// Dimension grouping behavior
		GroupingMode = GroupingMode.Multiple;
		IndentColumnWidth = 35;

		AllowGroupExpandCollapse = true;
		ShowColumnWhenGrouped = false;
		AutoExpandGroups = false;
		
		GroupExpandCollapseTemplate = new DataTemplate( () => new GridSort( true ) );
		
		// Register for callbacks
		DataGridLoaded += OnGridLoaded;
		QueryUnboundRow += OnPopulateTotals;

		GroupExpanding += OnGroupExpanding;
		GroupCollapsed += OnGroupCollapsed;

		// Do NOT expand vertically
		VerticalOptions = LayoutOptions.Start;

		// Defaults
		TextSize = 14; 
		
		// Create a style for DataGridCell
		dataGridCellStyle = new Style( typeof( DataGridCell ) );

		// Dynamic row background colors
		dataGridCellStyle.Setters.Add( new Setter
		{
			Property = DataGridCell.BackgroundProperty,
			Value = new Binding()
			{
				Source = new RelativeBindingSource( RelativeBindingSourceMode.Self ),
				Converter = new GridColorConverter(),
				ConverterParameter = this
			}
		});
		
		// Dynamic bold fonts
		dataGridCellStyle.Setters.Add( new Setter
		{
			Property = DataGridCell.FontAttributesProperty,
			Value = new Binding()
			{
				Source = new RelativeBindingSource( RelativeBindingSourceMode.Self ),
				Converter = new GridFontConverter(),
				ConverterParameter = this
			}
		});
		
		// Custom sort indicator
		SortIconTemplate = new GridSortSelector()
		{
			Ascending = new DataTemplate( () => new GridSort( true ) ),
			Descending = new DataTemplate( () => new GridSort( false ) )
		};
	}

	// Initializes all grid data
	public void Init( JsonGrid json )
	{
		Json = json;
		
		// Custom styling
		DefaultStyle = new GridStyle( this );
		
		// Custom handling?
		IsStatic = json.Static;
		IsNonStandard = IsStatic || json.IsSpecial;

		// Padding
		if ( DXDevice.IsTablet )
		{
			CellPadding = json.LessPad ? new Thickness( 1, 0 ) : new Thickness( 10, 5 );
		}
		else
		{
			CellPadding = json.LessPad ? new Thickness( 1, 0 ) : ((json.MorePad || IsStatic) ? new Thickness( 5, 0 ) :  new Thickness( 1, 0 ) );
		}
	}

	// Returns number of header rows including stacked headers
	private int GetHeaderCount()
	{
		List<DataRowBase> rows = GetRowGenerator().Items;
		
		return rows?.Count( row => row.RowType is RowType.HeaderRow or RowType.StackedHeaderRow ) ?? 0;
	}

	// Returns total number of rows in grid including headers
	public int GetRowCount()
	{
		int headerCount = GetHeaderCount();
		
		return (GetVisualContainer().RowCount - headerCount);
	}
	
	// Dynamically sets height of all grid rows
	private void SetRowHt( double ht )
	{
		HeaderRowHeight = ht;
		RowHeight = ht;
		MinimumHeightRequest = (ht * 2);
	}

	/* Update */

	// Populates all grid data given specified options
	public async Task UpdateData( DataConfig config )
	{
		dataConfig = config;

		// Cache current setting
		digits = Shell.Settings.AnalyzeDigits;

		// Determine if multi-dimensional
		HasInner = !IsStatic && (config.InnerDim != null);

		// Static non-dimensional grid
		if ( IsStatic )
		{
			await UpdateStatic();
		}
		// Normal dimensional grid
		else
		{
			UpdateDimensional();
		}

		// Resize
		AdjustHeight();
	}

	// Loads data and formats columns for static (non-dimensional) grid
	private async Task UpdateStatic()
	{
		SortingMode = DataGridSortingMode.None;

		// Create static rows from specified data set
		List<DataRow> staticData = await GridStatic.Generate( Json, dataConfig );

		// Must occur AFTER generating but BEFORE populating
		FormatColumns();

		// Populate grid (MUST be <dynamic>)
		ItemsSource = new ObservableCollection<dynamic>( staticData );

		rowCount = staticData.Count;
	}

	// Loads data and formats columns for standard dimensional grid
	private void UpdateDimensional()
	{
		// Optionally add dynamic 0-3/0-4 rating columns
		AddRatingColumns();

		SortingMode = Json.Sort ? DataGridSortingMode.Single : DataGridSortingMode.None;

		DataDimension dimension = dataConfig.Dimension;
		var dimensionData = dimension.Metrics.Values;

		// Must occur BEFORE populating
		FormatStacks();
		FormatColumns();

		rowCount = (HasInner ? dimension.OuterCount : dimensionData.Count);

		// Grid MUST map to concrete type
		SourceType = GetDataType();

		// Populate grid (MUST be <dynamic>)
		ItemsSource = new ObservableCollection<dynamic>( dimensionData );

		// Must occur AFTER populating
		FormatTotal();
	}

	// Adds dynamic 0-3/0-4 pass rating columns
	private void AddRatingColumns()
	{
		// Receive/Defense/Pass
		string ratingKey = Json.Rating;

		if ( ratingKey != null )
		{
			// 0-3 or 0-4?
			int count = Shell.Settings.IsPass03 ? 3 : 4;

			// Must prevent duplicate entries
			if ( Json.Columns.Count < count )
			{
				// Must insert backward order
				for ( int rating = count; rating >= 0; rating-- )
				{
					// 'Receive_Rating0'
					string key = $"{ratingKey}_Rating{rating}";

					// Dynamically add JSON definition
					JsonColumn column = new()
					{
						Header = rating.ToString(),
						Key = key,
						Type = AnalyzeKeys.IntKey
					};

					Json.Columns.Insert( 0, column );
				}
			}
		}
	}

	// Returns class type for data set being used by this grid report
	private Type GetDataType()
	{
		// Default skill
		return dataConfig.DataSet switch
		{
			DataConfig.SideoutData => typeof( MetricsSideout ),
			DataConfig.PlayingData => typeof( MetricsPlaying ),
			DataConfig.ScoringData => typeof( MetricsScoring ),
			DataConfig.ResultsData => typeof( MetricsResults ),

			_ => typeof( MetricsSkill )
		};
	}

	// Resizes grid to accomodate current data rows
	private void AdjustHeight()
	{
		int extraRows = IsStacked ? 3 : 2;

		// Static tables have no total row
		if ( IsStatic )
		{
			extraRows--;
		}

		// Dynamically size to contents
		HeightRequest = (rowCount + extraRows) * RowHeight;
	}

	/* Format */

	// Formats grid columns based on JSON configuration
	private void FormatColumns()
	{
		// First column always dimension
		FormatFirstColumn();

		// Remaining columns defined via JSON
		foreach ( JsonColumn jsonCol in Json.Columns )
		{
			FormatColumn( jsonCol );
		}

		// Multi-dimensional grid must also define summary columns
		if ( HasInner )
		{
			FormatGroupColumns();
		}

		// Default sort on first (dimension) column
		if ( IsStatic )
		{
			FormatFirstColumn();
        }
        else
		{
			SortColumnDescriptions.Add( new SortColumnDescription()
			{
				ColumnName = "Label",
				SortDirection = ListSortDirection.Ascending
			});
		}
	}

	// Formats first (dimension) column
	private void FormatFirstColumn()
	{
		FrozenColumnCount = Json.Frozen;

		// Custom grids must manually handle first
		if ( !IsNonStandard )
		{
			string header = DXString.Get( $"dimension.{dataConfig.OuterDim}" );

			// Dimension label
			DataGridColumn column = new DataGridTextColumn
			{
				MappingName = "Label",
				HeaderText = header?.ToUpper()!,
				
				LoadUIView = false,
				CellStyle = dataGridCellStyle
			};

			// Optionally enforce min/max width
			SetFirstColumnWd( column );

			StyleFirstColumn( column );
			Columns.Add( column );
		}
	}

	// Calculates min/max first column width based on configuration
	private void SetFirstColumnWd( DataGridColumn column )
	{
		// Fixed width can be set in code
		if ( FixedFirstWd > 0 )
		{
			column.MinimumWidth = FixedFirstWd;
			column.MaximumWidth = FixedFirstWd;
		}
		// Enforced minimum width (but could be wider)
		else if ( Json.MinWidth )
		{
			column.MinimumWidth = GetMinimumWidth();
		}
	}
	
	// Formats an individual column from JSON definition
	private void FormatColumn( JsonColumn jsonCol )
	{
		string type = jsonCol.Type;
		string key = jsonCol.Key;

		string header = jsonCol.Header;
		string abbrev = jsonCol.Abbrev;

		string headerTxt = DXDevice.IsTablet ? header : (string.IsNullOrEmpty( abbrev ) ? header : abbrev);
		string headerUpper = headerTxt.ToUpper();

		DataGridColumn column = type switch
		{
			// Text or Time column
			AnalyzeKeys.TextKey => new DataGridTextColumn
			{
				MappingName = key, 
			},
			// Time column
			AnalyzeKeys.TimeKey => new DataGridTextColumn
			{
				MappingName = $"{key}Str", 
			},
			// Numeric column (int, float, fixed, percent)
			_ => new DataGridNumericColumn
			{
				MappingName = key, 
				Format = GetNumericFormat( type ),
			}
		};

		// Common config
		column.LoadUIView = false;
		column.HeaderText = headerUpper;
		column.CellStyle = dataGridCellStyle;
		
		StyleColumn( jsonCol, column );
		
		// Optionally enforce width
		if ( jsonCol.MinWidth )
		{
			column.MinimumWidth = GetMinimumWidth();
		}
		
		Columns.Add( column );
	}

	// Returns dynamically defined minimum column width
	private double GetMinimumWidth()
	{
		bool tablet = DXDevice.IsTablet;

		double minWidth;
	
		// Static wider
		if ( IsStatic )
		{
			minWidth = tablet ? 200 : 160;
		}
		// Otherwise depends on dimension
		else
		{
			bool playerDim = (dataConfig.OuterDim == KeyDimension.PlayerKey);
			
			minWidth = tablet ? (playerDim ? 185 : 155) : (playerDim ? 145 : 115);
		}
		
		return minWidth;
	}

	// Returns column format string for specified data type
	public static string GetNumericFormat( string columnType )
	{
		return columnType switch
		{
			AnalyzeKeys.IntKey => "N0",					// 1
			AnalyzeKeys.FloatKey => $"N{digits}",		// 1 - 1.000
			AnalyzeKeys.FixedKey => "N3",				// 0.100
			AnalyzeKeys.PercentKey => $"P{digits}",		// 1% - 1.000%
			AnalyzeKeys.DeltaKey => "+#;-#;0",			// +1

			_ => null,
		};
	}

	// Formats unbound total row
	private void FormatTotal()
	{
		// NOT using Syncfusion total summary
		totalRow = new DataGridUnboundRow
		{
			Position = DataGridUnboundRowPosition.FixedBottom
		};

		UnboundRows.Add( totalRow );
	}

	/* Stacked Header */

	// Formats any stacked headers rows for this grid
	private void FormatStacks()
	{
		IsStacked = Json.IsStacked;

		if ( IsStacked )
		{
			DataGridStackedHeaderRow stackRow = new();

			// Add each group in stacked row
			foreach ( JsonStack configStack in Json.Stacks )
			{
				FormatStack( configStack, stackRow );
			}

			StackedHeaderRows.Add( stackRow );
		}
	}

	// Formats individual stacked header in header row
	private void FormatStack( JsonStack stackJson, DataGridStackedHeaderRow row )
	{
		// Special tag indicates team name in header
		string header = stackJson.Header.Replace( "<team>", dataConfig.TeamName );

		// Create grouped column
		DataGridStackedColumn stackCol = new()
		{
			Text = header.ToUpper(),
			
			ColumnMappingNames = stackJson.GetKeyList(),
			MappingName = stackJson.Header
		};

		row.Columns.Add( stackCol );
	}

	/* Group Summary */

	// Defines group summary row for multidimensional grid
	private void FormatGroupColumns()
	{
		// Data field used to group inner dimensions
		GroupColumnDescription groupDesc = new()
		{
			ColumnName = "TypeKey"
		};

		GroupColumnDescriptions.Add( groupDesc );

		// Inner dimension summary row
		DataGridSummaryRow groupRow = new()
		{
			ShowSummaryInRow = false
		};

		// First column always dimension label
		FormatFirstGroupColumn( groupRow );

		// Create all inner dimension columns
		foreach ( JsonColumn column in Json.Columns )
		{
			FormatGroupColumn( groupRow, column );
		}

		CaptionSummaryRow = groupRow;
	}

	// Defines dimension label first column
	private static void FormatFirstGroupColumn( DataGridSummaryRow groupRow )
	{
		DataGridSummaryColumn column = new()
		{
			Name = "CaptionSummary",
			MappingName = "Label",
			Format = "{Label}",
			SummaryType = SummaryType.Custom,
			CustomAggregate = new GridLabelAggregate()
		};

		groupRow.SummaryColumns.Add( column );
	}

	// Defines column for group summary row with data type specific aggregation
	private static void FormatGroupColumn( DataGridSummaryRow groupRow, JsonColumn column )
	{
		string key = column.Key;

		DataGridSummaryColumn groupCol = new()
		{
			Name = key,
			MappingName = key,
			Format = GetGroupFormat( column.Type ),
			SummaryType = GetAggregate( column.Type )
		};

		groupRow.SummaryColumns.Add( groupCol );
	}

	// Returns format string for specified summary data type
	private static string GetGroupFormat( string columnType )
	{
		return columnType switch
		{
			AnalyzeKeys.IntKey => "{Sum}",
			AnalyzeKeys.FloatKey => "{Average:N1}",
			AnalyzeKeys.FixedKey => "{Average:N3}",
			AnalyzeKeys.PercentKey => "{Average:P1}",

			_ => null,
		};
	}

	// Returns aggregation type for specified summary data type
	private static SummaryType GetAggregate( string columnType )
	{
		return columnType switch
		{
			AnalyzeKeys.IntKey => SummaryType.Int32Aggregate,	// Int
			_ => SummaryType.DoubleAggregate,					// Float, Fixed, Percent
		};
	}

	/* Styling */

	// Applies styling to auto first column
	private void StyleFirstColumn( DataGridColumn column )
	{
		const TextAlignment align = TextAlignment.Start;
		Thickness? padding = DXDevice.IsTablet ? null : DXUtils.AddPadding( CellPadding, 5, 0, 0, 0 );

		StyleHeader( null, column, align, padding );
		StyleCells( null, column, align );
		StylePadding( column, true );
	}

	// Applies custom styling for any middle column
	private void StyleColumn( JsonColumn configCol, DataGridColumn column )
	{
		const TextAlignment align = TextAlignment.Center;
			
		StyleHeader( configCol, column, align );
		StyleCells( configCol, column, align );
		StylePadding( column, false );
	}

	// Used internally to (re)set column specific padding
	private void StylePadding( DataGridColumn column, bool wide )
	{
		column.CellPadding = wide ? DXUtils.AddPadding( CellPadding, new Thickness( 4, 0, 1, 0 ) ) : CellPadding;
	}

	// Applies custom styling to a header cell
	private void StyleHeader( JsonColumn configCol, DataGridColumn column, TextAlignment align, Thickness? padding = null )
	{
		column.HeaderTextAlignment = GetAlignment( configCol?.HAlign, align );
		column.LineBreakMode = LineBreakMode.NoWrap;
		column.HeaderPadding = padding ?? CellPadding;
	}

	// Applies custom styling to any non-header cell
	private static void StyleCells( JsonColumn configCol, DataGridColumn column, TextAlignment align )
	{
		column.CellTextAlignment = GetAlignment( configCol?.Align, align );
		column.LineBreakMode = LineBreakMode.NoWrap;
	}

	// Returns custom column alignment based on JSON config
	private static TextAlignment GetAlignment( string value, TextAlignment align = TextAlignment.Center )
	{
		return string.IsNullOrEmpty( value ) ? align : ((value == "start") ? TextAlignment.Start : TextAlignment.Center);
	}

	/* Event Callbacks */

	// Populates unbound total row
	private void OnPopulateTotals( object sender, DataGridUnboundRowEventArgs args )
	{
		int index = args.RowColumnIndex.ColumnIndex;

		DataMetrics totals = dataConfig.Totals;

		// Custom grid does NOT have first dimension column
		if ( IsNonStandard )
		{
			JsonColumn jsonCol = Json.Columns[ index ];

			// Normal total column
			if ( index >= (Json.TotalStart - 1) )
			{
				args.Value = DXUtils.GetProperty( totals, jsonCol.Key );
			}
			// Text label from JSON config
			else
			{
				args.Value = jsonCol.Footer?.ToUpper()!;
			}
		}
		// Multi-dimensional grid must account for indent column
		else if ( HasInner )
		{
			// Must set dummy value for indent column
			if ( index == 0 )
			{
				args.Value = " ";
			}
			// Dimension column always 'TOTAL'
			else if ( index == 1 )
			{
				args.Value = TotalStr;
			}
			// Other values retrieved from totals data set
			else if ( (index > 1) && (index < (Json.Columns.Count + 2)) )
			{
				JsonColumn jsonCol = Json.Columns[ index - 2 ];
				PropertyInfo property = totals.GetType().GetProperty( jsonCol.Key );

				// Dynamically retrieve field
				args.Value = (property == null) ? NA : property.GetValue( totals )!;
			}
		}
		// Normal grid
		else if ( !HasInner )
		{
			// Dimension column always 'TOTAL'
			if ( index == 0 )
			{
				args.Value = TotalStr;
			}
			// Other values retrieved from totals data set
			else
			{
				if ( index <= Json.Columns.Count )
				{
					JsonColumn jsonCol = Json.Columns[ index - 1 ];
					Type type = totals.GetType();

					// Special handling for time fields
					string key = (jsonCol.Type == "time") ? $"{jsonCol.Key}Str" : jsonCol.Key;
					
					PropertyInfo property = type.GetProperty( key );

					// Dynamically retrieve field
					args.Value = (property == null) ? NA : property.GetValue( totals )!;
				}
			}
		}

		args.Handled = true;
	}

	// Custom column widths
	private void OnGridLoaded( object sender, EventArgs args )
	{
		foreach ( DataGridColumn column in Columns )
		{
			// Add width beyond default sizing to accomodate sort arrow
			if ( IsSortingOn || column.AllowSorting )
			{
				column.Width = column.ActualWidth + 12;
			}
		}

		// WORKAROUND: MUST set margin here
		if ( sender is Grid grid )
		{
			double pad = ReportCard.Pad;
			
			grid.Margin = new Thickness( pad, 0, pad, pad );
		}
		
		// Notify listener
		LoadComplete?.Invoke();
	}

	// Grows grid (and card) height when group expands
	private void OnGroupExpanding( object sender, DataGridColumnGroupChangingEventArgs args )
	{
		Group group = args.Group;
		
		if ( group != null )
		{
			int count = group.ItemsCount;

			// Increase height
			HeightRequest += (count * RowHeight);
		}
	}

	// Shrinks grid (and card) height when group collapses
	private void OnGroupCollapsed( object sender, DataGridColumnGroupChangedEventArgs args )
	{
		Group group = args.Group;

		if ( group != null )
		{
			int count = group.ItemsCount;

			// Decrease height
			HeightRequest -= (count * RowHeight);
		}
	}

	/* Layout */

	// Device/orientation specific layout
	public void UpdateLayout( LayoutType type )
	{
		// Mobile sizes differently to avoid internal scrolling
		if ( DXDevice.IsMobile )
		{
			bool portrait = DXDevice.IsPortrait();
			double wd = DXDevice.GetScreenWd();

			// Summary/BoxScore require extra width
			if ( IsStacked )
			{
				WidthRequest = wd * (portrait ? 2.25 : 10.0);
			}
			else
			{
				WidthRequest = wd * (portrait ? 1.5 : 1.0);
			}
		}
	}
}

//
