using System;
using System.Collections.Generic;
using Jellyfin.Plugin.JellyLMS.Models;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.JellyLMS.Services;
///
/// Event args for state transitions.
///
public class StateTransitionEventArgs : EventArgs
{
///
/// Gets the state before the transition.
///
public PlaybackState FromState { get; init; }
///
/// Gets the state after the transition.
///
public PlaybackState ToState { get; init; }
///
/// Gets the reason for the transition.
///
public string? Reason { get; init; }
}
///
/// Manages playback state transitions with validation.
///
public class PlaybackStateMachine
{
private readonly object _lock = new();
private readonly ILogger? _logger;
private PlaybackState _currentState = PlaybackState.Idle;
private PlaybackState _stateBeforeSeek = PlaybackState.Idle;
///
/// Valid state transitions. Key is the current state, value is the array of valid next states.
///
private static readonly Dictionary ValidTransitions = new()
{
[PlaybackState.Idle] = [PlaybackState.Loading, PlaybackState.Stopped],
[PlaybackState.Loading] = [PlaybackState.Playing, PlaybackState.Error, PlaybackState.Stopped],
[PlaybackState.Playing] = [PlaybackState.Paused, PlaybackState.Seeking, PlaybackState.Loading, PlaybackState.Stopped, PlaybackState.Error],
[PlaybackState.Paused] = [PlaybackState.Playing, PlaybackState.Seeking, PlaybackState.Loading, PlaybackState.Stopped, PlaybackState.Error],
[PlaybackState.Seeking] = [PlaybackState.Playing, PlaybackState.Paused, PlaybackState.Error, PlaybackState.Stopped],
[PlaybackState.Error] = [PlaybackState.Loading, PlaybackState.Idle, PlaybackState.Stopped],
[PlaybackState.Stopped] = [PlaybackState.Idle, PlaybackState.Loading]
};
///
/// Initializes a new instance of the class.
///
/// Optional logger for state transitions.
public PlaybackStateMachine(ILogger? logger = null)
{
_logger = logger;
}
///
/// Fired when the state changes.
///
public event EventHandler? StateChanged;
///
/// Gets the current playback state.
///
public PlaybackState CurrentState
{
get
{
lock (_lock)
{
return _currentState;
}
}
}
///
/// Gets the state before the current seek operation (if in Seeking state).
/// Used to restore the correct state after seeking completes.
///
public PlaybackState StateBeforeSeek
{
get
{
lock (_lock)
{
return _stateBeforeSeek;
}
}
}
///
/// Gets a value indicating whether playback is active (Playing or Paused).
///
public bool IsPlaybackActive
{
get
{
lock (_lock)
{
return _currentState == PlaybackState.Playing
|| _currentState == PlaybackState.Paused
|| _currentState == PlaybackState.Seeking
|| _currentState == PlaybackState.Loading;
}
}
}
///
/// Attempts to transition to a new state.
///
/// The target state.
/// Optional reason for the transition (for logging).
/// True if the transition was valid and completed.
public bool TryTransition(PlaybackState newState, string? reason = null)
{
lock (_lock)
{
if (_currentState == newState)
{
return true; // Already in this state
}
if (!IsValidTransition(_currentState, newState))
{
_logger?.LogWarning(
"Invalid state transition attempted: {From} -> {To} (reason: {Reason})",
_currentState,
newState,
reason ?? "none");
return false;
}
// Store state before seek for restoration
if (newState == PlaybackState.Seeking)
{
_stateBeforeSeek = _currentState;
}
var oldState = _currentState;
_currentState = newState;
_logger?.LogInformation(
"State transition: {From} -> {To} (reason: {Reason})",
oldState,
newState,
reason ?? "none");
// Fire event outside the lock to prevent deadlocks
var args = new StateTransitionEventArgs
{
FromState = oldState,
ToState = newState,
Reason = reason
};
try
{
StateChanged?.Invoke(this, args);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error in StateChanged event handler");
}
return true;
}
}
///
/// Forces a state change without validation. Use with caution.
/// Intended for error recovery scenarios.
///
/// The target state.
/// Reason for the forced transition.
public void ForceState(PlaybackState newState, string reason)
{
lock (_lock)
{
var oldState = _currentState;
_currentState = newState;
_logger?.LogWarning(
"Forced state transition: {From} -> {To} (reason: {Reason})",
oldState,
newState,
reason);
var args = new StateTransitionEventArgs
{
FromState = oldState,
ToState = newState,
Reason = $"FORCED: {reason}"
};
try
{
StateChanged?.Invoke(this, args);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error in StateChanged event handler");
}
}
}
///
/// Resets the state machine to Idle.
///
public void Reset()
{
ForceState(PlaybackState.Idle, "Reset");
}
///
/// Checks if a transition from one state to another is valid.
///
/// The current state.
/// The target state.
/// True if the transition is valid.
public static bool IsValidTransition(PlaybackState from, PlaybackState to)
{
if (!ValidTransitions.TryGetValue(from, out var validTargets))
{
return false;
}
return Array.IndexOf(validTargets, to) >= 0;
}
}