/* 
 * 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.Control;
using DXLib.UI.Container;

using DXLib.Video;
using DXLib.Log;
using DXLib.Utils;

namespace iStatVball3;

/*
 * Tool for playing video alongside a play-by-play event log. Filter controls are provided for limiting the scope of PbP
 * events. Tapping an event jumps to the corresponding location in the video.
 *
 * TEST VIDEOS
 * YouTube: uUonCw2QkMg
 * Vimeo: 466580077
 */
public class VideoTool : ToolForm
{
	/* Properties */
	private bool IsRallySync => set.Match.IsRallySync;
	private string FileId => set.Match.Video.FileId;
	
	/* Fields */
	private readonly LogHeader header;
	private readonly LogView log;

	private readonly FilterView filterView;
	private readonly FilterView filterPopupView;

	private DXPopup filterPopup;
	private FilterView filterActive;

	// Optional player filter
	private readonly string filterPlayerId;

	// Embedded video playback
	private readonly DXWebVideo videoPlayer;
	private bool videoReady;

	// Dynamic player sizing
	private double videoWd;
	private double videoHt;
	
	// Highlight tracking
	private DXTimer updateTimer;
	private LogEvent prevEvent;
	private bool filtered;
	
	// Timeout handling
	private DXTimer timeoutTimer;
	
	/* Methods */
	public VideoTool( Set set ) : base( set )
	{
		IsVisible = false;

		// Header
		header = new LogHeader
		{
			ShowClose = true,
			ShowCount = true,

			CloseTapped = OnToolClosed,
			FilterTapped = OnFilterTapped
		};

		header.Init();

		// Log
		log = new LogView( false )
		{
			Mode = LogView.LogMode.SelectAll,
			FontSize = 16,
			
			EventSelected = OnEventSelected
		};

		// Video player
		videoPlayer = new DXWebVideo()
		{
			PlayerLoaded = OnLoaded,
			PlayerReady = OnReady
		};
		
		filterPlayerId = Shell.CurrentUser.GetPermission();

		// Filter
		filterView = new FilterView
		{
			PlayerId = filterPlayerId,
			
			Cleared = OnCleared,
			Filtered = OnFiltered
		};

		layout.Add( header );
		layout.Add( log );
		layout.Add( filterView );
		layout.Add( videoPlayer  );

		// Filter (mobile popup)
		if ( DXDevice.IsMobile )
		{
			filterPopupView = new FilterView
			{
				PlayerId = filterPlayerId,

				Cleared = OnCleared,
				Filtered = OnFiltered
			};
		}

		filterActive = filterView;
	}
	
	// Post construction initialization
	public override async Task Init()
	{
		Shell.Instance.SetStatusBarColor( DefaultColor );

		// Log offsets use video time
		log.StartOffset = GetBaseline();
		log.Init( set.Match );

		string host = set.Match.Video.Host;
		
		// Init video player
		videoPlayer.Init( host );
		
		// Init filter(s)
		filterView.Init( set );
		filterPopupView?.Init( set );

		// May need to run initial filter
		if ( filterPlayerId != null )
		{
			Filter();
		}

		await base.Init();
	}

	// Preps player for video playback
	public override async Task Start()
	{
		// Start timeout timer
		timeoutTimer = DXTimer.Delay( LoadTimeout, async void () =>
		{
			try
			{
				// Must be successfully cued before timeout
				if ( !await videoPlayer.IsCued() )
				{
					await videoPlayer.StopVideo();
					HandleTimeout();
				}
			}
			catch ( Exception ex )
			{
				DXLog.Exception( "video.start", ex );
			}
		});

		// Re-load latest stats
		await LoadStats();

		await Task.FromResult( default( object ) );
	}

	// (Re)loads all stats for log view display
	private async Task LoadStats()
	{
		// Load stats for set
		await set.ReadCache( true );

		List<Stat> stats = set.StatCache;

		if ( stats != null )
		{
			// Event count
			header.SetCount( stats.Count, false );

			// Refresh log
			log.Load( stats );
		}
	}

	// Called repeatedly to update log highlight synced to video
	private async Task UpdateHighlight()
	{
		// Calc current video position (ms)
		int position = await GetPosition( null, false );

		// Find closest event
		LogEvent evt = log.GetEvent( position );

		if ( evt != null )
		{
			LogEvent selected = log.SelectedEvent;

			// Player position reached next event past selection
			if ( (selected == null) || (evt.Stat.Offset > log.SelectedEvent.Stat.Offset) )
			{
				// Erase previous highlight
				ResetPrev();
				
				// Move highlight
				evt.Color = DXColors.Dark3;

				log.Refresh( evt );
				log.ScrollTo( evt );

				// Remove selection
				log.Deselect();

				prevEvent = evt;
			}
		}
	}

	// Redraws previously highlighted event
	private void ResetPrev()
	{
		if ( prevEvent != null )
		{
			prevEvent.Color = DXColors.Dark2;
			log.Refresh( prevEvent );
		}
	}

	// Filters log given current filter configuration
	private void Filter()
	{
		DXSpinner.Start();

		// Filter stats against current selections
		Filter filter = filterActive.GetFilter();
		List<Stat> stats = filter.FilterStats( set.StatCache );

		int count = stats.Count;
		
		// Update log
		log.Load( stats );

		filtered = (count < set.StatCount);

		// Update event count (indicate filtered)
		header.SetCount( count, filtered );

		DXSpinner.Stop();
	}

	/* Video Position */

	// Returns starting position for video playback (ms)
	private int GetStart()
	{
		// Default to first serve
		int start = set.Video.Offset;

		// RallySync uses time of first event
		if ( IsRallySync )
		{
			start = (set.StatCount > 0) ? set.StatCache[0].VideoOffset : 0;
		}
		
		int delay = GetDelay();

		// Adjust for delay
		int position = (start - delay);
		
		return Math.Max( position, 0 );
	}
	
	// Determines optimal video position (ms) for specified stat
	private async Task<int> GetPosition( Stat stat, bool delay )
	{
		int baseline = GetBaseline();
		int offset = await GetOffset( stat );  

		// Calculate current position (relative to video start or first serve)
		int position = (IsRallySync || (stat == null)) ? (offset - baseline) : (baseline + offset);

		// Optionally seek backwards slightly
		if ( delay )
		{
			position -= GetDelay();
		}

		// Must be >= 0
		return Math.Max( position, 0 );
	}
	
	// Returns baseline for offsets (ms), either video start or first serve
	private int GetBaseline()
	{
		return IsRallySync ? 0 : set.Video.Offset;
	}
	
	// Get video playback offset for specified stat
	private async Task<int> GetOffset( Stat stat )
	{
		// Use current video player position
		if ( stat == null )
		{
			double offset = await videoPlayer.GetVideoTime();

			return (int)offset;
		}

		// Otherwise either RallySync video position or offset from first serve
		return IsRallySync ? stat.VideoOffset : stat.Offset;
	}
	
	// Returns amount of time (ms) for padding before point of contact		
	private int GetDelay( Stat stat = null )
	{
		// Baseline
		const int delay = 1000;

		// QuickSelect uses base delay
		if ( stat != null )
		{
			switch ( stat.Action )
			{
				case Stats.ServeKey:
				case Stats.SecondKey:
				case Stats.BlockKey:
				case Stats.PutbackKey:
				{
					return delay;
				}
			}
		}

		// All others use longer
		return IsRallySync ? (delay * 2) : (delay * 3);
	}
	
	/* Event Callbacks */
	
	// Player API loaded
	private async void OnLoaded()
	{
		videoReady = true;
		
		// Remove previous selection
		log.Deselect();
		log.ScrollTo( null );

		ResetPrev();

		// May need to set initial size here
		await videoPlayer.UpdateSize( videoWd, videoHt );

		int start = GetStart();
		
		// Dynamically set video (starts paused,muted)
		await videoPlayer.SetVideo( FileId, start );
		
		// Required for correct <iframe> sizing
		if ( DXDevice.IsMobile )
		{
			UpdateLayout();
		}
	}

	// Player ready for video playback
	private async void OnReady()
	{
		// Repeatedly update highlight position to sync with video
		await UpdateHighlight();
		
		updateTimer = new DXTimer
		{
			Interval = VideoPlayer.ClockUpdate,
			IsRepeating = true,
			Callback = async void () =>
			{
				try
				{	
					await UpdateHighlight();
				}
				catch ( Exception ex )
				{
					DXLog.Exception( "video.ready", ex );
				}
			}
		};

		// Not while filtering
		if ( !filtered )
		{
			updateTimer.Start();
		}

		DXSpinner.Stop();
	}
	
	// User tapped log event, jump to location in video
	private async void OnEventSelected( LogEvent evt )
	{
		ResetPrev();
		
		// Calculate playback position
		double position = await GetPosition( evt.Stat, true );

		// Jump to location
		await videoPlayer.SeekTo( position );
	}

	// User exiting tool
	protected override async void OnToolClosed()
	{
		videoReady = false;
		
		// Stop timers
		timeoutTimer?.Stop();
		updateTimer?.Stop();

		// Must manually stop
		await videoPlayer.StopVideo();
		
		Shell shell = Shell.Instance;
		
		// Return to main UI
		shell.SetStatusBarColor( AppBar.DefaultColor );
		shell.HideTool();
	}

	// User opening filter popup
	private void OnFilterTapped()
	{
		// Create popup
		filterPopup = new DXPopup( filterPopupView )
		{
			IsModal = false,

			ViewWidth = 355,
			ViewHeight = 545,

			PopupClosed = OnClosed
		};

		// Show
		filterPopup.ShowFromView( header.FilterBtn );
	}

	// User reset to default filters
	private void OnCleared()
	{
		filterPopup?.Hide();

		DXSpinner.Start();

		// Reload log
		log.Load( set.StatCache );
		header.SetCount( set.StatCount, false );

		filtered = false;

		// Might need to re-run initial filter
		if ( filterPlayerId != null )
		{
			Filter();
		}

		DXSpinner.Stop();
	}

	// User updated event filter
	private void OnFiltered()
	{
		filterPopup?.Hide();

		Filter();
	}

	// User closed (cancelled) filter popup
	private void OnClosed()
	{
		header.FilterBtn.Reset();
	}

	/* Layout */

	// Redraws all tool components
	public override async void UpdateLayout( LayoutType type )
	{
		Padding = 0;

		// Layout for orientation
		base.UpdateLayout( type );

		filterView.UpdateLayout( type );
		filterPopupView?.UpdateLayout( type );

		// Dynamic video sizing
		if ( videoReady )
		{
			await videoPlayer.UpdateSize( videoWd, videoHt );
		}

		// Make sure mobile starts at top
		if ( DXDevice.IsMobile )
		{
			await scroll.ScrollToTopWait();
		}
	}

	// Landscape (4:3)
	protected override void Landscape()
	{
		// Dynamic sizing
		double wd = DXDevice.GetScreenWd();
		double ht = (DXDevice.GetScreenHt() - Padding.VerticalThickness);

		double headerY = DXDevice.SafeArea().Top;
		const double headerHt = LogHeader.HeaderHt;

		double logY = (headerY + headerHt);
		double logWd = (wd * 0.35);
		double logHt = (ht - logY);

		double videoY = headerY;
		videoWd = (wd - logWd);
		videoHt = (videoWd * VideoPlayer.RatioHt);

		double filterX = logWd;
		double filterY = (videoY + videoHt);
		double filterHt = (ht - filterY);

		// Static layout
		layout.SetBounds( header, 0, headerY, logWd, headerHt );
		layout.SetBounds( log, 0, logY, logWd, logHt );
		layout.SetBounds( videoPlayer, logWd, videoY, videoWd, videoHt );
		layout.SetBounds( filterView, filterX, filterY, videoWd, filterHt );
	}

	// Portrait (3:4)
	protected override void Portrait()
	{
		// Dynamic sizing
		double wd = DXDevice.GetScreenWd();
		double ht = (DXDevice.GetScreenHt() - Padding.VerticalThickness);

		double videoY = DXDevice.SafeArea().Top;
		videoWd = wd;
		videoHt = (videoWd * VideoPlayer.RatioHt);

		double headerY = (videoY + videoHt);
		const double headerHt = LogHeader.HeaderHt;

		double logY = (headerY + headerHt);
		double logWd = (wd * 0.50);
		double logHt = (ht - logY);

		double filterX = logWd;
		double filterY = headerY;
		double filterWd = (wd - logWd);
		double filterHt = (ht - filterY);

		// Static layout
		layout.SetBounds( header, 0, headerY, logWd, headerHt );
		layout.SetBounds( videoPlayer, 0, videoY, videoWd, videoHt );
		layout.SetBounds( log, 0, logY, logWd, logHt );
		layout.SetBounds( filterView, filterX, filterY, filterWd, filterHt );
	}

	// Mobile Landscape
	protected override void MobileLandscape()
	{
		Thickness safeArea = DXDevice.ExtraSafeArea();
		
		// Dynamic sizing
		double wd = DXDevice.GetScreenWd();
		double ht = DXDevice.GetScreenHt();

		// Header
		double headerX = safeArea.Left;
		double headerY = safeArea.Top;

		double headerWd = (wd * 0.4);
		const double headerHt = LogHeader.HeaderHt;

		// Log
		double logX = headerX;
		double logY = (headerY + headerHt);

		double logWd = headerWd;
		double logHt = (ht - headerHt);

		// Video
		videoWd = (wd - headerWd);
		videoHt = (videoWd * VideoPlayer.RatioHt);
		
		double videoX = (headerX + headerWd);
		double videoY = ((ht - videoHt) / 2.0);

		header.ShowFilter = true;

		// Uses popup filter NOT view
		filterView.IsVisible = false;
		filterActive = filterPopupView;
		
		// Static layout
		layout.SetBounds( header, headerX, headerY, logWd, headerHt );
		layout.SetBounds( log, logX, logY, logWd, logHt );
		layout.SetBounds( videoPlayer, videoX, videoY, videoWd, videoHt );
	}

	// Mobile Portrait
	protected override void MobilePortrait()
	{
		Thickness safeArea = DXDevice.SafeArea();
		
		// Dynamic sizing
		double wd = DXDevice.GetScreenWd();
		double ht = DXDevice.GetScreenHt();

		// Video
		const double videoX = 0;
		double videoY = safeArea.Top;
		
		videoWd = wd;
		videoHt = (videoWd * VideoPlayer.RatioHt);

		// Header
		const double headerX = videoX;
		double headerY = (videoY + videoHt);
		
		double headerWd = wd;
		const double headerHt = LogHeader.HeaderHt;

		// Log
		const double logX = headerX;
		double logY = (headerY + headerHt);
		
		double logWd = headerWd;
		double logHt = (ht - headerHt - videoHt - videoY);

		header.ShowFilter = true;

		// Uses popup filter NOT view
		filterView.IsVisible = false;
		filterActive = filterPopupView;

		// Static layout
		layout.SetBounds( videoPlayer, videoX, videoY, videoWd, videoHt );
		layout.SetBounds( header, headerX, headerY, headerWd, headerHt );
		layout.SetBounds( log, logX, logY, logWd, logHt );
	}
}

//
