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

using CommunityToolkit.Mvvm.Messaging;

using DXLib.UI.Card;
using DXLib.UI.Alert;
using DXLib.UI.Layout;
using DXLib.UI.Container;
using DXLib.UI.Form.Message;

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

using DXLib.Data;
using DXLib.Data.Model;

using DXLib.Log;
using DXLib.Utils;

namespace DXLib.UI.Form;

/*
 * Base class for all CRUD data entry forms. Subclasses must implement functionality specific to their particular object
 * type. All forms have an image area, header, and a primary and secondary list of controls. The primary list is typically
 * used for required fields and the secondary for optional fields. The bottom of the form contains Delete, Cancel, and
 * Save buttons. If the form is read-only, a lock icon will be shown.
 */
public abstract class DXForm : DXContent
{
	/* Constants */
	protected const double BasePad = 15;
	
	// Material style
	private const int DefaultElevation = 4;
	private const int DefaultRadius = 12;

	// Available form editing modes
	public enum ModeType
	{
		Create,
		Edit,
		CreateReadOnly,
		EditReadOnly
	};

	/* Properties */
	public DXFormControlList PrimaryList { get; private set; }
	public DXFormControlList SecondaryList { get; private set; }

	public ModeType Mode { get; private set; }

	// Has data if in edit mode, read-only or not
	public bool HasData => Mode is ModeType.Edit or ModeType.EditReadOnly;

	// Can image panel be edited?
	public bool IsIconOnly { get; set; }

	// Read-only can have data or not
	public bool IsReadOnly => Mode is ModeType.CreateReadOnly or ModeType.EditReadOnly;

	// Context specific custom header button 
	public DXIconButton CustomHeaderBtn => header.CustomBtn;

	public string CustomHeader { set => header.SetCustom( value ); }
	public Color CustomHeaderColor { set => header.SetCustomColor( value ); }

	// Context specific custom footer button 
	public DXIconButton CustomFooterBtn => buttons.CustomBtn;

	// Underlying layouts

	/* Fields */
	protected readonly object data;
	protected readonly string dataType;

	// Underlying UI controls
	protected readonly DXFormImage imagePanel;
	protected readonly DXFormHeader header;
	protected readonly DXFormButtons buttons;

	// Underlying layouts
	public new DXGridLayout Layout { get; private set; }

	public DXGridLayout LayoutL { get; private set; }
	public DXVerticalLayout LayoutP { get; private set; }

	// Mobile only
	protected readonly DXScroll scroll;

	/* Methods */
	protected DXForm( object data, string dataType, bool readOnly = false, bool hasImage = true, bool scrollable = true )
	{
		this.data = data;
		this.dataType = dataType;

		Color = DXColors.Dark1;

		Padding = 0;
		Margin = 0;

		Horizontal = LayoutOptions.Fill;
		Vertical = LayoutOptions.Fill;

		// Required for rounded corners 
		DXBorder frame = new()
		{
			Color = DXColors.Dark1,
			
			CornerRadius = DefaultRadius,
			Elevation = DefaultElevation,
			BorderWidth = 0,
			
			Padding = 0,
			Margin = 0,
			
			Horizontal = LayoutOptions.Fill,
			Vertical = LayoutOptions.Fill
		};

		// Actual data form layout
		Layout = new DXGridLayout
		{
			BackgroundColor = DXColors.Light3,
			
			Padding = 0,
			Margin = 0,
			
			Horizontal = LayoutOptions.Fill,
			Vertical = LayoutOptions.Fill,

			// REQUIRED
			IgnoreSafeArea = true
		};
		
		frame.Content = Layout;
		frame.Init();
		
		Content = frame;
		
		// Can force entire object tree to read-only
		bool locked = (readOnly || DXCard.GlobalLock);

		// Create or edit? Read only?
		Mode = (data == null) ? (locked ? ModeType.CreateReadOnly : ModeType.Create) : (locked ? ModeType.EditReadOnly : ModeType.Edit);

		// Spacing
		Layout.RowSpacing = 0;
		Layout.ColumnSpacing = 0;

		string defaultIcon = dataType;
		int index = dataType.IndexOf( '.' );

		// Support subtypes using parent icon
		if ( index > 0 )
		{
			defaultIcon = dataType[ ..index ];
		}

		// Image area
		if ( hasImage )
		{
			imagePanel = new DXFormImage( this )
			{
				DefaultIcon = defaultIcon
			};
		}

		// Header
		header = new DXFormHeader( this )
		{
			CustomTapped = OnCustomHeaderTapped
		};

		// Primary list fields (always scrollable)
		PrimaryList = new DXFormControlList( true )
		{
			Color = DXColors.Light4,
			Title = "form.required",

			IsReadOnly = readOnly,
		};

		// Secondary list fields (scroll can be disabled)
		SecondaryList = new DXFormControlList( scrollable ) 
		{
			Color = DXColors.Light3,
			Title = "form.optional",

			IsReadOnly = readOnly
		};

		// Delete, Cancel, Save buttons
		buttons = new DXFormButtons( readOnly, (Mode == ModeType.Edit) )
		{
			DeleteTapped = OnDeleteTapped,
			SaveTapped = OnSaveTapped,
			CancelTapped = OnCancelTapped,

			CustomTapped = OnCustomFooterTapped
		};

		// Entire form may need to scroll
		if ( DXDevice.IsMobile || !scrollable )
		{
			LayoutP = new DXVerticalLayout
			{
				BackgroundColor = DXColors.Light3,

				Padding = 0,
				Margin = 0,
				Spacing = 0,
				
				Horizontal = LayoutOptions.Fill,
				Vertical = LayoutOptions.Fill
			};

			scroll = new DXScroll
			{
				BackgroundColor = DXColors.Light2,
				
				Padding = 0,
				Margin = 0,
				
				Horizontal = LayoutOptions.Fill,
				Vertical = LayoutOptions.Fill
			};
		}

		// Listen for messages from child controls
		WeakReferenceMessenger.Default.Register<DXFormChangedMessage>( this, OnControlChanged );
        WeakReferenceMessenger.Default.Register<DXFormFocusedMessage>( this, OnControlFocused );
    }

    // Post-construction control initialization
    public virtual void Init()
	{
		bool canEdit = !IsIconOnly && !IsReadOnly;

		// Can image be edited?
		imagePanel?.Init( canEdit );

		PrimaryList.Init();
		SecondaryList.Init();
	}

	// Unsubscribes from all messages when form is closed (not required)
	private void Unsubscribe()
	{
		WeakReferenceMessenger.Default.UnregisterAll( this );
	}

	// Determines if any control on this form has changes to its value
	protected virtual bool HasChanges()
	{
		return (imagePanel is { HasChanges: true }) || PrimaryList.HasChanges() || SecondaryList.HasChanges();
	}

	// Enables/disables save button based on validity of required fields
	public virtual bool IsValid()
	{
		bool valid = PrimaryList.IsValid() && SecondaryList.IsValid();

		buttons.SaveEnabled = valid;

		return valid;
	}

	// Enables save button if entire form is valid
	public void UpdateSave( bool permission = true )
	{
		buttons.SaveEnabled = permission && IsValid();
	}

	// Adds a control of any type to the primary or secondary fields list
	protected void AddControl( DXFormControl control, bool primary, bool required )
	{
		if ( primary )
		{
			PrimaryList.Add( control, required, IsReadOnly );
		}
		else
		{
			SecondaryList.Add( control, required, IsReadOnly );
		}
	}

	// Inserts a control of any type at specified position in list
	protected void InsertControl( DXFormControl control, bool primary, int index, bool required )
	{
		if ( primary )
		{
			PrimaryList.Insert( control, index, required, IsReadOnly );
		}
		else
		{
			SecondaryList.Insert( control, index, required, IsReadOnly );
		}
	}

	// Removes specified control from given list
	protected void RemoveControl( DXFormControl control, bool primary )
	{
		if ( primary )
		{
			PrimaryList.Remove( control );
		}
		else
		{
			SecondaryList.Remove( control );
		}
	}

	// Adds special extended control to primary or secondary list
	public void AddExtended( DXFormControl control, bool primary, bool required )
	{
		if ( primary ) 
		{ 
			PrimaryList.AddExtended( control, required, IsReadOnly ); 
		} 
		else 
		{ 
			SecondaryList.AddExtended( control, required, IsReadOnly ); 
		}
	}

	// Clears all extended controls from primary or secondary list
	protected void ClearExtended( bool primary )
	{
		if ( primary ) 
		{ 
			PrimaryList.ClearAllExtended(); 
		} 
		else 
		{ 
			SecondaryList.ClearAllExtended(); 
		}
	}

	// Enables/disables specified control
	protected void DisableControl( string key, bool primary, bool disabled )
	{
		DXFormControl control = GetControl( key, primary );

		control?.SetDisabled( disabled );
	}

	// Un-focuses all controls on this form except for specified control
	public void UnfocusAll( DXFormControl control )
	{
		PrimaryList.UnfocusAll( control );
		SecondaryList.UnfocusAll( control ); 
	}

	// Used internally to create 'New Foo' header for current object type
	protected string CreateHeader()
	{
		return $"{DXString.Get( "form.new" )} {DXString.Get( $"{dataType}.singular" )}";
	}

	// Sets state of control matching specified key
	protected void SetState( string key, DXFormControl.ControlState state, bool primary )
	{
		if ( primary ) { PrimaryList.SetState( key, state ); } else { SecondaryList.SetState( key, state ); }
	}

	// Shows reusable error message when new object not unique
	private protected static void ShowUniqueError( string obj, string parent )
	{
		string objectStr = $"{obj}.singular";
		string parentStr = $"{parent}.singular";

		DXAlert.ShowError( objectStr, "form.err.unique", DXString.Get( objectStr ), DXString.Get( parentStr ) );

		DXSpinner.Stop();
	}

	/* Getters */

	// Returns control on this form matching specified unique key
	public DXFormControl GetControl( string key, bool primary )
	{
		return primary ? PrimaryList.GetControl( key ) : SecondaryList.GetControl( key );
	}

	// Returns data value as string for field matching specified key
	public string GetString( string key, bool primary )
	{
		return primary ? PrimaryList.GetString( key ) : SecondaryList.GetString( key );
	}

	// Returns data value as an integer for field matching specified key
	public int? GetInt( string key, bool primary )
	{
		object obj = GetObject( key, primary );

		return (obj == null) ? null : Convert.ToInt32( obj );
	}

	// Returns data value as a byte for field matching specified key
	public byte? GetByte( string key, bool primary )
	{
		object obj = GetObject( key, primary );

		return (obj == null) ? null : Convert.ToByte( obj );
	}

	// Returns data value as boolean for field matching specified key
	public bool GetBool( string key, bool primary )
	{
		return primary ? PrimaryList.GetBool( key ) : SecondaryList.GetBool( key );
	}

	// Returns data value as a date for field matching specified key
	protected DateTimeOffset GetDate( string key, bool primary )
	{
		object obj = GetObject( key, primary );

		// MUST convert to DateTimeOffset
		return (obj is DateTime date) ? new DateTimeOffset( date ) : DateTimeOffset.MinValue;
	}

	// Returns data value as a time offset for field matching specified key
	protected TimeSpan GetTime( string key, bool primary )
	{
		return (TimeSpan) GetObject( key, primary );
	}

	// Returns data value as raw color for field matching specified key
	protected long GetColor( string key, bool primary )
	{
		return primary ? PrimaryList.GetColor( key ) : SecondaryList.GetColor( key );
	}

	// Returns data value as a list of selected items, applicable for multiselect controls
	protected IList<object> GetList( string key, bool primary )
	{
		object list = GetObject( key, primary );

		return list as IList<object>;
	}

	// Returns data value as list of selected item keys, applicable for multiselect controls
	protected IList<string> GetKeys( string key, bool primary )
	{
		return GetObject( key, primary ) as IList<string>;
	}

	// Returns data value as a raw object for field matching specified key
	protected object GetObject( string key, bool primary )
	{
		return primary ? PrimaryList.GetObject( key ) : SecondaryList.GetObject( key );
	}

	/* Image */

	// Sets header image to photo (raw bytes), default icon if NA
	protected void SetImage( byte[] bytes )
	{
		imagePanel?.SetImage( bytes );
	}

	// Sets header image to photo (remote URL), default icon if NA
	protected void SetImage( string url )
	{
		imagePanel?.SetImage( url );
	}

	// Writes header image for specified model to remote cloud storage
	protected async Task SaveImage( DXImageModel model )
	{
		// Avoid network call if no change
		if ( imagePanel is { HasChanges: true } )
		{
			// Get raw JPG data
			byte[] bytes = await imagePanel.GetImage();

			// Clear from image cache
			await model.ClearCache();
			
			// Might be empty/deleted
			if ( bytes == null )
			{
				// Remove from cloud
				await DXData.DeleteStorage( model.ImageUrl );

				model.ImageUrl = null;
			}
			// Save/update image
			else
			{
				// Save relative cloud path
				model.ImageUrl = DXImageModel.GetRelativeUrl( model );

				// Persist
				await DXData.WriteStorage( model.ImageUrl, bytes, DXImageModel.ContentType );
			}
		}
	}

	/* Event Callbacks */

	// Delete button tapped
	private void OnDeleteTapped() 
	{
		// Connection required
		if ( DXData.HasConnection() )
		{
			string typeStr = DXString.Get( $"{dataType}.singular" );

			// Confirm deletion with user
			DXDeleteAlert.Show( typeStr, OnDeleteConfirmed, buttons.Reset );
		}
		else
		{
			DXAlert.ShowNetworkError( "net.err.delete" );
		}
	}

	// Cancel button tapped, confirm with user if changes exist
	protected void OnCancelTapped()
	{
		// Confirm first
		if ( !IsReadOnly && HasChanges() )
		{
			DXAlert.ShowNegativeCancel( "form.cancel.title", "form.cancel.msg", "form.discard", OnCancelConfirmed, buttons.Reset );
		}
		// Directly cancel
		else
		{
			OnCancelConfirmed();
		}
	}

    // Called back when any child control changes value
    protected virtual void OnControlChanged( object sender, DXFormChangedMessage msg )
	{
        UpdateSave();
	}

    // Called back when any child control gains/loses focus
    protected virtual void OnControlFocused( object sender, DXFormFocusedMessage msg )
    {
		DXFormControl control = sender as DXFormControl;

        UnfocusAll( control );
    }

	// User confirmed delete, subclass must implement delete functionality
	protected virtual void OnDeleteConfirmed()
	{
		buttons.IsEnabled = false;
		buttons.DeleteEnabled = false;

		DXSpinner.Start();

		Unsubscribe();
	}

	// User confirmed cancel (or no changes), subclass must implement functionality
	protected virtual void OnCancelConfirmed()
	{
		Unsubscribe();
	}

	// Save tapped, subclass must implement save functionality
	protected virtual void OnSaveTapped()
	{
		buttons.IsEnabled = false;
		buttons.SaveEnabled = false;

		DXSpinner.Start();

		Unsubscribe();
	}

	// Checks for network connection, displays error if not available
	protected bool ValidateConnection()
	{
		if ( !DXData.HasConnection() )
		{
			DXAlert.ShowNetworkError( "net.err.form", DXUtils.ToFirstUpper( dataType ) );

			return false;
		}

		return true;
	}

	// Context specific custom header button tapped
	protected virtual void OnCustomHeaderTapped()
	{}

	// Context specific custom footer button tapped
	protected virtual void OnCustomFooterTapped()
	{}

	// Called by shell when new object added to any DXAddSelector field
	public virtual void OnObjectAdded( string key, DXModel model )
	{}

	/* Layout */
 
	// Removes and re-adds all controls to ensure proper layout 
	private void ClearAll()
	{
		Layout.ClearAll();
		
		PrimaryList.Clear();
		SecondaryList.Clear();
	}
	
	// Device/orientation specific layout
	public override void UpdateLayout( LayoutType type )
	{
		DXLog.Debug( "Form:{0}", dataType );

		ClearAll();

		Thickness safeArea = DXDevice.SafeArea();
		
		// Background bleeds over safe areas, form stays within
		Padding = new Thickness( (BasePad + safeArea.Left), BasePad, (BasePad + safeArea.Right), (BasePad + safeArea.Bottom) );

		// Size specific layouts
		base.UpdateLayout( type );

		// Update children
		imagePanel?.UpdateLayout( type );
		header.UpdateLayout( type );

		// Always start at top
		scroll?.ScrollToTop();
	
		PrimaryList.AddAll();
		SecondaryList.AddAll();
	}

	// Landscape (4:3)
	protected override void Landscape()
	{
		// 3 rows
		Layout.AddFixedRow( 76 );         // 0: header
		Layout.AddStarRow();              // 1: control lists
		Layout.AddFixedRow( 68 );         // 2: buttons

		// 3 columns
		Layout.AddStarColumn( 26 );		  // 0: image panel
		Layout.AddStarColumn( 37 );		  // 1: primary
		Layout.AddStarColumn( 37 );		  // 2: secondary

		// Add views
		Layout.Add( imagePanel, 0, 0, 1, 3 );
		Layout.Add( header, 1, 0, 2, 1 );

		Layout.Add( PrimaryList.View, 1, 1 );
		Layout.Add( SecondaryList.View, 2, 1 );

		Layout.Add( buttons, 1, 2, 2, 1 );
	}

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

		// 6 rows
		Layout.AddFixedRow( ios ? 250 : 265 );      // 0: image panel
		Layout.AddFixedRow( ios ? 68 : 53 );        // 1: header
		Layout.AddStarRow();                        // 2: controls lists
		Layout.AddFixedRow( 68 );                   // 3: buttons

		// 2 columns
		Layout.AddStarColumn();					    // 0: primary
		Layout.AddStarColumn();						// 1: secondary

		// Add views
		Layout.Add( imagePanel, 0, 0, 2, 1 );
		Layout.Add( header, 0, 1, 2, 1 );

		Layout.Add( PrimaryList.View, 0, 2 );
		Layout.Add( SecondaryList.View, 1, 2 );

		Layout.Add( buttons, 0, 3, 2, 1 );
	}

	// Mobile landscape
	protected override void MobileLandscape()
	{
		// Must recreate each time here
		LayoutL = new DXGridLayout
		{
			BackgroundColor = DXColors.Light4,

			RowSpacing = 0,
			ColumnSpacing = 0,
			
			Padding = 0,
			Margin = 0,
			
			Horizontal = LayoutOptions.Fill,
			Vertical = LayoutOptions.Fill,
			
			// Do NOT ignore
			IgnoreSafeArea = false
		};
		
		// 2 inner rows
		LayoutL.AddFixedRow( 50 );      	// 0: header
		LayoutL.AddAutoRow();               // 1: controls

		// 3 inner columns
		LayoutL.AddStarColumn( 26 );        // 0: image
		LayoutL.AddStarColumn( 37 );        // 1: primary
		LayoutL.AddStarColumn( 37 );        // 2: secondary

		// Add inner views
		LayoutL.Add( imagePanel, 0, 0 );
		LayoutL.SpanRows( imagePanel, 3 );

		LayoutL.Add( header, 1, 0 );
		LayoutL.SpanColumns( header, 2 );

		LayoutL.Add( PrimaryList.View, 1, 1 );
		LayoutL.Add( SecondaryList.View, 2, 1 );

		// WORKAROUND: force full width
		scroll.AdjustForSafeArea( LayoutL );
		
		// Entire grid scrolls
		scroll.Content = LayoutL;

		// 1 outer column 
		Layout.AddStarColumn();
		
		// 2 outer rows
		Layout.AddStarRow();				// 0: scroll
		Layout.AddFixedRow( 52 );			// 1: buttons
		
		Layout.Add( scroll, 0, 0 );
		Layout.Add( buttons, 0, 1 );
	}

	// Mobile portrait
	protected override void MobilePortrait()
	{
		LayoutP.Clear();

		// All views stacked vertically
		LayoutP.Add( imagePanel );
		LayoutP.Add( header );
		LayoutP.Add( PrimaryList.View );
		LayoutP.Add( SecondaryList.View );

		// Entire stack scrolls
		scroll.Content = LayoutP;

		// 1 outer column 
		Layout.AddStarColumn();

		// 2 outer rows
		Layout.AddStarRow();				// 0: scroll
		Layout.AddFixedRow( 52 );			// 1: buttons

		Layout.Add( scroll, 0, 0 );
		Layout.Add( buttons, 0, 1 );
	}
}

//
